diff --git a/Session.xcodeproj/project.pbxproj b/Session.xcodeproj/project.pbxproj index 2de43e709..a1d6257db 100644 --- a/Session.xcodeproj/project.pbxproj +++ b/Session.xcodeproj/project.pbxproj @@ -7,8 +7,7 @@ objects = { /* Begin PBXBuildFile section */ - 1FFD68A448D5A1439F2F02FD /* Pods_GlobalDependencies_FrameworkAndExtensionDependencies_ExtendedDependencies_SessionShareExtension.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = DBA125424EDD2417B515C63A /* Pods_GlobalDependencies_FrameworkAndExtensionDependencies_ExtendedDependencies_SessionShareExtension.framework */; }; - 3289CA2E9E89DA9D4D52A90C /* Pods_GlobalDependencies_FrameworkAndExtensionDependencies_ExtendedDependencies_SignalUtilitiesKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 0BF4561630A52BE96F164CF6 /* Pods_GlobalDependencies_FrameworkAndExtensionDependencies_ExtendedDependencies_SignalUtilitiesKit.framework */; }; + 056ED47155A04437A1EF58C2 /* Pods_GlobalDependencies_Session_SessionTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 32C10A2A536B58FC42C46C3C /* Pods_GlobalDependencies_Session_SessionTests.framework */; }; 3427C64320F500E000EEC730 /* OWSMessageTimerView.m in Sources */ = {isa = PBXBuildFile; fileRef = 3427C64220F500DF00EEC730 /* OWSMessageTimerView.m */; }; 3430FE181F7751D4000EC51B /* GiphyAPI.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3430FE171F7751D4000EC51B /* GiphyAPI.swift */; }; 34661FB820C1C0D60056EDD6 /* message_sent.aiff in Resources */ = {isa = PBXBuildFile; fileRef = 34661FB720C1C0D60056EDD6 /* message_sent.aiff */; }; @@ -34,6 +33,8 @@ 34D1F0521F7E8EA30066283D /* GiphyDownloader.swift in Sources */ = {isa = PBXBuildFile; fileRef = 34D1F0511F7E8EA30066283D /* GiphyDownloader.swift */; }; 34D99CE4217509C2000AFB39 /* AppEnvironment.swift in Sources */ = {isa = PBXBuildFile; fileRef = 34D99CE3217509C1000AFB39 /* AppEnvironment.swift */; }; 34F308A21ECB469700BB7697 /* OWSBezierPathView.m in Sources */ = {isa = PBXBuildFile; fileRef = 34F308A11ECB469700BB7697 /* OWSBezierPathView.m */; }; + 41BA6B5C1C693C3A86070C15 /* Pods_GlobalDependencies_Session.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = CC8D3B6504946442A0CF775C /* Pods_GlobalDependencies_Session.framework */; }; + 42C48489AFF26BC9034C736C /* Pods_GlobalDependencies_FrameworkAndExtensionDependencies_ExtendedDependencies_SessionUtilitiesKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 86A3D36084020C9118DBCEE3 /* Pods_GlobalDependencies_FrameworkAndExtensionDependencies_ExtendedDependencies_SessionUtilitiesKit.framework */; }; 4503F1BE20470A5B00CEE724 /* classic-quiet.aifc in Resources */ = {isa = PBXBuildFile; fileRef = 4503F1BB20470A5B00CEE724 /* classic-quiet.aifc */; }; 4503F1BF20470A5B00CEE724 /* classic.aifc in Resources */ = {isa = PBXBuildFile; fileRef = 4503F1BC20470A5B00CEE724 /* classic.aifc */; }; 450DF2091E0DD2C6003D14BE /* UserNotificationsAdaptee.swift in Sources */ = {isa = PBXBuildFile; fileRef = 450DF2081E0DD2C6003D14BE /* UserNotificationsAdaptee.swift */; }; @@ -84,6 +85,7 @@ 4C1885D2218F8E1C00B67051 /* PhotoGridViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C1885D1218F8E1C00B67051 /* PhotoGridViewCell.swift */; }; 4C21D5D8223AC60F00EF8A77 /* PhotoCapture.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C21D5D7223AC60F00EF8A77 /* PhotoCapture.swift */; }; 4C4AE6A1224AF35700D4AF6F /* SendMediaNavigationController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C4AE69F224AF21900D4AF6F /* SendMediaNavigationController.swift */; }; + 4C4FE46740136D591D04261F /* Pods_GlobalDependencies_FrameworkAndExtensionDependencies_ExtendedDependencies_SessionShareExtension.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = E0B0F4C34363FE679EE3F203 /* Pods_GlobalDependencies_FrameworkAndExtensionDependencies_ExtendedDependencies_SessionShareExtension.framework */; }; 4C586926224FAB83003FD070 /* AVAudioSession+OWS.m in Sources */ = {isa = PBXBuildFile; fileRef = 4C586925224FAB83003FD070 /* AVAudioSession+OWS.m */; }; 4C63CC00210A620B003AE45C /* SignalTSan.supp in Resources */ = {isa = PBXBuildFile; fileRef = 4C63CBFF210A620B003AE45C /* SignalTSan.supp */; }; 4C6F527C20FFE8400097DEEE /* SignalUBSan.supp in Resources */ = {isa = PBXBuildFile; fileRef = 4C6F527B20FFE8400097DEEE /* SignalUBSan.supp */; }; @@ -91,9 +93,8 @@ 4CA46F4C219CCC630038ABDE /* CaptionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CA46F4B219CCC630038ABDE /* CaptionView.swift */; }; 4CA485BB2232339F004B9E7D /* PhotoCaptureViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CA485BA2232339F004B9E7D /* PhotoCaptureViewController.swift */; }; 4CC613362227A00400E21A3A /* ConversationSearch.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CC613352227A00400E21A3A /* ConversationSearch.swift */; }; - 5163CBC4F53274C88D1F88F8 /* Pods_GlobalDependencies_FrameworkAndExtensionDependencies_ExtendedDependencies_SessionUtilitiesKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 782B65234A707D762FEAFD3B /* Pods_GlobalDependencies_FrameworkAndExtensionDependencies_ExtendedDependencies_SessionUtilitiesKit.framework */; }; + 6C1ADD1127CED42854542F78 /* Pods_GlobalDependencies_FrameworkAndExtensionDependencies_ExtendedDependencies_SignalUtilitiesKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D3BE3061D535DBC1DF0C94D4 /* Pods_GlobalDependencies_FrameworkAndExtensionDependencies_ExtendedDependencies_SignalUtilitiesKit.framework */; }; 70377AAB1918450100CAF501 /* MobileCoreServices.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 70377AAA1918450100CAF501 /* MobileCoreServices.framework */; }; - 71B1D8AF3ADE2BD191256496 /* Pods_GlobalDependencies_Session.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 48AD214D67ABED845101E795 /* Pods_GlobalDependencies_Session.framework */; }; 768A1A2B17FC9CD300E00ED8 /* libz.dylib in Frameworks */ = {isa = PBXBuildFile; fileRef = 768A1A2A17FC9CD300E00ED8 /* libz.dylib */; }; 76C87F19181EFCE600C4ACAB /* MediaPlayer.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 76C87F18181EFCE600C4ACAB /* MediaPlayer.framework */; }; 7B0EFDEE274F598600FFAAE7 /* TimestampUtils.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B0EFDED274F598600FFAAE7 /* TimestampUtils.swift */; }; @@ -159,14 +160,12 @@ 7BFD1A8A2745C4F000FB91B9 /* Permissions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7BFD1A892745C4F000FB91B9 /* Permissions.swift */; }; 7BFD1A8C2747150E00FB91B9 /* TurnServerInfo.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7BFD1A8B2747150E00FB91B9 /* TurnServerInfo.swift */; }; 7BFD1A972747689000FB91B9 /* Session-Turn-Server in Resources */ = {isa = PBXBuildFile; fileRef = 7BFD1A962747689000FB91B9 /* Session-Turn-Server */; }; - 821EFD1644285AC2D3733D27 /* Pods_GlobalDependencies_SessionUIKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = A9C58C3ADF46C718488458C2 /* Pods_GlobalDependencies_SessionUIKit.framework */; }; - 92EB2776D36B22D2E0552A05 /* Pods_GlobalDependencies_FrameworkAndExtensionDependencies_ExtendedDependencies_SessionMessagingKit_SessionMessagingKitTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 2691123A7F231EDD8226C4B5 /* Pods_GlobalDependencies_FrameworkAndExtensionDependencies_ExtendedDependencies_SessionMessagingKit_SessionMessagingKitTests.framework */; }; - 98547545DAF8E7916DF9F0BF /* Pods_GlobalDependencies_FrameworkAndExtensionDependencies_SessionNotificationServiceExtension.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 6F84A214B9A1C0CCF6DB09C8 /* Pods_GlobalDependencies_FrameworkAndExtensionDependencies_SessionNotificationServiceExtension.framework */; }; + 9A88F90C33C394513CB4C18A /* Pods_GlobalDependencies_FrameworkAndExtensionDependencies_ExtendedDependencies_SessionUtilitiesKit_SessionUtilitiesKitTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 07B7038C849F53378CD36B83 /* Pods_GlobalDependencies_FrameworkAndExtensionDependencies_ExtendedDependencies_SessionUtilitiesKit_SessionUtilitiesKitTests.framework */; }; + A0A69C9CB213DDDD85BF2207 /* Pods_GlobalDependencies_SessionUIKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 678CFB04E76F7E388AFCFA86 /* Pods_GlobalDependencies_SessionUIKit.framework */; }; A11CD70D17FA230600A2D1B1 /* QuartzCore.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = A11CD70C17FA230600A2D1B1 /* QuartzCore.framework */; }; A163E8AB16F3F6AA0094D68B /* Security.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = A163E8AA16F3F6A90094D68B /* Security.framework */; }; A1C32D5017A06538000A904E /* AddressBookUI.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = A1C32D4F17A06537000A904E /* AddressBookUI.framework */; }; A1C32D5117A06544000A904E /* AddressBook.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = A1C32D4D17A0652C000A904E /* AddressBook.framework */; }; - A49760F37A9AE09D57ECE415 /* Pods_GlobalDependencies_FrameworkAndExtensionDependencies_ExtendedDependencies_SessionUtilitiesKit_SessionUtilitiesKitTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 5442DF945D862CEDF7F8AC49 /* Pods_GlobalDependencies_FrameworkAndExtensionDependencies_ExtendedDependencies_SessionUtilitiesKit_SessionUtilitiesKitTests.framework */; }; B66DBF4A19D5BBC8006EA940 /* Images.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = B66DBF4919D5BBC8006EA940 /* Images.xcassets */; }; B67EBF5D19194AC60084CCFD /* Settings.bundle in Resources */ = {isa = PBXBuildFile; fileRef = B67EBF5C19194AC60084CCFD /* Settings.bundle */; }; B6B226971BE4B7D200860F4D /* ContactsUI.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = B6B226961BE4B7D200860F4D /* ContactsUI.framework */; settings = {ATTRIBUTES = (Weak, ); }; }; @@ -262,7 +261,7 @@ B8FF8E7425C10FC3004D1F22 /* GeoLite2-Country-Locations-English in Resources */ = {isa = PBXBuildFile; fileRef = B8FF8E7325C10FC3004D1F22 /* GeoLite2-Country-Locations-English */; }; B8FF8EA625C11FEF004D1F22 /* IPv4.swift in Sources */ = {isa = PBXBuildFile; fileRef = B8FF8EA525C11FEF004D1F22 /* IPv4.swift */; }; B9EB5ABD1884C002007CBB57 /* MessageUI.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = B9EB5ABC1884C002007CBB57 /* MessageUI.framework */; }; - C2CAA4A9737D865B34560B8C /* Pods_GlobalDependencies_FrameworkAndExtensionDependencies_SessionSnodeKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 6737124ECBC2DFEE2DD716D3 /* Pods_GlobalDependencies_FrameworkAndExtensionDependencies_SessionSnodeKit.framework */; }; + BE25D9230CA2C3A40A9216EF /* Pods_GlobalDependencies_FrameworkAndExtensionDependencies_ExtendedDependencies_SessionMessagingKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 05E68C7F291EC08B8A43A534 /* Pods_GlobalDependencies_FrameworkAndExtensionDependencies_ExtendedDependencies_SessionMessagingKit.framework */; }; C300A5D32554B05A00555489 /* TypingIndicator.swift in Sources */ = {isa = PBXBuildFile; fileRef = C300A5D22554B05A00555489 /* TypingIndicator.swift */; }; C300A5F22554B09800555489 /* MessageSender.swift in Sources */ = {isa = PBXBuildFile; fileRef = C300A5F12554B09800555489 /* MessageSender.swift */; }; C300A60D2554B31900555489 /* Logging.swift in Sources */ = {isa = PBXBuildFile; fileRef = C3C2A5CE2553860700C340D1 /* Logging.swift */; }; @@ -379,9 +378,7 @@ C38EF30C255B6DBF007E1867 /* ScreenLock.swift in Sources */ = {isa = PBXBuildFile; fileRef = C38EF2E2255B6DB9007E1867 /* ScreenLock.swift */; }; C38EF31C255B6DBF007E1867 /* Searcher.swift in Sources */ = {isa = PBXBuildFile; fileRef = C38EF2F2255B6DBC007E1867 /* Searcher.swift */; }; C38EF324255B6DBF007E1867 /* Bench.swift in Sources */ = {isa = PBXBuildFile; fileRef = C38EF2FA255B6DBD007E1867 /* Bench.swift */; }; - C38EF32B255B6DBF007E1867 /* OWSFormat.h in Headers */ = {isa = PBXBuildFile; fileRef = C38EF301255B6DBD007E1867 /* OWSFormat.h */; settings = {ATTRIBUTES = (Public, ); }; }; C38EF32E255B6DBF007E1867 /* ImageCache.swift in Sources */ = {isa = PBXBuildFile; fileRef = C38EF304255B6DBE007E1867 /* ImageCache.swift */; }; - C38EF32F255B6DBF007E1867 /* OWSFormat.m in Sources */ = {isa = PBXBuildFile; fileRef = C38EF305255B6DBE007E1867 /* OWSFormat.m */; }; C38EF331255B6DBF007E1867 /* UIGestureRecognizer+OWS.swift in Sources */ = {isa = PBXBuildFile; fileRef = C38EF307255B6DBE007E1867 /* UIGestureRecognizer+OWS.swift */; }; C38EF35E255B6DCC007E1867 /* OWSViewController.h in Headers */ = {isa = PBXBuildFile; fileRef = C38EF344255B6DC5007E1867 /* OWSViewController.h */; settings = {ATTRIBUTES = (Public, ); }; }; C38EF363255B6DCC007E1867 /* ModalActivityIndicatorViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = C38EF349255B6DC7007E1867 /* ModalActivityIndicatorViewController.swift */; }; @@ -440,13 +437,8 @@ C3BBE0C72554F1570050F1E3 /* FixedWidthInteger+BigEndian.swift in Sources */ = {isa = PBXBuildFile; fileRef = C3BBE0C62554F1570050F1E3 /* FixedWidthInteger+BigEndian.swift */; }; C3C2A5A3255385C100C340D1 /* SessionSnodeKit.h in Headers */ = {isa = PBXBuildFile; fileRef = C3C2A5A1255385C100C340D1 /* SessionSnodeKit.h */; settings = {ATTRIBUTES = (Public, ); }; }; C3C2A5A7255385C100C340D1 /* SessionSnodeKit.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = C3C2A59F255385C100C340D1 /* SessionSnodeKit.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; - C3C2A5BF255385EE00C340D1 /* SnodeMessage.swift in Sources */ = {isa = PBXBuildFile; fileRef = C3C2A5B6255385EC00C340D1 /* SnodeMessage.swift */; }; C3C2A5C0255385EE00C340D1 /* Snode.swift in Sources */ = {isa = PBXBuildFile; fileRef = C3C2A5B7255385EC00C340D1 /* Snode.swift */; }; C3C2A5C2255385EE00C340D1 /* Configuration.swift in Sources */ = {isa = PBXBuildFile; fileRef = C3C2A5B9255385ED00C340D1 /* Configuration.swift */; }; - C3C2A5C3255385EE00C340D1 /* OnionRequestAPI.swift in Sources */ = {isa = PBXBuildFile; fileRef = C3C2A5BA255385ED00C340D1 /* OnionRequestAPI.swift */; }; - C3C2A5C4255385EE00C340D1 /* OnionRequestAPI+Encryption.swift in Sources */ = {isa = PBXBuildFile; fileRef = C3C2A5BB255385ED00C340D1 /* OnionRequestAPI+Encryption.swift */; }; - C3C2A5C6255385EE00C340D1 /* Notification+OnionRequestAPI.swift in Sources */ = {isa = PBXBuildFile; fileRef = C3C2A5BD255385EE00C340D1 /* Notification+OnionRequestAPI.swift */; }; - C3C2A5C7255385EE00C340D1 /* SnodeAPI.swift in Sources */ = {isa = PBXBuildFile; fileRef = C3C2A5BE255385EE00C340D1 /* SnodeAPI.swift */; }; C3C2A5DE2553860B00C340D1 /* String+Trimming.swift in Sources */ = {isa = PBXBuildFile; fileRef = C3C2A5D22553860900C340D1 /* String+Trimming.swift */; }; C3C2A5E02553860B00C340D1 /* Threading.swift in Sources */ = {isa = PBXBuildFile; fileRef = C3C2A5D42553860A00C340D1 /* Threading.swift */; }; C3C2A5E42553860B00C340D1 /* Data+Utilities.swift in Sources */ = {isa = PBXBuildFile; fileRef = C3C2A5D82553860B00C340D1 /* Data+Utilities.swift */; }; @@ -487,7 +479,7 @@ C3DB66C3260ACCE6001EFC55 /* OpenGroupPoller.swift in Sources */ = {isa = PBXBuildFile; fileRef = C3DB66C2260ACCE6001EFC55 /* OpenGroupPoller.swift */; }; C3E5C2FA251DBABB0040DFFC /* EditClosedGroupVC.swift in Sources */ = {isa = PBXBuildFile; fileRef = C3E5C2F9251DBABB0040DFFC /* EditClosedGroupVC.swift */; }; C3F0A530255C80BC007BE2A3 /* NoopNotificationsManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = C3F0A52F255C80BC007BE2A3 /* NoopNotificationsManager.swift */; }; - CEE449BA3596483519120D91 /* Pods_GlobalDependencies_FrameworkAndExtensionDependencies_ExtendedDependencies_SessionMessagingKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 7A8A44E3F8AC9282AC5E6E5A /* Pods_GlobalDependencies_FrameworkAndExtensionDependencies_ExtendedDependencies_SessionMessagingKit.framework */; }; + CB54B7E519F525FF27A7DAD3 /* Pods_GlobalDependencies_FrameworkAndExtensionDependencies_SessionNotificationServiceExtension.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = C5828D9A55CD76B75E8FA367 /* Pods_GlobalDependencies_FrameworkAndExtensionDependencies_SessionNotificationServiceExtension.framework */; }; D2179CFC16BB0B3A0006F3AB /* CoreTelephony.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D2179CFB16BB0B3A0006F3AB /* CoreTelephony.framework */; }; D2179CFE16BB0B480006F3AB /* SystemConfiguration.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D2179CFD16BB0B480006F3AB /* SystemConfiguration.framework */; }; D221A08E169C9E5E00537ABF /* UIKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D221A08D169C9E5E00537ABF /* UIKit.framework */; }; @@ -495,7 +487,7 @@ D221A0E8169DFFC500537ABF /* AVFoundation.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D221A0E7169DFFC500537ABF /* AVFoundation.framework */; }; D24B5BD5169F568C00681372 /* AudioToolbox.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D24B5BD4169F568C00681372 /* AudioToolbox.framework */; }; D2AEACDC16C426DA00C364C0 /* CFNetwork.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D2AEACDB16C426DA00C364C0 /* CFNetwork.framework */; }; - DA2AE22FA77136442EF669E9 /* Pods_GlobalDependencies_Session_SessionTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = B4C92F6ADBECCD47A6B6008E /* Pods_GlobalDependencies_Session_SessionTests.framework */; }; + DE0001574AC103562A7CF31F /* Pods_GlobalDependencies_FrameworkAndExtensionDependencies_SessionSnodeKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 9E358E7590EB145A5047F885 /* Pods_GlobalDependencies_FrameworkAndExtensionDependencies_SessionSnodeKit.framework */; }; FC3BD9881A30A790005B96BB /* Social.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = FC3BD9871A30A790005B96BB /* Social.framework */; }; FCB11D8C1A129A76002F93FB /* CoreMedia.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = FCB11D8B1A129A76002F93FB /* CoreMedia.framework */; }; FD078E4827E02561000769AF /* CommonMockedExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD078E4727E02561000769AF /* CommonMockedExtensions.swift */; }; @@ -506,7 +498,6 @@ FD078E5427E197CA000769AF /* OpenGroupManagerSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDC2909D27D85751005DAE71 /* OpenGroupManagerSpec.swift */; }; FD078E5A27E29F09000769AF /* MockNonce16Generator.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD078E5927E29F09000769AF /* MockNonce16Generator.swift */; }; FD078E5C27E29F78000769AF /* MockNonce24Generator.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD078E5B27E29F78000769AF /* MockNonce24Generator.swift */; }; - FD09796927F6BEA700936362 /* SwarmSnode.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD09796827F6BEA700936362 /* SwarmSnode.swift */; }; FD09796B27F6C67500936362 /* Failable.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD09796A27F6C67500936362 /* Failable.swift */; }; FD09796E27FA6D0000936362 /* Contact.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD09796D27FA6D0000936362 /* Contact.swift */; }; FD09797027FA6FF300936362 /* Profile.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD09796F27FA6FF300936362 /* Profile.swift */; }; @@ -554,10 +545,6 @@ FD17D7C727F5207C00122BE0 /* DatabaseMigrator+Utilities.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD17D7C627F5207C00122BE0 /* DatabaseMigrator+Utilities.swift */; }; FD17D7CA27F546D900122BE0 /* _001_InitialSetupMigration.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD17D7C927F546D900122BE0 /* _001_InitialSetupMigration.swift */; }; FD17D7CD27F546FF00122BE0 /* Setting.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD17D7CC27F546FF00122BE0 /* Setting.swift */; }; - FD17D7D227F5797A00122BE0 /* SnodeAPIEndpoint.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD17D7D127F5797A00122BE0 /* SnodeAPIEndpoint.swift */; }; - FD17D7D427F6584600122BE0 /* OnionRequestAPIError.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD17D7D327F6584600122BE0 /* OnionRequestAPIError.swift */; }; - FD17D7D827F658E200122BE0 /* OnionRequestAPIDestination.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD17D7D727F658E200122BE0 /* OnionRequestAPIDestination.swift */; }; - FD17D7E127F67BD400122BE0 /* SnodeReceivedMessage.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD17D7E027F67BD400122BE0 /* SnodeReceivedMessage.swift */; }; FD17D7E527F6A09900122BE0 /* Identity.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD17D7E427F6A09900122BE0 /* Identity.swift */; }; FD17D7E727F6A16700122BE0 /* _003_YDBToGRDBMigration.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD17D7E627F6A16700122BE0 /* _003_YDBToGRDBMigration.swift */; }; FD17D7EA27F6A1C600122BE0 /* SUKLegacy.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD17D7E927F6A1C600122BE0 /* SUKLegacy.swift */; }; @@ -597,28 +584,10 @@ FD245C6B2850667400B966DD /* VisibleMessage+Profile.swift in Sources */ = {isa = PBXBuildFile; fileRef = C300A5B12554AF9800555489 /* VisibleMessage+Profile.swift */; }; FD245C6C2850669200B966DD /* MessageReceiveJob.swift in Sources */ = {isa = PBXBuildFile; fileRef = C352A31225574F5200338F3E /* MessageReceiveJob.swift */; }; FD245C6D285066A400B966DD /* NotifyPushServerJob.swift in Sources */ = {isa = PBXBuildFile; fileRef = C352A32E2557549C00338F3E /* NotifyPushServerJob.swift */; }; - FD26FA512919F9CE005801D8 /* GroupDeleteMessage.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD26FA502919F9CE005801D8 /* GroupDeleteMessage.swift */; }; - FD26FA53291CACA9005801D8 /* BatchResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDC4386A27B4E88F00C60D73 /* BatchResponse.swift */; }; - FD26FA54291CAD31005801D8 /* Request.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD83B9CD27D17A04005E1583 /* Request.swift */; }; - FD26FA55291CAD44005801D8 /* HTTPQueryParam.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDC4385027B4807400C60D73 /* HTTPQueryParam.swift */; }; - FD26FA57291CADAE005801D8 /* HTTPQueryParam+OpenGroup.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD26FA56291CADAE005801D8 /* HTTPQueryParam+OpenGroup.swift */; }; - FD26FA58291CAE38005801D8 /* HTTPHeader.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDC4384E27B4804F00C60D73 /* HTTPHeader.swift */; }; - FD26FA5A291CAE9B005801D8 /* HTTPHeader+OpenGroup.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD26FA59291CAE9B005801D8 /* HTTPHeader+OpenGroup.swift */; }; - FD26FA5E291CAFF9005801D8 /* Data+Utilities.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD26FA5D291CAFF9005801D8 /* Data+Utilities.swift */; }; - FD26FA60291CB098005801D8 /* ResponseInfo.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD26FA5F291CB098005801D8 /* ResponseInfo.swift */; }; - FD26FA62291CB46D005801D8 /* RequestInfo.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDC438B027BB159600C60D73 /* RequestInfo.swift */; }; - FD26FA66291CC981005801D8 /* HTTPError.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD26FA65291CC981005801D8 /* HTTPError.swift */; }; - FD26FA68291CC99E005801D8 /* HTTPMethod.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD26FA67291CC99E005801D8 /* HTTPMethod.swift */; }; - FD26FA6B291DA6BC005801D8 /* SSKDependencies.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD26FA6A291DA6BC005801D8 /* SSKDependencies.swift */; }; - FD26FA6D291DADAE005801D8 /* SnodeRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD26FA6C291DADAE005801D8 /* SnodeRequest.swift */; }; - FD26FA6F291DB171005801D8 /* ONSResolveRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD26FA6E291DB171005801D8 /* ONSResolveRequest.swift */; }; - FD26FA71291DB253005801D8 /* OxenDaemonRPCRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD26FA70291DB253005801D8 /* OxenDaemonRPCRequest.swift */; }; - FD26FA73291DB5F3005801D8 /* SnodeBatchRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD26FA72291DB5F3005801D8 /* SnodeBatchRequest.swift */; }; - FD26FA75291DBC8B005801D8 /* ONSResolveResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD26FA74291DBC8B005801D8 /* ONSResolveResponse.swift */; }; - FD26FA77291DE2C7005801D8 /* SnodeResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD26FA76291DE2C7005801D8 /* SnodeResponse.swift */; }; - FD26FA79291DEDD7005801D8 /* GetMessagesRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD26FA78291DEDD7005801D8 /* GetMessagesRequest.swift */; }; - FD26FA7B291DF8F3005801D8 /* SnodeAPINamespace.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD26FA7A291DF8F3005801D8 /* SnodeAPINamespace.swift */; }; - FD26FA7D291E0B10005801D8 /* GetMessagesResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD26FA7C291E0B10005801D8 /* GetMessagesResponse.swift */; }; + FD26FA512919F9CE005801D8 /* (null) in Sources */ = {isa = PBXBuildFile; }; + FD26FA5E291CAFF9005801D8 /* (null) in Sources */ = {isa = PBXBuildFile; }; + FD26FA6D291DADAE005801D8 /* (null) in Sources */ = {isa = PBXBuildFile; }; + FD26FA7B291DF8F3005801D8 /* (null) in Sources */ = {isa = PBXBuildFile; }; FD2AAAED28ED3E1000A49611 /* MockGeneralCache.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDFD645C27F273F300808CA1 /* MockGeneralCache.swift */; }; FD2AAAEE28ED3E1100A49611 /* MockGeneralCache.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDFD645C27F273F300808CA1 /* MockGeneralCache.swift */; }; FD2AAAF028ED57B500A49611 /* SynchronousStorage.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD2AAAEF28ED57B500A49611 /* SynchronousStorage.swift */; }; @@ -707,7 +676,6 @@ FD71163228E2C42A00B47552 /* IconSize.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD71163128E2C42A00B47552 /* IconSize.swift */; }; FD71163728E2C50700B47552 /* SessionTableViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD37EA0628AA2CCA003AE748 /* SessionTableViewController.swift */; }; FD71163828E2C50700B47552 /* SessionTableViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD37EA0828AA2D27003AE748 /* SessionTableViewModel.swift */; }; - FD71163A28E2C53700B47552 /* SessionAvatarCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD7115ED28C5D79B00B47552 /* SessionAvatarCell.swift */; }; FD71163E28E2C82900B47552 /* SessionCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD37EA0A28AB12E2003AE748 /* SessionCell.swift */; }; FD71163F28E2C82C00B47552 /* SessionHeaderView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD7115EF28C5D7DE00B47552 /* SessionHeaderView.swift */; }; FD71164228E2C85A00B47552 /* TransitionType.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD71163328E2C48400B47552 /* TransitionType.swift */; }; @@ -715,7 +683,7 @@ FD71164628E2CC1300B47552 /* SessionHighlightingBackgroundLabel.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD71164528E2CC1300B47552 /* SessionHighlightingBackgroundLabel.swift */; }; FD71164828E2CE8700B47552 /* SessionCell+AccessoryView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD71164728E2CE8700B47552 /* SessionCell+AccessoryView.swift */; }; FD71164A28E3EA5B00B47552 /* DismissType.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD71164928E3EA5B00B47552 /* DismissType.swift */; }; - FD71164C28E3F5AA00B47552 /* SessionCell+ExtraAction.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD71164B28E3F5AA00B47552 /* SessionCell+ExtraAction.swift */; }; + FD71164C28E3F5AA00B47552 /* SessionCell+Accessibility.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD71164B28E3F5AA00B47552 /* SessionCell+Accessibility.swift */; }; FD71164E28E3F8CC00B47552 /* SessionCell+Info.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD71164D28E3F8CC00B47552 /* SessionCell+Info.swift */; }; FD71165028E3F9FA00B47552 /* SessionTableViewModel+NavItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD71164F28E3F9FA00B47552 /* SessionTableViewModel+NavItem.swift */; }; FD71165228E410BE00B47552 /* SessionTableSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD71165128E410BE00B47552 /* SessionTableSection.swift */; }; @@ -733,9 +701,7 @@ FD7728962849E7E90018502F /* String+Utilities.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD7728952849E7E90018502F /* String+Utilities.swift */; }; FD7728982849E8110018502F /* UITableView+ReusableView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD7728972849E8110018502F /* UITableView+ReusableView.swift */; }; FD77289A284AF1BD0018502F /* Sodium+Utilities.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD772899284AF1BD0018502F /* Sodium+Utilities.swift */; }; - FD77289C284DDCE10018502F /* SnodePoolResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD77289B284DDCE10018502F /* SnodePoolResponse.swift */; }; FD77289E284EF1C50018502F /* Sodium+Utilities.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD77289D284EF1C50018502F /* Sodium+Utilities.swift */; }; - FD7728A0284EF5810018502F /* SnodeAPIError.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD77289F284EF5810018502F /* SnodeAPIError.swift */; }; FD83B9B327CF200A005E1583 /* SessionUtilitiesKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = C3C2A679255388CC00C340D1 /* SessionUtilitiesKit.framework */; platformFilter = ios; }; FD83B9BB27CF20AF005E1583 /* SessionIdSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD83B9BA27CF20AF005E1583 /* SessionIdSpec.swift */; }; FD83B9BF27CF2294005E1583 /* TestConstants.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD83B9BD27CF2243005E1583 /* TestConstants.swift */; }; @@ -744,7 +710,6 @@ FD83B9C727CF3F10005E1583 /* CapabilitiesSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD83B9C627CF3F10005E1583 /* CapabilitiesSpec.swift */; }; FD83B9C927D0487A005E1583 /* SendDirectMessageResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD83B9C827D0487A005E1583 /* SendDirectMessageResponse.swift */; }; FD83B9CC27D179BC005E1583 /* FSEndpoint.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD83B9CB27D179BC005E1583 /* FSEndpoint.swift */; }; - FD83B9CE27D17A04005E1583 /* Request.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD83B9CD27D17A04005E1583 /* Request.swift */; }; FD83B9D227D59495005E1583 /* MockUserDefaults.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD83B9D127D59495005E1583 /* MockUserDefaults.swift */; }; FD848B8B283DC509000E298B /* PagedDatabaseObserver.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD848B8A283DC509000E298B /* PagedDatabaseObserver.swift */; }; FD848B8D283E0B26000E298B /* MessageInputTypes.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD848B8C283E0B26000E298B /* MessageInputTypes.swift */; }; @@ -760,51 +725,19 @@ FD859EFA27C2F5C500510D0C /* MockGenericHash.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD859EF927C2F5C500510D0C /* MockGenericHash.swift */; }; FD859EFC27C2F60700510D0C /* MockEd25519.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD859EFB27C2F60700510D0C /* MockEd25519.swift */; }; FD87DCFA28B74DB300AF0F98 /* ConversationSettingsViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD87DCF928B74DB300AF0F98 /* ConversationSettingsViewModel.swift */; }; - FD87DCFC28B755B800AF0F98 /* BlockedContactsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD87DCFB28B755B800AF0F98 /* BlockedContactsViewController.swift */; }; FD87DCFE28B7582C00AF0F98 /* BlockedContactsViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD87DCFD28B7582C00AF0F98 /* BlockedContactsViewModel.swift */; }; FD87DD0028B820F200AF0F98 /* BlockedContactCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD87DCFF28B820F200AF0F98 /* BlockedContactCell.swift */; }; FD87DD0428B8727D00AF0F98 /* Configuration.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD87DD0328B8727D00AF0F98 /* Configuration.swift */; }; - FD8ECF40292AF07900C0D1BB /* Poller.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD8ECF3F292AF07900C0D1BB /* Poller.swift */; }; - FD8ECF42292B340D00C0D1BB /* SOGSBatchRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD8ECF41292B340D00C0D1BB /* SOGSBatchRequest.swift */; }; - FD8ECF44292B397E00C0D1BB /* SendMessageRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD8ECF43292B397E00C0D1BB /* SendMessageRequest.swift */; }; - FD8ECF46292B4BD500C0D1BB /* SendMessageResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD8ECF45292B4BD500C0D1BB /* SendMessageResponse.swift */; }; - FD8ECF48292C287500C0D1BB /* UpdateExpiryRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD8ECF47292C287500C0D1BB /* UpdateExpiryRequest.swift */; }; - FD8ECF4A292C2A7300C0D1BB /* DeleteMessagesRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD8ECF49292C2A7300C0D1BB /* DeleteMessagesRequest.swift */; }; - FD8ECF4C292C2AC200C0D1BB /* RevokeSubkeyRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD8ECF4B292C2AC200C0D1BB /* RevokeSubkeyRequest.swift */; }; - FD8ECF4E292C2AF800C0D1BB /* DeleteAllMessagesRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD8ECF4D292C2AF800C0D1BB /* DeleteAllMessagesRequest.swift */; }; - FD8ECF50292C2B2B00C0D1BB /* DeleteAllBeforeRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD8ECF4F292C2B2B00C0D1BB /* DeleteAllBeforeRequest.swift */; }; - FD8ECF52292C2CAE00C0D1BB /* UpdateExpiryAllRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD8ECF51292C2CAE00C0D1BB /* UpdateExpiryAllRequest.swift */; }; - FD8ECF54292C2DB000C0D1BB /* SnodeAuthenticatedRequestBody.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD8ECF53292C2DB000C0D1BB /* SnodeAuthenticatedRequestBody.swift */; }; - FD8ECF56292C327700C0D1BB /* LegacyGetMessagesRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD8ECF55292C327700C0D1BB /* LegacyGetMessagesRequest.swift */; }; - FD8ECF58292C350500C0D1BB /* LegacySendMessageRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD8ECF57292C350500C0D1BB /* LegacySendMessageRequest.swift */; }; - FD8ECF5A292C431B00C0D1BB /* GetSwarmRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD8ECF59292C431B00C0D1BB /* GetSwarmRequest.swift */; }; - FD8ECF5C292C469100C0D1BB /* UpdateExpiryResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD8ECF5B292C469100C0D1BB /* UpdateExpiryResponse.swift */; }; - FD8ECF5E292C478900C0D1BB /* SnodeRecursiveResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD8ECF5D292C478900C0D1BB /* SnodeRecursiveResponse.swift */; }; - FD8ECF60292C4B2400C0D1BB /* SnodeSwarmItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD8ECF5F292C4B2400C0D1BB /* SnodeSwarmItem.swift */; }; - FD8ECF62292C4C8200C0D1BB /* DeleteMessagesResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD8ECF61292C4C8200C0D1BB /* DeleteMessagesResponse.swift */; }; - FD8ECF64292C4D6600C0D1BB /* DeleteAllMessagesResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD8ECF63292C4D6600C0D1BB /* DeleteAllMessagesResponse.swift */; }; - FD8ECF66292C6F8200C0D1BB /* DeleteAllBeforeResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD8ECF65292C6F8200C0D1BB /* DeleteAllBeforeResponse.swift */; }; - FD8ECF68292C72BA00C0D1BB /* UpdateExpiryAllResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD8ECF67292C72B900C0D1BB /* UpdateExpiryAllResponse.swift */; }; - FD8ECF6A292C74A000C0D1BB /* RevokeSubkeyResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD8ECF69292C74A000C0D1BB /* RevokeSubkeyResponse.swift */; }; - FD8ECF6C292C9B6400C0D1BB /* GetNetworkTimestampResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD8ECF6B292C9B6400C0D1BB /* GetNetworkTimestampResponse.swift */; }; - FD8ECF6E292C9EA100C0D1BB /* GetServiceNodesRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD8ECF6D292C9EA100C0D1BB /* GetServiceNodesRequest.swift */; }; - FD8ECF72292DCD1A00C0D1BB /* _013_AutoDownloadAttachments.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD8ECF71292DCD1A00C0D1BB /* _013_AutoDownloadAttachments.swift */; }; - FD8ECF74292DDB4A00C0D1BB /* Format.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD8ECF73292DDB4A00C0D1BB /* Format.swift */; }; FD8ECF7929340F7200C0D1BB /* libsession-util.xcframework in Frameworks */ = {isa = PBXBuildFile; fileRef = FD8ECF7829340F7100C0D1BB /* libsession-util.xcframework */; }; FD8ECF7B29340FFD00C0D1BB /* SessionUtil.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD8ECF7A29340FFD00C0D1BB /* SessionUtil.swift */; }; FD8ECF7D2934293A00C0D1BB /* _011_SharedUtilChanges.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD8ECF7C2934293A00C0D1BB /* _011_SharedUtilChanges.swift */; }; FD8ECF7F2934298100C0D1BB /* SharedConfigDump.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD8ECF7E2934298100C0D1BB /* SharedConfigDump.swift */; }; FD8ECF822934387A00C0D1BB /* ConfigUserProfileSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD8ECF812934387A00C0D1BB /* ConfigUserProfileSpec.swift */; }; - FD8ECF852934508B00C0D1BB /* BatchResponseSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD8ECF842934508B00C0D1BB /* BatchResponseSpec.swift */; }; - FD8ECF8629346DA100C0D1BB /* HeaderSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD3C906127E411AF00CD579F /* HeaderSpec.swift */; }; - FD8ECF8729346DB500C0D1BB /* RequestSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD3C906327E4122F00CD579F /* RequestSpec.swift */; }; FD8ECF892935AB7200C0D1BB /* SessionUtilError.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD8ECF882935AB7200C0D1BB /* SessionUtilError.swift */; }; FD8ECF8B2935DB4B00C0D1BB /* SharedConfigMessage.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD8ECF8A2935DB4B00C0D1BB /* SharedConfigMessage.swift */; }; FD8ECF9029381FC200C0D1BB /* SessionUtil+UserProfile.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD8ECF8F29381FC200C0D1BB /* SessionUtil+UserProfile.swift */; }; FD8ECF922938552800C0D1BB /* Threading.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD8ECF912938552800C0D1BB /* Threading.swift */; }; FD8ECF94293856AF00C0D1BB /* Randomness.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD8ECF93293856AF00C0D1BB /* Randomness.swift */; }; - FD8ECFA1293D8FDD00C0D1BB /* URLResponse+Utilities.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD8ECFA0293D8FDD00C0D1BB /* URLResponse+Utilities.swift */; }; - FD90040F2818AB6D00ABAAF6 /* GetSnodePoolJob.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD90040E2818AB6D00ABAAF6 /* GetSnodePoolJob.swift */; }; FD9004122818ABDC00ABAAF6 /* Job.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDF0B73F280402C4004C14C5 /* Job.swift */; }; FD9004142818AD0B00ABAAF6 /* _002_SetupStandardJobs.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD9004132818AD0B00ABAAF6 /* _002_SetupStandardJobs.swift */; }; FD9004152818B46300ABAAF6 /* JobRunner.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDF0B7432804EF1B004C14C5 /* JobRunner.swift */; }; @@ -839,27 +772,21 @@ FDC4382027B36ADC00C60D73 /* SOGSEndpoint.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDC4381F27B36ADC00C60D73 /* SOGSEndpoint.swift */; }; FDC4382F27B383AF00C60D73 /* PushServerResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDC4382E27B383AF00C60D73 /* PushServerResponse.swift */; }; FDC4383827B3863200C60D73 /* VersionResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDC4383727B3863200C60D73 /* VersionResponse.swift */; }; - FDC4384F27B4804F00C60D73 /* Header.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDC4384E27B4804F00C60D73 /* Header.swift */; }; - FDC4385127B4807400C60D73 /* QueryParam.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDC4385027B4807400C60D73 /* QueryParam.swift */; }; FDC4385D27B4C18900C60D73 /* Room.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDC4385C27B4C18900C60D73 /* Room.swift */; }; FDC4385F27B4C4A200C60D73 /* PinnedMessage.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDC4385E27B4C4A200C60D73 /* PinnedMessage.swift */; }; FDC4386327B4D94E00C60D73 /* SOGSMessage.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDC4386227B4D94E00C60D73 /* SOGSMessage.swift */; }; FDC4386527B4DE7600C60D73 /* RoomPollInfo.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDC4386427B4DE7600C60D73 /* RoomPollInfo.swift */; }; FDC4386727B4E10E00C60D73 /* Capabilities.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDC4386627B4E10E00C60D73 /* Capabilities.swift */; }; FDC4386927B4E6B800C60D73 /* String+Utlities.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDC4386827B4E6B700C60D73 /* String+Utlities.swift */; }; - FDC4386B27B4E88F00C60D73 /* BatchRequestInfo.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDC4386A27B4E88F00C60D73 /* BatchRequestInfo.swift */; }; FDC4386C27B4E90300C60D73 /* SessionUtilitiesKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = C3C2A679255388CC00C340D1 /* SessionUtilitiesKit.framework */; }; FDC4387227B5BB3B00C60D73 /* FileUploadResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDC4387127B5BB3B00C60D73 /* FileUploadResponse.swift */; }; - FDC4387427B5BB9B00C60D73 /* Promise+Utilities.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDC4387327B5BB9B00C60D73 /* Promise+Utilities.swift */; }; + FDC4387427B5BB9B00C60D73 /* (null) in Sources */ = {isa = PBXBuildFile; }; FDC4387827B5C35400C60D73 /* SendMessageRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDC4387727B5C35400C60D73 /* SendMessageRequest.swift */; }; FDC4389227B9FFC700C60D73 /* SessionMessagingKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = C3C2A6F025539DE700C340D1 /* SessionMessagingKit.framework */; platformFilter = ios; }; FDC4389A27BA002500C60D73 /* OpenGroupAPISpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDC4389927BA002500C60D73 /* OpenGroupAPISpec.swift */; }; FDC438A427BB107F00C60D73 /* UserBanRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDC438A327BB107F00C60D73 /* UserBanRequest.swift */; }; FDC438A627BB113A00C60D73 /* UserUnbanRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDC438A527BB113A00C60D73 /* UserUnbanRequest.swift */; }; FDC438AA27BB12BB00C60D73 /* UserModeratorRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDC438A927BB12BB00C60D73 /* UserModeratorRequest.swift */; }; - FDC438B127BB159600C60D73 /* RequestInfo.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDC438B027BB159600C60D73 /* RequestInfo.swift */; }; - FDC438B327BB15B400C60D73 /* ResponseInfo.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDC438B227BB15B400C60D73 /* ResponseInfo.swift */; }; - FDC438B927BB161E00C60D73 /* OnionRequestAPIVersion.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDC438B827BB161E00C60D73 /* OnionRequestAPIVersion.swift */; }; FDC438BD27BB2AB400C60D73 /* Mockable.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDC438BC27BB2AB400C60D73 /* Mockable.swift */; }; FDC438C127BB4E6800C60D73 /* SMKDependencies.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDC438C027BB4E6800C60D73 /* SMKDependencies.swift */; }; FDC438C327BB512200C60D73 /* SodiumProtocols.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDC438C227BB512200C60D73 /* SodiumProtocols.swift */; }; @@ -887,21 +814,82 @@ FDF0B75A2807F3A3004C14C5 /* MessageSenderError.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDF0B7592807F3A3004C14C5 /* MessageSenderError.swift */; }; FDF0B75C2807F41D004C14C5 /* MessageSender+Convenience.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDF0B75B2807F41D004C14C5 /* MessageSender+Convenience.swift */; }; FDF0B75E280AAF35004C14C5 /* Preferences.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDF0B75D280AAF35004C14C5 /* Preferences.swift */; }; - FDF1AD5F28FF5F930080A701 /* EditGroupViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDF1AD5E28FF5F930080A701 /* EditGroupViewModel.swift */; }; - FDF1AD6128FF61110080A701 /* _012_AddClosedGroupInfo.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDF1AD6028FF61110080A701 /* _012_AddClosedGroupInfo.swift */; }; FDF222072818CECF000A4995 /* ConversationViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDF222062818CECF000A4995 /* ConversationViewModel.swift */; }; FDF222092818D2B0000A4995 /* NSAttributedString+Utilities.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDF222082818D2B0000A4995 /* NSAttributedString+Utilities.swift */; }; FDF2220B2818F38D000A4995 /* SessionApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDF2220A2818F38D000A4995 /* SessionApp.swift */; }; FDF2220F281B55E6000A4995 /* QueryInterfaceRequest+Utilities.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDF2220E281B55E6000A4995 /* QueryInterfaceRequest+Utilities.swift */; }; FDF22211281B5E0B000A4995 /* TableRecord+Utilities.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDF22210281B5E0B000A4995 /* TableRecord+Utilities.swift */; }; FDF40CDE2897A1BC006A0CC4 /* _004_RemoveLegacyYDB.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDF40CDD2897A1BC006A0CC4 /* _004_RemoveLegacyYDB.swift */; }; + FDF8487929405906007DCAE5 /* HTTPQueryParam.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDF8487529405906007DCAE5 /* HTTPQueryParam.swift */; }; + FDF8487A29405906007DCAE5 /* HTTPError.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDF8487629405906007DCAE5 /* HTTPError.swift */; }; + FDF8487B29405906007DCAE5 /* HTTPHeader.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDF8487729405906007DCAE5 /* HTTPHeader.swift */; }; + FDF8487C29405906007DCAE5 /* HTTPMethod.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDF8487829405906007DCAE5 /* HTTPMethod.swift */; }; + FDF8487F29405994007DCAE5 /* HTTPHeader+OpenGroup.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDF8487D29405993007DCAE5 /* HTTPHeader+OpenGroup.swift */; }; + FDF8488029405994007DCAE5 /* HTTPQueryParam+OpenGroup.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDF8487E29405994007DCAE5 /* HTTPQueryParam+OpenGroup.swift */; }; + FDF84881294059F5007DCAE5 /* ResponseInfo.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDC438B227BB15B400C60D73 /* ResponseInfo.swift */; }; + FDF8488329405A12007DCAE5 /* BatchResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDF8488229405A12007DCAE5 /* BatchResponse.swift */; }; + FDF8488429405A2B007DCAE5 /* RequestInfo.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDC438B027BB159600C60D73 /* RequestInfo.swift */; }; + FDF8488629405A61007DCAE5 /* Request.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDF8488529405A60007DCAE5 /* Request.swift */; }; + FDF8488829405A9A007DCAE5 /* SOGSBatchRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDF8488729405A9A007DCAE5 /* SOGSBatchRequest.swift */; }; + FDF8488929405B27007DCAE5 /* Data+Utilities.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDFD645A27F26D4600808CA1 /* Data+Utilities.swift */; }; + FDF8488B29405BF2007DCAE5 /* SSKDependencies.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDF8488A29405BF2007DCAE5 /* SSKDependencies.swift */; }; + FDF8488E29405C04007DCAE5 /* GetSnodePoolJob.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDF8488D29405C04007DCAE5 /* GetSnodePoolJob.swift */; }; + FDF8489129405C13007DCAE5 /* SnodeAPINamespace.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDF8489029405C13007DCAE5 /* SnodeAPINamespace.swift */; }; + FDF8489429405C1B007DCAE5 /* SnodeAPI.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDF8489329405C1B007DCAE5 /* SnodeAPI.swift */; }; + FDF848BC29405C5A007DCAE5 /* SnodeRecursiveResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDF8489A29405C5A007DCAE5 /* SnodeRecursiveResponse.swift */; }; + FDF848BD29405C5A007DCAE5 /* GetMessagesRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDF8489B29405C5A007DCAE5 /* GetMessagesRequest.swift */; }; + FDF848BE29405C5A007DCAE5 /* GetServiceNodesRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDF8489C29405C5A007DCAE5 /* GetServiceNodesRequest.swift */; }; + FDF848BF29405C5A007DCAE5 /* SnodeResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDF8489D29405C5A007DCAE5 /* SnodeResponse.swift */; }; + FDF848C029405C5A007DCAE5 /* ONSResolveResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDF8489E29405C5A007DCAE5 /* ONSResolveResponse.swift */; }; + FDF848C129405C5A007DCAE5 /* UpdateExpiryRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDF8489F29405C5A007DCAE5 /* UpdateExpiryRequest.swift */; }; + FDF848C229405C5A007DCAE5 /* OxenDaemonRPCRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDF848A029405C5A007DCAE5 /* OxenDaemonRPCRequest.swift */; }; + FDF848C329405C5A007DCAE5 /* DeleteMessagesRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDF848A129405C5A007DCAE5 /* DeleteMessagesRequest.swift */; }; + FDF848C429405C5A007DCAE5 /* RevokeSubkeyResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDF848A229405C5A007DCAE5 /* RevokeSubkeyResponse.swift */; }; + FDF848C529405C5B007DCAE5 /* GetSwarmRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDF848A329405C5A007DCAE5 /* GetSwarmRequest.swift */; }; + FDF848C629405C5B007DCAE5 /* DeleteAllMessagesRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDF848A429405C5A007DCAE5 /* DeleteAllMessagesRequest.swift */; }; + FDF848C729405C5B007DCAE5 /* SendMessageResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDF848A529405C5A007DCAE5 /* SendMessageResponse.swift */; }; + FDF848C829405C5B007DCAE5 /* ONSResolveRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDF848A629405C5A007DCAE5 /* ONSResolveRequest.swift */; }; + FDF848C929405C5B007DCAE5 /* SnodeRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDF848A729405C5A007DCAE5 /* SnodeRequest.swift */; }; + FDF848CA29405C5B007DCAE5 /* DeleteAllBeforeRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDF848A829405C5A007DCAE5 /* DeleteAllBeforeRequest.swift */; }; + FDF848CB29405C5B007DCAE5 /* SnodePoolResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDF848A929405C5A007DCAE5 /* SnodePoolResponse.swift */; }; + FDF848CC29405C5B007DCAE5 /* SnodeReceivedMessage.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDF848AA29405C5A007DCAE5 /* SnodeReceivedMessage.swift */; }; + FDF848CD29405C5B007DCAE5 /* GetNetworkTimestampResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDF848AB29405C5A007DCAE5 /* GetNetworkTimestampResponse.swift */; }; + FDF848CE29405C5B007DCAE5 /* UpdateExpiryAllRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDF848AC29405C5A007DCAE5 /* UpdateExpiryAllRequest.swift */; }; + FDF848CF29405C5B007DCAE5 /* SendMessageRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDF848AD29405C5A007DCAE5 /* SendMessageRequest.swift */; }; + FDF848D029405C5B007DCAE5 /* UpdateExpiryResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDF848AE29405C5A007DCAE5 /* UpdateExpiryResponse.swift */; }; + FDF848D129405C5B007DCAE5 /* SnodeSwarmItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDF848AF29405C5A007DCAE5 /* SnodeSwarmItem.swift */; }; + FDF848D229405C5B007DCAE5 /* LegacyGetMessagesRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDF848B029405C5A007DCAE5 /* LegacyGetMessagesRequest.swift */; }; + FDF848D329405C5B007DCAE5 /* UpdateExpiryAllResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDF848B129405C5A007DCAE5 /* UpdateExpiryAllResponse.swift */; }; + FDF848D429405C5B007DCAE5 /* DeleteAllBeforeResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDF848B229405C5A007DCAE5 /* DeleteAllBeforeResponse.swift */; }; + FDF848D529405C5B007DCAE5 /* DeleteAllMessagesResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDF848B329405C5A007DCAE5 /* DeleteAllMessagesResponse.swift */; }; + FDF848D629405C5B007DCAE5 /* SnodeMessage.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDF848B429405C5A007DCAE5 /* SnodeMessage.swift */; }; + FDF848D729405C5B007DCAE5 /* SnodeBatchRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDF848B529405C5A007DCAE5 /* SnodeBatchRequest.swift */; }; + FDF848D829405C5B007DCAE5 /* SwarmSnode.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDF848B629405C5A007DCAE5 /* SwarmSnode.swift */; }; + FDF848D929405C5B007DCAE5 /* SnodeAuthenticatedRequestBody.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDF848B729405C5A007DCAE5 /* SnodeAuthenticatedRequestBody.swift */; }; + FDF848DA29405C5B007DCAE5 /* GetMessagesResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDF848B829405C5A007DCAE5 /* GetMessagesResponse.swift */; }; + FDF848DB29405C5B007DCAE5 /* DeleteMessagesResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDF848B929405C5A007DCAE5 /* DeleteMessagesResponse.swift */; }; + FDF848DC29405C5B007DCAE5 /* RevokeSubkeyRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDF848BA29405C5A007DCAE5 /* RevokeSubkeyRequest.swift */; }; + FDF848DD29405C5B007DCAE5 /* LegacySendMessageRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDF848BB29405C5A007DCAE5 /* LegacySendMessageRequest.swift */; }; + FDF848E329405D6E007DCAE5 /* OnionRequestAPIVersion.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDF848DE29405D6E007DCAE5 /* OnionRequestAPIVersion.swift */; }; + FDF848E429405D6E007DCAE5 /* SnodeAPIEndpoint.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDF848DF29405D6E007DCAE5 /* SnodeAPIEndpoint.swift */; }; + FDF848E529405D6E007DCAE5 /* SnodeAPIError.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDF848E029405D6E007DCAE5 /* SnodeAPIError.swift */; }; + FDF848E629405D6E007DCAE5 /* OnionRequestAPIDestination.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDF848E129405D6E007DCAE5 /* OnionRequestAPIDestination.swift */; }; + FDF848E729405D6E007DCAE5 /* OnionRequestAPIError.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDF848E229405D6E007DCAE5 /* OnionRequestAPIError.swift */; }; + FDF848EB29405E4F007DCAE5 /* OnionRequestAPI.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDF848E829405E4E007DCAE5 /* OnionRequestAPI.swift */; }; + FDF848EC29405E4F007DCAE5 /* OnionRequestAPI+Encryption.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDF848E929405E4E007DCAE5 /* OnionRequestAPI+Encryption.swift */; }; + FDF848ED29405E4F007DCAE5 /* Notification+OnionRequestAPI.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDF848EA29405E4E007DCAE5 /* Notification+OnionRequestAPI.swift */; }; + FDF848EF294067E4007DCAE5 /* URLResponse+Utilities.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDF848EE294067E4007DCAE5 /* URLResponse+Utilities.swift */; }; + FDF848F129406A30007DCAE5 /* Format.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDF848F029406A30007DCAE5 /* Format.swift */; }; + FDF848F329413DB0007DCAE5 /* ImagePickerHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDF848F229413DB0007DCAE5 /* ImagePickerHandler.swift */; }; + FDF848F529413EEC007DCAE5 /* SessionCell+Styling.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDF848F429413EEC007DCAE5 /* SessionCell+Styling.swift */; }; + FDF848F729414477007DCAE5 /* CurrentUserPoller.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDF848F629414477007DCAE5 /* CurrentUserPoller.swift */; }; FDFD645927F26C6800808CA1 /* Array+Utilities.swift in Sources */ = {isa = PBXBuildFile; fileRef = C3C2A5D12553860800C340D1 /* Array+Utilities.swift */; }; - FDFD645B27F26D4600808CA1 /* Data+Utilities.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDFD645A27F26D4600808CA1 /* Data+Utilities.swift */; }; FDFD645D27F273F300808CA1 /* MockGeneralCache.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDFD645C27F273F300808CA1 /* MockGeneralCache.swift */; }; FDFDE124282D04F20098B17F /* MediaDismissAnimationController.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDFDE123282D04F20098B17F /* MediaDismissAnimationController.swift */; }; FDFDE126282D05380098B17F /* MediaInteractiveDismiss.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDFDE125282D05380098B17F /* MediaInteractiveDismiss.swift */; }; FDFDE128282D05530098B17F /* MediaPresentationContext.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDFDE127282D05530098B17F /* MediaPresentationContext.swift */; }; FDFDE12A282D056B0098B17F /* MediaZoomAnimationController.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDFDE129282D056B0098B17F /* MediaZoomAnimationController.swift */; }; + FE43694493EC2E1E438EBEB3 /* Pods_GlobalDependencies_FrameworkAndExtensionDependencies_ExtendedDependencies_SessionMessagingKit_SessionMessagingKitTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 13D1714FDC4DAB121DA2C73A /* Pods_GlobalDependencies_FrameworkAndExtensionDependencies_ExtendedDependencies_SessionMessagingKit_SessionMessagingKitTests.framework */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -1099,16 +1087,14 @@ /* End PBXCopyFilesBuildPhase section */ /* Begin PBXFileReference section */ - 06160ECE3FE5A06A916FF8C5 /* Pods-GlobalDependencies-Session-SessionTests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-GlobalDependencies-Session-SessionTests.debug.xcconfig"; path = "Target Support Files/Pods-GlobalDependencies-Session-SessionTests/Pods-GlobalDependencies-Session-SessionTests.debug.xcconfig"; sourceTree = ""; }; - 0BF4561630A52BE96F164CF6 /* Pods_GlobalDependencies_FrameworkAndExtensionDependencies_ExtendedDependencies_SignalUtilitiesKit.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_GlobalDependencies_FrameworkAndExtensionDependencies_ExtendedDependencies_SignalUtilitiesKit.framework; sourceTree = BUILT_PRODUCTS_DIR; }; - 0E836037CC97CE5A47735596 /* Pods-GlobalDependencies-FrameworkAndExtensionDependencies-SessionSnodeKit.app store release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-GlobalDependencies-FrameworkAndExtensionDependencies-SessionSnodeKit.app store release.xcconfig"; path = "Target Support Files/Pods-GlobalDependencies-FrameworkAndExtensionDependencies-SessionSnodeKit/Pods-GlobalDependencies-FrameworkAndExtensionDependencies-SessionSnodeKit.app store release.xcconfig"; sourceTree = ""; }; - 0E8564674E3201E218939AFB /* Pods-GlobalDependencies-Session-SessionTests.app store release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-GlobalDependencies-Session-SessionTests.app store release.xcconfig"; path = "Target Support Files/Pods-GlobalDependencies-Session-SessionTests/Pods-GlobalDependencies-Session-SessionTests.app store release.xcconfig"; sourceTree = ""; }; - 18EAE958B8C12503F2C294DF /* Pods-GlobalDependencies-SessionUIKit.app store release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-GlobalDependencies-SessionUIKit.app store release.xcconfig"; path = "Target Support Files/Pods-GlobalDependencies-SessionUIKit/Pods-GlobalDependencies-SessionUIKit.app store release.xcconfig"; sourceTree = ""; }; - 1A0882BF820F5B44969F91F1 /* Pods-GlobalDependencies-FrameworkAndExtensionDependencies-SessionNotificationServiceExtension.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-GlobalDependencies-FrameworkAndExtensionDependencies-SessionNotificationServiceExtension.debug.xcconfig"; path = "Target Support Files/Pods-GlobalDependencies-FrameworkAndExtensionDependencies-SessionNotificationServiceExtension/Pods-GlobalDependencies-FrameworkAndExtensionDependencies-SessionNotificationServiceExtension.debug.xcconfig"; sourceTree = ""; }; - 245BF74EF6348E2D4125033F /* Pods-GlobalDependencies-FrameworkAndExtensionDependencies-SessionNotificationServiceExtension.app store release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-GlobalDependencies-FrameworkAndExtensionDependencies-SessionNotificationServiceExtension.app store release.xcconfig"; path = "Target Support Files/Pods-GlobalDependencies-FrameworkAndExtensionDependencies-SessionNotificationServiceExtension/Pods-GlobalDependencies-FrameworkAndExtensionDependencies-SessionNotificationServiceExtension.app store release.xcconfig"; sourceTree = ""; }; - 2581AFACDDDC1404866D7B8C /* Pods-GlobalDependencies-FrameworkAndExtensionDependencies-ExtendedDependencies-SessionUtilitiesKit.app store release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-GlobalDependencies-FrameworkAndExtensionDependencies-ExtendedDependencies-SessionUtilitiesKit.app store release.xcconfig"; path = "Target Support Files/Pods-GlobalDependencies-FrameworkAndExtensionDependencies-ExtendedDependencies-SessionUtilitiesKit/Pods-GlobalDependencies-FrameworkAndExtensionDependencies-ExtendedDependencies-SessionUtilitiesKit.app store release.xcconfig"; sourceTree = ""; }; - 2691123A7F231EDD8226C4B5 /* Pods_GlobalDependencies_FrameworkAndExtensionDependencies_ExtendedDependencies_SessionMessagingKit_SessionMessagingKitTests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_GlobalDependencies_FrameworkAndExtensionDependencies_ExtendedDependencies_SessionMessagingKit_SessionMessagingKitTests.framework; sourceTree = BUILT_PRODUCTS_DIR; }; - 29CF8C79F41BF00B1C2E59A0 /* Pods-SessionUIKit.app store release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-SessionUIKit.app store release.xcconfig"; path = "Target Support Files/Pods-SessionUIKit/Pods-SessionUIKit.app store release.xcconfig"; sourceTree = ""; }; + 05C76EFA593DD507061C50B2 /* Pods-GlobalDependencies-SessionUIKit.app store release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-GlobalDependencies-SessionUIKit.app store release.xcconfig"; path = "Target Support Files/Pods-GlobalDependencies-SessionUIKit/Pods-GlobalDependencies-SessionUIKit.app store release.xcconfig"; sourceTree = ""; }; + 05E68C7F291EC08B8A43A534 /* Pods_GlobalDependencies_FrameworkAndExtensionDependencies_ExtendedDependencies_SessionMessagingKit.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_GlobalDependencies_FrameworkAndExtensionDependencies_ExtendedDependencies_SessionMessagingKit.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + 0772459E7D5F6747EDC889F3 /* Pods-GlobalDependencies-Session-SessionTests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-GlobalDependencies-Session-SessionTests.debug.xcconfig"; path = "Target Support Files/Pods-GlobalDependencies-Session-SessionTests/Pods-GlobalDependencies-Session-SessionTests.debug.xcconfig"; sourceTree = ""; }; + 07B7038C849F53378CD36B83 /* Pods_GlobalDependencies_FrameworkAndExtensionDependencies_ExtendedDependencies_SessionUtilitiesKit_SessionUtilitiesKitTests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_GlobalDependencies_FrameworkAndExtensionDependencies_ExtendedDependencies_SessionUtilitiesKit_SessionUtilitiesKitTests.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + 13D1714FDC4DAB121DA2C73A /* Pods_GlobalDependencies_FrameworkAndExtensionDependencies_ExtendedDependencies_SessionMessagingKit_SessionMessagingKitTests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_GlobalDependencies_FrameworkAndExtensionDependencies_ExtendedDependencies_SessionMessagingKit_SessionMessagingKitTests.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + 285705D20F792E174C8A9BBA /* Pods-GlobalDependencies-FrameworkAndExtensionDependencies-SessionNotificationServiceExtension.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-GlobalDependencies-FrameworkAndExtensionDependencies-SessionNotificationServiceExtension.debug.xcconfig"; path = "Target Support Files/Pods-GlobalDependencies-FrameworkAndExtensionDependencies-SessionNotificationServiceExtension/Pods-GlobalDependencies-FrameworkAndExtensionDependencies-SessionNotificationServiceExtension.debug.xcconfig"; sourceTree = ""; }; + 32C10A2A536B58FC42C46C3C /* Pods_GlobalDependencies_Session_SessionTests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_GlobalDependencies_Session_SessionTests.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + 34040971CC7AF9C8A6C1E838 /* Pods-GlobalDependencies-Session.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-GlobalDependencies-Session.debug.xcconfig"; path = "Target Support Files/Pods-GlobalDependencies-Session/Pods-GlobalDependencies-Session.debug.xcconfig"; sourceTree = ""; }; 3427C64120F500DE00EEC730 /* OWSMessageTimerView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = OWSMessageTimerView.h; sourceTree = ""; }; 3427C64220F500DF00EEC730 /* OWSMessageTimerView.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = OWSMessageTimerView.m; sourceTree = ""; }; 3430FE171F7751D4000EC51B /* GiphyAPI.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GiphyAPI.swift; sourceTree = ""; }; @@ -1186,7 +1172,6 @@ 45CB2FA71CB7146C00E1B343 /* Launch Screen.storyboard */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.storyboard; name = "Launch Screen.storyboard"; path = "Session/Meta/Launch Screen.storyboard"; sourceTree = SOURCE_ROOT; }; 45CD81EE1DC030E7004C9430 /* SyncPushTokensJob.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SyncPushTokensJob.swift; sourceTree = ""; }; 45F32C1D205718B000A300D5 /* MediaPageViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = MediaPageViewController.swift; path = "Session/Media Viewing & Editing/MediaPageViewController.swift"; sourceTree = SOURCE_ROOT; }; - 48AD214D67ABED845101E795 /* Pods_GlobalDependencies_Session.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_GlobalDependencies_Session.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 4C090A1A210FD9C7001FD7F9 /* HapticFeedback.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HapticFeedback.swift; sourceTree = ""; }; 4C1885D1218F8E1C00B67051 /* PhotoGridViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PhotoGridViewCell.swift; sourceTree = ""; }; 4C1D2337218B6BA000A0598F /* it */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = it; path = it.lproj/Localizable.strings; sourceTree = ""; }; @@ -1200,20 +1185,16 @@ 4CA46F4B219CCC630038ABDE /* CaptionView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CaptionView.swift; sourceTree = ""; }; 4CA485BA2232339F004B9E7D /* PhotoCaptureViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PhotoCaptureViewController.swift; sourceTree = ""; }; 4CC613352227A00400E21A3A /* ConversationSearch.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConversationSearch.swift; sourceTree = ""; }; - 506FA2159653FF9F446D97D1 /* Pods-GlobalDependencies-FrameworkAndExtensionDependencies-ExtendedDependencies-SessionMessagingKit.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-GlobalDependencies-FrameworkAndExtensionDependencies-ExtendedDependencies-SessionMessagingKit.debug.xcconfig"; path = "Target Support Files/Pods-GlobalDependencies-FrameworkAndExtensionDependencies-ExtendedDependencies-SessionMessagingKit/Pods-GlobalDependencies-FrameworkAndExtensionDependencies-ExtendedDependencies-SessionMessagingKit.debug.xcconfig"; sourceTree = ""; }; - 510955DC99A0FD84F2D1C159 /* Pods-GlobalDependencies-SessionUIKit.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-GlobalDependencies-SessionUIKit.debug.xcconfig"; path = "Target Support Files/Pods-GlobalDependencies-SessionUIKit/Pods-GlobalDependencies-SessionUIKit.debug.xcconfig"; sourceTree = ""; }; - 5442DF945D862CEDF7F8AC49 /* Pods_GlobalDependencies_FrameworkAndExtensionDependencies_ExtendedDependencies_SessionUtilitiesKit_SessionUtilitiesKitTests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_GlobalDependencies_FrameworkAndExtensionDependencies_ExtendedDependencies_SessionUtilitiesKit_SessionUtilitiesKitTests.framework; sourceTree = BUILT_PRODUCTS_DIR; }; - 5626DC0D5F62C1C2C64E4AFC /* Pods-GlobalDependencies-FrameworkAndExtensionDependencies-ExtendedDependencies-SessionShareExtension.app store release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-GlobalDependencies-FrameworkAndExtensionDependencies-ExtendedDependencies-SessionShareExtension.app store release.xcconfig"; path = "Target Support Files/Pods-GlobalDependencies-FrameworkAndExtensionDependencies-ExtendedDependencies-SessionShareExtension/Pods-GlobalDependencies-FrameworkAndExtensionDependencies-ExtendedDependencies-SessionShareExtension.app store release.xcconfig"; sourceTree = ""; }; - 56F41C56FC7B2F381E440FB0 /* Pods-GlobalDependencies-Session.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-GlobalDependencies-Session.debug.xcconfig"; path = "Target Support Files/Pods-GlobalDependencies-Session/Pods-GlobalDependencies-Session.debug.xcconfig"; sourceTree = ""; }; - 58A6BA91F634756FA0BEC9E5 /* Pods-GlobalDependencies-FrameworkAndExtensionDependencies-ExtendedDependencies-SessionUtilitiesKit-SessionUtilitiesKitTests.app store release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-GlobalDependencies-FrameworkAndExtensionDependencies-ExtendedDependencies-SessionUtilitiesKit-SessionUtilitiesKitTests.app store release.xcconfig"; path = "Target Support Files/Pods-GlobalDependencies-FrameworkAndExtensionDependencies-ExtendedDependencies-SessionUtilitiesKit-SessionUtilitiesKitTests/Pods-GlobalDependencies-FrameworkAndExtensionDependencies-ExtendedDependencies-SessionUtilitiesKit-SessionUtilitiesKitTests.app store release.xcconfig"; sourceTree = ""; }; - 6737124ECBC2DFEE2DD716D3 /* Pods_GlobalDependencies_FrameworkAndExtensionDependencies_SessionSnodeKit.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_GlobalDependencies_FrameworkAndExtensionDependencies_SessionSnodeKit.framework; sourceTree = BUILT_PRODUCTS_DIR; }; - 6BE8FBF62464A7177034A0AB /* Pods-GlobalDependencies-Session.app store release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-GlobalDependencies-Session.app store release.xcconfig"; path = "Target Support Files/Pods-GlobalDependencies-Session/Pods-GlobalDependencies-Session.app store release.xcconfig"; sourceTree = ""; }; - 6F84A214B9A1C0CCF6DB09C8 /* Pods_GlobalDependencies_FrameworkAndExtensionDependencies_SessionNotificationServiceExtension.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_GlobalDependencies_FrameworkAndExtensionDependencies_SessionNotificationServiceExtension.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + 55C13C7B4B700846E49C0E25 /* Pods-GlobalDependencies-FrameworkAndExtensionDependencies-ExtendedDependencies-SignalUtilitiesKit.app store release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-GlobalDependencies-FrameworkAndExtensionDependencies-ExtendedDependencies-SignalUtilitiesKit.app store release.xcconfig"; path = "Target Support Files/Pods-GlobalDependencies-FrameworkAndExtensionDependencies-ExtendedDependencies-SignalUtilitiesKit/Pods-GlobalDependencies-FrameworkAndExtensionDependencies-ExtendedDependencies-SignalUtilitiesKit.app store release.xcconfig"; sourceTree = ""; }; + 5DA3BDDFFB9E937A49C35FCC /* Pods-GlobalDependencies-FrameworkAndExtensionDependencies-ExtendedDependencies-SessionUtilitiesKit-SessionUtilitiesKitTests.app store release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-GlobalDependencies-FrameworkAndExtensionDependencies-ExtendedDependencies-SessionUtilitiesKit-SessionUtilitiesKitTests.app store release.xcconfig"; path = "Target Support Files/Pods-GlobalDependencies-FrameworkAndExtensionDependencies-ExtendedDependencies-SessionUtilitiesKit-SessionUtilitiesKitTests/Pods-GlobalDependencies-FrameworkAndExtensionDependencies-ExtendedDependencies-SessionUtilitiesKit-SessionUtilitiesKitTests.app store release.xcconfig"; sourceTree = ""; }; + 621B42AC592F3456ACD82F8B /* Pods-GlobalDependencies-FrameworkAndExtensionDependencies-ExtendedDependencies-SessionMessagingKit-SessionMessagingKitTests.app store release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-GlobalDependencies-FrameworkAndExtensionDependencies-ExtendedDependencies-SessionMessagingKit-SessionMessagingKitTests.app store release.xcconfig"; path = "Target Support Files/Pods-GlobalDependencies-FrameworkAndExtensionDependencies-ExtendedDependencies-SessionMessagingKit-SessionMessagingKitTests/Pods-GlobalDependencies-FrameworkAndExtensionDependencies-ExtendedDependencies-SessionMessagingKit-SessionMessagingKitTests.app store release.xcconfig"; sourceTree = ""; }; + 62B512CEB14BD4A4A53CF532 /* Pods-GlobalDependencies-FrameworkAndExtensionDependencies-SessionNotificationServiceExtension.app store release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-GlobalDependencies-FrameworkAndExtensionDependencies-SessionNotificationServiceExtension.app store release.xcconfig"; path = "Target Support Files/Pods-GlobalDependencies-FrameworkAndExtensionDependencies-SessionNotificationServiceExtension/Pods-GlobalDependencies-FrameworkAndExtensionDependencies-SessionNotificationServiceExtension.app store release.xcconfig"; sourceTree = ""; }; + 678CFB04E76F7E388AFCFA86 /* Pods_GlobalDependencies_SessionUIKit.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_GlobalDependencies_SessionUIKit.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + 6A71AD9BEAFF0C9E8016BC23 /* Pods-GlobalDependencies-FrameworkAndExtensionDependencies-ExtendedDependencies-SessionShareExtension.app store release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-GlobalDependencies-FrameworkAndExtensionDependencies-ExtendedDependencies-SessionShareExtension.app store release.xcconfig"; path = "Target Support Files/Pods-GlobalDependencies-FrameworkAndExtensionDependencies-ExtendedDependencies-SessionShareExtension/Pods-GlobalDependencies-FrameworkAndExtensionDependencies-ExtendedDependencies-SessionShareExtension.app store release.xcconfig"; sourceTree = ""; }; + 6DA09080DD9779C860023A60 /* Pods-GlobalDependencies-FrameworkAndExtensionDependencies-ExtendedDependencies-SessionMessagingKit.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-GlobalDependencies-FrameworkAndExtensionDependencies-ExtendedDependencies-SessionMessagingKit.debug.xcconfig"; path = "Target Support Files/Pods-GlobalDependencies-FrameworkAndExtensionDependencies-ExtendedDependencies-SessionMessagingKit/Pods-GlobalDependencies-FrameworkAndExtensionDependencies-ExtendedDependencies-SessionMessagingKit.debug.xcconfig"; sourceTree = ""; }; 70377AAA1918450100CAF501 /* MobileCoreServices.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = MobileCoreServices.framework; path = System/Library/Frameworks/MobileCoreServices.framework; sourceTree = SDKROOT; }; 768A1A2A17FC9CD300E00ED8 /* libz.dylib */ = {isa = PBXFileReference; lastKnownFileType = "compiled.mach-o.dylib"; name = libz.dylib; path = usr/lib/libz.dylib; sourceTree = SDKROOT; }; 76C87F18181EFCE600C4ACAB /* MediaPlayer.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = MediaPlayer.framework; path = System/Library/Frameworks/MediaPlayer.framework; sourceTree = SDKROOT; }; - 782B65234A707D762FEAFD3B /* Pods_GlobalDependencies_FrameworkAndExtensionDependencies_ExtendedDependencies_SessionUtilitiesKit.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_GlobalDependencies_FrameworkAndExtensionDependencies_ExtendedDependencies_SessionUtilitiesKit.framework; sourceTree = BUILT_PRODUCTS_DIR; }; - 7A8A44E3F8AC9282AC5E6E5A /* Pods_GlobalDependencies_FrameworkAndExtensionDependencies_ExtendedDependencies_SessionMessagingKit.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_GlobalDependencies_FrameworkAndExtensionDependencies_ExtendedDependencies_SessionMessagingKit.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 7B0EFDED274F598600FFAAE7 /* TimestampUtils.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimestampUtils.swift; sourceTree = ""; }; 7B0EFDEF275084AA00FFAAE7 /* CallMessageCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CallMessageCell.swift; sourceTree = ""; }; 7B0EFDF3275490EA00FFAAE7 /* ringing.mp3 */ = {isa = PBXFileReference; lastKnownFileType = audio.mp3; path = ringing.mp3; sourceTree = ""; }; @@ -1281,16 +1262,20 @@ 7BFD1A892745C4F000FB91B9 /* Permissions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Permissions.swift; sourceTree = ""; }; 7BFD1A8B2747150E00FB91B9 /* TurnServerInfo.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TurnServerInfo.swift; sourceTree = ""; }; 7BFD1A962747689000FB91B9 /* Session-Turn-Server */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = "Session-Turn-Server"; sourceTree = ""; }; - 82099864FD91C9126A750313 /* Pods-GlobalDependencies-FrameworkAndExtensionDependencies-ExtendedDependencies-SignalUtilitiesKit.app store release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-GlobalDependencies-FrameworkAndExtensionDependencies-ExtendedDependencies-SignalUtilitiesKit.app store release.xcconfig"; path = "Target Support Files/Pods-GlobalDependencies-FrameworkAndExtensionDependencies-ExtendedDependencies-SignalUtilitiesKit/Pods-GlobalDependencies-FrameworkAndExtensionDependencies-ExtendedDependencies-SignalUtilitiesKit.app store release.xcconfig"; sourceTree = ""; }; - 8E029A324780A800DE6B70B3 /* Pods-GlobalDependencies-FrameworkAndExtensionDependencies-ExtendedDependencies-SessionMessagingKit-SessionMessagingKitTests.app store release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-GlobalDependencies-FrameworkAndExtensionDependencies-ExtendedDependencies-SessionMessagingKit-SessionMessagingKitTests.app store release.xcconfig"; path = "Target Support Files/Pods-GlobalDependencies-FrameworkAndExtensionDependencies-ExtendedDependencies-SessionMessagingKit-SessionMessagingKitTests/Pods-GlobalDependencies-FrameworkAndExtensionDependencies-ExtendedDependencies-SessionMessagingKit-SessionMessagingKitTests.app store release.xcconfig"; sourceTree = ""; }; - 96ED0C9B69379BE6FF4E9DA6 /* Pods-GlobalDependencies-FrameworkAndExtensionDependencies-ExtendedDependencies-SessionShareExtension.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-GlobalDependencies-FrameworkAndExtensionDependencies-ExtendedDependencies-SessionShareExtension.debug.xcconfig"; path = "Target Support Files/Pods-GlobalDependencies-FrameworkAndExtensionDependencies-ExtendedDependencies-SessionShareExtension/Pods-GlobalDependencies-FrameworkAndExtensionDependencies-ExtendedDependencies-SessionShareExtension.debug.xcconfig"; sourceTree = ""; }; + 8448EFF76CD3CA5B2283B8A0 /* Pods-GlobalDependencies-FrameworkAndExtensionDependencies-ExtendedDependencies-SessionUtilitiesKit-SessionUtilitiesKitTests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-GlobalDependencies-FrameworkAndExtensionDependencies-ExtendedDependencies-SessionUtilitiesKit-SessionUtilitiesKitTests.debug.xcconfig"; path = "Target Support Files/Pods-GlobalDependencies-FrameworkAndExtensionDependencies-ExtendedDependencies-SessionUtilitiesKit-SessionUtilitiesKitTests/Pods-GlobalDependencies-FrameworkAndExtensionDependencies-ExtendedDependencies-SessionUtilitiesKit-SessionUtilitiesKitTests.debug.xcconfig"; sourceTree = ""; }; + 847091A12D82E41B1EBB8FB3 /* Pods-GlobalDependencies-FrameworkAndExtensionDependencies-SessionSnodeKit.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-GlobalDependencies-FrameworkAndExtensionDependencies-SessionSnodeKit.debug.xcconfig"; path = "Target Support Files/Pods-GlobalDependencies-FrameworkAndExtensionDependencies-SessionSnodeKit/Pods-GlobalDependencies-FrameworkAndExtensionDependencies-SessionSnodeKit.debug.xcconfig"; sourceTree = ""; }; + 8603226ED1C6F61F1F2D3734 /* Pods-GlobalDependencies-Session.app store release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-GlobalDependencies-Session.app store release.xcconfig"; path = "Target Support Files/Pods-GlobalDependencies-Session/Pods-GlobalDependencies-Session.app store release.xcconfig"; sourceTree = ""; }; + 86A3D36084020C9118DBCEE3 /* Pods_GlobalDependencies_FrameworkAndExtensionDependencies_ExtendedDependencies_SessionUtilitiesKit.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_GlobalDependencies_FrameworkAndExtensionDependencies_ExtendedDependencies_SessionUtilitiesKit.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + 8727C47348B6EFA767EE583A /* Pods-GlobalDependencies-FrameworkAndExtensionDependencies-ExtendedDependencies-SessionMessagingKit-SessionMessagingKitTests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-GlobalDependencies-FrameworkAndExtensionDependencies-ExtendedDependencies-SessionMessagingKit-SessionMessagingKitTests.debug.xcconfig"; path = "Target Support Files/Pods-GlobalDependencies-FrameworkAndExtensionDependencies-ExtendedDependencies-SessionMessagingKit-SessionMessagingKitTests/Pods-GlobalDependencies-FrameworkAndExtensionDependencies-ExtendedDependencies-SessionMessagingKit-SessionMessagingKitTests.debug.xcconfig"; sourceTree = ""; }; + 8E946CB54A221018E23599DE /* Pods-GlobalDependencies-FrameworkAndExtensionDependencies-ExtendedDependencies-SessionUtilitiesKit.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-GlobalDependencies-FrameworkAndExtensionDependencies-ExtendedDependencies-SessionUtilitiesKit.debug.xcconfig"; path = "Target Support Files/Pods-GlobalDependencies-FrameworkAndExtensionDependencies-ExtendedDependencies-SessionUtilitiesKit/Pods-GlobalDependencies-FrameworkAndExtensionDependencies-ExtendedDependencies-SessionUtilitiesKit.debug.xcconfig"; sourceTree = ""; }; + 9E358E7590EB145A5047F885 /* Pods_GlobalDependencies_FrameworkAndExtensionDependencies_SessionSnodeKit.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_GlobalDependencies_FrameworkAndExtensionDependencies_SessionSnodeKit.framework; sourceTree = BUILT_PRODUCTS_DIR; }; A11CD70C17FA230600A2D1B1 /* QuartzCore.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = QuartzCore.framework; path = System/Library/Frameworks/QuartzCore.framework; sourceTree = SDKROOT; }; A163E8AA16F3F6A90094D68B /* Security.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Security.framework; path = System/Library/Frameworks/Security.framework; sourceTree = SDKROOT; }; A1C32D4D17A0652C000A904E /* AddressBook.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = AddressBook.framework; path = System/Library/Frameworks/AddressBook.framework; sourceTree = SDKROOT; }; A1C32D4F17A06537000A904E /* AddressBookUI.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = AddressBookUI.framework; path = System/Library/Frameworks/AddressBookUI.framework; sourceTree = SDKROOT; }; A1FDCBEE16DAA6C300868894 /* AVFoundation.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = AVFoundation.framework; path = System/Library/Frameworks/AVFoundation.framework; sourceTree = SDKROOT; }; - A9C58C3ADF46C718488458C2 /* Pods_GlobalDependencies_SessionUIKit.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_GlobalDependencies_SessionUIKit.framework; sourceTree = BUILT_PRODUCTS_DIR; }; - B4C92F6ADBECCD47A6B6008E /* Pods_GlobalDependencies_Session_SessionTests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_GlobalDependencies_Session_SessionTests.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + B1910A32EB2AD01913629646 /* Pods-GlobalDependencies-FrameworkAndExtensionDependencies-ExtendedDependencies-SessionShareExtension.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-GlobalDependencies-FrameworkAndExtensionDependencies-ExtendedDependencies-SessionShareExtension.debug.xcconfig"; path = "Target Support Files/Pods-GlobalDependencies-FrameworkAndExtensionDependencies-ExtendedDependencies-SessionShareExtension/Pods-GlobalDependencies-FrameworkAndExtensionDependencies-ExtendedDependencies-SessionShareExtension.debug.xcconfig"; sourceTree = ""; }; + B4F9FCBDA07F07CB48220D4C /* Pods-GlobalDependencies-FrameworkAndExtensionDependencies-ExtendedDependencies-SignalUtilitiesKit.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-GlobalDependencies-FrameworkAndExtensionDependencies-ExtendedDependencies-SignalUtilitiesKit.debug.xcconfig"; path = "Target Support Files/Pods-GlobalDependencies-FrameworkAndExtensionDependencies-ExtendedDependencies-SignalUtilitiesKit/Pods-GlobalDependencies-FrameworkAndExtensionDependencies-ExtendedDependencies-SignalUtilitiesKit.debug.xcconfig"; sourceTree = ""; }; B60EDE031A05A01700D73516 /* AudioToolbox.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = AudioToolbox.framework; path = System/Library/Frameworks/AudioToolbox.framework; sourceTree = SDKROOT; }; B646D10E1AA5461A004133BA /* fr */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = fr; path = fr.lproj/Localizable.strings; sourceTree = ""; }; B657DDC91911A40500F45B0C /* Signal.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = Signal.entitlements; sourceTree = ""; }; @@ -1396,7 +1381,6 @@ B8FF8E7325C10FC3004D1F22 /* GeoLite2-Country-Locations-English */ = {isa = PBXFileReference; lastKnownFileType = file.bplist; name = "GeoLite2-Country-Locations-English"; path = "Countries/GeoLite2-Country-Locations-English"; sourceTree = ""; }; B8FF8EA525C11FEF004D1F22 /* IPv4.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IPv4.swift; sourceTree = ""; }; B9EB5ABC1884C002007CBB57 /* MessageUI.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = MessageUI.framework; path = System/Library/Frameworks/MessageUI.framework; sourceTree = SDKROOT; }; - BE11AFA6FD8CAE894CABC28D /* Pods-GlobalDependencies-FrameworkAndExtensionDependencies-ExtendedDependencies-SessionUtilitiesKit-SessionUtilitiesKitTests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-GlobalDependencies-FrameworkAndExtensionDependencies-ExtendedDependencies-SessionUtilitiesKit-SessionUtilitiesKitTests.debug.xcconfig"; path = "Target Support Files/Pods-GlobalDependencies-FrameworkAndExtensionDependencies-ExtendedDependencies-SessionUtilitiesKit-SessionUtilitiesKitTests/Pods-GlobalDependencies-FrameworkAndExtensionDependencies-ExtendedDependencies-SessionUtilitiesKit-SessionUtilitiesKitTests.debug.xcconfig"; sourceTree = ""; }; C300A5B12554AF9800555489 /* VisibleMessage+Profile.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "VisibleMessage+Profile.swift"; sourceTree = ""; }; C300A5BC2554B00D00555489 /* ReadReceipt.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReadReceipt.swift; sourceTree = ""; }; C300A5D22554B05A00555489 /* TypingIndicator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TypingIndicator.swift; sourceTree = ""; }; @@ -1535,9 +1519,7 @@ C38EF2F7255B6DBC007E1867 /* OWSAudioPlayer.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = OWSAudioPlayer.m; path = SessionMessagingKit/Utilities/OWSAudioPlayer.m; sourceTree = SOURCE_ROOT; }; C38EF2FA255B6DBD007E1867 /* Bench.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = Bench.swift; path = SignalUtilitiesKit/Utilities/Bench.swift; sourceTree = SOURCE_ROOT; }; C38EF2FB255B6DBD007E1867 /* OWSWindowManager.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = OWSWindowManager.h; path = SessionMessagingKit/Utilities/OWSWindowManager.h; sourceTree = SOURCE_ROOT; }; - C38EF301255B6DBD007E1867 /* OWSFormat.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = OWSFormat.h; path = SignalUtilitiesKit/Utilities/OWSFormat.h; sourceTree = SOURCE_ROOT; }; C38EF304255B6DBE007E1867 /* ImageCache.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = ImageCache.swift; path = SignalUtilitiesKit/Utilities/ImageCache.swift; sourceTree = SOURCE_ROOT; }; - C38EF305255B6DBE007E1867 /* OWSFormat.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = OWSFormat.m; path = SignalUtilitiesKit/Utilities/OWSFormat.m; sourceTree = SOURCE_ROOT; }; C38EF306255B6DBE007E1867 /* OWSWindowManager.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = OWSWindowManager.m; path = SessionMessagingKit/Utilities/OWSWindowManager.m; sourceTree = SOURCE_ROOT; }; C38EF307255B6DBE007E1867 /* UIGestureRecognizer+OWS.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = "UIGestureRecognizer+OWS.swift"; path = "SignalUtilitiesKit/Utilities/UIGestureRecognizer+OWS.swift"; sourceTree = SOURCE_ROOT; }; C38EF309255B6DBE007E1867 /* DeviceSleepManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = DeviceSleepManager.swift; path = SessionMessagingKit/Utilities/DeviceSleepManager.swift; sourceTree = SOURCE_ROOT; }; @@ -1596,20 +1578,15 @@ C3A8AF752665B03900A467FE /* hi */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = hi; path = hi.lproj/Localizable.strings; sourceTree = ""; }; C3A8AF762665F97A00A467FE /* fi */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = fi; path = fi.lproj/Localizable.strings; sourceTree = ""; }; C3AAFFF125AE99710089E6DD /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; - C3ADC66026426688005F1414 /* ShareVC.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShareVC.swift; sourceTree = ""; }; + C3ADC66026426688005F1414 /* ShareNavController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShareNavController.swift; sourceTree = ""; }; C3AECBEA24EF5244005743DE /* fa */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = fa; path = fa.lproj/Localizable.strings; sourceTree = ""; }; C3BBE0C62554F1570050F1E3 /* FixedWidthInteger+BigEndian.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "FixedWidthInteger+BigEndian.swift"; sourceTree = ""; }; C3C2A59F255385C100C340D1 /* SessionSnodeKit.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = SessionSnodeKit.framework; sourceTree = BUILT_PRODUCTS_DIR; }; C3C2A5A1255385C100C340D1 /* SessionSnodeKit.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = SessionSnodeKit.h; sourceTree = ""; }; C3C2A5A2255385C100C340D1 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; - C3C2A5B6255385EC00C340D1 /* SnodeMessage.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SnodeMessage.swift; sourceTree = ""; }; C3C2A5B7255385EC00C340D1 /* Snode.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Snode.swift; sourceTree = ""; }; C3C2A5B9255385ED00C340D1 /* Configuration.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Configuration.swift; sourceTree = ""; }; - C3C2A5BA255385ED00C340D1 /* OnionRequestAPI.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OnionRequestAPI.swift; sourceTree = ""; }; - C3C2A5BB255385ED00C340D1 /* OnionRequestAPI+Encryption.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "OnionRequestAPI+Encryption.swift"; sourceTree = ""; }; C3C2A5BC255385EE00C340D1 /* HTTP.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = HTTP.swift; sourceTree = ""; }; - C3C2A5BD255385EE00C340D1 /* Notification+OnionRequestAPI.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "Notification+OnionRequestAPI.swift"; sourceTree = ""; }; - C3C2A5BE255385EE00C340D1 /* SnodeAPI.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SnodeAPI.swift; sourceTree = ""; }; C3C2A5CE2553860700C340D1 /* Logging.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Logging.swift; sourceTree = ""; }; C3C2A5D12553860800C340D1 /* Array+Utilities.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "Array+Utilities.swift"; sourceTree = ""; }; C3C2A5D22553860900C340D1 /* String+Trimming.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "String+Trimming.swift"; sourceTree = ""; }; @@ -1647,7 +1624,8 @@ C3ECBF7A257056B700EA7FCE /* Threading.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Threading.swift; sourceTree = ""; }; C3F0A52F255C80BC007BE2A3 /* NoopNotificationsManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NoopNotificationsManager.swift; sourceTree = ""; }; C3F0A5B2255C915C007BE2A3 /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/Localizable.strings; sourceTree = ""; }; - D0CE0424239A1574F683D2D7 /* Pods-GlobalDependencies-FrameworkAndExtensionDependencies-ExtendedDependencies-SessionMessagingKit-SessionMessagingKitTests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-GlobalDependencies-FrameworkAndExtensionDependencies-ExtendedDependencies-SessionMessagingKit-SessionMessagingKitTests.debug.xcconfig"; path = "Target Support Files/Pods-GlobalDependencies-FrameworkAndExtensionDependencies-ExtendedDependencies-SessionMessagingKit-SessionMessagingKitTests/Pods-GlobalDependencies-FrameworkAndExtensionDependencies-ExtendedDependencies-SessionMessagingKit-SessionMessagingKitTests.debug.xcconfig"; sourceTree = ""; }; + C5828D9A55CD76B75E8FA367 /* Pods_GlobalDependencies_FrameworkAndExtensionDependencies_SessionNotificationServiceExtension.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_GlobalDependencies_FrameworkAndExtensionDependencies_SessionNotificationServiceExtension.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + CC8D3B6504946442A0CF775C /* Pods_GlobalDependencies_Session.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_GlobalDependencies_Session.framework; sourceTree = BUILT_PRODUCTS_DIR; }; D2179CFB16BB0B3A0006F3AB /* CoreTelephony.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = CoreTelephony.framework; path = System/Library/Frameworks/CoreTelephony.framework; sourceTree = SDKROOT; }; D2179CFD16BB0B480006F3AB /* SystemConfiguration.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = SystemConfiguration.framework; path = System/Library/Frameworks/SystemConfiguration.framework; sourceTree = SDKROOT; }; D221A089169C9E5E00537ABF /* Session.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Session.app; sourceTree = BUILT_PRODUCTS_DIR; }; @@ -1658,12 +1636,14 @@ D221A0E7169DFFC500537ABF /* AVFoundation.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = AVFoundation.framework; path = ../../../../../../System/Library/Frameworks/AVFoundation.framework; sourceTree = ""; }; D24B5BD4169F568C00681372 /* AudioToolbox.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = AudioToolbox.framework; path = ../../../../../../System/Library/Frameworks/AudioToolbox.framework; sourceTree = ""; }; D2AEACDB16C426DA00C364C0 /* CFNetwork.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = CFNetwork.framework; path = System/Library/Frameworks/CFNetwork.framework; sourceTree = SDKROOT; }; - DAF57FAAF30631D0E99DA361 /* Pods-GlobalDependencies-FrameworkAndExtensionDependencies-ExtendedDependencies-SignalUtilitiesKit.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-GlobalDependencies-FrameworkAndExtensionDependencies-ExtendedDependencies-SignalUtilitiesKit.debug.xcconfig"; path = "Target Support Files/Pods-GlobalDependencies-FrameworkAndExtensionDependencies-ExtendedDependencies-SignalUtilitiesKit/Pods-GlobalDependencies-FrameworkAndExtensionDependencies-ExtendedDependencies-SignalUtilitiesKit.debug.xcconfig"; sourceTree = ""; }; - DBA125424EDD2417B515C63A /* Pods_GlobalDependencies_FrameworkAndExtensionDependencies_ExtendedDependencies_SessionShareExtension.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_GlobalDependencies_FrameworkAndExtensionDependencies_ExtendedDependencies_SessionShareExtension.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + D3BE3061D535DBC1DF0C94D4 /* Pods_GlobalDependencies_FrameworkAndExtensionDependencies_ExtendedDependencies_SignalUtilitiesKit.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_GlobalDependencies_FrameworkAndExtensionDependencies_ExtendedDependencies_SignalUtilitiesKit.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + E0B0F4C34363FE679EE3F203 /* Pods_GlobalDependencies_FrameworkAndExtensionDependencies_ExtendedDependencies_SessionShareExtension.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_GlobalDependencies_FrameworkAndExtensionDependencies_ExtendedDependencies_SessionShareExtension.framework; sourceTree = BUILT_PRODUCTS_DIR; }; E1A0AD8B16E13FDD0071E604 /* CoreFoundation.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = CoreFoundation.framework; path = System/Library/Frameworks/CoreFoundation.framework; sourceTree = SDKROOT; }; - E23C1E6B7E0C12BF4ACD9CBE /* Pods-GlobalDependencies-FrameworkAndExtensionDependencies-SessionSnodeKit.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-GlobalDependencies-FrameworkAndExtensionDependencies-SessionSnodeKit.debug.xcconfig"; path = "Target Support Files/Pods-GlobalDependencies-FrameworkAndExtensionDependencies-SessionSnodeKit/Pods-GlobalDependencies-FrameworkAndExtensionDependencies-SessionSnodeKit.debug.xcconfig"; sourceTree = ""; }; - EC5C23F9D234F558BE5E41DE /* Pods-SessionUIKit.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-SessionUIKit.debug.xcconfig"; path = "Target Support Files/Pods-SessionUIKit/Pods-SessionUIKit.debug.xcconfig"; sourceTree = ""; }; - F705826F79C4A591AB35D68F /* Pods-GlobalDependencies-FrameworkAndExtensionDependencies-ExtendedDependencies-SessionUtilitiesKit.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-GlobalDependencies-FrameworkAndExtensionDependencies-ExtendedDependencies-SessionUtilitiesKit.debug.xcconfig"; path = "Target Support Files/Pods-GlobalDependencies-FrameworkAndExtensionDependencies-ExtendedDependencies-SessionUtilitiesKit/Pods-GlobalDependencies-FrameworkAndExtensionDependencies-ExtendedDependencies-SessionUtilitiesKit.debug.xcconfig"; sourceTree = ""; }; + EB5B8ACA4C6F512FA3E21859 /* Pods-GlobalDependencies-SessionUIKit.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-GlobalDependencies-SessionUIKit.debug.xcconfig"; path = "Target Support Files/Pods-GlobalDependencies-SessionUIKit/Pods-GlobalDependencies-SessionUIKit.debug.xcconfig"; sourceTree = ""; }; + EED1CF82CAB23FE3345564F9 /* Pods-GlobalDependencies-FrameworkAndExtensionDependencies-SessionSnodeKit.app store release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-GlobalDependencies-FrameworkAndExtensionDependencies-SessionSnodeKit.app store release.xcconfig"; path = "Target Support Files/Pods-GlobalDependencies-FrameworkAndExtensionDependencies-SessionSnodeKit/Pods-GlobalDependencies-FrameworkAndExtensionDependencies-SessionSnodeKit.app store release.xcconfig"; sourceTree = ""; }; + F154A10CE1ADA33C16B45357 /* Pods-GlobalDependencies-Session-SessionTests.app store release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-GlobalDependencies-Session-SessionTests.app store release.xcconfig"; path = "Target Support Files/Pods-GlobalDependencies-Session-SessionTests/Pods-GlobalDependencies-Session-SessionTests.app store release.xcconfig"; sourceTree = ""; }; + F390F8E34CA76B3F7D3B1826 /* Pods-GlobalDependencies-FrameworkAndExtensionDependencies-ExtendedDependencies-SessionMessagingKit.app store release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-GlobalDependencies-FrameworkAndExtensionDependencies-ExtendedDependencies-SessionMessagingKit.app store release.xcconfig"; path = "Target Support Files/Pods-GlobalDependencies-FrameworkAndExtensionDependencies-ExtendedDependencies-SessionMessagingKit/Pods-GlobalDependencies-FrameworkAndExtensionDependencies-ExtendedDependencies-SessionMessagingKit.app store release.xcconfig"; sourceTree = ""; }; + F60C5B6CD14329816B0E8CC0 /* Pods-GlobalDependencies-FrameworkAndExtensionDependencies-ExtendedDependencies-SessionUtilitiesKit.app store release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-GlobalDependencies-FrameworkAndExtensionDependencies-ExtendedDependencies-SessionUtilitiesKit.app store release.xcconfig"; path = "Target Support Files/Pods-GlobalDependencies-FrameworkAndExtensionDependencies-ExtendedDependencies-SessionUtilitiesKit/Pods-GlobalDependencies-FrameworkAndExtensionDependencies-ExtendedDependencies-SessionUtilitiesKit.app store release.xcconfig"; sourceTree = ""; }; FC3BD9871A30A790005B96BB /* Social.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Social.framework; path = System/Library/Frameworks/Social.framework; sourceTree = SDKROOT; }; FCB11D8B1A129A76002F93FB /* CoreMedia.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = CoreMedia.framework; path = System/Library/Frameworks/CoreMedia.framework; sourceTree = SDKROOT; }; FD078E4727E02561000769AF /* CommonMockedExtensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CommonMockedExtensions.swift; sourceTree = ""; }; @@ -1672,7 +1652,6 @@ FD078E5127E1760A000769AF /* OGMDependencyExtensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OGMDependencyExtensions.swift; sourceTree = ""; }; FD078E5927E29F09000769AF /* MockNonce16Generator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockNonce16Generator.swift; sourceTree = ""; }; FD078E5B27E29F78000769AF /* MockNonce24Generator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockNonce24Generator.swift; sourceTree = ""; }; - FD09796827F6BEA700936362 /* SwarmSnode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SwarmSnode.swift; sourceTree = ""; }; FD09796A27F6C67500936362 /* Failable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Failable.swift; sourceTree = ""; }; FD09796D27FA6D0000936362 /* Contact.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Contact.swift; sourceTree = ""; }; FD09796F27FA6FF300936362 /* Profile.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Profile.swift; sourceTree = ""; }; @@ -1720,10 +1699,6 @@ FD17D7C627F5207C00122BE0 /* DatabaseMigrator+Utilities.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "DatabaseMigrator+Utilities.swift"; sourceTree = ""; }; FD17D7C927F546D900122BE0 /* _001_InitialSetupMigration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = _001_InitialSetupMigration.swift; sourceTree = ""; }; FD17D7CC27F546FF00122BE0 /* Setting.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Setting.swift; sourceTree = ""; }; - FD17D7D127F5797A00122BE0 /* SnodeAPIEndpoint.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SnodeAPIEndpoint.swift; sourceTree = ""; }; - FD17D7D327F6584600122BE0 /* OnionRequestAPIError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OnionRequestAPIError.swift; sourceTree = ""; }; - FD17D7D727F658E200122BE0 /* OnionRequestAPIDestination.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OnionRequestAPIDestination.swift; sourceTree = ""; }; - FD17D7E027F67BD400122BE0 /* SnodeReceivedMessage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SnodeReceivedMessage.swift; sourceTree = ""; }; FD17D7E427F6A09900122BE0 /* Identity.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Identity.swift; sourceTree = ""; }; FD17D7E627F6A16700122BE0 /* _003_YDBToGRDBMigration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = _003_YDBToGRDBMigration.swift; sourceTree = ""; }; FD17D7E927F6A1C600122BE0 /* SUKLegacy.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SUKLegacy.swift; sourceTree = ""; }; @@ -1762,20 +1737,11 @@ FD37EA1428AB42CB003AE748 /* IdentitySpec.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IdentitySpec.swift; sourceTree = ""; }; FD37EA1628AC5605003AE748 /* NotificationContentViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationContentViewModel.swift; sourceTree = ""; }; FD37EA1828AC5CCA003AE748 /* NotificationSoundViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationSoundViewModel.swift; sourceTree = ""; }; - FD37F5572908F5C3005A5E92 /* RemoveUsersModal.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RemoveUsersModal.swift; sourceTree = ""; }; - FD37F559290F9E9A005A5E92 /* GroupMembersViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GroupMembersViewModel.swift; sourceTree = ""; }; - FD37F55F291867A8005A5E92 /* ImagePickerHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImagePickerHandler.swift; sourceTree = ""; }; - FD37F5612918C471005A5E92 /* GroupMemberLeftMessage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GroupMemberLeftMessage.swift; sourceTree = ""; }; - FD37F5632918C58D005A5E92 /* GroupInviteMessage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GroupInviteMessage.swift; sourceTree = ""; }; - FD37F5652918C697005A5E92 /* GroupPromoteMessage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GroupPromoteMessage.swift; sourceTree = ""; }; - FD37F5672918D458005A5E92 /* MessageReceiver+ClosedGroups.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "MessageReceiver+ClosedGroups.swift"; sourceTree = ""; }; FD39352B28F382920084DADA /* VersionFooterView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VersionFooterView.swift; sourceTree = ""; }; FD39353528F7C3390084DADA /* _004_FlagMessageHashAsDeletedOrInvalid.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = _004_FlagMessageHashAsDeletedOrInvalid.swift; sourceTree = ""; }; FD3AABE828306BBD00E5099A /* ThreadPickerViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ThreadPickerViewModel.swift; sourceTree = ""; }; FD3C905B27E3FBEF00CD579F /* BatchRequestInfoSpec.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BatchRequestInfoSpec.swift; sourceTree = ""; }; FD3C905F27E410F700CD579F /* FileUploadResponseSpec.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FileUploadResponseSpec.swift; sourceTree = ""; }; - FD3C906127E411AF00CD579F /* HeaderSpec.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HeaderSpec.swift; sourceTree = ""; }; - FD3C906327E4122F00CD579F /* RequestSpec.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RequestSpec.swift; sourceTree = ""; }; FD3C906627E416AF00CD579F /* BlindedIdLookupSpec.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BlindedIdLookupSpec.swift; sourceTree = ""; }; FD3C906927E417CE00CD579F /* SodiumUtilitiesSpec.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SodiumUtilitiesSpec.swift; sourceTree = ""; }; FD3C906C27E43C4B00CD579F /* MessageSenderEncryptionSpec.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageSenderEncryptionSpec.swift; sourceTree = ""; }; @@ -1805,7 +1771,6 @@ FD6A7A6C2818C61500035AC1 /* _002_SetupStandardJobs.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = _002_SetupStandardJobs.swift; sourceTree = ""; }; FD705A91278D051200F16121 /* ReusableView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReusableView.swift; sourceTree = ""; }; FD7115EA28C5D78E00B47552 /* ThreadSettingsViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ThreadSettingsViewModel.swift; sourceTree = ""; }; - FD7115ED28C5D79B00B47552 /* SessionAvatarCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SessionAvatarCell.swift; sourceTree = ""; }; FD7115EF28C5D7DE00B47552 /* SessionHeaderView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SessionHeaderView.swift; sourceTree = ""; }; FD7115F128C6CB3900B47552 /* _010_AddThreadIdToFTS.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = _010_AddThreadIdToFTS.swift; sourceTree = ""; }; FD7115F328C71EB200B47552 /* ThreadDisappearingMessagesViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ThreadDisappearingMessagesViewModel.swift; sourceTree = ""; }; @@ -1832,7 +1797,7 @@ FD71164528E2CC1300B47552 /* SessionHighlightingBackgroundLabel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SessionHighlightingBackgroundLabel.swift; sourceTree = ""; }; FD71164728E2CE8700B47552 /* SessionCell+AccessoryView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "SessionCell+AccessoryView.swift"; sourceTree = ""; }; FD71164928E3EA5B00B47552 /* DismissType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DismissType.swift; sourceTree = ""; }; - FD71164B28E3F5AA00B47552 /* SessionCell+ExtraAction.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "SessionCell+ExtraAction.swift"; sourceTree = ""; }; + FD71164B28E3F5AA00B47552 /* SessionCell+Accessibility.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "SessionCell+Accessibility.swift"; sourceTree = ""; }; FD71164D28E3F8CC00B47552 /* SessionCell+Info.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "SessionCell+Info.swift"; sourceTree = ""; }; FD71164F28E3F9FA00B47552 /* SessionTableViewModel+NavItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "SessionTableViewModel+NavItem.swift"; sourceTree = ""; }; FD71165128E410BE00B47552 /* SessionTableSection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SessionTableSection.swift; sourceTree = ""; }; @@ -1847,9 +1812,7 @@ FD7728952849E7E90018502F /* String+Utilities.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "String+Utilities.swift"; sourceTree = ""; }; FD7728972849E8110018502F /* UITableView+ReusableView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "UITableView+ReusableView.swift"; sourceTree = ""; }; FD772899284AF1BD0018502F /* Sodium+Utilities.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "Sodium+Utilities.swift"; sourceTree = ""; }; - FD77289B284DDCE10018502F /* SnodePoolResponse.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SnodePoolResponse.swift; sourceTree = ""; }; FD77289D284EF1C50018502F /* Sodium+Utilities.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Sodium+Utilities.swift"; sourceTree = ""; }; - FD77289F284EF5810018502F /* SnodeAPIError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SnodeAPIError.swift; sourceTree = ""; }; FD83B9AF27CF200A005E1583 /* SessionUtilitiesKitTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = SessionUtilitiesKitTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; FD83B9BA27CF20AF005E1583 /* SessionIdSpec.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SessionIdSpec.swift; sourceTree = ""; }; FD83B9BD27CF2243005E1583 /* TestConstants.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TestConstants.swift; sourceTree = ""; }; @@ -1857,7 +1820,6 @@ FD83B9C627CF3F10005E1583 /* CapabilitiesSpec.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CapabilitiesSpec.swift; sourceTree = ""; }; FD83B9C827D0487A005E1583 /* SendDirectMessageResponse.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SendDirectMessageResponse.swift; sourceTree = ""; }; FD83B9CB27D179BC005E1583 /* FSEndpoint.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FSEndpoint.swift; sourceTree = ""; }; - FD83B9CD27D17A04005E1583 /* Request.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Request.swift; sourceTree = ""; }; FD83B9D127D59495005E1583 /* MockUserDefaults.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockUserDefaults.swift; sourceTree = ""; }; FD848B86283B844B000E298B /* MessageViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageViewModel.swift; sourceTree = ""; }; FD848B8A283DC509000E298B /* PagedDatabaseObserver.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PagedDatabaseObserver.swift; sourceTree = ""; }; @@ -1876,49 +1838,19 @@ FD859EF927C2F5C500510D0C /* MockGenericHash.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockGenericHash.swift; sourceTree = ""; }; FD859EFB27C2F60700510D0C /* MockEd25519.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockEd25519.swift; sourceTree = ""; }; FD87DCF928B74DB300AF0F98 /* ConversationSettingsViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConversationSettingsViewModel.swift; sourceTree = ""; }; - FD87DCFB28B755B800AF0F98 /* BlockedContactsViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BlockedContactsViewController.swift; sourceTree = ""; }; FD87DCFD28B7582C00AF0F98 /* BlockedContactsViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BlockedContactsViewModel.swift; sourceTree = ""; }; FD87DCFF28B820F200AF0F98 /* BlockedContactCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BlockedContactCell.swift; sourceTree = ""; }; FD87DD0328B8727D00AF0F98 /* Configuration.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Configuration.swift; sourceTree = ""; }; - FD8ECF3F292AF07900C0D1BB /* Poller.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Poller.swift; sourceTree = ""; }; - FD8ECF41292B340D00C0D1BB /* SOGSBatchRequest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SOGSBatchRequest.swift; sourceTree = ""; }; - FD8ECF43292B397E00C0D1BB /* SendMessageRequest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SendMessageRequest.swift; sourceTree = ""; }; - FD8ECF45292B4BD500C0D1BB /* SendMessageResponse.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SendMessageResponse.swift; sourceTree = ""; }; - FD8ECF47292C287500C0D1BB /* UpdateExpiryRequest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UpdateExpiryRequest.swift; sourceTree = ""; }; - FD8ECF49292C2A7300C0D1BB /* DeleteMessagesRequest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeleteMessagesRequest.swift; sourceTree = ""; }; - FD8ECF4B292C2AC200C0D1BB /* RevokeSubkeyRequest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RevokeSubkeyRequest.swift; sourceTree = ""; }; - FD8ECF4D292C2AF800C0D1BB /* DeleteAllMessagesRequest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeleteAllMessagesRequest.swift; sourceTree = ""; }; - FD8ECF4F292C2B2B00C0D1BB /* DeleteAllBeforeRequest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeleteAllBeforeRequest.swift; sourceTree = ""; }; - FD8ECF51292C2CAE00C0D1BB /* UpdateExpiryAllRequest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UpdateExpiryAllRequest.swift; sourceTree = ""; }; - FD8ECF53292C2DB000C0D1BB /* SnodeAuthenticatedRequestBody.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SnodeAuthenticatedRequestBody.swift; sourceTree = ""; }; - FD8ECF55292C327700C0D1BB /* LegacyGetMessagesRequest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LegacyGetMessagesRequest.swift; sourceTree = ""; }; - FD8ECF57292C350500C0D1BB /* LegacySendMessageRequest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LegacySendMessageRequest.swift; sourceTree = ""; }; - FD8ECF59292C431B00C0D1BB /* GetSwarmRequest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GetSwarmRequest.swift; sourceTree = ""; }; - FD8ECF5B292C469100C0D1BB /* UpdateExpiryResponse.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UpdateExpiryResponse.swift; sourceTree = ""; }; - FD8ECF5D292C478900C0D1BB /* SnodeRecursiveResponse.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SnodeRecursiveResponse.swift; sourceTree = ""; }; - FD8ECF5F292C4B2400C0D1BB /* SnodeSwarmItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SnodeSwarmItem.swift; sourceTree = ""; }; - FD8ECF61292C4C8200C0D1BB /* DeleteMessagesResponse.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeleteMessagesResponse.swift; sourceTree = ""; }; - FD8ECF63292C4D6600C0D1BB /* DeleteAllMessagesResponse.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeleteAllMessagesResponse.swift; sourceTree = ""; }; - FD8ECF65292C6F8200C0D1BB /* DeleteAllBeforeResponse.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeleteAllBeforeResponse.swift; sourceTree = ""; }; - FD8ECF67292C72B900C0D1BB /* UpdateExpiryAllResponse.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UpdateExpiryAllResponse.swift; sourceTree = ""; }; - FD8ECF69292C74A000C0D1BB /* RevokeSubkeyResponse.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RevokeSubkeyResponse.swift; sourceTree = ""; }; - FD8ECF6B292C9B6400C0D1BB /* GetNetworkTimestampResponse.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GetNetworkTimestampResponse.swift; sourceTree = ""; }; - FD8ECF6D292C9EA100C0D1BB /* GetServiceNodesRequest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GetServiceNodesRequest.swift; sourceTree = ""; }; - FD8ECF71292DCD1A00C0D1BB /* _013_AutoDownloadAttachments.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = _013_AutoDownloadAttachments.swift; sourceTree = ""; }; - FD8ECF73292DDB4A00C0D1BB /* Format.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Format.swift; sourceTree = ""; }; FD8ECF7829340F7100C0D1BB /* libsession-util.xcframework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcframework; path = "libsession-util.xcframework"; sourceTree = ""; }; FD8ECF7A29340FFD00C0D1BB /* SessionUtil.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SessionUtil.swift; sourceTree = ""; }; FD8ECF7C2934293A00C0D1BB /* _011_SharedUtilChanges.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = _011_SharedUtilChanges.swift; sourceTree = ""; }; FD8ECF7E2934298100C0D1BB /* SharedConfigDump.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SharedConfigDump.swift; sourceTree = ""; }; FD8ECF812934387A00C0D1BB /* ConfigUserProfileSpec.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConfigUserProfileSpec.swift; sourceTree = ""; }; - FD8ECF842934508B00C0D1BB /* BatchResponseSpec.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BatchResponseSpec.swift; sourceTree = ""; }; FD8ECF882935AB7200C0D1BB /* SessionUtilError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SessionUtilError.swift; sourceTree = ""; }; FD8ECF8A2935DB4B00C0D1BB /* SharedConfigMessage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SharedConfigMessage.swift; sourceTree = ""; }; FD8ECF8F29381FC200C0D1BB /* SessionUtil+UserProfile.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "SessionUtil+UserProfile.swift"; sourceTree = ""; }; FD8ECF912938552800C0D1BB /* Threading.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Threading.swift; sourceTree = ""; }; FD8ECF93293856AF00C0D1BB /* Randomness.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Randomness.swift; sourceTree = ""; }; - FD8ECFA0293D8FDD00C0D1BB /* URLResponse+Utilities.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "URLResponse+Utilities.swift"; sourceTree = ""; }; - FD90040E2818AB6D00ABAAF6 /* GetSnodePoolJob.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GetSnodePoolJob.swift; sourceTree = ""; }; FD9004132818AD0B00ABAAF6 /* _002_SetupStandardJobs.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = _002_SetupStandardJobs.swift; sourceTree = ""; }; FD9B30F2293EA0BF008DEE3E /* BatchResponseSpec.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BatchResponseSpec.swift; sourceTree = ""; }; FDA8EAFD280E8B78002B68E5 /* FailedMessageSendsJob.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FailedMessageSendsJob.swift; sourceTree = ""; }; @@ -1950,15 +1882,12 @@ FDC4382E27B383AF00C60D73 /* PushServerResponse.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PushServerResponse.swift; sourceTree = ""; }; FDC4383727B3863200C60D73 /* VersionResponse.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VersionResponse.swift; sourceTree = ""; }; FDC4383D27B4708600C60D73 /* Atomic.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Atomic.swift; sourceTree = ""; }; - FDC4384E27B4804F00C60D73 /* Header.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Header.swift; sourceTree = ""; }; - FDC4385027B4807400C60D73 /* QueryParam.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = QueryParam.swift; sourceTree = ""; }; FDC4385C27B4C18900C60D73 /* Room.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Room.swift; sourceTree = ""; }; FDC4385E27B4C4A200C60D73 /* PinnedMessage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PinnedMessage.swift; sourceTree = ""; }; FDC4386227B4D94E00C60D73 /* SOGSMessage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SOGSMessage.swift; sourceTree = ""; }; FDC4386427B4DE7600C60D73 /* RoomPollInfo.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomPollInfo.swift; sourceTree = ""; }; FDC4386627B4E10E00C60D73 /* Capabilities.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Capabilities.swift; sourceTree = ""; }; FDC4386827B4E6B700C60D73 /* String+Utlities.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "String+Utlities.swift"; sourceTree = ""; }; - FDC4386A27B4E88F00C60D73 /* BatchRequestInfo.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BatchRequestInfo.swift; sourceTree = ""; }; FDC4387127B5BB3B00C60D73 /* FileUploadResponse.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FileUploadResponse.swift; sourceTree = ""; }; FDC4387727B5C35400C60D73 /* SendMessageRequest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SendMessageRequest.swift; sourceTree = ""; }; FDC4388E27B9FFC700C60D73 /* SessionMessagingKitTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = SessionMessagingKitTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; @@ -1968,7 +1897,6 @@ FDC438A927BB12BB00C60D73 /* UserModeratorRequest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserModeratorRequest.swift; sourceTree = ""; }; FDC438B027BB159600C60D73 /* RequestInfo.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RequestInfo.swift; sourceTree = ""; }; FDC438B227BB15B400C60D73 /* ResponseInfo.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ResponseInfo.swift; sourceTree = ""; }; - FDC438B827BB161E00C60D73 /* OnionRequestAPIVersion.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OnionRequestAPIVersion.swift; sourceTree = ""; }; FDC438BC27BB2AB400C60D73 /* Mockable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Mockable.swift; sourceTree = ""; }; FDC438C027BB4E6800C60D73 /* SMKDependencies.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SMKDependencies.swift; sourceTree = ""; }; FDC438C227BB512200C60D73 /* SodiumProtocols.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SodiumProtocols.swift; sourceTree = ""; }; @@ -2001,21 +1929,78 @@ FDF0B7592807F3A3004C14C5 /* MessageSenderError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageSenderError.swift; sourceTree = ""; }; FDF0B75B2807F41D004C14C5 /* MessageSender+Convenience.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "MessageSender+Convenience.swift"; sourceTree = ""; }; FDF0B75D280AAF35004C14C5 /* Preferences.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Preferences.swift; sourceTree = ""; }; - FDF1AD5E28FF5F930080A701 /* EditGroupViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EditGroupViewModel.swift; sourceTree = ""; }; - FDF1AD6028FF61110080A701 /* _012_AddClosedGroupInfo.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = _012_AddClosedGroupInfo.swift; sourceTree = ""; }; FDF222062818CECF000A4995 /* ConversationViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConversationViewModel.swift; sourceTree = ""; }; FDF222082818D2B0000A4995 /* NSAttributedString+Utilities.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NSAttributedString+Utilities.swift"; sourceTree = ""; }; FDF2220A2818F38D000A4995 /* SessionApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SessionApp.swift; sourceTree = ""; }; FDF2220E281B55E6000A4995 /* QueryInterfaceRequest+Utilities.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "QueryInterfaceRequest+Utilities.swift"; sourceTree = ""; }; FDF22210281B5E0B000A4995 /* TableRecord+Utilities.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "TableRecord+Utilities.swift"; sourceTree = ""; }; FDF40CDD2897A1BC006A0CC4 /* _004_RemoveLegacyYDB.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = _004_RemoveLegacyYDB.swift; sourceTree = ""; }; + FDF8487529405906007DCAE5 /* HTTPQueryParam.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = HTTPQueryParam.swift; sourceTree = ""; }; + FDF8487629405906007DCAE5 /* HTTPError.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = HTTPError.swift; sourceTree = ""; }; + FDF8487729405906007DCAE5 /* HTTPHeader.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = HTTPHeader.swift; sourceTree = ""; }; + FDF8487829405906007DCAE5 /* HTTPMethod.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = HTTPMethod.swift; sourceTree = ""; }; + FDF8487D29405993007DCAE5 /* HTTPHeader+OpenGroup.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "HTTPHeader+OpenGroup.swift"; sourceTree = ""; }; + FDF8487E29405994007DCAE5 /* HTTPQueryParam+OpenGroup.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "HTTPQueryParam+OpenGroup.swift"; sourceTree = ""; }; + FDF8488229405A12007DCAE5 /* BatchResponse.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BatchResponse.swift; sourceTree = ""; }; + FDF8488529405A60007DCAE5 /* Request.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Request.swift; sourceTree = ""; }; + FDF8488729405A9A007DCAE5 /* SOGSBatchRequest.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SOGSBatchRequest.swift; sourceTree = ""; }; + FDF8488A29405BF2007DCAE5 /* SSKDependencies.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SSKDependencies.swift; sourceTree = ""; }; + FDF8488D29405C04007DCAE5 /* GetSnodePoolJob.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GetSnodePoolJob.swift; sourceTree = ""; }; + FDF8489029405C13007DCAE5 /* SnodeAPINamespace.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SnodeAPINamespace.swift; sourceTree = ""; }; + FDF8489329405C1B007DCAE5 /* SnodeAPI.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SnodeAPI.swift; sourceTree = ""; }; + FDF8489A29405C5A007DCAE5 /* SnodeRecursiveResponse.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SnodeRecursiveResponse.swift; sourceTree = ""; }; + FDF8489B29405C5A007DCAE5 /* GetMessagesRequest.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GetMessagesRequest.swift; sourceTree = ""; }; + FDF8489C29405C5A007DCAE5 /* GetServiceNodesRequest.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GetServiceNodesRequest.swift; sourceTree = ""; }; + FDF8489D29405C5A007DCAE5 /* SnodeResponse.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SnodeResponse.swift; sourceTree = ""; }; + FDF8489E29405C5A007DCAE5 /* ONSResolveResponse.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ONSResolveResponse.swift; sourceTree = ""; }; + FDF8489F29405C5A007DCAE5 /* UpdateExpiryRequest.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UpdateExpiryRequest.swift; sourceTree = ""; }; + FDF848A029405C5A007DCAE5 /* OxenDaemonRPCRequest.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OxenDaemonRPCRequest.swift; sourceTree = ""; }; + FDF848A129405C5A007DCAE5 /* DeleteMessagesRequest.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DeleteMessagesRequest.swift; sourceTree = ""; }; + FDF848A229405C5A007DCAE5 /* RevokeSubkeyResponse.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RevokeSubkeyResponse.swift; sourceTree = ""; }; + FDF848A329405C5A007DCAE5 /* GetSwarmRequest.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GetSwarmRequest.swift; sourceTree = ""; }; + FDF848A429405C5A007DCAE5 /* DeleteAllMessagesRequest.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DeleteAllMessagesRequest.swift; sourceTree = ""; }; + FDF848A529405C5A007DCAE5 /* SendMessageResponse.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SendMessageResponse.swift; sourceTree = ""; }; + FDF848A629405C5A007DCAE5 /* ONSResolveRequest.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ONSResolveRequest.swift; sourceTree = ""; }; + FDF848A729405C5A007DCAE5 /* SnodeRequest.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SnodeRequest.swift; sourceTree = ""; }; + FDF848A829405C5A007DCAE5 /* DeleteAllBeforeRequest.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DeleteAllBeforeRequest.swift; sourceTree = ""; }; + FDF848A929405C5A007DCAE5 /* SnodePoolResponse.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SnodePoolResponse.swift; sourceTree = ""; }; + FDF848AA29405C5A007DCAE5 /* SnodeReceivedMessage.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SnodeReceivedMessage.swift; sourceTree = ""; }; + FDF848AB29405C5A007DCAE5 /* GetNetworkTimestampResponse.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GetNetworkTimestampResponse.swift; sourceTree = ""; }; + FDF848AC29405C5A007DCAE5 /* UpdateExpiryAllRequest.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UpdateExpiryAllRequest.swift; sourceTree = ""; }; + FDF848AD29405C5A007DCAE5 /* SendMessageRequest.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SendMessageRequest.swift; sourceTree = ""; }; + FDF848AE29405C5A007DCAE5 /* UpdateExpiryResponse.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UpdateExpiryResponse.swift; sourceTree = ""; }; + FDF848AF29405C5A007DCAE5 /* SnodeSwarmItem.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SnodeSwarmItem.swift; sourceTree = ""; }; + FDF848B029405C5A007DCAE5 /* LegacyGetMessagesRequest.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LegacyGetMessagesRequest.swift; sourceTree = ""; }; + FDF848B129405C5A007DCAE5 /* UpdateExpiryAllResponse.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UpdateExpiryAllResponse.swift; sourceTree = ""; }; + FDF848B229405C5A007DCAE5 /* DeleteAllBeforeResponse.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DeleteAllBeforeResponse.swift; sourceTree = ""; }; + FDF848B329405C5A007DCAE5 /* DeleteAllMessagesResponse.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DeleteAllMessagesResponse.swift; sourceTree = ""; }; + FDF848B429405C5A007DCAE5 /* SnodeMessage.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SnodeMessage.swift; sourceTree = ""; }; + FDF848B529405C5A007DCAE5 /* SnodeBatchRequest.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SnodeBatchRequest.swift; sourceTree = ""; }; + FDF848B629405C5A007DCAE5 /* SwarmSnode.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SwarmSnode.swift; sourceTree = ""; }; + FDF848B729405C5A007DCAE5 /* SnodeAuthenticatedRequestBody.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SnodeAuthenticatedRequestBody.swift; sourceTree = ""; }; + FDF848B829405C5A007DCAE5 /* GetMessagesResponse.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GetMessagesResponse.swift; sourceTree = ""; }; + FDF848B929405C5A007DCAE5 /* DeleteMessagesResponse.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DeleteMessagesResponse.swift; sourceTree = ""; }; + FDF848BA29405C5A007DCAE5 /* RevokeSubkeyRequest.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RevokeSubkeyRequest.swift; sourceTree = ""; }; + FDF848BB29405C5A007DCAE5 /* LegacySendMessageRequest.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LegacySendMessageRequest.swift; sourceTree = ""; }; + FDF848DE29405D6E007DCAE5 /* OnionRequestAPIVersion.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OnionRequestAPIVersion.swift; sourceTree = ""; }; + FDF848DF29405D6E007DCAE5 /* SnodeAPIEndpoint.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SnodeAPIEndpoint.swift; sourceTree = ""; }; + FDF848E029405D6E007DCAE5 /* SnodeAPIError.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SnodeAPIError.swift; sourceTree = ""; }; + FDF848E129405D6E007DCAE5 /* OnionRequestAPIDestination.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OnionRequestAPIDestination.swift; sourceTree = ""; }; + FDF848E229405D6E007DCAE5 /* OnionRequestAPIError.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OnionRequestAPIError.swift; sourceTree = ""; }; + FDF848E829405E4E007DCAE5 /* OnionRequestAPI.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OnionRequestAPI.swift; sourceTree = ""; }; + FDF848E929405E4E007DCAE5 /* OnionRequestAPI+Encryption.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "OnionRequestAPI+Encryption.swift"; sourceTree = ""; }; + FDF848EA29405E4E007DCAE5 /* Notification+OnionRequestAPI.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "Notification+OnionRequestAPI.swift"; sourceTree = ""; }; + FDF848EE294067E4007DCAE5 /* URLResponse+Utilities.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "URLResponse+Utilities.swift"; sourceTree = ""; }; + FDF848F029406A30007DCAE5 /* Format.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = Format.swift; path = "SessionUIKit/Style Guide/Format.swift"; sourceTree = SOURCE_ROOT; }; + FDF848F229413DB0007DCAE5 /* ImagePickerHandler.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ImagePickerHandler.swift; sourceTree = ""; }; + FDF848F429413EEC007DCAE5 /* SessionCell+Styling.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "SessionCell+Styling.swift"; sourceTree = ""; }; + FDF848F629414477007DCAE5 /* CurrentUserPoller.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CurrentUserPoller.swift; sourceTree = ""; }; FDFD645A27F26D4600808CA1 /* Data+Utilities.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "Data+Utilities.swift"; sourceTree = ""; }; FDFD645C27F273F300808CA1 /* MockGeneralCache.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockGeneralCache.swift; sourceTree = ""; }; FDFDE123282D04F20098B17F /* MediaDismissAnimationController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MediaDismissAnimationController.swift; sourceTree = ""; }; FDFDE125282D05380098B17F /* MediaInteractiveDismiss.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MediaInteractiveDismiss.swift; sourceTree = ""; }; FDFDE127282D05530098B17F /* MediaPresentationContext.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MediaPresentationContext.swift; sourceTree = ""; }; FDFDE129282D056B0098B17F /* MediaZoomAnimationController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MediaZoomAnimationController.swift; sourceTree = ""; }; - FF694C71BE4B41B6AFD252A0 /* Pods-GlobalDependencies-FrameworkAndExtensionDependencies-ExtendedDependencies-SessionMessagingKit.app store release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-GlobalDependencies-FrameworkAndExtensionDependencies-ExtendedDependencies-SessionMessagingKit.app store release.xcconfig"; path = "Target Support Files/Pods-GlobalDependencies-FrameworkAndExtensionDependencies-ExtendedDependencies-SessionMessagingKit/Pods-GlobalDependencies-FrameworkAndExtensionDependencies-ExtendedDependencies-SessionMessagingKit.app store release.xcconfig"; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -2028,7 +2013,7 @@ C3D90A5C25773A25002C9DF5 /* SessionUtilitiesKit.framework in Frameworks */, C3402FE52559036600EA6424 /* SessionUIKit.framework in Frameworks */, B8D64FCB25BA78A90029CFC0 /* SignalUtilitiesKit.framework in Frameworks */, - 1FFD68A448D5A1439F2F02FD /* Pods_GlobalDependencies_FrameworkAndExtensionDependencies_ExtendedDependencies_SessionShareExtension.framework in Frameworks */, + 4C4FE46740136D591D04261F /* Pods_GlobalDependencies_FrameworkAndExtensionDependencies_ExtendedDependencies_SessionShareExtension.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -2040,7 +2025,7 @@ B8D64FBD25BA78310029CFC0 /* SessionSnodeKit.framework in Frameworks */, B8D64FBE25BA78310029CFC0 /* SessionUtilitiesKit.framework in Frameworks */, C38EF00C255B61CC007E1867 /* SignalUtilitiesKit.framework in Frameworks */, - 98547545DAF8E7916DF9F0BF /* Pods_GlobalDependencies_FrameworkAndExtensionDependencies_SessionNotificationServiceExtension.framework in Frameworks */, + CB54B7E519F525FF27A7DAD3 /* Pods_GlobalDependencies_FrameworkAndExtensionDependencies_SessionNotificationServiceExtension.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -2049,7 +2034,7 @@ buildActionMask = 2147483647; files = ( FD37E9EF28A5ED70003AE748 /* SessionUtilitiesKit.framework in Frameworks */, - 821EFD1644285AC2D3733D27 /* Pods_GlobalDependencies_SessionUIKit.framework in Frameworks */, + A0A69C9CB213DDDD85BF2207 /* Pods_GlobalDependencies_SessionUIKit.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -2061,7 +2046,7 @@ C33FD9C2255A54EF00E217F9 /* SessionMessagingKit.framework in Frameworks */, C33FD9C4255A54EF00E217F9 /* SessionSnodeKit.framework in Frameworks */, C33FD9C5255A54EF00E217F9 /* SessionUtilitiesKit.framework in Frameworks */, - 3289CA2E9E89DA9D4D52A90C /* Pods_GlobalDependencies_FrameworkAndExtensionDependencies_ExtendedDependencies_SignalUtilitiesKit.framework in Frameworks */, + 6C1ADD1127CED42854542F78 /* Pods_GlobalDependencies_FrameworkAndExtensionDependencies_ExtendedDependencies_SignalUtilitiesKit.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -2070,7 +2055,7 @@ buildActionMask = 2147483647; files = ( C3C2A6C62553896A00C340D1 /* SessionUtilitiesKit.framework in Frameworks */, - C2CAA4A9737D865B34560B8C /* Pods_GlobalDependencies_FrameworkAndExtensionDependencies_SessionSnodeKit.framework in Frameworks */, + DE0001574AC103562A7CF31F /* Pods_GlobalDependencies_FrameworkAndExtensionDependencies_SessionSnodeKit.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -2078,7 +2063,7 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( - 5163CBC4F53274C88D1F88F8 /* Pods_GlobalDependencies_FrameworkAndExtensionDependencies_ExtendedDependencies_SessionUtilitiesKit.framework in Frameworks */, + 42C48489AFF26BC9034C736C /* Pods_GlobalDependencies_FrameworkAndExtensionDependencies_ExtendedDependencies_SessionUtilitiesKit.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -2089,7 +2074,7 @@ FD8ECF7929340F7200C0D1BB /* libsession-util.xcframework in Frameworks */, FDC4386C27B4E90300C60D73 /* SessionUtilitiesKit.framework in Frameworks */, C3C2A70B25539E1E00C340D1 /* SessionSnodeKit.framework in Frameworks */, - CEE449BA3596483519120D91 /* Pods_GlobalDependencies_FrameworkAndExtensionDependencies_ExtendedDependencies_SessionMessagingKit.framework in Frameworks */, + BE25D9230CA2C3A40A9216EF /* Pods_GlobalDependencies_FrameworkAndExtensionDependencies_ExtendedDependencies_SessionMessagingKit.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -2128,7 +2113,7 @@ D221A090169C9E5E00537ABF /* Foundation.framework in Frameworks */, D221A0E8169DFFC500537ABF /* AVFoundation.framework in Frameworks */, D24B5BD5169F568C00681372 /* AudioToolbox.framework in Frameworks */, - 71B1D8AF3ADE2BD191256496 /* Pods_GlobalDependencies_Session.framework in Frameworks */, + 41BA6B5C1C693C3A86070C15 /* Pods_GlobalDependencies_Session.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -2136,7 +2121,7 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( - DA2AE22FA77136442EF669E9 /* Pods_GlobalDependencies_Session_SessionTests.framework in Frameworks */, + 056ED47155A04437A1EF58C2 /* Pods_GlobalDependencies_Session_SessionTests.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -2145,7 +2130,7 @@ buildActionMask = 2147483647; files = ( FD83B9B327CF200A005E1583 /* SessionUtilitiesKit.framework in Frameworks */, - A49760F37A9AE09D57ECE415 /* Pods_GlobalDependencies_FrameworkAndExtensionDependencies_ExtendedDependencies_SessionUtilitiesKit_SessionUtilitiesKitTests.framework in Frameworks */, + 9A88F90C33C394513CB4C18A /* Pods_GlobalDependencies_FrameworkAndExtensionDependencies_ExtendedDependencies_SessionUtilitiesKit_SessionUtilitiesKitTests.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -2154,7 +2139,7 @@ buildActionMask = 2147483647; files = ( FDC4389227B9FFC700C60D73 /* SessionMessagingKit.framework in Frameworks */, - 92EB2776D36B22D2E0552A05 /* Pods_GlobalDependencies_FrameworkAndExtensionDependencies_ExtendedDependencies_SessionMessagingKit_SessionMessagingKitTests.framework in Frameworks */, + FE43694493EC2E1E438EBEB3 /* Pods_GlobalDependencies_FrameworkAndExtensionDependencies_ExtendedDependencies_SessionMessagingKit_SessionMessagingKitTests.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -2164,30 +2149,28 @@ 2BADBA206E0B8D297E313FBA /* Pods */ = { isa = PBXGroup; children = ( - 506FA2159653FF9F446D97D1 /* Pods-GlobalDependencies-FrameworkAndExtensionDependencies-ExtendedDependencies-SessionMessagingKit.debug.xcconfig */, - FF694C71BE4B41B6AFD252A0 /* Pods-GlobalDependencies-FrameworkAndExtensionDependencies-ExtendedDependencies-SessionMessagingKit.app store release.xcconfig */, - D0CE0424239A1574F683D2D7 /* Pods-GlobalDependencies-FrameworkAndExtensionDependencies-ExtendedDependencies-SessionMessagingKit-SessionMessagingKitTests.debug.xcconfig */, - 8E029A324780A800DE6B70B3 /* Pods-GlobalDependencies-FrameworkAndExtensionDependencies-ExtendedDependencies-SessionMessagingKit-SessionMessagingKitTests.app store release.xcconfig */, - 96ED0C9B69379BE6FF4E9DA6 /* Pods-GlobalDependencies-FrameworkAndExtensionDependencies-ExtendedDependencies-SessionShareExtension.debug.xcconfig */, - 5626DC0D5F62C1C2C64E4AFC /* Pods-GlobalDependencies-FrameworkAndExtensionDependencies-ExtendedDependencies-SessionShareExtension.app store release.xcconfig */, - F705826F79C4A591AB35D68F /* Pods-GlobalDependencies-FrameworkAndExtensionDependencies-ExtendedDependencies-SessionUtilitiesKit.debug.xcconfig */, - 2581AFACDDDC1404866D7B8C /* Pods-GlobalDependencies-FrameworkAndExtensionDependencies-ExtendedDependencies-SessionUtilitiesKit.app store release.xcconfig */, - BE11AFA6FD8CAE894CABC28D /* Pods-GlobalDependencies-FrameworkAndExtensionDependencies-ExtendedDependencies-SessionUtilitiesKit-SessionUtilitiesKitTests.debug.xcconfig */, - 58A6BA91F634756FA0BEC9E5 /* Pods-GlobalDependencies-FrameworkAndExtensionDependencies-ExtendedDependencies-SessionUtilitiesKit-SessionUtilitiesKitTests.app store release.xcconfig */, - DAF57FAAF30631D0E99DA361 /* Pods-GlobalDependencies-FrameworkAndExtensionDependencies-ExtendedDependencies-SignalUtilitiesKit.debug.xcconfig */, - 82099864FD91C9126A750313 /* Pods-GlobalDependencies-FrameworkAndExtensionDependencies-ExtendedDependencies-SignalUtilitiesKit.app store release.xcconfig */, - 1A0882BF820F5B44969F91F1 /* Pods-GlobalDependencies-FrameworkAndExtensionDependencies-SessionNotificationServiceExtension.debug.xcconfig */, - 245BF74EF6348E2D4125033F /* Pods-GlobalDependencies-FrameworkAndExtensionDependencies-SessionNotificationServiceExtension.app store release.xcconfig */, - E23C1E6B7E0C12BF4ACD9CBE /* Pods-GlobalDependencies-FrameworkAndExtensionDependencies-SessionSnodeKit.debug.xcconfig */, - 0E836037CC97CE5A47735596 /* Pods-GlobalDependencies-FrameworkAndExtensionDependencies-SessionSnodeKit.app store release.xcconfig */, - 56F41C56FC7B2F381E440FB0 /* Pods-GlobalDependencies-Session.debug.xcconfig */, - 6BE8FBF62464A7177034A0AB /* Pods-GlobalDependencies-Session.app store release.xcconfig */, - EC5C23F9D234F558BE5E41DE /* Pods-SessionUIKit.debug.xcconfig */, - 29CF8C79F41BF00B1C2E59A0 /* Pods-SessionUIKit.app store release.xcconfig */, - 510955DC99A0FD84F2D1C159 /* Pods-GlobalDependencies-SessionUIKit.debug.xcconfig */, - 18EAE958B8C12503F2C294DF /* Pods-GlobalDependencies-SessionUIKit.app store release.xcconfig */, - 06160ECE3FE5A06A916FF8C5 /* Pods-GlobalDependencies-Session-SessionTests.debug.xcconfig */, - 0E8564674E3201E218939AFB /* Pods-GlobalDependencies-Session-SessionTests.app store release.xcconfig */, + 6DA09080DD9779C860023A60 /* Pods-GlobalDependencies-FrameworkAndExtensionDependencies-ExtendedDependencies-SessionMessagingKit.debug.xcconfig */, + F390F8E34CA76B3F7D3B1826 /* Pods-GlobalDependencies-FrameworkAndExtensionDependencies-ExtendedDependencies-SessionMessagingKit.app store release.xcconfig */, + 8727C47348B6EFA767EE583A /* Pods-GlobalDependencies-FrameworkAndExtensionDependencies-ExtendedDependencies-SessionMessagingKit-SessionMessagingKitTests.debug.xcconfig */, + 621B42AC592F3456ACD82F8B /* Pods-GlobalDependencies-FrameworkAndExtensionDependencies-ExtendedDependencies-SessionMessagingKit-SessionMessagingKitTests.app store release.xcconfig */, + B1910A32EB2AD01913629646 /* Pods-GlobalDependencies-FrameworkAndExtensionDependencies-ExtendedDependencies-SessionShareExtension.debug.xcconfig */, + 6A71AD9BEAFF0C9E8016BC23 /* Pods-GlobalDependencies-FrameworkAndExtensionDependencies-ExtendedDependencies-SessionShareExtension.app store release.xcconfig */, + 8E946CB54A221018E23599DE /* Pods-GlobalDependencies-FrameworkAndExtensionDependencies-ExtendedDependencies-SessionUtilitiesKit.debug.xcconfig */, + F60C5B6CD14329816B0E8CC0 /* Pods-GlobalDependencies-FrameworkAndExtensionDependencies-ExtendedDependencies-SessionUtilitiesKit.app store release.xcconfig */, + 8448EFF76CD3CA5B2283B8A0 /* Pods-GlobalDependencies-FrameworkAndExtensionDependencies-ExtendedDependencies-SessionUtilitiesKit-SessionUtilitiesKitTests.debug.xcconfig */, + 5DA3BDDFFB9E937A49C35FCC /* Pods-GlobalDependencies-FrameworkAndExtensionDependencies-ExtendedDependencies-SessionUtilitiesKit-SessionUtilitiesKitTests.app store release.xcconfig */, + B4F9FCBDA07F07CB48220D4C /* Pods-GlobalDependencies-FrameworkAndExtensionDependencies-ExtendedDependencies-SignalUtilitiesKit.debug.xcconfig */, + 55C13C7B4B700846E49C0E25 /* Pods-GlobalDependencies-FrameworkAndExtensionDependencies-ExtendedDependencies-SignalUtilitiesKit.app store release.xcconfig */, + 285705D20F792E174C8A9BBA /* Pods-GlobalDependencies-FrameworkAndExtensionDependencies-SessionNotificationServiceExtension.debug.xcconfig */, + 62B512CEB14BD4A4A53CF532 /* Pods-GlobalDependencies-FrameworkAndExtensionDependencies-SessionNotificationServiceExtension.app store release.xcconfig */, + 847091A12D82E41B1EBB8FB3 /* Pods-GlobalDependencies-FrameworkAndExtensionDependencies-SessionSnodeKit.debug.xcconfig */, + EED1CF82CAB23FE3345564F9 /* Pods-GlobalDependencies-FrameworkAndExtensionDependencies-SessionSnodeKit.app store release.xcconfig */, + 34040971CC7AF9C8A6C1E838 /* Pods-GlobalDependencies-Session.debug.xcconfig */, + 8603226ED1C6F61F1F2D3734 /* Pods-GlobalDependencies-Session.app store release.xcconfig */, + 0772459E7D5F6747EDC889F3 /* Pods-GlobalDependencies-Session-SessionTests.debug.xcconfig */, + F154A10CE1ADA33C16B45357 /* Pods-GlobalDependencies-Session-SessionTests.app store release.xcconfig */, + EB5B8ACA4C6F512FA3E21859 /* Pods-GlobalDependencies-SessionUIKit.debug.xcconfig */, + 05C76EFA593DD507061C50B2 /* Pods-GlobalDependencies-SessionUIKit.app store release.xcconfig */, ); path = Pods; sourceTree = ""; @@ -2270,7 +2253,7 @@ 7BAF54D227ACCF01003D12F8 /* SAEScreenLockViewController.swift */, 7BAF54D127ACCF01003D12F8 /* ShareAppExtensionContext.swift */, 4535186C1FC635DD00210559 /* MainInterface.storyboard */, - C3ADC66026426688005F1414 /* ShareVC.swift */, + C3ADC66026426688005F1414 /* ShareNavController.swift */, FD3AABE828306BBD00E5099A /* ThreadPickerViewModel.swift */, B817AD9B26436F73009DF825 /* ThreadPickerVC.swift */, B817AD9926436593009DF825 /* SimplifiedConversationCell.swift */, @@ -2537,12 +2520,19 @@ isa = PBXGroup; children = ( C33FDB68255A580F00E217F9 /* ContentProxy.swift */, - C3C2A5BC255385EE00C340D1 /* HTTP.swift */, + FDF8487629405906007DCAE5 /* HTTPError.swift */, + FDF8487729405906007DCAE5 /* HTTPHeader.swift */, + FDF8487829405906007DCAE5 /* HTTPMethod.swift */, + FDF8487529405906007DCAE5 /* HTTPQueryParam.swift */, B8FF8EA525C11FEF004D1F22 /* IPv4.swift */, C3C2A5D92553860B00C340D1 /* JSON.swift */, + FDF8488529405A60007DCAE5 /* Request.swift */, + FDC438B027BB159600C60D73 /* RequestInfo.swift */, + FDF8488229405A12007DCAE5 /* BatchResponse.swift */, + FDC438B227BB15B400C60D73 /* ResponseInfo.swift */, C33FDAF2255A580500E217F9 /* ProxiedContentDownloader.swift */, C3C2A5BC255385EE00C340D1 /* HTTP.swift */, - FD8ECFA0293D8FDD00C0D1BB /* URLResponse+Utilities.swift */, + FDF848EE294067E4007DCAE5 /* URLResponse+Utilities.swift */, ); path = Networking; sourceTree = ""; @@ -2577,10 +2567,9 @@ FDC4383D27B4708600C60D73 /* Atomic.swift */, FDC438CC27BC641200C60D73 /* Set+Utilities.swift */, C33FDAA8255A57FF00E217F9 /* BuildConfiguration.swift */, - B8F5F58225EC94A6003BF8D4 /* Collection+Subscripting.swift */, + B8F5F58225EC94A6003BF8D4 /* Collection+Utilities.swift */, FDFD645A27F26D4600808CA1 /* Data+Utilities.swift */, FDC6D75F2862B3F600B04575 /* Dependencies.swift */, - FD26FA5D291CAFF9005801D8 /* Data+Utilities.swift */, C3C2A5D52553860A00C340D1 /* Dictionary+Utilities.swift */, B87EF18026377A1D00124B3C /* Features.swift */, B8BC00BF257D90E30032E807 /* General.swift */, @@ -2711,7 +2700,6 @@ C3C2A7702553A41E00C340D1 /* ControlMessage.swift */, B8DE1FB526C22FCB0079C9CE /* CallMessage.swift */, C34A977325A3E34A00852C71 /* ClosedGroupControlMessage.swift */, - C34A977325A3E34A00852C71 /* LegacyClosedGroupControlMessage.swift */, FD8ECF8A2935DB4B00C0D1BB /* SharedConfigMessage.swift */, C3DA9C0625AE7396008F7C7E /* ConfigurationMessage.swift */, 7B93D06E27CF194000811CB6 /* ConfigurationMessage+Convenience.swift */, @@ -2808,9 +2796,10 @@ C32C59F8256DB5A6003C73A2 /* Pollers */ = { isa = PBXGroup; children = ( + C33FDB3A255A580B00E217F9 /* Poller.swift */, C33FDB34255A580B00E217F9 /* ClosedGroupPoller.swift */, C3DB66C2260ACCE6001EFC55 /* OpenGroupPoller.swift */, - C33FDB3A255A580B00E217F9 /* Poller.swift */, + FDF848F629414477007DCAE5 /* CurrentUserPoller.swift */, ); path = Pollers; sourceTree = ""; @@ -2873,6 +2862,7 @@ FD37E9C228A1C6F3003AE748 /* ThemeManager.swift */, B8BB82BD2394D4CE00BA5194 /* Fonts.swift */, B8BB82A1238F356100BA5194 /* Values.swift */, + FDF848F029406A30007DCAE5 /* Format.swift */, ); path = "Style Guide"; sourceTree = ""; @@ -2983,11 +2973,11 @@ FD37EA1628AC5605003AE748 /* NotificationContentViewModel.swift */, FD87DCF928B74DB300AF0F98 /* ConversationSettingsViewModel.swift */, FD87DCFD28B7582C00AF0F98 /* BlockedContactsViewModel.swift */, - FD87DCFB28B755B800AF0F98 /* BlockedContactsViewController.swift */, FD37E9CB28A1E578003AE748 /* AppearanceViewController.swift */, FD37EA0228A9FDCC003AE748 /* HelpViewModel.swift */, B86BD08523399CEF000F5AE3 /* SeedModal.swift */, B894D0742339EDCF00B4D94D /* NukeDataModal.swift */, + FDF848F229413DB0007DCAE5 /* ImagePickerHandler.swift */, ); path = Settings; sourceTree = ""; @@ -3199,7 +3189,6 @@ C33FDB75255A581000E217F9 /* AppReadiness.m */, FDF0B7542807C4BB004C14C5 /* Environment.swift */, FD859EF127BF6BA200510D0C /* Data+Utilities.swift */, - FDC438C027BB4E6800C60D73 /* SMKDependencies.swift */, C38EF309255B6DBE007E1867 /* DeviceSleepManager.swift */, C3BBE0C62554F1570050F1E3 /* FixedWidthInteger+BigEndian.swift */, C3A71D0A2558989C0043A11F /* MessageWrapper.swift */, @@ -3214,7 +3203,6 @@ C38EF2EC255B6DBA007E1867 /* ProximityMonitoringManager.swift */, C38EEF09255B49A8007E1867 /* SNProtoEnvelope+Conversion.swift */, FDC4386827B4E6B700C60D73 /* String+Utlities.swift */, - FDC4387327B5BB9B00C60D73 /* Promise+Utilities.swift */, FD772899284AF1BD0018502F /* Sodium+Utilities.swift */, C3A3A170256E1D25004D228D /* SSKReachabilityManager.swift */, C3ECBF7A257056B700EA7FCE /* Threading.swift */, @@ -3227,15 +3215,13 @@ children = ( C3C2A5B0255385C700C340D1 /* Meta */, FD17D79D27F40CAA00122BE0 /* Database */, - FDC438AF27BB158500C60D73 /* Models */, + FDF8489929405C5A007DCAE5 /* Models */, + FDF8488F29405C13007DCAE5 /* Types */, + FDF8488C29405C04007DCAE5 /* Jobs */, + FDF8489229405C1B007DCAE5 /* Networking */, C3C2A5CD255385F300C340D1 /* Utilities */, + FDF8488A29405BF2007DCAE5 /* SSKDependencies.swift */, C3C2A5B9255385ED00C340D1 /* Configuration.swift */, - C3C2A5BD255385EE00C340D1 /* Notification+OnionRequestAPI.swift */, - C3C2A5BA255385ED00C340D1 /* OnionRequestAPI.swift */, - C3C2A5BB255385ED00C340D1 /* OnionRequestAPI+Encryption.swift */, - C3C2A5BE255385EE00C340D1 /* SnodeAPI.swift */, - FD90040E2818AB6D00ABAAF6 /* GetSnodePoolJob.swift */, - C3C2A5B6255385EC00C340D1 /* SnodeMessage.swift */, ); path = SessionSnodeKit; sourceTree = ""; @@ -3394,8 +3380,6 @@ C38EF3DD255B6DF1007E1867 /* UIAlertController+OWS.swift */, C38EF241255B6D67007E1867 /* Collection+OWS.swift */, C38EF3AE255B6DE5007E1867 /* OrderedDictionary.swift */, - C38EF301255B6DBD007E1867 /* OWSFormat.h */, - C38EF305255B6DBE007E1867 /* OWSFormat.m */, C38EF226255B6D5D007E1867 /* ShareViewDelegate.swift */, C38EF2FA255B6DBD007E1867 /* Bench.swift */, C38EF23F255B6D67007E1867 /* NSAttributedString+OWS.h */, @@ -3518,17 +3502,17 @@ D221A08D169C9E5E00537ABF /* UIKit.framework */, D221A08F169C9E5E00537ABF /* Foundation.framework */, D221A091169C9E5E00537ABF /* CoreGraphics.framework */, - 7A8A44E3F8AC9282AC5E6E5A /* Pods_GlobalDependencies_FrameworkAndExtensionDependencies_ExtendedDependencies_SessionMessagingKit.framework */, - 2691123A7F231EDD8226C4B5 /* Pods_GlobalDependencies_FrameworkAndExtensionDependencies_ExtendedDependencies_SessionMessagingKit_SessionMessagingKitTests.framework */, - DBA125424EDD2417B515C63A /* Pods_GlobalDependencies_FrameworkAndExtensionDependencies_ExtendedDependencies_SessionShareExtension.framework */, - 782B65234A707D762FEAFD3B /* Pods_GlobalDependencies_FrameworkAndExtensionDependencies_ExtendedDependencies_SessionUtilitiesKit.framework */, - 5442DF945D862CEDF7F8AC49 /* Pods_GlobalDependencies_FrameworkAndExtensionDependencies_ExtendedDependencies_SessionUtilitiesKit_SessionUtilitiesKitTests.framework */, - 0BF4561630A52BE96F164CF6 /* Pods_GlobalDependencies_FrameworkAndExtensionDependencies_ExtendedDependencies_SignalUtilitiesKit.framework */, - 6F84A214B9A1C0CCF6DB09C8 /* Pods_GlobalDependencies_FrameworkAndExtensionDependencies_SessionNotificationServiceExtension.framework */, - 6737124ECBC2DFEE2DD716D3 /* Pods_GlobalDependencies_FrameworkAndExtensionDependencies_SessionSnodeKit.framework */, - 48AD214D67ABED845101E795 /* Pods_GlobalDependencies_Session.framework */, - A9C58C3ADF46C718488458C2 /* Pods_GlobalDependencies_SessionUIKit.framework */, - B4C92F6ADBECCD47A6B6008E /* Pods_GlobalDependencies_Session_SessionTests.framework */, + 05E68C7F291EC08B8A43A534 /* Pods_GlobalDependencies_FrameworkAndExtensionDependencies_ExtendedDependencies_SessionMessagingKit.framework */, + 13D1714FDC4DAB121DA2C73A /* Pods_GlobalDependencies_FrameworkAndExtensionDependencies_ExtendedDependencies_SessionMessagingKit_SessionMessagingKitTests.framework */, + E0B0F4C34363FE679EE3F203 /* Pods_GlobalDependencies_FrameworkAndExtensionDependencies_ExtendedDependencies_SessionShareExtension.framework */, + 86A3D36084020C9118DBCEE3 /* Pods_GlobalDependencies_FrameworkAndExtensionDependencies_ExtendedDependencies_SessionUtilitiesKit.framework */, + 07B7038C849F53378CD36B83 /* Pods_GlobalDependencies_FrameworkAndExtensionDependencies_ExtendedDependencies_SessionUtilitiesKit_SessionUtilitiesKitTests.framework */, + D3BE3061D535DBC1DF0C94D4 /* Pods_GlobalDependencies_FrameworkAndExtensionDependencies_ExtendedDependencies_SignalUtilitiesKit.framework */, + C5828D9A55CD76B75E8FA367 /* Pods_GlobalDependencies_FrameworkAndExtensionDependencies_SessionNotificationServiceExtension.framework */, + 9E358E7590EB145A5047F885 /* Pods_GlobalDependencies_FrameworkAndExtensionDependencies_SessionSnodeKit.framework */, + CC8D3B6504946442A0CF775C /* Pods_GlobalDependencies_Session.framework */, + 32C10A2A536B58FC42C46C3C /* Pods_GlobalDependencies_Session_SessionTests.framework */, + 678CFB04E76F7E388AFCFA86 /* Pods_GlobalDependencies_SessionUIKit.framework */, ); name = Frameworks; sourceTree = ""; @@ -3612,8 +3596,6 @@ 7BAA7B6528D2DE4700AE1489 /* _009_OpenGroupPermission.swift */, FD7115F128C6CB3900B47552 /* _010_AddThreadIdToFTS.swift */, FD8ECF7C2934293A00C0D1BB /* _011_SharedUtilChanges.swift */, - FDF1AD6028FF61110080A701 /* _012_AddClosedGroupInfo.swift */, - FD8ECF71292DCD1A00C0D1BB /* _013_AutoDownloadAttachments.swift */, ); path = Migrations; sourceTree = ""; @@ -3917,7 +3899,6 @@ isa = PBXGroup; children = ( FD7115EF28C5D7DE00B47552 /* SessionHeaderView.swift */, - FD7115ED28C5D79B00B47552 /* SessionAvatarCell.swift */, FD37EA0A28AB12E2003AE748 /* SessionCell.swift */, FD71164728E2CE8700B47552 /* SessionCell+AccessoryView.swift */, FD71164528E2CC1300B47552 /* SessionHighlightingBackgroundLabel.swift */, @@ -3932,7 +3913,8 @@ FD71163328E2C48400B47552 /* TransitionType.swift */, FD71164D28E3F8CC00B47552 /* SessionCell+Info.swift */, FD71164328E2CB8A00B47552 /* SessionCell+Accessory.swift */, - FD71164B28E3F5AA00B47552 /* SessionCell+ExtraAction.swift */, + FD71164B28E3F5AA00B47552 /* SessionCell+Accessibility.swift */, + FDF848F429413EEC007DCAE5 /* SessionCell+Styling.swift */, FD71165128E410BE00B47552 /* SessionTableSection.swift */, FD71164F28E3F9FA00B47552 /* SessionTableViewModel+NavItem.swift */, ); @@ -4040,16 +4022,6 @@ path = LibSessionUtil; sourceTree = ""; }; - FD8ECF832934507500C0D1BB /* Networking */ = { - isa = PBXGroup; - children = ( - FD3C906127E411AF00CD579F /* HeaderSpec.swift */, - FD3C906327E4122F00CD579F /* RequestSpec.swift */, - FD8ECF842934508B00C0D1BB /* BatchResponseSpec.swift */, - ); - path = Networking; - sourceTree = ""; - }; FD8ECF8E29381FB200C0D1BB /* Config Handling */ = { isa = PBXGroup; children = ( @@ -4090,6 +4062,8 @@ FDC4380727B31D3A00C60D73 /* Types */ = { isa = PBXGroup; children = ( + FDF8487D29405993007DCAE5 /* HTTPHeader+OpenGroup.swift */, + FDF8487E29405994007DCAE5 /* HTTPQueryParam+OpenGroup.swift */, FDC4381F27B36ADC00C60D73 /* SOGSEndpoint.swift */, FDC4380827B31D4E00C60D73 /* OpenGroupAPIError.swift */, FDC4381627B32EC700C60D73 /* Personalization.swift */, @@ -4102,7 +4076,7 @@ FDC4381827B34EAD00C60D73 /* Models */ = { isa = PBXGroup; children = ( - FDC4386A27B4E88F00C60D73 /* BatchRequestInfo.swift */, + FDF8488729405A9A007DCAE5 /* SOGSBatchRequest.swift */, FDC4386627B4E10E00C60D73 /* Capabilities.swift */, FDC4385C27B4C18900C60D73 /* Room.swift */, FDC4386427B4DE7600C60D73 /* RoomPollInfo.swift */, @@ -4142,9 +4116,6 @@ isa = PBXGroup; children = ( FDC4385527B484AE00C60D73 /* Models */, - FDC4384E27B4804F00C60D73 /* Header.swift */, - FDC4385027B4807400C60D73 /* QueryParam.swift */, - FD83B9CD27D17A04005E1583 /* Request.swift */, 7B81682228A4C1210069F315 /* UpdateTypes.swift */, ); path = "Common Networking"; @@ -4204,23 +4175,6 @@ path = _TestUtilities; sourceTree = ""; }; - FDC438AF27BB158500C60D73 /* Models */ = { - isa = PBXGroup; - children = ( - FD17D7D127F5797A00122BE0 /* SnodeAPIEndpoint.swift */, - FD77289F284EF5810018502F /* SnodeAPIError.swift */, - FDC438B827BB161E00C60D73 /* OnionRequestAPIVersion.swift */, - FD17D7D727F658E200122BE0 /* OnionRequestAPIDestination.swift */, - FD17D7D327F6584600122BE0 /* OnionRequestAPIError.swift */, - FDC438B027BB159600C60D73 /* RequestInfo.swift */, - FDC438B227BB15B400C60D73 /* ResponseInfo.swift */, - FD09796827F6BEA700936362 /* SwarmSnode.swift */, - FD77289B284DDCE10018502F /* SnodePoolResponse.swift */, - FD17D7E027F67BD400122BE0 /* SnodeReceivedMessage.swift */, - ); - path = Models; - sourceTree = ""; - }; FDE7214E287E50D50093DF33 /* Scripts */ = { isa = PBXGroup; children = ( @@ -4259,6 +4213,79 @@ path = Errors; sourceTree = ""; }; + FDF8488C29405C04007DCAE5 /* Jobs */ = { + isa = PBXGroup; + children = ( + FDF8488D29405C04007DCAE5 /* GetSnodePoolJob.swift */, + ); + path = Jobs; + sourceTree = ""; + }; + FDF8488F29405C13007DCAE5 /* Types */ = { + isa = PBXGroup; + children = ( + FDF848DF29405D6E007DCAE5 /* SnodeAPIEndpoint.swift */, + FDF848E029405D6E007DCAE5 /* SnodeAPIError.swift */, + FDF8489029405C13007DCAE5 /* SnodeAPINamespace.swift */, + FDF848DE29405D6E007DCAE5 /* OnionRequestAPIVersion.swift */, + FDF848E229405D6E007DCAE5 /* OnionRequestAPIError.swift */, + FDF848E129405D6E007DCAE5 /* OnionRequestAPIDestination.swift */, + ); + path = Types; + sourceTree = ""; + }; + FDF8489229405C1B007DCAE5 /* Networking */ = { + isa = PBXGroup; + children = ( + FDF8489329405C1B007DCAE5 /* SnodeAPI.swift */, + FDF848EA29405E4E007DCAE5 /* Notification+OnionRequestAPI.swift */, + FDF848E829405E4E007DCAE5 /* OnionRequestAPI.swift */, + FDF848E929405E4E007DCAE5 /* OnionRequestAPI+Encryption.swift */, + ); + path = Networking; + sourceTree = ""; + }; + FDF8489929405C5A007DCAE5 /* Models */ = { + isa = PBXGroup; + children = ( + FDF848A729405C5A007DCAE5 /* SnodeRequest.swift */, + FDF8489D29405C5A007DCAE5 /* SnodeResponse.swift */, + FDF8489A29405C5A007DCAE5 /* SnodeRecursiveResponse.swift */, + FDF848AF29405C5A007DCAE5 /* SnodeSwarmItem.swift */, + FDF848B729405C5A007DCAE5 /* SnodeAuthenticatedRequestBody.swift */, + FDF848B529405C5A007DCAE5 /* SnodeBatchRequest.swift */, + FDF8489B29405C5A007DCAE5 /* GetMessagesRequest.swift */, + FDF848B029405C5A007DCAE5 /* LegacyGetMessagesRequest.swift */, + FDF848B829405C5A007DCAE5 /* GetMessagesResponse.swift */, + FDF848AD29405C5A007DCAE5 /* SendMessageRequest.swift */, + FDF848BB29405C5A007DCAE5 /* LegacySendMessageRequest.swift */, + FDF848A529405C5A007DCAE5 /* SendMessageResponse.swift */, + FDF848A129405C5A007DCAE5 /* DeleteMessagesRequest.swift */, + FDF848B929405C5A007DCAE5 /* DeleteMessagesResponse.swift */, + FDF848A429405C5A007DCAE5 /* DeleteAllMessagesRequest.swift */, + FDF848B329405C5A007DCAE5 /* DeleteAllMessagesResponse.swift */, + FDF848A829405C5A007DCAE5 /* DeleteAllBeforeRequest.swift */, + FDF848B229405C5A007DCAE5 /* DeleteAllBeforeResponse.swift */, + FDF8489F29405C5A007DCAE5 /* UpdateExpiryRequest.swift */, + FDF848AE29405C5A007DCAE5 /* UpdateExpiryResponse.swift */, + FDF848AC29405C5A007DCAE5 /* UpdateExpiryAllRequest.swift */, + FDF848B129405C5A007DCAE5 /* UpdateExpiryAllResponse.swift */, + FDF848BA29405C5A007DCAE5 /* RevokeSubkeyRequest.swift */, + FDF848A229405C5A007DCAE5 /* RevokeSubkeyResponse.swift */, + FDF848B629405C5A007DCAE5 /* SwarmSnode.swift */, + FDF848A929405C5A007DCAE5 /* SnodePoolResponse.swift */, + FDF848AA29405C5A007DCAE5 /* SnodeReceivedMessage.swift */, + FDF848B429405C5A007DCAE5 /* SnodeMessage.swift */, + FDF848AB29405C5A007DCAE5 /* GetNetworkTimestampResponse.swift */, + FDF848A329405C5A007DCAE5 /* GetSwarmRequest.swift */, + FDF848A029405C5A007DCAE5 /* OxenDaemonRPCRequest.swift */, + FDF848A629405C5A007DCAE5 /* ONSResolveRequest.swift */, + FDF8489E29405C5A007DCAE5 /* ONSResolveResponse.swift */, + FDF8489C29405C5A007DCAE5 /* GetServiceNodesRequest.swift */, + ); + path = Models; + sourceTree = ""; + }; FDFDE122282D04E30098B17F /* Transitions */ = { isa = PBXGroup; children = ( @@ -4290,7 +4317,6 @@ C33FDD5B255A582000E217F9 /* OWSOperation.h in Headers */, C33FDD7C255A582000E217F9 /* SSKAsserts.h in Headers */, C38EF24C255B6D67007E1867 /* NSAttributedString+OWS.h in Headers */, - C38EF32B255B6DBF007E1867 /* OWSFormat.h in Headers */, C33FDDCC255A582000E217F9 /* TSConstants.h in Headers */, C33FDDBD255A582000E217F9 /* ByteParser.h in Headers */, C33FDDB3255A582000E217F9 /* OWSError.h in Headers */, @@ -4349,7 +4375,7 @@ isa = PBXNativeTarget; buildConfigurationList = 453518761FC635DD00210559 /* Build configuration list for PBXNativeTarget "SessionShareExtension" */; buildPhases = ( - BE7147D3A1A96E25B7541FD7 /* [CP] Check Pods Manifest.lock */, + 55CE11E14880742A24ADC127 /* [CP] Check Pods Manifest.lock */, 453518641FC635DD00210559 /* Sources */, 453518651FC635DD00210559 /* Frameworks */, 453518661FC635DD00210559 /* Resources */, @@ -4372,7 +4398,7 @@ isa = PBXNativeTarget; buildConfigurationList = 7BC01A45241F40AB00BC7C55 /* Build configuration list for PBXNativeTarget "SessionNotificationServiceExtension" */; buildPhases = ( - D08C2A7507F29F5B2E8686D0 /* [CP] Check Pods Manifest.lock */, + 18CDA58AE057F8C9AE71F46E /* [CP] Check Pods Manifest.lock */, 7BC01A37241F40AB00BC7C55 /* Sources */, 7BC01A38241F40AB00BC7C55 /* Frameworks */, 7BC01A39241F40AB00BC7C55 /* Resources */, @@ -4394,7 +4420,7 @@ isa = PBXNativeTarget; buildConfigurationList = C331FF262558F9D400070591 /* Build configuration list for PBXNativeTarget "SessionUIKit" */; buildPhases = ( - 3EB40416B01456AA719B686B /* [CP] Check Pods Manifest.lock */, + D5AFDC09857840D2D2631E2D /* [CP] Check Pods Manifest.lock */, C331FF162558F9D300070591 /* Headers */, C331FF172558F9D300070591 /* Sources */, C331FF182558F9D300070591 /* Frameworks */, @@ -4414,7 +4440,7 @@ isa = PBXNativeTarget; buildConfigurationList = C33FD9B6255A548A00E217F9 /* Build configuration list for PBXNativeTarget "SignalUtilitiesKit" */; buildPhases = ( - B3755B8F0046FD78A100ADF5 /* [CP] Check Pods Manifest.lock */, + 5CE8055024B876590AED6DEA /* [CP] Check Pods Manifest.lock */, C33FD9A6255A548A00E217F9 /* Headers */, C33FD9A7255A548A00E217F9 /* Sources */, C33FD9A8255A548A00E217F9 /* Frameworks */, @@ -4433,7 +4459,7 @@ isa = PBXNativeTarget; buildConfigurationList = C3C2A5AA255385C100C340D1 /* Build configuration list for PBXNativeTarget "SessionSnodeKit" */; buildPhases = ( - 0DAEB0CC30945175049E8D88 /* [CP] Check Pods Manifest.lock */, + 77F55C879DAF28750120D343 /* [CP] Check Pods Manifest.lock */, C3C2A59A255385C100C340D1 /* Headers */, C3C2A59B255385C100C340D1 /* Sources */, C3C2A59C255385C100C340D1 /* Frameworks */, @@ -4452,7 +4478,7 @@ isa = PBXNativeTarget; buildConfigurationList = C3C2A684255388CC00C340D1 /* Build configuration list for PBXNativeTarget "SessionUtilitiesKit" */; buildPhases = ( - AF68D547A722E10BF230F662 /* [CP] Check Pods Manifest.lock */, + 446B0E16474DF9F15509BC64 /* [CP] Check Pods Manifest.lock */, C3C2A674255388CC00C340D1 /* Headers */, C3C2A675255388CC00C340D1 /* Sources */, C3C2A676255388CC00C340D1 /* Frameworks */, @@ -4471,7 +4497,7 @@ isa = PBXNativeTarget; buildConfigurationList = C3C2A6F925539DE700C340D1 /* Build configuration list for PBXNativeTarget "SessionMessagingKit" */; buildPhases = ( - 216EE1F2AA1CC3CBD9416785 /* [CP] Check Pods Manifest.lock */, + 2014435DF351DF6C60122751 /* [CP] Check Pods Manifest.lock */, C3C2A6EB25539DE700C340D1 /* Headers */, C3C2A6EC25539DE700C340D1 /* Sources */, C3C2A6ED25539DE700C340D1 /* Frameworks */, @@ -4491,14 +4517,14 @@ isa = PBXNativeTarget; buildConfigurationList = D221A0BC169C9E5F00537ABF /* Build configuration list for PBXNativeTarget "Session" */; buildPhases = ( - 0401967CF3320CC84B175A3B /* [CP] Check Pods Manifest.lock */, + 351E727E03A8F141EA25FBF4 /* [CP] Check Pods Manifest.lock */, FDE7214D287E50820093DF33 /* Lint Localizable.strings */, D221A085169C9E5E00537ABF /* Sources */, D221A086169C9E5E00537ABF /* Frameworks */, D221A087169C9E5E00537ABF /* Resources */, 453518771FC635DD00210559 /* Embed Foundation Extensions */, 4535189F1FC63DBF00210559 /* Embed Frameworks */, - 6E75B456D9C7705F6FD9C9D4 /* [CP] Embed Pods Frameworks */, + 90DF4725BB1271EBA2C66A12 /* [CP] Embed Pods Frameworks */, ); buildRules = ( ); @@ -4520,11 +4546,11 @@ isa = PBXNativeTarget; buildConfigurationList = FD71160F28D00BAE00B47552 /* Build configuration list for PBXNativeTarget "SessionTests" */; buildPhases = ( - 567FCF8CB93B411EE1FD4BBF /* [CP] Check Pods Manifest.lock */, + 19CD7B4EDC153293FB61CBA1 /* [CP] Check Pods Manifest.lock */, FD71160528D00BAE00B47552 /* Sources */, FD71160628D00BAE00B47552 /* Frameworks */, FD71160728D00BAE00B47552 /* Resources */, - A08B0675BD19884F61FF48D9 /* [CP] Embed Pods Frameworks */, + 23B58F2A2BA9E3295EA451C1 /* [CP] Embed Pods Frameworks */, ); buildRules = ( ); @@ -4540,11 +4566,11 @@ isa = PBXNativeTarget; buildConfigurationList = FD83B9B627CF200A005E1583 /* Build configuration list for PBXNativeTarget "SessionUtilitiesKitTests" */; buildPhases = ( - 73A908879E7A14832EF8B14A /* [CP] Check Pods Manifest.lock */, + EDDFB3BFBD5E1378BD03AAAB /* [CP] Check Pods Manifest.lock */, FD83B9AB27CF200A005E1583 /* Sources */, FD83B9AC27CF200A005E1583 /* Frameworks */, FD83B9AD27CF200A005E1583 /* Resources */, - 4124998F864C780E396BCF56 /* [CP] Embed Pods Frameworks */, + 49439A35FE57D3C0768A8127 /* [CP] Embed Pods Frameworks */, ); buildRules = ( ); @@ -4561,11 +4587,11 @@ isa = PBXNativeTarget; buildConfigurationList = FDC4389527B9FFC700C60D73 /* Build configuration list for PBXNativeTarget "SessionMessagingKitTests" */; buildPhases = ( - F30F419364B564D672BA0940 /* [CP] Check Pods Manifest.lock */, + 0E6C1748F41E48ED59563D96 /* [CP] Check Pods Manifest.lock */, FDC4388A27B9FFC700C60D73 /* Sources */, FDC4388B27B9FFC700C60D73 /* Frameworks */, FDC4388C27B9FFC700C60D73 /* Resources */, - BE3DD274CEE45348F1CA328A /* [CP] Embed Pods Frameworks */, + E0D19D723F633D7EE6163A84 /* [CP] Embed Pods Frameworks */, ); buildRules = ( ); @@ -4879,7 +4905,7 @@ /* End PBXResourcesBuildPhase section */ /* Begin PBXShellScriptBuildPhase section */ - 0401967CF3320CC84B175A3B /* [CP] Check Pods Manifest.lock */ = { + 0E6C1748F41E48ED59563D96 /* [CP] Check Pods Manifest.lock */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; files = ( @@ -4894,258 +4920,14 @@ outputFileListPaths = ( ); outputPaths = ( - "$(DERIVED_FILE_DIR)/Pods-GlobalDependencies-Session-checkManifestLockResult.txt", + "$(DERIVED_FILE_DIR)/Pods-GlobalDependencies-FrameworkAndExtensionDependencies-ExtendedDependencies-SessionMessagingKit-SessionMessagingKitTests-checkManifestLockResult.txt", ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; showEnvVarsInLog = 0; }; - 0DAEB0CC30945175049E8D88 /* [CP] Check Pods Manifest.lock */ = { - isa = PBXShellScriptBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - inputFileListPaths = ( - ); - inputPaths = ( - "${PODS_PODFILE_DIR_PATH}/Podfile.lock", - "${PODS_ROOT}/Manifest.lock", - ); - name = "[CP] Check Pods Manifest.lock"; - outputFileListPaths = ( - ); - outputPaths = ( - "$(DERIVED_FILE_DIR)/Pods-GlobalDependencies-FrameworkAndExtensionDependencies-SessionSnodeKit-checkManifestLockResult.txt", - ); - runOnlyForDeploymentPostprocessing = 0; - shellPath = /bin/sh; - shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; - showEnvVarsInLog = 0; - }; - 216EE1F2AA1CC3CBD9416785 /* [CP] Check Pods Manifest.lock */ = { - isa = PBXShellScriptBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - inputFileListPaths = ( - ); - inputPaths = ( - "${PODS_PODFILE_DIR_PATH}/Podfile.lock", - "${PODS_ROOT}/Manifest.lock", - ); - name = "[CP] Check Pods Manifest.lock"; - outputFileListPaths = ( - ); - outputPaths = ( - "$(DERIVED_FILE_DIR)/Pods-GlobalDependencies-FrameworkAndExtensionDependencies-ExtendedDependencies-SessionMessagingKit-checkManifestLockResult.txt", - ); - runOnlyForDeploymentPostprocessing = 0; - shellPath = /bin/sh; - shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; - showEnvVarsInLog = 0; - }; - 3EB40416B01456AA719B686B /* [CP] Check Pods Manifest.lock */ = { - isa = PBXShellScriptBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - inputFileListPaths = ( - ); - inputPaths = ( - "${PODS_PODFILE_DIR_PATH}/Podfile.lock", - "${PODS_ROOT}/Manifest.lock", - ); - name = "[CP] Check Pods Manifest.lock"; - outputFileListPaths = ( - ); - outputPaths = ( - "$(DERIVED_FILE_DIR)/Pods-GlobalDependencies-SessionUIKit-checkManifestLockResult.txt", - ); - runOnlyForDeploymentPostprocessing = 0; - shellPath = /bin/sh; - shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; - showEnvVarsInLog = 0; - }; - 4124998F864C780E396BCF56 /* [CP] Embed Pods Frameworks */ = { - isa = PBXShellScriptBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - inputFileListPaths = ( - "${PODS_ROOT}/Target Support Files/Pods-GlobalDependencies-FrameworkAndExtensionDependencies-ExtendedDependencies-SessionUtilitiesKit-SessionUtilitiesKitTests/Pods-GlobalDependencies-FrameworkAndExtensionDependencies-ExtendedDependencies-SessionUtilitiesKit-SessionUtilitiesKitTests-frameworks-${CONFIGURATION}-input-files.xcfilelist", - ); - name = "[CP] Embed Pods Frameworks"; - outputFileListPaths = ( - "${PODS_ROOT}/Target Support Files/Pods-GlobalDependencies-FrameworkAndExtensionDependencies-ExtendedDependencies-SessionUtilitiesKit-SessionUtilitiesKitTests/Pods-GlobalDependencies-FrameworkAndExtensionDependencies-ExtendedDependencies-SessionUtilitiesKit-SessionUtilitiesKitTests-frameworks-${CONFIGURATION}-output-files.xcfilelist", - ); - runOnlyForDeploymentPostprocessing = 0; - shellPath = /bin/sh; - shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-GlobalDependencies-FrameworkAndExtensionDependencies-ExtendedDependencies-SessionUtilitiesKit-SessionUtilitiesKitTests/Pods-GlobalDependencies-FrameworkAndExtensionDependencies-ExtendedDependencies-SessionUtilitiesKit-SessionUtilitiesKitTests-frameworks.sh\"\n"; - showEnvVarsInLog = 0; - }; - 567FCF8CB93B411EE1FD4BBF /* [CP] Check Pods Manifest.lock */ = { - isa = PBXShellScriptBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - inputFileListPaths = ( - ); - inputPaths = ( - "${PODS_PODFILE_DIR_PATH}/Podfile.lock", - "${PODS_ROOT}/Manifest.lock", - ); - name = "[CP] Check Pods Manifest.lock"; - outputFileListPaths = ( - ); - outputPaths = ( - "$(DERIVED_FILE_DIR)/Pods-GlobalDependencies-Session-SessionTests-checkManifestLockResult.txt", - ); - runOnlyForDeploymentPostprocessing = 0; - shellPath = /bin/sh; - shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; - showEnvVarsInLog = 0; - }; - 6E75B456D9C7705F6FD9C9D4 /* [CP] Embed Pods Frameworks */ = { - isa = PBXShellScriptBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - inputFileListPaths = ( - "${PODS_ROOT}/Target Support Files/Pods-GlobalDependencies-Session/Pods-GlobalDependencies-Session-frameworks-${CONFIGURATION}-input-files.xcfilelist", - ); - name = "[CP] Embed Pods Frameworks"; - outputFileListPaths = ( - "${PODS_ROOT}/Target Support Files/Pods-GlobalDependencies-Session/Pods-GlobalDependencies-Session-frameworks-${CONFIGURATION}-output-files.xcfilelist", - ); - runOnlyForDeploymentPostprocessing = 0; - shellPath = /bin/sh; - shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-GlobalDependencies-Session/Pods-GlobalDependencies-Session-frameworks.sh\"\n"; - showEnvVarsInLog = 0; - }; - 73A908879E7A14832EF8B14A /* [CP] Check Pods Manifest.lock */ = { - isa = PBXShellScriptBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - inputFileListPaths = ( - ); - inputPaths = ( - "${PODS_PODFILE_DIR_PATH}/Podfile.lock", - "${PODS_ROOT}/Manifest.lock", - ); - name = "[CP] Check Pods Manifest.lock"; - outputFileListPaths = ( - ); - outputPaths = ( - "$(DERIVED_FILE_DIR)/Pods-GlobalDependencies-FrameworkAndExtensionDependencies-ExtendedDependencies-SessionUtilitiesKit-SessionUtilitiesKitTests-checkManifestLockResult.txt", - ); - runOnlyForDeploymentPostprocessing = 0; - shellPath = /bin/sh; - shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; - showEnvVarsInLog = 0; - }; - A08B0675BD19884F61FF48D9 /* [CP] Embed Pods Frameworks */ = { - isa = PBXShellScriptBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - inputFileListPaths = ( - "${PODS_ROOT}/Target Support Files/Pods-GlobalDependencies-Session-SessionTests/Pods-GlobalDependencies-Session-SessionTests-frameworks-${CONFIGURATION}-input-files.xcfilelist", - ); - name = "[CP] Embed Pods Frameworks"; - outputFileListPaths = ( - "${PODS_ROOT}/Target Support Files/Pods-GlobalDependencies-Session-SessionTests/Pods-GlobalDependencies-Session-SessionTests-frameworks-${CONFIGURATION}-output-files.xcfilelist", - ); - runOnlyForDeploymentPostprocessing = 0; - shellPath = /bin/sh; - shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-GlobalDependencies-Session-SessionTests/Pods-GlobalDependencies-Session-SessionTests-frameworks.sh\"\n"; - showEnvVarsInLog = 0; - }; - AF68D547A722E10BF230F662 /* [CP] Check Pods Manifest.lock */ = { - isa = PBXShellScriptBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - inputFileListPaths = ( - ); - inputPaths = ( - "${PODS_PODFILE_DIR_PATH}/Podfile.lock", - "${PODS_ROOT}/Manifest.lock", - ); - name = "[CP] Check Pods Manifest.lock"; - outputFileListPaths = ( - ); - outputPaths = ( - "$(DERIVED_FILE_DIR)/Pods-GlobalDependencies-FrameworkAndExtensionDependencies-ExtendedDependencies-SessionUtilitiesKit-checkManifestLockResult.txt", - ); - runOnlyForDeploymentPostprocessing = 0; - shellPath = /bin/sh; - shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; - showEnvVarsInLog = 0; - }; - B3755B8F0046FD78A100ADF5 /* [CP] Check Pods Manifest.lock */ = { - isa = PBXShellScriptBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - inputFileListPaths = ( - ); - inputPaths = ( - "${PODS_PODFILE_DIR_PATH}/Podfile.lock", - "${PODS_ROOT}/Manifest.lock", - ); - name = "[CP] Check Pods Manifest.lock"; - outputFileListPaths = ( - ); - outputPaths = ( - "$(DERIVED_FILE_DIR)/Pods-GlobalDependencies-FrameworkAndExtensionDependencies-ExtendedDependencies-SignalUtilitiesKit-checkManifestLockResult.txt", - ); - runOnlyForDeploymentPostprocessing = 0; - shellPath = /bin/sh; - shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; - showEnvVarsInLog = 0; - }; - BE3DD274CEE45348F1CA328A /* [CP] Embed Pods Frameworks */ = { - isa = PBXShellScriptBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - inputFileListPaths = ( - "${PODS_ROOT}/Target Support Files/Pods-GlobalDependencies-FrameworkAndExtensionDependencies-ExtendedDependencies-SessionMessagingKit-SessionMessagingKitTests/Pods-GlobalDependencies-FrameworkAndExtensionDependencies-ExtendedDependencies-SessionMessagingKit-SessionMessagingKitTests-frameworks-${CONFIGURATION}-input-files.xcfilelist", - ); - name = "[CP] Embed Pods Frameworks"; - outputFileListPaths = ( - "${PODS_ROOT}/Target Support Files/Pods-GlobalDependencies-FrameworkAndExtensionDependencies-ExtendedDependencies-SessionMessagingKit-SessionMessagingKitTests/Pods-GlobalDependencies-FrameworkAndExtensionDependencies-ExtendedDependencies-SessionMessagingKit-SessionMessagingKitTests-frameworks-${CONFIGURATION}-output-files.xcfilelist", - ); - runOnlyForDeploymentPostprocessing = 0; - shellPath = /bin/sh; - shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-GlobalDependencies-FrameworkAndExtensionDependencies-ExtendedDependencies-SessionMessagingKit-SessionMessagingKitTests/Pods-GlobalDependencies-FrameworkAndExtensionDependencies-ExtendedDependencies-SessionMessagingKit-SessionMessagingKitTests-frameworks.sh\"\n"; - showEnvVarsInLog = 0; - }; - BE7147D3A1A96E25B7541FD7 /* [CP] Check Pods Manifest.lock */ = { - isa = PBXShellScriptBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - inputFileListPaths = ( - ); - inputPaths = ( - "${PODS_PODFILE_DIR_PATH}/Podfile.lock", - "${PODS_ROOT}/Manifest.lock", - ); - name = "[CP] Check Pods Manifest.lock"; - outputFileListPaths = ( - ); - outputPaths = ( - "$(DERIVED_FILE_DIR)/Pods-GlobalDependencies-FrameworkAndExtensionDependencies-ExtendedDependencies-SessionShareExtension-checkManifestLockResult.txt", - ); - runOnlyForDeploymentPostprocessing = 0; - shellPath = /bin/sh; - shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; - showEnvVarsInLog = 0; - }; - D08C2A7507F29F5B2E8686D0 /* [CP] Check Pods Manifest.lock */ = { + 18CDA58AE057F8C9AE71F46E /* [CP] Check Pods Manifest.lock */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; files = ( @@ -5167,7 +4949,7 @@ shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; showEnvVarsInLog = 0; }; - F30F419364B564D672BA0940 /* [CP] Check Pods Manifest.lock */ = { + 19CD7B4EDC153293FB61CBA1 /* [CP] Check Pods Manifest.lock */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; files = ( @@ -5182,7 +4964,251 @@ outputFileListPaths = ( ); outputPaths = ( - "$(DERIVED_FILE_DIR)/Pods-GlobalDependencies-FrameworkAndExtensionDependencies-ExtendedDependencies-SessionMessagingKit-SessionMessagingKitTests-checkManifestLockResult.txt", + "$(DERIVED_FILE_DIR)/Pods-GlobalDependencies-Session-SessionTests-checkManifestLockResult.txt", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; + showEnvVarsInLog = 0; + }; + 2014435DF351DF6C60122751 /* [CP] Check Pods Manifest.lock */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + "${PODS_PODFILE_DIR_PATH}/Podfile.lock", + "${PODS_ROOT}/Manifest.lock", + ); + name = "[CP] Check Pods Manifest.lock"; + outputFileListPaths = ( + ); + outputPaths = ( + "$(DERIVED_FILE_DIR)/Pods-GlobalDependencies-FrameworkAndExtensionDependencies-ExtendedDependencies-SessionMessagingKit-checkManifestLockResult.txt", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; + showEnvVarsInLog = 0; + }; + 23B58F2A2BA9E3295EA451C1 /* [CP] Embed Pods Frameworks */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-GlobalDependencies-Session-SessionTests/Pods-GlobalDependencies-Session-SessionTests-frameworks-${CONFIGURATION}-input-files.xcfilelist", + ); + name = "[CP] Embed Pods Frameworks"; + outputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-GlobalDependencies-Session-SessionTests/Pods-GlobalDependencies-Session-SessionTests-frameworks-${CONFIGURATION}-output-files.xcfilelist", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-GlobalDependencies-Session-SessionTests/Pods-GlobalDependencies-Session-SessionTests-frameworks.sh\"\n"; + showEnvVarsInLog = 0; + }; + 351E727E03A8F141EA25FBF4 /* [CP] Check Pods Manifest.lock */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + "${PODS_PODFILE_DIR_PATH}/Podfile.lock", + "${PODS_ROOT}/Manifest.lock", + ); + name = "[CP] Check Pods Manifest.lock"; + outputFileListPaths = ( + ); + outputPaths = ( + "$(DERIVED_FILE_DIR)/Pods-GlobalDependencies-Session-checkManifestLockResult.txt", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; + showEnvVarsInLog = 0; + }; + 446B0E16474DF9F15509BC64 /* [CP] Check Pods Manifest.lock */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + "${PODS_PODFILE_DIR_PATH}/Podfile.lock", + "${PODS_ROOT}/Manifest.lock", + ); + name = "[CP] Check Pods Manifest.lock"; + outputFileListPaths = ( + ); + outputPaths = ( + "$(DERIVED_FILE_DIR)/Pods-GlobalDependencies-FrameworkAndExtensionDependencies-ExtendedDependencies-SessionUtilitiesKit-checkManifestLockResult.txt", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; + showEnvVarsInLog = 0; + }; + 49439A35FE57D3C0768A8127 /* [CP] Embed Pods Frameworks */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-GlobalDependencies-FrameworkAndExtensionDependencies-ExtendedDependencies-SessionUtilitiesKit-SessionUtilitiesKitTests/Pods-GlobalDependencies-FrameworkAndExtensionDependencies-ExtendedDependencies-SessionUtilitiesKit-SessionUtilitiesKitTests-frameworks-${CONFIGURATION}-input-files.xcfilelist", + ); + name = "[CP] Embed Pods Frameworks"; + outputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-GlobalDependencies-FrameworkAndExtensionDependencies-ExtendedDependencies-SessionUtilitiesKit-SessionUtilitiesKitTests/Pods-GlobalDependencies-FrameworkAndExtensionDependencies-ExtendedDependencies-SessionUtilitiesKit-SessionUtilitiesKitTests-frameworks-${CONFIGURATION}-output-files.xcfilelist", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-GlobalDependencies-FrameworkAndExtensionDependencies-ExtendedDependencies-SessionUtilitiesKit-SessionUtilitiesKitTests/Pods-GlobalDependencies-FrameworkAndExtensionDependencies-ExtendedDependencies-SessionUtilitiesKit-SessionUtilitiesKitTests-frameworks.sh\"\n"; + showEnvVarsInLog = 0; + }; + 55CE11E14880742A24ADC127 /* [CP] Check Pods Manifest.lock */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + "${PODS_PODFILE_DIR_PATH}/Podfile.lock", + "${PODS_ROOT}/Manifest.lock", + ); + name = "[CP] Check Pods Manifest.lock"; + outputFileListPaths = ( + ); + outputPaths = ( + "$(DERIVED_FILE_DIR)/Pods-GlobalDependencies-FrameworkAndExtensionDependencies-ExtendedDependencies-SessionShareExtension-checkManifestLockResult.txt", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; + showEnvVarsInLog = 0; + }; + 5CE8055024B876590AED6DEA /* [CP] Check Pods Manifest.lock */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + "${PODS_PODFILE_DIR_PATH}/Podfile.lock", + "${PODS_ROOT}/Manifest.lock", + ); + name = "[CP] Check Pods Manifest.lock"; + outputFileListPaths = ( + ); + outputPaths = ( + "$(DERIVED_FILE_DIR)/Pods-GlobalDependencies-FrameworkAndExtensionDependencies-ExtendedDependencies-SignalUtilitiesKit-checkManifestLockResult.txt", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; + showEnvVarsInLog = 0; + }; + 77F55C879DAF28750120D343 /* [CP] Check Pods Manifest.lock */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + "${PODS_PODFILE_DIR_PATH}/Podfile.lock", + "${PODS_ROOT}/Manifest.lock", + ); + name = "[CP] Check Pods Manifest.lock"; + outputFileListPaths = ( + ); + outputPaths = ( + "$(DERIVED_FILE_DIR)/Pods-GlobalDependencies-FrameworkAndExtensionDependencies-SessionSnodeKit-checkManifestLockResult.txt", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; + showEnvVarsInLog = 0; + }; + 90DF4725BB1271EBA2C66A12 /* [CP] Embed Pods Frameworks */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-GlobalDependencies-Session/Pods-GlobalDependencies-Session-frameworks-${CONFIGURATION}-input-files.xcfilelist", + ); + name = "[CP] Embed Pods Frameworks"; + outputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-GlobalDependencies-Session/Pods-GlobalDependencies-Session-frameworks-${CONFIGURATION}-output-files.xcfilelist", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-GlobalDependencies-Session/Pods-GlobalDependencies-Session-frameworks.sh\"\n"; + showEnvVarsInLog = 0; + }; + D5AFDC09857840D2D2631E2D /* [CP] Check Pods Manifest.lock */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + "${PODS_PODFILE_DIR_PATH}/Podfile.lock", + "${PODS_ROOT}/Manifest.lock", + ); + name = "[CP] Check Pods Manifest.lock"; + outputFileListPaths = ( + ); + outputPaths = ( + "$(DERIVED_FILE_DIR)/Pods-GlobalDependencies-SessionUIKit-checkManifestLockResult.txt", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; + showEnvVarsInLog = 0; + }; + E0D19D723F633D7EE6163A84 /* [CP] Embed Pods Frameworks */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-GlobalDependencies-FrameworkAndExtensionDependencies-ExtendedDependencies-SessionMessagingKit-SessionMessagingKitTests/Pods-GlobalDependencies-FrameworkAndExtensionDependencies-ExtendedDependencies-SessionMessagingKit-SessionMessagingKitTests-frameworks-${CONFIGURATION}-input-files.xcfilelist", + ); + name = "[CP] Embed Pods Frameworks"; + outputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-GlobalDependencies-FrameworkAndExtensionDependencies-ExtendedDependencies-SessionMessagingKit-SessionMessagingKitTests/Pods-GlobalDependencies-FrameworkAndExtensionDependencies-ExtendedDependencies-SessionMessagingKit-SessionMessagingKitTests-frameworks-${CONFIGURATION}-output-files.xcfilelist", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-GlobalDependencies-FrameworkAndExtensionDependencies-ExtendedDependencies-SessionMessagingKit-SessionMessagingKitTests/Pods-GlobalDependencies-FrameworkAndExtensionDependencies-ExtendedDependencies-SessionMessagingKit-SessionMessagingKitTests-frameworks.sh\"\n"; + showEnvVarsInLog = 0; + }; + EDDFB3BFBD5E1378BD03AAAB /* [CP] Check Pods Manifest.lock */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + "${PODS_PODFILE_DIR_PATH}/Podfile.lock", + "${PODS_ROOT}/Manifest.lock", + ); + name = "[CP] Check Pods Manifest.lock"; + outputFileListPaths = ( + ); + outputPaths = ( + "$(DERIVED_FILE_DIR)/Pods-GlobalDependencies-FrameworkAndExtensionDependencies-ExtendedDependencies-SessionUtilitiesKit-SessionUtilitiesKitTests-checkManifestLockResult.txt", ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; @@ -5217,7 +5243,7 @@ files = ( FD3AABE928306BBD00E5099A /* ThreadPickerViewModel.swift in Sources */, B817AD9A26436593009DF825 /* SimplifiedConversationCell.swift in Sources */, - C3ADC66126426688005F1414 /* ShareVC.swift in Sources */, + C3ADC66126426688005F1414 /* ShareNavController.swift in Sources */, 7BAF54D427ACCF01003D12F8 /* SAEScreenLockViewController.swift in Sources */, B817AD9C26436F73009DF825 /* ThreadPickerVC.swift in Sources */, 7BAF54D327ACCF01003D12F8 /* ShareAppExtensionContext.swift in Sources */, @@ -5249,6 +5275,7 @@ FD71165B28E6DDBC00B47552 /* StyledNavigationController.swift in Sources */, C331FFE32558FB0000070591 /* TabBar.swift in Sources */, FD37E9D528A1FCE8003AE748 /* Theme+OceanLight.swift in Sources */, + FDF848F129406A30007DCAE5 /* Format.swift in Sources */, FD37E9C828A1D73F003AE748 /* Theme+Colors.swift in Sources */, FD37EA0128A60473003AE748 /* UIKit+Theme.swift in Sources */, FD37E9CF28A1EB1B003AE748 /* Theme.swift in Sources */, @@ -5314,7 +5341,6 @@ C38EF3C7255B6DE7007E1867 /* ImageEditorCanvasView.swift in Sources */, C38EF400255B6DF7007E1867 /* GalleryRailView.swift in Sources */, C38EF32E255B6DBF007E1867 /* ImageCache.swift in Sources */, - C38EF32F255B6DBF007E1867 /* OWSFormat.m in Sources */, FD87DD0428B8727D00AF0F98 /* Configuration.swift in Sources */, C38EF3BA255B6DE7007E1867 /* ImageEditorItem.swift in Sources */, C33FDD3A255A582000E217F9 /* Notification+Loki.swift in Sources */, @@ -5362,45 +5388,68 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( - FD77289C284DDCE10018502F /* SnodePoolResponse.swift in Sources */, - FD09796927F6BEA700936362 /* SwarmSnode.swift in Sources */, + FDF8488B29405BF2007DCAE5 /* SSKDependencies.swift in Sources */, + FDF8488E29405C04007DCAE5 /* GetSnodePoolJob.swift in Sources */, + FDF848C329405C5A007DCAE5 /* DeleteMessagesRequest.swift in Sources */, + FDF8489129405C13007DCAE5 /* SnodeAPINamespace.swift in Sources */, C3C2A5E02553860B00C340D1 /* Threading.swift in Sources */, - C3C2A5BF255385EE00C340D1 /* SnodeMessage.swift in Sources */, - FD17D7E127F67BD400122BE0 /* SnodeReceivedMessage.swift in Sources */, - FDC438B127BB159600C60D73 /* RequestInfo.swift in Sources */, - FDC438B927BB161E00C60D73 /* OnionRequestAPIVersion.swift in Sources */, - FD7728A0284EF5810018502F /* SnodeAPIError.swift in Sources */, + FDF848C529405C5B007DCAE5 /* GetSwarmRequest.swift in Sources */, + FDF848D729405C5B007DCAE5 /* SnodeBatchRequest.swift in Sources */, C3C2A5C0255385EE00C340D1 /* Snode.swift in Sources */, + FDF848D829405C5B007DCAE5 /* SwarmSnode.swift in Sources */, + FDF848CE29405C5B007DCAE5 /* UpdateExpiryAllRequest.swift in Sources */, + FDF848C229405C5A007DCAE5 /* OxenDaemonRPCRequest.swift in Sources */, + FDF848DC29405C5B007DCAE5 /* RevokeSubkeyRequest.swift in Sources */, FD17D7A027F40CC800122BE0 /* _001_InitialSetupMigration.swift in Sources */, - C3C2A5C7255385EE00C340D1 /* SnodeAPI.swift in Sources */, - C3C2A5C6255385EE00C340D1 /* Notification+OnionRequestAPI.swift in Sources */, + FDF848EC29405E4F007DCAE5 /* OnionRequestAPI+Encryption.swift in Sources */, FD17D7AA27F41BF500122BE0 /* SnodeSet.swift in Sources */, + FDF848D029405C5B007DCAE5 /* UpdateExpiryResponse.swift in Sources */, + FDF848D329405C5B007DCAE5 /* UpdateExpiryAllResponse.swift in Sources */, + FDF848BC29405C5A007DCAE5 /* SnodeRecursiveResponse.swift in Sources */, + FDF848C029405C5A007DCAE5 /* ONSResolveResponse.swift in Sources */, FD17D7A427F40F8100122BE0 /* _003_YDBToGRDBMigration.swift in Sources */, - C3C2A5C4255385EE00C340D1 /* OnionRequestAPI+Encryption.swift in Sources */, - FD17D7D227F5797A00122BE0 /* SnodeAPIEndpoint.swift in Sources */, + FDF848C629405C5B007DCAE5 /* DeleteAllMessagesRequest.swift in Sources */, C3C2A5DE2553860B00C340D1 /* String+Trimming.swift in Sources */, - FD26FA7B291DF8F3005801D8 /* SnodeAPINamespace.swift in Sources */, - FD8ECF6E292C9EA100C0D1BB /* GetServiceNodesRequest.swift in Sources */, - C3C2A5C4255385EE00C340D1 /* OnionRequestAPI+Encryption.swift in Sources */, - FD17D7D227F5797A00122BE0 /* SnodeAPIEndpoint.swift in Sources */, + FDF848D429405C5B007DCAE5 /* DeleteAllBeforeResponse.swift in Sources */, + FDF848D629405C5B007DCAE5 /* SnodeMessage.swift in Sources */, + FDF848D129405C5B007DCAE5 /* SnodeSwarmItem.swift in Sources */, + FDF848DD29405C5B007DCAE5 /* LegacySendMessageRequest.swift in Sources */, + FDF848BD29405C5A007DCAE5 /* GetMessagesRequest.swift in Sources */, + FD26FA7B291DF8F3005801D8 /* (null) in Sources */, + FDF848DB29405C5B007DCAE5 /* DeleteMessagesResponse.swift in Sources */, + FDF848E629405D6E007DCAE5 /* OnionRequestAPIDestination.swift in Sources */, + FDF848CC29405C5B007DCAE5 /* SnodeReceivedMessage.swift in Sources */, + FDF848C129405C5A007DCAE5 /* UpdateExpiryRequest.swift in Sources */, + FDF848C729405C5B007DCAE5 /* SendMessageResponse.swift in Sources */, + FDF848CA29405C5B007DCAE5 /* DeleteAllBeforeRequest.swift in Sources */, + FDF848D229405C5B007DCAE5 /* LegacyGetMessagesRequest.swift in Sources */, + FDF848CB29405C5B007DCAE5 /* SnodePoolResponse.swift in Sources */, + FDF848C429405C5A007DCAE5 /* RevokeSubkeyResponse.swift in Sources */, C3C2A5DE2553860B00C340D1 /* String+Trimming.swift in Sources */, - FD8ECF56292C327700C0D1BB /* LegacyGetMessagesRequest.swift in Sources */, - FD8ECF54292C2DB000C0D1BB /* SnodeAuthenticatedRequestBody.swift in Sources */, - FD26FA6D291DADAE005801D8 /* SnodeRequest.swift in Sources */, - FD8ECF50292C2B2B00C0D1BB /* DeleteAllBeforeRequest.swift in Sources */, - FD8ECF5A292C431B00C0D1BB /* GetSwarmRequest.swift in Sources */, + FD26FA6D291DADAE005801D8 /* (null) in Sources */, + FDF848E529405D6E007DCAE5 /* SnodeAPIError.swift in Sources */, + FDF848D529405C5B007DCAE5 /* DeleteAllMessagesResponse.swift in Sources */, + FDF848E329405D6E007DCAE5 /* OnionRequestAPIVersion.swift in Sources */, + FDF848BF29405C5A007DCAE5 /* SnodeResponse.swift in Sources */, C3C2A5E42553860B00C340D1 /* Data+Utilities.swift in Sources */, + FDF848D929405C5B007DCAE5 /* SnodeAuthenticatedRequestBody.swift in Sources */, + FDF848ED29405E4F007DCAE5 /* Notification+OnionRequestAPI.swift in Sources */, + FDF848CD29405C5B007DCAE5 /* GetNetworkTimestampResponse.swift in Sources */, + FDF848DA29405C5B007DCAE5 /* GetMessagesResponse.swift in Sources */, FD17D7A727F41AF000122BE0 /* SSKLegacy.swift in Sources */, - FDC438B327BB15B400C60D73 /* ResponseInfo.swift in Sources */, FD39353628F7C3390084DADA /* _004_FlagMessageHashAsDeletedOrInvalid.swift in Sources */, + FDF8489429405C1B007DCAE5 /* SnodeAPI.swift in Sources */, + FDF848C829405C5B007DCAE5 /* ONSResolveRequest.swift in Sources */, C3C2A5C2255385EE00C340D1 /* Configuration.swift in Sources */, - FD17D7D827F658E200122BE0 /* OnionRequestAPIDestination.swift in Sources */, + FDF848C929405C5B007DCAE5 /* SnodeRequest.swift in Sources */, + FDF848CF29405C5B007DCAE5 /* SendMessageRequest.swift in Sources */, FD6A7A6D2818C61500035AC1 /* _002_SetupStandardJobs.swift in Sources */, FD17D7B327F51E5B00122BE0 /* SSKSetting.swift in Sources */, + FDF848E429405D6E007DCAE5 /* SnodeAPIEndpoint.swift in Sources */, + FDF848E729405D6E007DCAE5 /* OnionRequestAPIError.swift in Sources */, + FDF848BE29405C5A007DCAE5 /* GetServiceNodesRequest.swift in Sources */, + FDF848EB29405E4F007DCAE5 /* OnionRequestAPI.swift in Sources */, FD17D7AE27F41C4300122BE0 /* SnodeReceivedMessageInfo.swift in Sources */, - C3C2A5C3255385EE00C340D1 /* OnionRequestAPI.swift in Sources */, - FD90040F2818AB6D00ABAAF6 /* GetSnodePoolJob.swift in Sources */, - FD17D7D427F6584600122BE0 /* OnionRequestAPIError.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -5412,6 +5461,7 @@ C32C5A47256DB8F0003C73A2 /* ECKeyPair+Hexadecimal.swift in Sources */, 7B1D74B027C365960030B423 /* Timer+MainThread.swift in Sources */, C32C5D83256DD5B6003C73A2 /* SSKKeychainStorage.swift in Sources */, + FDF8488329405A12007DCAE5 /* BatchResponse.swift in Sources */, C3D9E39B256763C20040E4F3 /* AppContext.m in Sources */, C3BBE0A82554D4DE0050F1E3 /* JSON.swift in Sources */, FD17D7C127F5200100122BE0 /* TypedTableDefinition.swift in Sources */, @@ -5426,6 +5476,7 @@ B8856DEF256F161F001CE70E /* NSString+SSK.m in Sources */, FD09C5E2282212B3000CE219 /* JobDependencies.swift in Sources */, FDED2E3C282E1B5D00B2CD2A /* UICollectionView+ReusableView.swift in Sources */, + FDF8487B29405906007DCAE5 /* HTTPHeader.swift in Sources */, FD9004122818ABDC00ABAAF6 /* Job.swift in Sources */, FD09797927FAB7E800936362 /* ImageFormat.swift in Sources */, FD7115FE28C8202D00B47552 /* ReplaySubject.swift in Sources */, @@ -5434,8 +5485,8 @@ C32C5DD2256DD9E5003C73A2 /* LRUCache.swift in Sources */, FD71160228C8255900B47552 /* UIControl+Combine.swift in Sources */, FD9004152818B46300ABAAF6 /* JobRunner.swift in Sources */, + FDF8487929405906007DCAE5 /* HTTPQueryParam.swift in Sources */, C3A7211A2558BCA10043A11F /* DiffieHellman.swift in Sources */, - FD26FA53291CACA9005801D8 /* BatchResponse.swift in Sources */, FD17D7CA27F546D900122BE0 /* _001_InitialSetupMigration.swift in Sources */, C3D9E4D12567777D0040E4F3 /* OWSMediaUtils.swift in Sources */, C3BBE0AA2554D4DE0050F1E3 /* Dictionary+Utilities.swift in Sources */, @@ -5453,7 +5504,6 @@ FD848B9328420164000E298B /* UnicodeScalar+Utilities.swift in Sources */, FD09796B27F6C67500936362 /* Failable.swift in Sources */, FD7115FA28C8153400B47552 /* UIBarButtonItem+Combine.swift in Sources */, - FD8ECFA1293D8FDD00C0D1BB /* URLResponse+Utilities.swift in Sources */, FD705A92278D051200F16121 /* ReusableView.swift in Sources */, FD17D7BA27F51F2100122BE0 /* TargetMigrations.swift in Sources */, FD17D7C327F5204C00122BE0 /* Database+Utilities.swift in Sources */, @@ -5464,6 +5514,7 @@ B8856DE6256F15F2001CE70E /* String+SSK.swift in Sources */, FDF2220F281B55E6000A4995 /* QueryInterfaceRequest+Utilities.swift in Sources */, C3471ED42555386B00297E91 /* AESGCM.swift in Sources */, + FDF848EF294067E4007DCAE5 /* URLResponse+Utilities.swift in Sources */, FD848B9A28442CE6000E298B /* StorageError.swift in Sources */, FD17D7B827F51ECA00122BE0 /* Migration.swift in Sources */, FD7728982849E8110018502F /* UITableView+ReusableView.swift in Sources */, @@ -5474,6 +5525,7 @@ B8F5F58325EC94A6003BF8D4 /* Collection+Utilities.swift in Sources */, 7BD477A827EC39F5004E2822 /* Atomic.swift in Sources */, B8BC00C0257D90E30032E807 /* General.swift in Sources */, + FDF8488629405A61007DCAE5 /* Request.swift in Sources */, FD17D7A127F40D2500122BE0 /* Storage.swift in Sources */, FD1A94FB2900D1C2000D73D3 /* PersistableRecord+Utilities.swift in Sources */, FD5D201E27B0D87C00FEA984 /* SessionId.swift in Sources */, @@ -5487,24 +5539,29 @@ C3D9E4F4256778AF0040E4F3 /* NSData+Image.m in Sources */, FDF222092818D2B0000A4995 /* NSAttributedString+Utilities.swift in Sources */, C32C5E0C256DDAFA003C73A2 /* NSRegularExpression+SSK.swift in Sources */, + FDF8487C29405906007DCAE5 /* HTTPMethod.swift in Sources */, + FDF8488429405A2B007DCAE5 /* RequestInfo.swift in Sources */, C3BBE0A92554D4DE0050F1E3 /* HTTP.swift in Sources */, FD71160028C8253500B47552 /* UIView+Combine.swift in Sources */, B8856D23256F116B001CE70E /* Weak.swift in Sources */, FD17D7CD27F546FF00122BE0 /* Setting.swift in Sources */, FD7115FC28C8155800B47552 /* Publisher+Utilities.swift in Sources */, C32C5A48256DB8F0003C73A2 /* BuildConfiguration.swift in Sources */, + FDF84881294059F5007DCAE5 /* ResponseInfo.swift in Sources */, FD17D7BF27F51F8200122BE0 /* ColumnExpressible.swift in Sources */, FD17D7E527F6A09900122BE0 /* Identity.swift in Sources */, FD9004142818AD0B00ABAAF6 /* _002_SetupStandardJobs.swift in Sources */, + FDF8487A29405906007DCAE5 /* HTTPError.swift in Sources */, B87EF18126377A1D00124B3C /* Features.swift in Sources */, FD09797727FAB7A600936362 /* Data+Image.swift in Sources */, C300A60D2554B31900555489 /* Logging.swift in Sources */, B8FF8EA625C11FEF004D1F22 /* IPv4.swift in Sources */, C3D9E35525675EE10040E4F3 /* MIMETypeUtil.m in Sources */, FD9004162818B46700ABAAF6 /* JobRunnerError.swift in Sources */, + FDF8488929405B27007DCAE5 /* Data+Utilities.swift in Sources */, FD09797227FAA2F500936362 /* Optional+Utilities.swift in Sources */, FD7162DB281B6C440060647B /* TypedTableAlias.swift in Sources */, - FD26FA5E291CAFF9005801D8 /* Data+Utilities.swift in Sources */, + FD26FA5E291CAFF9005801D8 /* (null) in Sources */, FD7115F828C8151C00B47552 /* DisposableBarButtonItem.swift in Sources */, FD17D7E727F6A16700122BE0 /* _003_YDBToGRDBMigration.swift in Sources */, ); @@ -5532,6 +5589,7 @@ FD71161C28D194FB00B47552 /* MentionInfo.swift in Sources */, 7B4C75CB26B37E0F0000AC89 /* UnsendRequest.swift in Sources */, C300A5F22554B09800555489 /* MessageSender.swift in Sources */, + FDF848F729414477007DCAE5 /* CurrentUserPoller.swift in Sources */, B8B558FF26C4E05E00693325 /* WebRTCSession+MessageHandling.swift in Sources */, C3C2A74D2553A39700C340D1 /* VisibleMessage.swift in Sources */, FD09B7E7288670FD00ED0B66 /* Reaction.swift in Sources */, @@ -5554,7 +5612,7 @@ FD245C6A2850666F00B966DD /* FileServerAPI.swift in Sources */, FDC4386927B4E6B800C60D73 /* String+Utlities.swift in Sources */, FD716E6628502EE200C96BF4 /* CurrentCallProtocol.swift in Sources */, - FD26FA512919F9CE005801D8 /* GroupDeleteMessage.swift in Sources */, + FD26FA512919F9CE005801D8 /* (null) in Sources */, FD8ECF9029381FC200C0D1BB /* SessionUtil+UserProfile.swift in Sources */, FD09B7E5288670BB00ED0B66 /* _008_EmojiReacts.swift in Sources */, FDC4385F27B4C4A200C60D73 /* PinnedMessage.swift in Sources */, @@ -5582,7 +5640,7 @@ FD09C5EC282B8F18000CE219 /* AttachmentError.swift in Sources */, FD17D79927F40AB800122BE0 /* _003_YDBToGRDBMigration.swift in Sources */, FDF0B7512807BA56004C14C5 /* NotificationsProtocol.swift in Sources */, - FDC4387427B5BB9B00C60D73 /* Promise+Utilities.swift in Sources */, + FDC4387427B5BB9B00C60D73 /* (null) in Sources */, B8DE1FB426C22F2F0079C9CE /* WebRTCSession.swift in Sources */, FDC6D6F32860607300B04575 /* Environment.swift in Sources */, C3A3A171256E1D25004D228D /* SSKReachabilityManager.swift in Sources */, @@ -5590,11 +5648,11 @@ B8DE1FB626C22FCB0079C9CE /* CallMessage.swift in Sources */, FD245C50285065C700B966DD /* VisibleMessage+Quote.swift in Sources */, FD8ECF892935AB7200C0D1BB /* SessionUtilError.swift in Sources */, - FD8ECF42292B340D00C0D1BB /* SOGSBatchRequest.swift in Sources */, FD5C7307284F103B0029977D /* MessageReceiver+MessageRequests.swift in Sources */, C3A71D0B2558989C0043A11F /* MessageWrapper.swift in Sources */, FDE77F6B280FEB28002CFC5D /* ControlMessageProcessRecord.swift in Sources */, 7B93D07127CF194000811CB6 /* MessageRequestResponse.swift in Sources */, + FDF8488829405A9A007DCAE5 /* SOGSBatchRequest.swift in Sources */, FD245C662850665900B966DD /* OpenGroupAPI.swift in Sources */, FD245C5B2850660500B966DD /* ReadReceipt.swift in Sources */, B8F5F60325EDE16F003BF8D4 /* DataExtractionNotification.swift in Sources */, @@ -5603,6 +5661,7 @@ FDF0B74F28079E5E004C14C5 /* SendReadReceiptsJob.swift in Sources */, FDF0B7422804EA4F004C14C5 /* _002_SetupStandardJobs.swift in Sources */, B8EB20EE2640F28000773E52 /* VisibleMessage+OpenGroupInvitation.swift in Sources */, + FDF8487F29405994007DCAE5 /* HTTPHeader+OpenGroup.swift in Sources */, FD8ECF7D2934293A00C0D1BB /* _011_SharedUtilChanges.swift in Sources */, FD17D7A227F40F0500122BE0 /* _001_InitialSetupMigration.swift in Sources */, FD245C5D2850660F00B966DD /* OWSAudioPlayer.m in Sources */, @@ -5619,7 +5678,6 @@ FD7115F228C6CB3900B47552 /* _010_AddThreadIdToFTS.swift in Sources */, FD716E6428502DDD00C96BF4 /* CallManagerProtocol.swift in Sources */, FDC438C727BB6DF000C60D73 /* DirectMessage.swift in Sources */, - FDC4384F27B4804F00C60D73 /* Header.swift in Sources */, FDC4381727B32EC700C60D73 /* Personalization.swift in Sources */, FD245C51285065CC00B966DD /* MessageReceiver.swift in Sources */, FD245C652850665400B966DD /* ClosedGroupControlMessage.swift in Sources */, @@ -5647,27 +5705,25 @@ FD09796E27FA6D0000936362 /* Contact.swift in Sources */, C38D5E8D2575011E00B6A65C /* MessageSender+ClosedGroups.swift in Sources */, FD5C72F7284F0E560029977D /* MessageReceiver+ReadReceipts.swift in Sources */, - FD83B9CE27D17A04005E1583 /* Request.swift in Sources */, C32C598A256D0664003C73A2 /* SNProtoEnvelope+Conversion.swift in Sources */, FDC438CB27BB7DB100C60D73 /* UpdateMessageRequest.swift in Sources */, FD8ECF7F2934298100C0D1BB /* SharedConfigDump.swift in Sources */, C352A2FF25574B6300338F3E /* MessageSendJob.swift in Sources */, FDC438C327BB512200C60D73 /* SodiumProtocols.swift in Sources */, B8856D11256F112A001CE70E /* OWSAudioSession.swift in Sources */, - FD8ECF72292DCD1A00C0D1BB /* _013_AutoDownloadAttachments.swift in Sources */, C3DB66C3260ACCE6001EFC55 /* OpenGroupPoller.swift in Sources */, FD716E722850647600C96BF4 /* Data+Utilities.swift in Sources */, FD245C5C2850660A00B966DD /* ConfigurationMessage.swift in Sources */, FD5C7301284F0F7A0029977D /* MessageReceiver+UnsendRequests.swift in Sources */, C3C2A75F2553A3C500C340D1 /* VisibleMessage+LinkPreview.swift in Sources */, FD245C642850664F00B966DD /* Threading.swift in Sources */, - FDF1AD6128FF61110080A701 /* _012_AddClosedGroupInfo.swift in Sources */, FD848B8D283E0B26000E298B /* MessageInputTypes.swift in Sources */, C32C5C3D256DCBAF003C73A2 /* AppReadiness.m in Sources */, FD09799B27FFC82D00936362 /* Quote.swift in Sources */, FD245C6D285066A400B966DD /* NotifyPushServerJob.swift in Sources */, C3C2A74425539EB700C340D1 /* Message.swift in Sources */, FD245C682850666300B966DD /* Message+Destination.swift in Sources */, + FDF8488029405994007DCAE5 /* HTTPQueryParam+OpenGroup.swift in Sources */, FD09798527FD1A6500936362 /* ClosedGroupKeyPair.swift in Sources */, FD245C632850664600B966DD /* Configuration.swift in Sources */, C32C5DBF256DD743003C73A2 /* ClosedGroupPoller.swift in Sources */, @@ -5687,8 +5743,6 @@ 7BCD116C27016062006330F1 /* WebRTCSession+DataChannel.swift in Sources */, FD5C72F9284F0E880029977D /* MessageReceiver+TypingIndicators.swift in Sources */, FD5C7303284F0FA50029977D /* MessageReceiver+Calls.swift in Sources */, - FDC4386B27B4E88F00C60D73 /* BatchRequestInfo.swift in Sources */, - FDC4385127B4807400C60D73 /* QueryParam.swift in Sources */, FD83B9C927D0487A005E1583 /* SendDirectMessageResponse.swift in Sources */, FDC438AA27BB12BB00C60D73 /* UserModeratorRequest.swift in Sources */, FD245C54285065E000B966DD /* ThumbnailService.swift in Sources */, @@ -5707,7 +5761,7 @@ B8CCF63723961D6D0091D419 /* NewDMVC.swift in Sources */, FDFDE12A282D056B0098B17F /* MediaZoomAnimationController.swift in Sources */, 4C1885D2218F8E1C00B67051 /* PhotoGridViewCell.swift in Sources */, - FD71164C28E3F5AA00B47552 /* SessionCell+ExtraAction.swift in Sources */, + FD71164C28E3F5AA00B47552 /* SessionCell+Accessibility.swift in Sources */, 34D1F0501F7D45A60066283D /* GifPickerCell.swift in Sources */, 7BFA8AE32831D0D4001876F3 /* ContextMenuVC+EmojiReactsView.swift in Sources */, C3E5C2FA251DBABB0040DFFC /* EditClosedGroupVC.swift in Sources */, @@ -5757,7 +5811,6 @@ C3548F0624456447009433A8 /* PNModeVC.swift in Sources */, FD71164828E2CE8700B47552 /* SessionCell+AccessoryView.swift in Sources */, B80A579F23DFF1F300876683 /* NewClosedGroupVC.swift in Sources */, - FD71163A28E2C53700B47552 /* SessionAvatarCell.swift in Sources */, 7BA68909272A27BE00EFC32F /* SessionCall.swift in Sources */, B835247925C38D880089A44F /* MessageCell.swift in Sources */, B86BD08623399CEF000F5AE3 /* SeedModal.swift in Sources */, @@ -5788,6 +5841,7 @@ 7BA6890D27325CCC00EFC32F /* SessionCallManager+CXCallController.swift in Sources */, C374EEEB25DA3CA70073A857 /* ConversationTitleView.swift in Sources */, FD716E7128505E5200C96BF4 /* MessageRequestsCell.swift in Sources */, + FDF848F329413DB0007DCAE5 /* ImagePickerHandler.swift in Sources */, B88FA7F2260C3EB10049422F /* OpenGroupSuggestionGrid.swift in Sources */, FD71160428C95B5600B47552 /* PhotoCollectionPickerViewModel.swift in Sources */, FD37EA1928AC5CCA003AE748 /* NotificationSoundViewModel.swift in Sources */, @@ -5824,7 +5878,6 @@ FD7115F428C71EB200B47552 /* ThreadDisappearingMessagesViewModel.swift in Sources */, B8D84ECF25E3108A005A043E /* ExpandingAttachmentsButton.swift in Sources */, 7B7CB190270FB2150079FF93 /* MiniCallView.swift in Sources */, - FD87DCFC28B755B800AF0F98 /* BlockedContactsViewController.swift in Sources */, 7B13E1E92810F01300BD4F64 /* SessionCallManager+Action.swift in Sources */, FD71163728E2C50700B47552 /* SessionTableViewController.swift in Sources */, C354E75A23FE2A7600CE22E3 /* BaseVC.swift in Sources */, @@ -5866,6 +5919,7 @@ 7B9F71D22852EEE2006DFE7B /* Emoji+SkinTones.swift in Sources */, 7B7CB18E270D066F0079FF93 /* IncomingCallBanner.swift in Sources */, B8569AE325CBB19A00DBA3DB /* DocumentView.swift in Sources */, + FDF848F529413EEC007DCAE5 /* SessionCell+Styling.swift in Sources */, 7BFD1A8A2745C4F000FB91B9 /* Permissions.swift in Sources */, B85357BF23A1AE0800AAF6CD /* SeedReminderView.swift in Sources */, FD716E6C28505E1C00C96BF4 /* MessageRequestsViewModel.swift in Sources */, @@ -5916,15 +5970,12 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( - FD8ECF8729346DB500C0D1BB /* RequestSpec.swift in Sources */, FD2AAAF228ED57B500A49611 /* SynchronousStorage.swift in Sources */, FD078E4927E02576000769AF /* CommonMockedExtensions.swift in Sources */, FD83B9BF27CF2294005E1583 /* TestConstants.swift in Sources */, - FD8ECF8629346DA100C0D1BB /* HeaderSpec.swift in Sources */, FD83B9BB27CF20AF005E1583 /* SessionIdSpec.swift in Sources */, FDC290A927D9B46D005DAE71 /* NimbleExtensions.swift in Sources */, FD23EA6328ED0B260058676E /* CombineExtensions.swift in Sources */, - FD8ECF852934508B00C0D1BB /* BatchResponseSpec.swift in Sources */, FD2AAAEE28ED3E1100A49611 /* MockGeneralCache.swift in Sources */, FD9B30F3293EA0BF008DEE3E /* BatchResponseSpec.swift in Sources */, FD37EA1528AB42CB003AE748 /* IdentitySpec.swift in Sources */, @@ -6149,7 +6200,7 @@ /* Begin XCBuildConfiguration section */ 453518731FC635DD00210559 /* Debug */ = { isa = XCBuildConfiguration; - baseConfigurationReference = 96ED0C9B69379BE6FF4E9DA6 /* Pods-GlobalDependencies-FrameworkAndExtensionDependencies-ExtendedDependencies-SessionShareExtension.debug.xcconfig */; + baseConfigurationReference = B1910A32EB2AD01913629646 /* Pods-GlobalDependencies-FrameworkAndExtensionDependencies-ExtendedDependencies-SessionShareExtension.debug.xcconfig */; buildSettings = { CLANG_ANALYZER_NONNULL = YES; CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; @@ -6207,7 +6258,7 @@ }; 453518751FC635DD00210559 /* App Store Release */ = { isa = XCBuildConfiguration; - baseConfigurationReference = 5626DC0D5F62C1C2C64E4AFC /* Pods-GlobalDependencies-FrameworkAndExtensionDependencies-ExtendedDependencies-SessionShareExtension.app store release.xcconfig */; + baseConfigurationReference = 6A71AD9BEAFF0C9E8016BC23 /* Pods-GlobalDependencies-FrameworkAndExtensionDependencies-ExtendedDependencies-SessionShareExtension.app store release.xcconfig */; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; CLANG_ANALYZER_NONNULL = YES; @@ -6286,7 +6337,7 @@ }; 7BC01A43241F40AB00BC7C55 /* Debug */ = { isa = XCBuildConfiguration; - baseConfigurationReference = 1A0882BF820F5B44969F91F1 /* Pods-GlobalDependencies-FrameworkAndExtensionDependencies-SessionNotificationServiceExtension.debug.xcconfig */; + baseConfigurationReference = 285705D20F792E174C8A9BBA /* Pods-GlobalDependencies-FrameworkAndExtensionDependencies-SessionNotificationServiceExtension.debug.xcconfig */; buildSettings = { CLANG_ANALYZER_NONNULL = YES; CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; @@ -6343,7 +6394,7 @@ }; 7BC01A44241F40AB00BC7C55 /* App Store Release */ = { isa = XCBuildConfiguration; - baseConfigurationReference = 245BF74EF6348E2D4125033F /* Pods-GlobalDependencies-FrameworkAndExtensionDependencies-SessionNotificationServiceExtension.app store release.xcconfig */; + baseConfigurationReference = 62B512CEB14BD4A4A53CF532 /* Pods-GlobalDependencies-FrameworkAndExtensionDependencies-SessionNotificationServiceExtension.app store release.xcconfig */; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; CLANG_ANALYZER_NONNULL = YES; @@ -6424,7 +6475,7 @@ }; C331FF242558F9D400070591 /* Debug */ = { isa = XCBuildConfiguration; - baseConfigurationReference = 510955DC99A0FD84F2D1C159 /* Pods-GlobalDependencies-SessionUIKit.debug.xcconfig */; + baseConfigurationReference = EB5B8ACA4C6F512FA3E21859 /* Pods-GlobalDependencies-SessionUIKit.debug.xcconfig */; buildSettings = { APPLICATION_EXTENSION_API_ONLY = YES; CLANG_ANALYZER_NONNULL = YES; @@ -6480,7 +6531,7 @@ }; C331FF252558F9D400070591 /* App Store Release */ = { isa = XCBuildConfiguration; - baseConfigurationReference = 18EAE958B8C12503F2C294DF /* Pods-GlobalDependencies-SessionUIKit.app store release.xcconfig */; + baseConfigurationReference = 05C76EFA593DD507061C50B2 /* Pods-GlobalDependencies-SessionUIKit.app store release.xcconfig */; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; APPLICATION_EXTENSION_API_ONLY = YES; @@ -6560,7 +6611,7 @@ }; C33FD9B4255A548A00E217F9 /* Debug */ = { isa = XCBuildConfiguration; - baseConfigurationReference = DAF57FAAF30631D0E99DA361 /* Pods-GlobalDependencies-FrameworkAndExtensionDependencies-ExtendedDependencies-SignalUtilitiesKit.debug.xcconfig */; + baseConfigurationReference = B4F9FCBDA07F07CB48220D4C /* Pods-GlobalDependencies-FrameworkAndExtensionDependencies-ExtendedDependencies-SignalUtilitiesKit.debug.xcconfig */; buildSettings = { APPLICATION_EXTENSION_API_ONLY = YES; CLANG_ANALYZER_NONNULL = YES; @@ -6624,7 +6675,7 @@ }; C33FD9B5255A548A00E217F9 /* App Store Release */ = { isa = XCBuildConfiguration; - baseConfigurationReference = 82099864FD91C9126A750313 /* Pods-GlobalDependencies-FrameworkAndExtensionDependencies-ExtendedDependencies-SignalUtilitiesKit.app store release.xcconfig */; + baseConfigurationReference = 55C13C7B4B700846E49C0E25 /* Pods-GlobalDependencies-FrameworkAndExtensionDependencies-ExtendedDependencies-SignalUtilitiesKit.app store release.xcconfig */; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; APPLICATION_EXTENSION_API_ONLY = YES; @@ -6711,7 +6762,7 @@ }; C3C2A5A8255385C100C340D1 /* Debug */ = { isa = XCBuildConfiguration; - baseConfigurationReference = E23C1E6B7E0C12BF4ACD9CBE /* Pods-GlobalDependencies-FrameworkAndExtensionDependencies-SessionSnodeKit.debug.xcconfig */; + baseConfigurationReference = 847091A12D82E41B1EBB8FB3 /* Pods-GlobalDependencies-FrameworkAndExtensionDependencies-SessionSnodeKit.debug.xcconfig */; buildSettings = { APPLICATION_EXTENSION_API_ONLY = YES; CLANG_ANALYZER_NONNULL = YES; @@ -6768,7 +6819,7 @@ }; C3C2A5A9255385C100C340D1 /* App Store Release */ = { isa = XCBuildConfiguration; - baseConfigurationReference = 0E836037CC97CE5A47735596 /* Pods-GlobalDependencies-FrameworkAndExtensionDependencies-SessionSnodeKit.app store release.xcconfig */; + baseConfigurationReference = EED1CF82CAB23FE3345564F9 /* Pods-GlobalDependencies-FrameworkAndExtensionDependencies-SessionSnodeKit.app store release.xcconfig */; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; APPLICATION_EXTENSION_API_ONLY = YES; @@ -6848,7 +6899,7 @@ }; C3C2A682255388CC00C340D1 /* Debug */ = { isa = XCBuildConfiguration; - baseConfigurationReference = F705826F79C4A591AB35D68F /* Pods-GlobalDependencies-FrameworkAndExtensionDependencies-ExtendedDependencies-SessionUtilitiesKit.debug.xcconfig */; + baseConfigurationReference = 8E946CB54A221018E23599DE /* Pods-GlobalDependencies-FrameworkAndExtensionDependencies-ExtendedDependencies-SessionUtilitiesKit.debug.xcconfig */; buildSettings = { APPLICATION_EXTENSION_API_ONLY = YES; CLANG_ANALYZER_NONNULL = YES; @@ -6890,15 +6941,7 @@ ); MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; MTL_FAST_MATH = YES; - OTHER_LDFLAGS = ( - "$(inherited)", - "-framework", - "\"Foundation\"", - "-framework", - "\"PromiseKit\"", - "-framework", - "\"UIKit\"", - ); + OTHER_LDFLAGS = "$(inherited)"; PRODUCT_BUNDLE_IDENTIFIER = "com.loki-project.SessionUtilitiesKit"; PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; SKIP_INSTALL = YES; @@ -6914,7 +6957,7 @@ }; C3C2A683255388CC00C340D1 /* App Store Release */ = { isa = XCBuildConfiguration; - baseConfigurationReference = 2581AFACDDDC1404866D7B8C /* Pods-GlobalDependencies-FrameworkAndExtensionDependencies-ExtendedDependencies-SessionUtilitiesKit.app store release.xcconfig */; + baseConfigurationReference = F60C5B6CD14329816B0E8CC0 /* Pods-GlobalDependencies-FrameworkAndExtensionDependencies-ExtendedDependencies-SessionUtilitiesKit.app store release.xcconfig */; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; APPLICATION_EXTENSION_API_ONLY = YES; @@ -6977,15 +7020,7 @@ ); MTL_ENABLE_DEBUG_INFO = NO; MTL_FAST_MATH = YES; - OTHER_LDFLAGS = ( - "$(inherited)", - "-framework", - "\"Foundation\"", - "-framework", - "\"PromiseKit\"", - "-framework", - "\"UIKit\"", - ); + OTHER_LDFLAGS = "$(inherited)"; PRODUCT_BUNDLE_IDENTIFIER = "com.loki-project.SessionUtilitiesKit"; PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; SDKROOT = iphoneos; @@ -7003,7 +7038,7 @@ }; C3C2A6FA25539DE700C340D1 /* Debug */ = { isa = XCBuildConfiguration; - baseConfigurationReference = 506FA2159653FF9F446D97D1 /* Pods-GlobalDependencies-FrameworkAndExtensionDependencies-ExtendedDependencies-SessionMessagingKit.debug.xcconfig */; + baseConfigurationReference = 6DA09080DD9779C860023A60 /* Pods-GlobalDependencies-FrameworkAndExtensionDependencies-ExtendedDependencies-SessionMessagingKit.debug.xcconfig */; buildSettings = { APPLICATION_EXTENSION_API_ONLY = YES; CLANG_ANALYZER_NONNULL = YES; @@ -7060,7 +7095,7 @@ }; C3C2A6FB25539DE700C340D1 /* App Store Release */ = { isa = XCBuildConfiguration; - baseConfigurationReference = FF694C71BE4B41B6AFD252A0 /* Pods-GlobalDependencies-FrameworkAndExtensionDependencies-ExtendedDependencies-SessionMessagingKit.app store release.xcconfig */; + baseConfigurationReference = F390F8E34CA76B3F7D3B1826 /* Pods-GlobalDependencies-FrameworkAndExtensionDependencies-ExtendedDependencies-SessionMessagingKit.app store release.xcconfig */; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; APPLICATION_EXTENSION_API_ONLY = YES; @@ -7293,7 +7328,7 @@ }; D221A0BD169C9E5F00537ABF /* Debug */ = { isa = XCBuildConfiguration; - baseConfigurationReference = 56F41C56FC7B2F381E440FB0 /* Pods-GlobalDependencies-Session.debug.xcconfig */; + baseConfigurationReference = 34040971CC7AF9C8A6C1E838 /* Pods-GlobalDependencies-Session.debug.xcconfig */; buildSettings = { ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = "$(inherited)"; ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; @@ -7366,7 +7401,7 @@ }; D221A0BE169C9E5F00537ABF /* App Store Release */ = { isa = XCBuildConfiguration; - baseConfigurationReference = 6BE8FBF62464A7177034A0AB /* Pods-GlobalDependencies-Session.app store release.xcconfig */; + baseConfigurationReference = 8603226ED1C6F61F1F2D3734 /* Pods-GlobalDependencies-Session.app store release.xcconfig */; buildSettings = { ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = "$(inherited)"; ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; @@ -7434,7 +7469,7 @@ }; FD71161028D00BAE00B47552 /* Debug */ = { isa = XCBuildConfiguration; - baseConfigurationReference = 06160ECE3FE5A06A916FF8C5 /* Pods-GlobalDependencies-Session-SessionTests.debug.xcconfig */; + baseConfigurationReference = 0772459E7D5F6747EDC889F3 /* Pods-GlobalDependencies-Session-SessionTests.debug.xcconfig */; buildSettings = { BUNDLE_LOADER = "$(TEST_HOST)"; CLANG_ANALYZER_NONNULL = YES; @@ -7476,7 +7511,7 @@ }; FD71161128D00BAE00B47552 /* App Store Release */ = { isa = XCBuildConfiguration; - baseConfigurationReference = 0E8564674E3201E218939AFB /* Pods-GlobalDependencies-Session-SessionTests.app store release.xcconfig */; + baseConfigurationReference = F154A10CE1ADA33C16B45357 /* Pods-GlobalDependencies-Session-SessionTests.app store release.xcconfig */; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; BUNDLE_LOADER = "$(TEST_HOST)"; @@ -7542,7 +7577,7 @@ }; FD83B9B727CF200A005E1583 /* Debug */ = { isa = XCBuildConfiguration; - baseConfigurationReference = BE11AFA6FD8CAE894CABC28D /* Pods-GlobalDependencies-FrameworkAndExtensionDependencies-ExtendedDependencies-SessionUtilitiesKit-SessionUtilitiesKitTests.debug.xcconfig */; + baseConfigurationReference = 8448EFF76CD3CA5B2283B8A0 /* Pods-GlobalDependencies-FrameworkAndExtensionDependencies-ExtendedDependencies-SessionUtilitiesKit-SessionUtilitiesKitTests.debug.xcconfig */; buildSettings = { CLANG_ANALYZER_NONNULL = YES; CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; @@ -7584,7 +7619,7 @@ }; FD83B9B827CF200A005E1583 /* App Store Release */ = { isa = XCBuildConfiguration; - baseConfigurationReference = 58A6BA91F634756FA0BEC9E5 /* Pods-GlobalDependencies-FrameworkAndExtensionDependencies-ExtendedDependencies-SessionUtilitiesKit-SessionUtilitiesKitTests.app store release.xcconfig */; + baseConfigurationReference = 5DA3BDDFFB9E937A49C35FCC /* Pods-GlobalDependencies-FrameworkAndExtensionDependencies-ExtendedDependencies-SessionUtilitiesKit-SessionUtilitiesKitTests.app store release.xcconfig */; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; CLANG_ANALYZER_NONNULL = YES; @@ -7649,7 +7684,7 @@ }; FDC4389627B9FFC700C60D73 /* Debug */ = { isa = XCBuildConfiguration; - baseConfigurationReference = D0CE0424239A1574F683D2D7 /* Pods-GlobalDependencies-FrameworkAndExtensionDependencies-ExtendedDependencies-SessionMessagingKit-SessionMessagingKitTests.debug.xcconfig */; + baseConfigurationReference = 8727C47348B6EFA767EE583A /* Pods-GlobalDependencies-FrameworkAndExtensionDependencies-ExtendedDependencies-SessionMessagingKit-SessionMessagingKitTests.debug.xcconfig */; buildSettings = { CLANG_ANALYZER_NONNULL = YES; CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; @@ -7691,7 +7726,7 @@ }; FDC4389727B9FFC700C60D73 /* App Store Release */ = { isa = XCBuildConfiguration; - baseConfigurationReference = 8E029A324780A800DE6B70B3 /* Pods-GlobalDependencies-FrameworkAndExtensionDependencies-ExtendedDependencies-SessionMessagingKit-SessionMessagingKitTests.app store release.xcconfig */; + baseConfigurationReference = 621B42AC592F3456ACD82F8B /* Pods-GlobalDependencies-FrameworkAndExtensionDependencies-ExtendedDependencies-SessionMessagingKit-SessionMessagingKitTests.app store release.xcconfig */; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; CLANG_ANALYZER_NONNULL = YES; diff --git a/Session/Calls/Views & Modals/IncomingCallBanner.swift b/Session/Calls/Views & Modals/IncomingCallBanner.swift index cecb6c01b..75ce31583 100644 --- a/Session/Calls/Views & Modals/IncomingCallBanner.swift +++ b/Session/Calls/Views & Modals/IncomingCallBanner.swift @@ -118,8 +118,10 @@ final class IncomingCallBanner: UIView, UIGestureRecognizerDelegate { profilePictureView.update( publicKey: call.sessionId, + threadVariant: .contact, + customImageData: nil, profile: Profile.fetchOrCreate(id: call.sessionId), - threadVariant: .contact + additionalProfile: nil ) displayNameLabel.text = call.contactName diff --git a/Session/Closed Groups/EditClosedGroupVC.swift b/Session/Closed Groups/EditClosedGroupVC.swift index 1c67c9e25..8a9a74e1f 100644 --- a/Session/Closed Groups/EditClosedGroupVC.swift +++ b/Session/Closed Groups/EditClosedGroupVC.swift @@ -1,9 +1,9 @@ // Copyright © 2022 Rangeproof Pty Ltd. All rights reserved. import UIKit +import Combine import GRDB import DifferenceKit -import PromiseKit import SessionUIKit import SessionMessagingKit import SignalUtilitiesKit @@ -220,7 +220,8 @@ final class EditClosedGroupVC: BaseVC, UITableViewDataSource, UITableViewDelegat cell.update( with: SessionCell.Info( id: displayInfo, - leftAccessory: .profile(displayInfo.profileId, displayInfo.profile), + position: Position.with(indexPath.row, count: membersAndZombies.count), + leftAccessory: .profile(id: displayInfo.profileId, profile: displayInfo.profile), title: ( displayInfo.profile?.displayName() ?? Profile.truncated(id: displayInfo.profileId, threadVariant: .contact) @@ -231,10 +232,9 @@ final class EditClosedGroupVC: BaseVC, UITableViewDataSource, UITableViewDelegat .withRenderingMode(.alwaysTemplate), customTint: .textSecondary ) - ) - ), - style: .edgeToEdge, - position: Position.with(indexPath.row, count: membersAndZombies.count) + ), + styling: SessionCell.StyleInfo(backgroundStyle: .edgeToEdge) + ) ) return cell @@ -449,7 +449,7 @@ final class EditClosedGroupVC: BaseVC, UITableViewDataSource, UITableViewDelegat ModalActivityIndicatorViewController.present(fromViewController: navigationController) { _ in Storage.shared - .writeAsync { db in + .writePublisherFlatMap { db -> AnyPublisher in if !updatedMemberIds.contains(userPublicKey) { return try MessageSender.leave(db, groupPublicKey: threadId) } @@ -461,15 +461,20 @@ final class EditClosedGroupVC: BaseVC, UITableViewDataSource, UITableViewDelegat name: updatedName ) } - .done(on: DispatchQueue.main) { [weak self] in - self?.dismiss(animated: true, completion: nil) // Dismiss the loader - popToConversationVC(self) - } - .catch(on: DispatchQueue.main) { [weak self] error in - self?.dismiss(animated: true, completion: nil) // Dismiss the loader - self?.showError(title: "GROUP_UPDATE_ERROR_TITLE".localized(), message: error.localizedDescription) - } - .retainUntilComplete() + .sinkUntilComplete( + receiveCompletion: { [weak self] result in + self?.dismiss(animated: true, completion: nil) // Dismiss the loader + + switch result { + case .finished: popToConversationVC(self) + case .failure(let error): + self?.showError( + title: "GROUP_UPDATE_ERROR_TITLE".localized(), + message: error.localizedDescription + ) + } + } + ) } } diff --git a/Session/Closed Groups/NewClosedGroupVC.swift b/Session/Closed Groups/NewClosedGroupVC.swift index eee544e92..3e799825d 100644 --- a/Session/Closed Groups/NewClosedGroupVC.swift +++ b/Session/Closed Groups/NewClosedGroupVC.swift @@ -205,15 +205,17 @@ final class NewClosedGroupVC: BaseVC, UITableViewDataSource, UITableViewDelegate cell.update( with: SessionCell.Info( id: profile, - leftAccessory: .profile(profile.id, profile), + position: Position.with(indexPath.row, count: data[indexPath.section].elements.count), + leftAccessory: .profile(id: profile.id, profile: profile), title: profile.displayName(), rightAccessory: .radio(isSelected: { [weak self] in self?.selectedContacts.contains(profile.id) == true }), - accessibilityIdentifier: "Contact" - ), - style: .edgeToEdge, - position: Position.with(indexPath.row, count: data[indexPath.section].elements.count) + styling: SessionCell.StyleInfo(backgroundStyle: .edgeToEdge), + accessibility: SessionCell.Accessibility( + identifier: "Contact" + ) + ) ) return cell diff --git a/Session/Conversations/ConversationVC+Interaction.swift b/Session/Conversations/ConversationVC+Interaction.swift index 91d739cf8..766689672 100644 --- a/Session/Conversations/ConversationVC+Interaction.swift +++ b/Session/Conversations/ConversationVC+Interaction.swift @@ -1872,7 +1872,7 @@ extension ConversationVC: deleteRemotely( from: self, request: SnodeAPI - .deleteMessage( + .deleteMessages( publicKey: threadId, serverHashes: [serverHash] ) @@ -2328,10 +2328,11 @@ extension ConversationVC { ) .save(db) - // Send a sync message with the details of the contact + + // Update the config with the approved contact try MessageSender .syncConfiguration(db, forceSyncNow: true) - .retainUntilComplete() + .sinkUntilComplete() }, completion: { _, _ in updateNavigationBackStack() } ) @@ -2347,79 +2348,31 @@ extension ConversationVC { } @objc func deleteMessageRequest() { - guard self.viewModel.threadData.threadVariant == .contact else { return } - - let threadId: String = self.viewModel.threadData.threadId - let alertVC: UIAlertController = UIAlertController( - title: "MESSAGE_REQUESTS_DELETE_CONFIRMATION_ACTON".localized(), - message: nil, - preferredStyle: .actionSheet - ) - alertVC.addAction(UIAlertAction(title: "TXT_DELETE_TITLE".localized(), style: .destructive) { _ in - // Delete the request - Storage.shared.writeAsync( - updates: { db in - _ = try SessionThread - .filter(id: threadId) - .deleteAll(db) - }, - completion: { db, _ in - DispatchQueue.main.async { [weak self] in - self?.navigationController?.popViewController(animated: true) - } - } - ) - }) - alertVC.addAction(UIAlertAction(title: "TXT_CANCEL_TITLE".localized(), style: .cancel, handler: nil)) - - self.present(alertVC, animated: true, completion: nil) + MessageRequestsViewModel.deleteMessageRequest( + threadId: self.viewModel.threadData.threadId, + threadVariant: self.viewModel.threadData.threadVariant, + viewController: self + ) { [weak self] in + self?.stopObservingChanges() + + DispatchQueue.main.async { + self?.navigationController?.popViewController(animated: true) + } + } } - @objc func block() { - guard self.viewModel.threadData.threadVariant == .contact else { return } - - let threadId: String = self.viewModel.threadData.threadId - let alertVC: UIAlertController = UIAlertController( - title: "MESSAGE_REQUESTS_BLOCK_CONFIRMATION_ACTON".localized(), - message: nil, - preferredStyle: .actionSheet - ) - alertVC.addAction(UIAlertAction(title: "BLOCK_LIST_BLOCK_BUTTON".localized(), style: .destructive) { _ in - // Delete the request - Storage.shared.writeAsync( - updates: { db in - // Update the contact - _ = try Contact - .fetchOrCreate(db, id: threadId) - .with( - isApproved: false, - isBlocked: true, - - // Note: We set this to true so the current user will be able to send a - // message to the person who originally sent them the message request in - // the future if they unblock them - didApproveMe: true - ) - .saved(db) - - _ = try SessionThread - .filter(id: threadId) - .deleteAll(db) - - try MessageSender - .syncConfiguration(db, forceSyncNow: true) - .retainUntilComplete() - }, - completion: { db, _ in - DispatchQueue.main.async { [weak self] in - self?.navigationController?.popViewController(animated: true) - } - } - ) - }) - alertVC.addAction(UIAlertAction(title: "TXT_CANCEL_TITLE".localized(), style: .cancel, handler: nil)) - - self.present(alertVC, animated: true, completion: nil) + @objc func blockMessageRequest() { + MessageRequestsViewModel.blockMessageRequest( + threadId: self.viewModel.threadData.threadId, + threadVariant: self.viewModel.threadData.threadVariant, + viewController: self + ) { [weak self] in + self?.stopObservingChanges() + + DispatchQueue.main.async { + self?.navigationController?.popViewController(animated: true) + } + } } } diff --git a/Session/Conversations/ConversationVC.swift b/Session/Conversations/ConversationVC.swift index 87376ffa3..dbb8c9f7e 100644 --- a/Session/Conversations/ConversationVC.swift +++ b/Session/Conversations/ConversationVC.swift @@ -27,10 +27,6 @@ final class ConversationVC: BaseVC, ConversationSearchControllerDelegate, UITabl var focusedInteractionId: Int64? var shouldHighlightNextScrollToInteraction: Bool = false - var scrollButtonBottomConstraint: NSLayoutConstraint? - var scrollButtonMessageRequestsBottomConstraint: NSLayoutConstraint? - var scrollButtonPendingMessageRequestInfoBottomConstraint: NSLayoutConstraint? - var messageRequestsViewBotomConstraint: NSLayoutConstraint? // Search var isShowingSearchUI = false @@ -40,8 +36,6 @@ final class ConversationVC: BaseVC, ConversationSearchControllerDelegate, UITabl var audioRecorder: AVAudioRecorder? var audioTimer: Timer? - private var searchBarWidth: NSLayoutConstraint? - // Context menu var contextMenuWindow: ContextMenuWindow? var contextMenuVC: ContextMenuVC? @@ -129,6 +123,12 @@ final class ConversationVC: BaseVC, ConversationSearchControllerDelegate, UITabl // MARK: - UI + var scrollButtonBottomConstraint: NSLayoutConstraint? + var scrollButtonMessageRequestsBottomConstraint: NSLayoutConstraint? + var scrollButtonPendingMessageRequestInfoBottomConstraint: NSLayoutConstraint? + var messageRequestsViewBotomConstraint: NSLayoutConstraint? + var messageRequestDescriptionLabelBottomConstraint: NSLayoutConstraint? + lazy var titleView: ConversationTitleView = { let result: ConversationTitleView = ConversationTitleView() let tapGestureRecognizer = UITapGestureRecognizer( @@ -221,11 +221,22 @@ final class ConversationVC: BaseVC, ConversationSearchControllerDelegate, UITabl }() lazy var scrollButton: ScrollToBottomButton = ScrollToBottomButton(delegate: self) - - lazy var messageRequestView: UIView = { + + lazy var messageRequestBackgroundView: UIView = { let result: UIView = UIView() result.translatesAutoresizingMaskIntoConstraints = false result.themeBackgroundColor = .backgroundPrimary + result.isHidden = messageRequestStackView.isHidden + + return result + }() + + lazy var messageRequestStackView: UIStackView = { + let result: UIStackView = UIStackView() + result.translatesAutoresizingMaskIntoConstraints = false + result.axis = .vertical + result.alignment = .fill + result.distribution = .fill result.isHidden = ( self.viewModel.threadData.threadIsMessageRequest == false || self.viewModel.threadData.threadRequiresApproval == true @@ -233,18 +244,40 @@ final class ConversationVC: BaseVC, ConversationSearchControllerDelegate, UITabl return result }() + + private lazy var messageRequestDescriptionContainerView: UIView = { + let result: UIView = UIView() + result.translatesAutoresizingMaskIntoConstraints = false + + return result + }() - private let messageRequestDescriptionLabel: UILabel = { + private lazy var messageRequestDescriptionLabel: UILabel = { let result: UILabel = UILabel() result.translatesAutoresizingMaskIntoConstraints = false + result.setContentCompressionResistancePriority(.required, for: .vertical) result.font = UIFont.systemFont(ofSize: 12) - result.text = "MESSAGE_REQUESTS_INFO".localized() + result.text = (self.viewModel.threadData.threadRequiresApproval == false ? + "MESSAGE_REQUESTS_INFO".localized() : + "MESSAGE_REQUEST_PENDING_APPROVAL_INFO".localized() + ) result.themeTextColor = .textSecondary result.textAlignment = .center result.numberOfLines = 0 return result }() + + private lazy var messageRequestActionStackView: UIStackView = { + let result: UIStackView = UIStackView() + result.translatesAutoresizingMaskIntoConstraints = false + result.axis = .horizontal + result.alignment = .fill + result.distribution = .fill + result.spacing = (UIDevice.current.isIPad ? Values.iPadButtonSpacing : 20) + + return result + }() private lazy var messageRequestAcceptButton: UIButton = { let result: SessionButton = SessionButton(style: .bordered, size: .medium) @@ -276,27 +309,11 @@ final class ConversationVC: BaseVC, ConversationSearchControllerDelegate, UITabl result.titleLabel?.font = UIFont.boldSystemFont(ofSize: 16) result.setTitle("TXT_BLOCK_USER_TITLE".localized(), for: .normal) result.setThemeTitleColor(.danger, for: .normal) - result.addTarget(self, action: #selector(block), for: .touchUpInside) + result.addTarget(self, action: #selector(blockMessageRequest), for: .touchUpInside) + result.isHidden = (self.viewModel.threadData.threadVariant != .contact) return result }() - - private lazy var pendingMessageRequestExplanationLabel: UILabel = { - let result: UILabel = UILabel() - result.translatesAutoresizingMaskIntoConstraints = false - result.setContentCompressionResistancePriority(.required, for: .vertical) - result.font = UIFont.systemFont(ofSize: 12) - result.text = "MESSAGE_REQUEST_PENDING_APPROVAL_INFO".localized() - result.themeTextColor = .textSecondary - result.textAlignment = .center - result.numberOfLines = 0 - result.isHidden = ( - !self.messageRequestStackView.isHidden || - self.viewModel.threadData.threadRequiresApproval == false - ) - - return result - }() // MARK: - Settings @@ -352,46 +369,32 @@ final class ConversationVC: BaseVC, ConversationSearchControllerDelegate, UITabl view.addSubview(scrollButton) view.addSubview(messageRequestBackgroundView) view.addSubview(messageRequestStackView) - view.addSubview(pendingMessageRequestExplanationLabel) - messageRequestView.addSubview(messageRequestBlockButton) - messageRequestView.addSubview(messageRequestDescriptionLabel) - messageRequestView.addSubview(messageRequestAcceptButton) - messageRequestView.addSubview(messageRequestDeleteButton) + messageRequestStackView.addArrangedSubview(messageRequestBlockButton) + messageRequestStackView.addArrangedSubview(messageRequestDescriptionContainerView) + messageRequestStackView.addArrangedSubview(messageRequestActionStackView) + messageRequestDescriptionContainerView.addSubview(messageRequestDescriptionLabel) + messageRequestActionStackView.addArrangedSubview(messageRequestAcceptButton) + messageRequestActionStackView.addArrangedSubview(messageRequestDeleteButton) - scrollButton.pin(.right, to: .right, of: view, withInset: -20) - messageRequestView.pin(.left, to: .left, of: view) - messageRequestView.pin(.right, to: .right, of: view) - self.messageRequestsViewBotomConstraint = messageRequestView.pin(.bottom, to: .bottom, of: view, withInset: -16) + scrollButton.pin(.trailing, to: .trailing, of: view, withInset: -20) + messageRequestStackView.pin(.leading, to: .leading, of: view, withInset: 16) + messageRequestStackView.pin(.trailing, to: .trailing, of: view, withInset: -16) + self.messageRequestsViewBotomConstraint = messageRequestStackView.pin(.bottom, to: .bottom, of: view, withInset: -16) self.scrollButtonBottomConstraint = scrollButton.pin(.bottom, to: .bottom, of: view, withInset: -16) self.scrollButtonBottomConstraint?.isActive = false // Note: Need to disable this to avoid a conflict with the other bottom constraint - self.scrollButtonMessageRequestsBottomConstraint = scrollButton.pin(.bottom, to: .top, of: messageRequestStackView) - self.scrollButtonPendingMessageRequestInfoBottomConstraint = scrollButton.pin(.bottom, to: .top, of: pendingMessageRequestExplanationLabel, withInset: -16) + self.scrollButtonMessageRequestsBottomConstraint = scrollButton.pin(.bottom, to: .top, of: messageRequestStackView, withInset: -4) - messageRequestBlockButton.pin(.top, to: .top, of: messageRequestView, withInset: 10) - messageRequestBlockButton.center(.horizontal, in: messageRequestView) - - messageRequestDescriptionLabel.pin(.top, to: .bottom, of: messageRequestBlockButton, withInset: 5) - messageRequestDescriptionLabel.pin(.left, to: .left, of: messageRequestView, withInset: 40) - messageRequestDescriptionLabel.pin(.right, to: .right, of: messageRequestView, withInset: -40) - - messageRequestAcceptButton.pin(.top, to: .bottom, of: messageRequestDescriptionLabel, withInset: 20) - messageRequestAcceptButton.pin(.left, to: .left, of: messageRequestView, withInset: 20) - messageRequestAcceptButton.pin(.bottom, to: .bottom, of: messageRequestView) - - messageRequestDeleteButton.pin(.top, to: .bottom, of: messageRequestDescriptionLabel, withInset: 20) - messageRequestDeleteButton.pin(.left, to: .right, of: messageRequestAcceptButton, withInset: UIDevice.current.isIPad ? Values.iPadButtonSpacing : 20) - messageRequestDeleteButton.pin(.right, to: .right, of: messageRequestView, withInset: -20) - messageRequestDeleteButton.pin(.bottom, to: .bottom, of: messageRequestView) + messageRequestDescriptionLabel.pin(.top, to: .top, of: messageRequestDescriptionContainerView, withInset: 4) + messageRequestDescriptionLabel.pin(.leading, to: .leading, of: messageRequestDescriptionContainerView, withInset: 20) + messageRequestDescriptionLabel.pin(.trailing, to: .trailing, of: messageRequestDescriptionContainerView, withInset: -20) + self.messageRequestDescriptionLabelBottomConstraint = messageRequestDescriptionLabel.pin(.bottom, to: .bottom, of: messageRequestDescriptionContainerView, withInset: -20) + messageRequestActionStackView.pin(.top, to: .bottom, of: messageRequestDescriptionContainerView) messageRequestDeleteButton.set(.width, to: .width, of: messageRequestAcceptButton) messageRequestBackgroundView.pin(.top, to: .top, of: messageRequestStackView) messageRequestBackgroundView.pin(.leading, to: .leading, of: view) messageRequestBackgroundView.pin(.trailing, to: .trailing, of: view) messageRequestBackgroundView.pin(.bottom, to: .bottom, of: view) - - pendingMessageRequestExplanationLabel.pin(.left, to: .left, of: messageRequestStackView, withInset: 40) - pendingMessageRequestExplanationLabel.pin(.right, to: .right, of: messageRequestStackView, withInset: -40) - pendingMessageRequestExplanationLabel.pin(.bottom, to: .bottom, of: messageRequestStackView, withInset: -16) // Unread count view view.addSubview(unreadCountView) @@ -505,12 +508,6 @@ final class ConversationVC: BaseVC, ConversationSearchControllerDelegate, UITabl stopObservingChanges() } - override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) { - super.viewWillTransition(to: size, with: coordinator) - searchBarWidth?.constant = size.width - 32 - tableView.reloadData() - } - // MARK: - Updating private func startObservingChanges(didReturnFromBackground: Bool = false) { @@ -571,7 +568,7 @@ final class ConversationVC: BaseVC, ConversationSearchControllerDelegate, UITabl ) } - private func stopObservingChanges() { + func stopObservingChanges() { // Stop observing database changes dataChangeObservable?.cancel() self.viewModel.onInteractionChange = nil @@ -619,6 +616,7 @@ final class ConversationVC: BaseVC, ConversationSearchControllerDelegate, UITabl if initialLoad || + viewModel.threadData.threadVariant != updatedThreadData.threadVariant || viewModel.threadData.threadRequiresApproval != updatedThreadData.threadRequiresApproval || viewModel.threadData.threadIsMessageRequest != updatedThreadData.threadIsMessageRequest || viewModel.threadData.profile != updatedThreadData.profile @@ -628,47 +626,33 @@ final class ConversationVC: BaseVC, ConversationSearchControllerDelegate, UITabl let messageRequestsViewWasVisible: Bool = ( messageRequestStackView.isHidden == false ) - let pendingMessageRequestInfoWasVisible: Bool = ( - pendingMessageRequestExplanationLabel.isHidden == false - ) UIView.animate(withDuration: 0.3) { [weak self] in - self?.messageRequestView.isHidden = ( - updatedThreadData.threadIsMessageRequest == false || + self?.messageRequestBlockButton.isHidden = ( + self?.viewModel.threadData.threadVariant != .contact || updatedThreadData.threadRequiresApproval == true ) - self?.messageRequestBackgroundView.isHidden = (self?.messageRequestStackView.isHidden == true) - self?.pendingMessageRequestExplanationLabel.isHidden = ( - self?.messageRequestStackView.isHidden == false || + self?.messageRequestActionStackView.isHidden = ( + updatedThreadData.threadRequiresApproval == true + ) + self?.messageRequestStackView.isHidden = ( + updatedThreadData.threadIsMessageRequest == false && updatedThreadData.threadRequiresApproval == false ) + self?.messageRequestBackgroundView.isHidden = (self?.messageRequestStackView.isHidden == true) + self?.messageRequestDescriptionLabelBottomConstraint?.constant = (updatedThreadData.threadRequiresApproval == true ? -4 : -20) self?.scrollButtonMessageRequestsBottomConstraint?.isActive = ( self?.messageRequestStackView.isHidden == false ) - self?.scrollButtonPendingMessageRequestInfoBottomConstraint?.isActive = ( - self?.scrollButtonPendingMessageRequestInfoBottomConstraint?.isActive == false && - self?.pendingMessageRequestExplanationLabel.isHidden == false - ) self?.scrollButtonBottomConstraint?.isActive = ( - self?.scrollButtonMessageRequestsBottomConstraint?.isActive == false && - self?.scrollButtonPendingMessageRequestInfoBottomConstraint?.isActive == false + self?.scrollButtonMessageRequestsBottomConstraint?.isActive == false ) // Update the table content inset and offset to account for // the dissapearance of the messageRequestsView - if messageRequestsViewWasVisible { - let messageRequestsOffset: CGFloat = ((self?.messageRequestView.bounds.height ?? 0) + 16) - let oldContentInset: UIEdgeInsets = (self?.tableView.contentInset ?? UIEdgeInsets.zero) - self?.tableView.contentInset = UIEdgeInsets( - top: 0, - leading: 0, - bottom: max(oldContentInset.bottom - messageRequestsOffset, 0), - trailing: 0 - ) - } - else if pendingMessageRequestInfoWasVisible { - let messageRequestsOffset: CGFloat = ((self?.pendingMessageRequestExplanationLabel.bounds.height ?? 0) + (16 * 2)) + if messageRequestsViewWasVisible != (self?.messageRequestStackView.isHidden == false) { + let messageRequestsOffset: CGFloat = ((self?.messageRequestStackView.bounds.height ?? 0) + 12) let oldContentInset: UIEdgeInsets = (self?.tableView.contentInset ?? UIEdgeInsets.zero) self?.tableView.contentInset = UIEdgeInsets( top: 0, @@ -1103,9 +1087,12 @@ final class ConversationVC: BaseVC, ConversationSearchControllerDelegate, UITabl profilePictureView.size = Values.verySmallProfilePictureSize profilePictureView.update( publicKey: threadData.threadId, // Contact thread uses the contactId + threadVariant: threadData.threadVariant, + customImageData: nil, profile: threadData.profile, - threadVariant: threadData.threadVariant + additionalProfile: nil ) + profilePictureView.set(.width, to: (44 - 16)) // Width of the standard back button profilePictureView.set(.height, to: Values.verySmallProfilePictureSize) @@ -1159,7 +1146,7 @@ final class ConversationVC: BaseVC, ConversationSearchControllerDelegate, UITabl // needed for proper calculations, so force an initial layout if it doesn't have a size) var hasDoneLayout: Bool = true - if messageRequestView.bounds.height <= CGFloat.leastNonzeroMagnitude { + if messageRequestStackView.bounds.height <= CGFloat.leastNonzeroMagnitude { hasDoneLayout = false UIView.performWithoutAnimation { @@ -1168,19 +1155,18 @@ final class ConversationVC: BaseVC, ConversationSearchControllerDelegate, UITabl } let keyboardTop = (UIScreen.main.bounds.height - keyboardRect.minY) - let messageRequestsOffset: CGFloat = (messageRequestStackView.isHidden ? 0 : messageRequestStackView.bounds.height + 16) - let pendingMessageRequestsOffset: CGFloat = (pendingMessageRequestExplanationLabel.isHidden ? 0 : (pendingMessageRequestExplanationLabel.bounds.height + (16 * 2))) + let messageRequestsOffset: CGFloat = (messageRequestStackView.isHidden ? 0 : messageRequestStackView.bounds.height + 12) let oldContentInset: UIEdgeInsets = tableView.contentInset let newContentInset: UIEdgeInsets = UIEdgeInsets( top: 0, leading: 0, - bottom: (Values.mediumSpacing + keyboardTop + messageRequestsOffset + pendingMessageRequestsOffset), + bottom: (Values.mediumSpacing + keyboardTop + messageRequestsOffset), trailing: 0 ) let newContentOffsetY: CGFloat = (tableView.contentOffset.y + (newContentInset.bottom - oldContentInset.bottom)) let changes = { [weak self] in - self?.scrollButtonBottomConstraint?.constant = -(keyboardTop + 16) - self?.messageRequestsViewBotomConstraint?.constant = -(keyboardTop + 16) + self?.scrollButtonBottomConstraint?.constant = -(keyboardTop + 12) + self?.messageRequestsViewBotomConstraint?.constant = -(keyboardTop + 12) self?.tableView.contentInset = newContentInset self?.tableView.contentOffset.y = newContentOffsetY @@ -1226,8 +1212,8 @@ final class ConversationVC: BaseVC, ConversationSearchControllerDelegate, UITabl delay: 0, options: options, animations: { [weak self] in - self?.scrollButtonBottomConstraint?.constant = -(keyboardTop + 16) - self?.messageRequestsViewBotomConstraint?.constant = -(keyboardTop + 16) + self?.scrollButtonBottomConstraint?.constant = -(keyboardTop + 12) + self?.messageRequestsViewBotomConstraint?.constant = -(keyboardTop + 12) let scrollButtonOpacity: CGFloat = (self?.getScrollButtonOpacity() ?? 0) self?.scrollButton.alpha = scrollButtonOpacity @@ -1536,7 +1522,7 @@ final class ConversationVC: BaseVC, ConversationSearchControllerDelegate, UITabl searchBar.sizeToFit() searchBar.layoutMargins = UIEdgeInsets.zero searchBarContainer.set(.height, to: 44) - searchBarWidth = searchBarContainer.set(.width, to: UIScreen.main.bounds.width - 32) + searchBarContainer.set(.width, to: UIScreen.main.bounds.width - 32) searchBarContainer.addSubview(searchBar) navigationItem.titleView = searchBarContainer @@ -1676,6 +1662,11 @@ final class ConversationVC: BaseVC, ConversationSearchControllerDelegate, UITabl animated: (self.didFinishInitialLayout && isAnimated) ) + // Need to explicitly call 'scrollViewDidScroll' here as it won't get triggered + // by 'scrollToRow' if a scroll doesn't occur (eg. if there is less than 1 screen + // of messages) + self.scrollViewDidScroll(self.tableView) + // If we haven't finished the initial layout then we want to delay the highlight slightly // so it doesn't look buggy with the push transition if highlight { diff --git a/Session/Conversations/Input View/MentionSelectionView.swift b/Session/Conversations/Input View/MentionSelectionView.swift index 881059cc3..b89cd884f 100644 --- a/Session/Conversations/Input View/MentionSelectionView.swift +++ b/Session/Conversations/Input View/MentionSelectionView.swift @@ -199,8 +199,10 @@ private extension MentionSelectionView { displayNameLabel.text = profile.displayName(for: threadVariant) profilePictureView.update( publicKey: profile.id, + threadVariant: .contact, + customImageData: nil, profile: profile, - threadVariant: threadVariant + additionalProfile: nil ) moderatorIconImageView.isHidden = !isUserModeratorOrAdmin separator.isHidden = isLast diff --git a/Session/Conversations/Message Cells/Content Views/DocumentView.swift b/Session/Conversations/Message Cells/Content Views/DocumentView.swift index cdcfe5fed..39115cbec 100644 --- a/Session/Conversations/Message Cells/Content Views/DocumentView.swift +++ b/Session/Conversations/Message Cells/Content Views/DocumentView.swift @@ -46,7 +46,7 @@ final class DocumentView: UIView { // Size label let sizeLabel = UILabel() sizeLabel.font = .systemFont(ofSize: Values.verySmallFontSize) - sizeLabel.text = OWSFormat.formatFileSize(UInt(attachment.byteCount)) + sizeLabel.text = Format.fileSize(attachment.byteCount) sizeLabel.themeTextColor = textColor sizeLabel.lineBreakMode = .byTruncatingTail diff --git a/Session/Conversations/Message Cells/Content Views/MediaAlbumView.swift b/Session/Conversations/Message Cells/Content Views/MediaAlbumView.swift index c6ca54b4e..c3f6a1c5b 100644 --- a/Session/Conversations/Message Cells/Content Views/MediaAlbumView.swift +++ b/Session/Conversations/Message Cells/Content Views/MediaAlbumView.swift @@ -111,11 +111,10 @@ public class MediaAlbumView: UIStackView { tintView.autoPinEdgesToSuperviewEdges() let moreCount = max(1, items.count - MediaAlbumView.kMaxItems) - let moreCountText = OWSFormat.formatInt(Int32(moreCount)) let moreText = String( // Format for the 'more items' indicator for media galleries. Embeds {{the number of additional items}}. format: "MEDIA_GALLERY_MORE_ITEMS_FORMAT".localized(), - moreCountText + "\(moreCount)" ) let moreLabel: UILabel = UILabel() moreLabel.font = .systemFont(ofSize: 24) diff --git a/Session/Conversations/Message Cells/VisibleMessageCell.swift b/Session/Conversations/Message Cells/VisibleMessageCell.swift index 45145bb22..7d74b4c0e 100644 --- a/Session/Conversations/Message Cells/VisibleMessageCell.swift +++ b/Session/Conversations/Message Cells/VisibleMessageCell.swift @@ -289,8 +289,10 @@ final class VisibleMessageCell: MessageCell, TappableLabelDelegate { profilePictureView.isHidden = (!cellViewModel.shouldShowProfile || cellViewModel.profile == nil) profilePictureView.update( publicKey: cellViewModel.authorId, + threadVariant: cellViewModel.threadVariant, + customImageData: nil, profile: cellViewModel.profile, - threadVariant: cellViewModel.threadVariant + additionalProfile: nil ) moderatorIconImageView.isHidden = (!cellViewModel.isSenderOpenGroupModerator || !cellViewModel.shouldShowProfile) diff --git a/Session/Conversations/Settings/ThreadDisappearingMessagesViewModel.swift b/Session/Conversations/Settings/ThreadDisappearingMessagesViewModel.swift index aace471aa..8cdfdce1a 100644 --- a/Session/Conversations/Settings/ThreadDisappearingMessagesViewModel.swift +++ b/Session/Conversations/Settings/ThreadDisappearingMessagesViewModel.swift @@ -85,10 +85,7 @@ class ThreadDisappearingMessagesViewModel: SessionTableViewModel [SectionModel] in return [ SectionModel( @@ -131,12 +128,9 @@ class ThreadDisappearingMessagesViewModel: SessionTableViewModel [SectionModel] in let userPublicKey: String = getUserHexEncodedPublicKey(db, dependencies: dependencies) let maybeThreadViewModel: SessionThreadViewModel? = try SessionThreadViewModel @@ -207,25 +207,88 @@ class ThreadSettingsViewModel: SessionTableViewModel #import #import -#import #import #import #import diff --git a/Session/Meta/Translations/de.lproj/Localizable.strings b/Session/Meta/Translations/de.lproj/Localizable.strings index 8d583d798..71f6adef7 100644 --- a/Session/Meta/Translations/de.lproj/Localizable.strings +++ b/Session/Meta/Translations/de.lproj/Localizable.strings @@ -596,3 +596,5 @@ "MESSAGE_STATE_READ" = "Read"; "MESSAGE_STATE_SENT" = "Sent"; "MESSAGE_REQUEST_PENDING_APPROVAL_INFO" = "You will be able to send voice messages and attachments once the recipient has approved this message request"; +"REMOVE_AVATAR" = "Remove"; +"CONTACT_NICKNAME_PLACEHOLDER" = "Enter a name"; diff --git a/Session/Meta/Translations/en.lproj/Localizable.strings b/Session/Meta/Translations/en.lproj/Localizable.strings index 3829aceab..4882c5a3c 100644 --- a/Session/Meta/Translations/en.lproj/Localizable.strings +++ b/Session/Meta/Translations/en.lproj/Localizable.strings @@ -596,3 +596,5 @@ "MESSAGE_STATE_READ" = "Read"; "MESSAGE_STATE_SENT" = "Sent"; "MESSAGE_REQUEST_PENDING_APPROVAL_INFO" = "You will be able to send voice messages and attachments once the recipient has approved this message request"; +"REMOVE_AVATAR" = "Remove"; +"CONTACT_NICKNAME_PLACEHOLDER" = "Enter a name"; diff --git a/Session/Meta/Translations/es.lproj/Localizable.strings b/Session/Meta/Translations/es.lproj/Localizable.strings index 31d0bc578..8e7e9d651 100644 --- a/Session/Meta/Translations/es.lproj/Localizable.strings +++ b/Session/Meta/Translations/es.lproj/Localizable.strings @@ -596,3 +596,5 @@ "MESSAGE_STATE_READ" = "Read"; "MESSAGE_STATE_SENT" = "Sent"; "MESSAGE_REQUEST_PENDING_APPROVAL_INFO" = "You will be able to send voice messages and attachments once the recipient has approved this message request"; +"REMOVE_AVATAR" = "Remove"; +"CONTACT_NICKNAME_PLACEHOLDER" = "Enter a name"; diff --git a/Session/Meta/Translations/fa.lproj/Localizable.strings b/Session/Meta/Translations/fa.lproj/Localizable.strings index 75cc6f737..27edb43c5 100644 --- a/Session/Meta/Translations/fa.lproj/Localizable.strings +++ b/Session/Meta/Translations/fa.lproj/Localizable.strings @@ -596,3 +596,5 @@ "MESSAGE_STATE_READ" = "خوانده شد"; "MESSAGE_STATE_SENT" = "ارسال شد"; "MESSAGE_REQUEST_PENDING_APPROVAL_INFO" = "You will be able to send voice messages and attachments once the recipient has approved this message request"; +"REMOVE_AVATAR" = "Remove"; +"CONTACT_NICKNAME_PLACEHOLDER" = "Enter a name"; diff --git a/Session/Meta/Translations/fi.lproj/Localizable.strings b/Session/Meta/Translations/fi.lproj/Localizable.strings index 92e2e6a11..8f6ddcc6b 100644 --- a/Session/Meta/Translations/fi.lproj/Localizable.strings +++ b/Session/Meta/Translations/fi.lproj/Localizable.strings @@ -596,3 +596,5 @@ "MESSAGE_STATE_READ" = "Read"; "MESSAGE_STATE_SENT" = "Sent"; "MESSAGE_REQUEST_PENDING_APPROVAL_INFO" = "You will be able to send voice messages and attachments once the recipient has approved this message request"; +"REMOVE_AVATAR" = "Remove"; +"CONTACT_NICKNAME_PLACEHOLDER" = "Enter a name"; diff --git a/Session/Meta/Translations/fr.lproj/Localizable.strings b/Session/Meta/Translations/fr.lproj/Localizable.strings index 17c7b0dff..e8559c883 100644 --- a/Session/Meta/Translations/fr.lproj/Localizable.strings +++ b/Session/Meta/Translations/fr.lproj/Localizable.strings @@ -596,3 +596,5 @@ "MESSAGE_STATE_READ" = "Read"; "MESSAGE_STATE_SENT" = "Sent"; "MESSAGE_REQUEST_PENDING_APPROVAL_INFO" = "You will be able to send voice messages and attachments once the recipient has approved this message request"; +"REMOVE_AVATAR" = "Remove"; +"CONTACT_NICKNAME_PLACEHOLDER" = "Enter a name"; diff --git a/Session/Meta/Translations/hi.lproj/Localizable.strings b/Session/Meta/Translations/hi.lproj/Localizable.strings index 8bab51375..e2cc9b89e 100644 --- a/Session/Meta/Translations/hi.lproj/Localizable.strings +++ b/Session/Meta/Translations/hi.lproj/Localizable.strings @@ -596,3 +596,5 @@ "MESSAGE_STATE_READ" = "Read"; "MESSAGE_STATE_SENT" = "Sent"; "MESSAGE_REQUEST_PENDING_APPROVAL_INFO" = "You will be able to send voice messages and attachments once the recipient has approved this message request"; +"REMOVE_AVATAR" = "Remove"; +"CONTACT_NICKNAME_PLACEHOLDER" = "Enter a name"; diff --git a/Session/Meta/Translations/hr.lproj/Localizable.strings b/Session/Meta/Translations/hr.lproj/Localizable.strings index 8af92e999..c626beb1f 100644 --- a/Session/Meta/Translations/hr.lproj/Localizable.strings +++ b/Session/Meta/Translations/hr.lproj/Localizable.strings @@ -596,3 +596,5 @@ "MESSAGE_STATE_READ" = "Read"; "MESSAGE_STATE_SENT" = "Sent"; "MESSAGE_REQUEST_PENDING_APPROVAL_INFO" = "You will be able to send voice messages and attachments once the recipient has approved this message request"; +"REMOVE_AVATAR" = "Remove"; +"CONTACT_NICKNAME_PLACEHOLDER" = "Enter a name"; diff --git a/Session/Meta/Translations/id-ID.lproj/Localizable.strings b/Session/Meta/Translations/id-ID.lproj/Localizable.strings index 5ff981ef7..367917f26 100644 --- a/Session/Meta/Translations/id-ID.lproj/Localizable.strings +++ b/Session/Meta/Translations/id-ID.lproj/Localizable.strings @@ -596,3 +596,5 @@ "MESSAGE_STATE_READ" = "Read"; "MESSAGE_STATE_SENT" = "Sent"; "MESSAGE_REQUEST_PENDING_APPROVAL_INFO" = "You will be able to send voice messages and attachments once the recipient has approved this message request"; +"REMOVE_AVATAR" = "Remove"; +"CONTACT_NICKNAME_PLACEHOLDER" = "Enter a name"; diff --git a/Session/Meta/Translations/it.lproj/Localizable.strings b/Session/Meta/Translations/it.lproj/Localizable.strings index f6725544a..7926decf8 100644 --- a/Session/Meta/Translations/it.lproj/Localizable.strings +++ b/Session/Meta/Translations/it.lproj/Localizable.strings @@ -596,3 +596,5 @@ "MESSAGE_STATE_READ" = "Read"; "MESSAGE_STATE_SENT" = "Sent"; "MESSAGE_REQUEST_PENDING_APPROVAL_INFO" = "You will be able to send voice messages and attachments once the recipient has approved this message request"; +"REMOVE_AVATAR" = "Remove"; +"CONTACT_NICKNAME_PLACEHOLDER" = "Enter a name"; diff --git a/Session/Meta/Translations/ja.lproj/Localizable.strings b/Session/Meta/Translations/ja.lproj/Localizable.strings index 6319ef7fd..f820dea7a 100644 --- a/Session/Meta/Translations/ja.lproj/Localizable.strings +++ b/Session/Meta/Translations/ja.lproj/Localizable.strings @@ -596,3 +596,5 @@ "MESSAGE_STATE_READ" = "Read"; "MESSAGE_STATE_SENT" = "Sent"; "MESSAGE_REQUEST_PENDING_APPROVAL_INFO" = "You will be able to send voice messages and attachments once the recipient has approved this message request"; +"REMOVE_AVATAR" = "Remove"; +"CONTACT_NICKNAME_PLACEHOLDER" = "Enter a name"; diff --git a/Session/Meta/Translations/nl.lproj/Localizable.strings b/Session/Meta/Translations/nl.lproj/Localizable.strings index 284441372..696e46828 100644 --- a/Session/Meta/Translations/nl.lproj/Localizable.strings +++ b/Session/Meta/Translations/nl.lproj/Localizable.strings @@ -596,3 +596,5 @@ "MESSAGE_STATE_READ" = "Read"; "MESSAGE_STATE_SENT" = "Sent"; "MESSAGE_REQUEST_PENDING_APPROVAL_INFO" = "You will be able to send voice messages and attachments once the recipient has approved this message request"; +"REMOVE_AVATAR" = "Remove"; +"CONTACT_NICKNAME_PLACEHOLDER" = "Enter a name"; diff --git a/Session/Meta/Translations/pl.lproj/Localizable.strings b/Session/Meta/Translations/pl.lproj/Localizable.strings index ab8b48d0b..a20c51602 100644 --- a/Session/Meta/Translations/pl.lproj/Localizable.strings +++ b/Session/Meta/Translations/pl.lproj/Localizable.strings @@ -596,3 +596,5 @@ "MESSAGE_STATE_READ" = "Read"; "MESSAGE_STATE_SENT" = "Sent"; "MESSAGE_REQUEST_PENDING_APPROVAL_INFO" = "You will be able to send voice messages and attachments once the recipient has approved this message request"; +"REMOVE_AVATAR" = "Remove"; +"CONTACT_NICKNAME_PLACEHOLDER" = "Enter a name"; diff --git a/Session/Meta/Translations/pt_BR.lproj/Localizable.strings b/Session/Meta/Translations/pt_BR.lproj/Localizable.strings index cd772f650..82c9c9d76 100644 --- a/Session/Meta/Translations/pt_BR.lproj/Localizable.strings +++ b/Session/Meta/Translations/pt_BR.lproj/Localizable.strings @@ -596,3 +596,5 @@ "MESSAGE_STATE_READ" = "Read"; "MESSAGE_STATE_SENT" = "Sent"; "MESSAGE_REQUEST_PENDING_APPROVAL_INFO" = "You will be able to send voice messages and attachments once the recipient has approved this message request"; +"REMOVE_AVATAR" = "Remove"; +"CONTACT_NICKNAME_PLACEHOLDER" = "Enter a name"; diff --git a/Session/Meta/Translations/ru.lproj/Localizable.strings b/Session/Meta/Translations/ru.lproj/Localizable.strings index 632e50aa4..52bd23dc3 100644 --- a/Session/Meta/Translations/ru.lproj/Localizable.strings +++ b/Session/Meta/Translations/ru.lproj/Localizable.strings @@ -596,3 +596,5 @@ "MESSAGE_STATE_READ" = "Read"; "MESSAGE_STATE_SENT" = "Sent"; "MESSAGE_REQUEST_PENDING_APPROVAL_INFO" = "You will be able to send voice messages and attachments once the recipient has approved this message request"; +"REMOVE_AVATAR" = "Remove"; +"CONTACT_NICKNAME_PLACEHOLDER" = "Enter a name"; diff --git a/Session/Meta/Translations/si.lproj/Localizable.strings b/Session/Meta/Translations/si.lproj/Localizable.strings index 753687545..96a72c635 100644 --- a/Session/Meta/Translations/si.lproj/Localizable.strings +++ b/Session/Meta/Translations/si.lproj/Localizable.strings @@ -596,3 +596,5 @@ "MESSAGE_STATE_READ" = "Read"; "MESSAGE_STATE_SENT" = "Sent"; "MESSAGE_REQUEST_PENDING_APPROVAL_INFO" = "You will be able to send voice messages and attachments once the recipient has approved this message request"; +"REMOVE_AVATAR" = "Remove"; +"CONTACT_NICKNAME_PLACEHOLDER" = "Enter a name"; diff --git a/Session/Meta/Translations/sk.lproj/Localizable.strings b/Session/Meta/Translations/sk.lproj/Localizable.strings index 1388f0536..b6b217183 100644 --- a/Session/Meta/Translations/sk.lproj/Localizable.strings +++ b/Session/Meta/Translations/sk.lproj/Localizable.strings @@ -596,3 +596,5 @@ "MESSAGE_STATE_READ" = "Read"; "MESSAGE_STATE_SENT" = "Sent"; "MESSAGE_REQUEST_PENDING_APPROVAL_INFO" = "You will be able to send voice messages and attachments once the recipient has approved this message request"; +"REMOVE_AVATAR" = "Remove"; +"CONTACT_NICKNAME_PLACEHOLDER" = "Enter a name"; diff --git a/Session/Meta/Translations/sv.lproj/Localizable.strings b/Session/Meta/Translations/sv.lproj/Localizable.strings index 2af704369..12f155930 100644 --- a/Session/Meta/Translations/sv.lproj/Localizable.strings +++ b/Session/Meta/Translations/sv.lproj/Localizable.strings @@ -596,3 +596,5 @@ "MESSAGE_STATE_READ" = "Read"; "MESSAGE_STATE_SENT" = "Sent"; "MESSAGE_REQUEST_PENDING_APPROVAL_INFO" = "You will be able to send voice messages and attachments once the recipient has approved this message request"; +"REMOVE_AVATAR" = "Remove"; +"CONTACT_NICKNAME_PLACEHOLDER" = "Enter a name"; diff --git a/Session/Meta/Translations/th.lproj/Localizable.strings b/Session/Meta/Translations/th.lproj/Localizable.strings index 24910f380..34d60ce88 100644 --- a/Session/Meta/Translations/th.lproj/Localizable.strings +++ b/Session/Meta/Translations/th.lproj/Localizable.strings @@ -596,3 +596,5 @@ "MESSAGE_STATE_READ" = "Read"; "MESSAGE_STATE_SENT" = "Sent"; "MESSAGE_REQUEST_PENDING_APPROVAL_INFO" = "You will be able to send voice messages and attachments once the recipient has approved this message request"; +"REMOVE_AVATAR" = "Remove"; +"CONTACT_NICKNAME_PLACEHOLDER" = "Enter a name"; diff --git a/Session/Meta/Translations/vi-VN.lproj/Localizable.strings b/Session/Meta/Translations/vi-VN.lproj/Localizable.strings index 6a27510bc..abfce5897 100644 --- a/Session/Meta/Translations/vi-VN.lproj/Localizable.strings +++ b/Session/Meta/Translations/vi-VN.lproj/Localizable.strings @@ -596,3 +596,5 @@ "MESSAGE_STATE_READ" = "Read"; "MESSAGE_STATE_SENT" = "Sent"; "MESSAGE_REQUEST_PENDING_APPROVAL_INFO" = "You will be able to send voice messages and attachments once the recipient has approved this message request"; +"REMOVE_AVATAR" = "Remove"; +"CONTACT_NICKNAME_PLACEHOLDER" = "Enter a name"; diff --git a/Session/Meta/Translations/zh-Hant.lproj/Localizable.strings b/Session/Meta/Translations/zh-Hant.lproj/Localizable.strings index d408fc648..329caa9a8 100644 --- a/Session/Meta/Translations/zh-Hant.lproj/Localizable.strings +++ b/Session/Meta/Translations/zh-Hant.lproj/Localizable.strings @@ -596,3 +596,5 @@ "MESSAGE_STATE_READ" = "Read"; "MESSAGE_STATE_SENT" = "Sent"; "MESSAGE_REQUEST_PENDING_APPROVAL_INFO" = "You will be able to send voice messages and attachments once the recipient has approved this message request"; +"REMOVE_AVATAR" = "Remove"; +"CONTACT_NICKNAME_PLACEHOLDER" = "Enter a name"; diff --git a/Session/Meta/Translations/zh_CN.lproj/Localizable.strings b/Session/Meta/Translations/zh_CN.lproj/Localizable.strings index f391b1784..fcca23455 100644 --- a/Session/Meta/Translations/zh_CN.lproj/Localizable.strings +++ b/Session/Meta/Translations/zh_CN.lproj/Localizable.strings @@ -596,3 +596,5 @@ "MESSAGE_STATE_READ" = "Read"; "MESSAGE_STATE_SENT" = "Sent"; "MESSAGE_REQUEST_PENDING_APPROVAL_INFO" = "You will be able to send voice messages and attachments once the recipient has approved this message request"; +"REMOVE_AVATAR" = "Remove"; +"CONTACT_NICKNAME_PLACEHOLDER" = "Enter a name"; diff --git a/Session/Notifications/AppNotifications.swift b/Session/Notifications/AppNotifications.swift index eb1830bec..a767635b4 100644 --- a/Session/Notifications/AppNotifications.swift +++ b/Session/Notifications/AppNotifications.swift @@ -538,12 +538,7 @@ class NotificationActionHandler { variant: .standardOutgoing, body: replyText, timestampMs: Int64(floor(Date().timeIntervalSince1970 * 1000)), - hasMention: Interaction.isUserMentioned( - db, - threadId: threadId, - threadVariant: thread.variant, - body: replyText - ), + hasMention: Interaction.isUserMentioned(db, threadId: threadId, body: replyText), expiresInSeconds: try? DisappearingMessagesConfiguration .select(.durationSeconds) .filter(id: threadId) diff --git a/Session/Settings/BlockedContactsViewController.swift b/Session/Settings/BlockedContactsViewController.swift deleted file mode 100644 index 7fbdb00af..000000000 --- a/Session/Settings/BlockedContactsViewController.swift +++ /dev/null @@ -1,515 +0,0 @@ -// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved. - -import UIKit -import GRDB -import DifferenceKit -import SessionUIKit -import SessionMessagingKit -import SignalUtilitiesKit - -class BlockedContactsViewController: BaseVC, UITableViewDelegate, UITableViewDataSource { - private static let loadingHeaderHeight: CGFloat = 40 - - private let viewModel: BlockedContactsViewModel = BlockedContactsViewModel() - private var dataChangeObservable: DatabaseCancellable? - private var hasLoadedInitialContactData: Bool = false - private var isLoadingMore: Bool = false - private var isAutoLoadingNextPage: Bool = false - private var viewHasAppeared: Bool = false - - // MARK: - Intialization - - init() { - Storage.shared.addObserver(viewModel.pagedDataObserver) - - super.init(nibName: nil, bundle: nil) - } - - required init?(coder: NSCoder) { - preconditionFailure("Use init() instead.") - } - - deinit { - NotificationCenter.default.removeObserver(self) - } - - // MARK: - UI - - private lazy var tableView: UITableView = { - let result: UITableView = UITableView() - result.translatesAutoresizingMaskIntoConstraints = false - result.clipsToBounds = true - result.separatorStyle = .none - result.themeBackgroundColor = .clear - result.showsVerticalScrollIndicator = false - result.register(view: SessionCell.self) - result.dataSource = self - result.delegate = self - result.layer.cornerRadius = SessionCell.cornerRadius - - if #available(iOS 15.0, *) { - result.sectionHeaderTopPadding = 0 - } - - return result - }() - - private lazy var emptyStateLabel: UILabel = { - let result: UILabel = UILabel() - result.translatesAutoresizingMaskIntoConstraints = false - result.isUserInteractionEnabled = false - result.font = .systemFont(ofSize: Values.smallFontSize) - result.text = "CONVERSATION_SETTINGS_BLOCKED_CONTACTS_EMPTY_STATE".localized() - result.themeTextColor = .textSecondary - result.textAlignment = .center - result.numberOfLines = 0 - result.isHidden = true - - return result - }() - - private lazy var fadeView: GradientView = { - let result: GradientView = GradientView() - result.themeBackgroundGradient = [ - .value(.backgroundPrimary, alpha: 0), // Want this to take up 20% (~25pt) - .backgroundPrimary, - .backgroundPrimary, - .backgroundPrimary, - .backgroundPrimary - ] - result.set(.height, to: Values.footerGradientHeight(window: UIApplication.shared.keyWindow)) - - return result - }() - - private lazy var unblockButton: SessionButton = { - let result: SessionButton = SessionButton(style: .destructive, size: .large) - result.translatesAutoresizingMaskIntoConstraints = false - result.setTitle("CONVERSATION_SETTINGS_BLOCKED_CONTACTS_UNBLOCK".localized(), for: .normal) - result.addTarget(self, action: #selector(unblockTapped), for: .touchUpInside) - - return result - }() - - // MARK: - Lifecycle - - override func viewDidLoad() { - super.viewDidLoad() - - view.themeBackgroundColor = .backgroundPrimary - - ViewControllerUtilities.setUpDefaultSessionStyle( - for: self, - title: "CONVERSATION_SETTINGS_BLOCKED_CONTACTS_TITLE".localized(), - hasCustomBackButton: false - ) - - view.addSubview(tableView) - view.addSubview(emptyStateLabel) - view.addSubview(fadeView) - view.addSubview(unblockButton) - setupLayout() - - // Notifications - NotificationCenter.default.addObserver( - self, - selector: #selector(applicationDidBecomeActive(_:)), - name: UIApplication.didBecomeActiveNotification, - object: nil - ) - NotificationCenter.default.addObserver( - self, - selector: #selector(applicationDidResignActive(_:)), - name: UIApplication.didEnterBackgroundNotification, object: nil - ) - } - - override func viewWillAppear(_ animated: Bool) { - super.viewWillAppear(animated) - - startObservingChanges() - } - - override func viewDidAppear(_ animated: Bool) { - super.viewDidAppear(animated) - - self.viewHasAppeared = true - self.autoLoadNextPageIfNeeded() - } - - override func viewWillDisappear(_ animated: Bool) { - super.viewWillDisappear(animated) - - // Stop observing database changes - dataChangeObservable?.cancel() - } - - @objc func applicationDidBecomeActive(_ notification: Notification) { - startObservingChanges(didReturnFromBackground: true) - } - - @objc func applicationDidResignActive(_ notification: Notification) { - // Stop observing database changes - dataChangeObservable?.cancel() - } - - // MARK: - Layout - - private func setupLayout() { - NSLayoutConstraint.activate([ - tableView.topAnchor.constraint(equalTo: view.topAnchor, constant: Values.smallSpacing), - tableView.leftAnchor.constraint(equalTo: view.leftAnchor, constant: Values.largeSpacing), - tableView.rightAnchor.constraint(equalTo: view.rightAnchor, constant: -Values.largeSpacing), - tableView.bottomAnchor.constraint( - equalTo: unblockButton.topAnchor, - constant: -Values.largeSpacing - ), - - emptyStateLabel.topAnchor.constraint(equalTo: view.topAnchor, constant: Values.massiveSpacing), - emptyStateLabel.leftAnchor.constraint(equalTo: view.leftAnchor, constant: Values.mediumSpacing), - emptyStateLabel.rightAnchor.constraint(equalTo: view.rightAnchor, constant: -Values.mediumSpacing), - emptyStateLabel.centerXAnchor.constraint(equalTo: view.centerXAnchor), - - fadeView.leftAnchor.constraint(equalTo: view.leftAnchor), - fadeView.rightAnchor.constraint(equalTo: view.rightAnchor), - fadeView.bottomAnchor.constraint(equalTo: view.bottomAnchor), - - unblockButton.centerXAnchor.constraint(equalTo: view.centerXAnchor), - unblockButton.bottomAnchor.constraint( - equalTo: view.safeAreaLayoutGuide.bottomAnchor, - constant: -Values.smallSpacing - ), - unblockButton.widthAnchor.constraint(equalToConstant: Values.iPadButtonWidth) - ]) - } - - // MARK: - Updating - - private func startObservingChanges(didReturnFromBackground: Bool = false) { - self.viewModel.onContactChange = { [weak self] updatedContactData, changeset in - self?.handleContactUpdates(updatedContactData, changeset: changeset) - } - - // Note: When returning from the background we could have received notifications but the - // PagedDatabaseObserver won't have them so we need to force a re-fetch of the current - // data to ensure everything is up to date - if didReturnFromBackground { - self.viewModel.pagedDataObserver?.reload() - } - } - - private func handleContactUpdates( - _ updatedData: [BlockedContactsViewModel.SectionModel], - changeset: StagedChangeset<[BlockedContactsViewModel.SectionModel]>, - initialLoad: Bool = false - ) { - // Ensure the first load runs without animations (if we don't do this the cells will animate - // in from a frame of CGRect.zero) - guard hasLoadedInitialContactData else { - hasLoadedInitialContactData = true - UIView.performWithoutAnimation { - handleContactUpdates(updatedData, changeset: changeset, initialLoad: true) - } - return - } - - // Show the empty state if there is no data - let hasContactsData: Bool = (updatedData - .first(where: { $0.model == .contacts })? - .elements - .isEmpty == false) - unblockButton.isEnabled = !viewModel.selectedContactIds.isEmpty - unblockButton.isHidden = !hasContactsData - emptyStateLabel.isHidden = hasContactsData - - CATransaction.begin() - CATransaction.setCompletionBlock { [weak self] in - // Complete page loading - self?.isLoadingMore = false - self?.autoLoadNextPageIfNeeded() - } - - // Reload the table content (animate changes after the first load) - tableView.reload( - using: changeset, - deleteSectionsAnimation: .none, - insertSectionsAnimation: .none, - reloadSectionsAnimation: .none, - deleteRowsAnimation: .bottom, - insertRowsAnimation: .top, - reloadRowsAnimation: .none, - interrupt: { $0.changeCount > 100 } // Prevent too many changes from causing performance issues - ) { [weak self] updatedData in - self?.viewModel.updateContactData(updatedData) - } - - CATransaction.commit() - } - - private func autoLoadNextPageIfNeeded() { - guard !self.isAutoLoadingNextPage && !self.isLoadingMore else { return } - - self.isAutoLoadingNextPage = true - - DispatchQueue.main.asyncAfter(deadline: .now() + PagedData.autoLoadNextPageDelay) { [weak self] in - self?.isAutoLoadingNextPage = false - - // Note: We sort the headers as we want to prioritise loading newer pages over older ones - let sections: [(BlockedContactsViewModel.Section, CGRect)] = (self?.viewModel.contactData - .enumerated() - .map { index, section in - (section.model, (self?.tableView.rectForHeader(inSection: index) ?? .zero)) - }) - .defaulting(to: []) - let shouldLoadMore: Bool = sections - .contains { section, headerRect in - section == .loadMore && - headerRect != .zero && - (self?.tableView.bounds.contains(headerRect) == true) - } - - guard shouldLoadMore else { return } - - self?.isLoadingMore = true - - DispatchQueue.global(qos: .userInitiated).async { [weak self] in - self?.viewModel.pagedDataObserver?.load(.pageAfter) - } - } - } - - // MARK: - UITableViewDataSource - - func numberOfSections(in tableView: UITableView) -> Int { - return viewModel.contactData.count - } - - func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { - let section: BlockedContactsViewModel.SectionModel = viewModel.contactData[section] - - return section.elements.count - } - - func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { - let section: BlockedContactsViewModel.SectionModel = viewModel.contactData[indexPath.section] - - switch section.model { - case .contacts: - let info: SessionCell.Info = section.elements[indexPath.row] - let cell: SessionCell = tableView.dequeue(type: SessionCell.self, for: indexPath) - cell.update( - with: info, - style: .roundedEdgeToEdge, - position: Position.with(indexPath.row, count: section.elements.count) - ) - - return cell - - default: preconditionFailure("Other sections should have no content") - } - } - - func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? { - let section: BlockedContactsViewModel.SectionModel = viewModel.contactData[section] - - switch section.model { - case .loadMore: - let loadingIndicator: UIActivityIndicatorView = UIActivityIndicatorView(style: .medium) - loadingIndicator.themeTintColor = .textPrimary - loadingIndicator.alpha = 0.5 - loadingIndicator.startAnimating() - - let view: UIView = UIView() - view.addSubview(loadingIndicator) - loadingIndicator.center(in: view) - - return view - - default: return nil - } - } - - // MARK: - UITableViewDelegate - - func tableView(_ tableView: UITableView, estimatedHeightForRowAt indexPath: IndexPath) -> CGFloat { - return UITableView.automaticDimension - } - - func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat { - return UITableView.automaticDimension - } - - func tableView(_ tableView: UITableView, heightForHeaderInSection section: Int) -> CGFloat { - let section: BlockedContactsViewModel.SectionModel = viewModel.contactData[section] - - switch section.model { - case .loadMore: return BlockedContactsViewController.loadingHeaderHeight - default: return 0 - } - } - - func tableView(_ tableView: UITableView, willDisplayHeaderView view: UIView, forSection section: Int) { - guard self.hasLoadedInitialContactData && self.viewHasAppeared && !self.isLoadingMore else { return } - - let section: BlockedContactsViewModel.SectionModel = self.viewModel.contactData[section] - - switch section.model { - case .loadMore: - self.isLoadingMore = true - - DispatchQueue.global(qos: .userInitiated).async { [weak self] in - self?.viewModel.pagedDataObserver?.load(.pageAfter) - } - - default: break - } - } - - func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { - tableView.deselectRow(at: indexPath, animated: true) - - let section: BlockedContactsViewModel.SectionModel = self.viewModel.contactData[indexPath.section] - - switch section.model { - case .contacts: - let info: SessionCell.Info = section.elements[indexPath.row] - - // Do nothing if the item is disabled - guard info.isEnabled else { return } - - // Get the view that was tapped (for presenting on iPad) - let tappedView: UIView? = tableView.cellForRow(at: indexPath) - let maybeOldSelection: (Int, SessionCell.Info)? = section.elements - .enumerated() - .first(where: { index, info in - switch (info.leftAccessory, info.rightAccessory) { - case (_, .radio(_, let isSelected, _)): return isSelected() - case (.radio(_, let isSelected, _), _): return isSelected() - default: return false - } - }) - - info.onTap?(tappedView) - self.manuallyReload(indexPath: indexPath, section: section, info: info) - self.unblockButton.isEnabled = !self.viewModel.selectedContactIds.isEmpty - - // Update the old selection as well - if let oldSelection: (index: Int, info: SessionCell.Info) = maybeOldSelection { - self.manuallyReload( - indexPath: IndexPath( - row: oldSelection.index, - section: indexPath.section - ), - section: section, - info: oldSelection.info - ) - } - - default: break - } - } - - private func manuallyReload( - indexPath: IndexPath, - section: BlockedContactsViewModel.SectionModel, - info: SessionCell.Info - ) { - // Try update the existing cell to have a nice animation instead of reloading the cell - if let existingCell: SessionCell = tableView.cellForRow(at: indexPath) as? SessionCell { - existingCell.update( - with: info, - style: .roundedEdgeToEdge, - position: Position.with(indexPath.row, count: section.elements.count) - ) - } - else { - tableView.reloadRows(at: [indexPath], with: .none) - } - } - - // MARK: - Interaction - - @objc private func unblockTapped() { - guard !viewModel.selectedContactIds.isEmpty else { return } - - let contactIds: Set = viewModel.selectedContactIds - let contactNames: [String] = contactIds - .map { contactId in - guard - let section: BlockedContactsViewModel.SectionModel = self.viewModel.contactData - .first(where: { section in section.model == .contacts }), - let info: SessionCell.Info = section.elements - .first(where: { info in info.id.id == contactId }) - else { return contactId } - - return info.title - } - let confirmationTitle: String = { - guard contactNames.count > 1 else { - // Show a single users name - return String( - format: "CONVERSATION_SETTINGS_BLOCKED_CONTACTS_UNBLOCK_CONFIRMATION_TITLE_SINGLE".localized(), - ( - contactNames.first ?? - "CONVERSATION_SETTINGS_BLOCKED_CONTACTS_UNBLOCK_CONFIRMATION_TITLE_FALLBACK".localized() - ) - ) - } - guard contactNames.count > 3 else { - // Show up to three users names - let initialNames: [String] = Array(contactNames.prefix(upTo: (contactNames.count - 1))) - let lastName: String = contactNames[contactNames.count - 1] - - return [ - String( - format: "CONVERSATION_SETTINGS_BLOCKED_CONTACTS_UNBLOCK_CONFIRMATION_TITLE_MULTIPLE_1".localized(), - initialNames.joined(separator: ", ") - ), - String( - format: "CONVERSATION_SETTINGS_BLOCKED_CONTACTS_UNBLOCK_CONFIRMATION_TITLE_MULTIPLE_2_SINGLE".localized(), - lastName - ) - ] - .reversed(if: CurrentAppContext().isRTL) - .joined(separator: " ") - } - - // If we have exactly 4 users, show the first two names followed by 'and X others', for - // more than 4 users, show the first 3 names followed by 'and X others' - let numNamesToShow: Int = (contactNames.count == 4 ? 2 : 3) - let initialNames: [String] = Array(contactNames.prefix(upTo: numNamesToShow)) - - return [ - String( - format: "CONVERSATION_SETTINGS_BLOCKED_CONTACTS_UNBLOCK_CONFIRMATION_TITLE_MULTIPLE_1".localized(), - initialNames.joined(separator: ", ") - ), - String( - format: "CONVERSATION_SETTINGS_BLOCKED_CONTACTS_UNBLOCK_CONFIRMATION_TITLE_MULTIPLE_3".localized(), - (contactNames.count - numNamesToShow) - ) - ] - .reversed(if: CurrentAppContext().isRTL) - .joined(separator: " ") - }() - let confirmationModal: ConfirmationModal = ConfirmationModal( - info: ConfirmationModal.Info( - title: confirmationTitle, - confirmTitle: "CONVERSATION_SETTINGS_BLOCKED_CONTACTS_UNBLOCK_CONFIRMATION_ACTON".localized(), - confirmStyle: .danger, - cancelStyle: .alert_text - ) { _ in - // Unblock the contacts - Storage.shared.write { db in - _ = try Contact - .filter(ids: contactIds) - .updateAll(db, Contact.Columns.isBlocked.set(to: false)) - - // Force a config sync - try MessageSender.syncConfiguration(db, forceSyncNow: true).retainUntilComplete() - } - } - ) - self.present(confirmationModal, animated: true, completion: nil) - } -} diff --git a/Session/Settings/BlockedContactsViewModel.swift b/Session/Settings/BlockedContactsViewModel.swift index 18957a07a..6c608c2bd 100644 --- a/Session/Settings/BlockedContactsViewModel.swift +++ b/Session/Settings/BlockedContactsViewModel.swift @@ -1,18 +1,25 @@ // Copyright © 2022 Rangeproof Pty Ltd. All rights reserved. import Foundation +import Combine import GRDB import DifferenceKit +import SessionUIKit import SignalUtilitiesKit -public class BlockedContactsViewModel { - public typealias SectionModel = ArraySection> - +class BlockedContactsViewModel: SessionTableViewModel { // MARK: - Section - public enum Section: Differentiable { + public enum Section: SessionTableSection { case contacts case loadMore + + var style: SessionTableSectionStyle { + switch self { + case .contacts: return .none + case .loadMore: return .loadMore + } + } } // MARK: - Variables @@ -21,14 +28,16 @@ public class BlockedContactsViewModel { // MARK: - Initialization - init() { - self.pagedDataObserver = nil + override init() { + _pagedDataObserver = nil + + super.init() // Note: Since this references self we need to finish initializing before setting it, we // also want to skip the initial query and trigger it async so that the push animation // doesn't stutter (it should load basically immediately but without this there is a // distinct stutter) - self.pagedDataObserver = PagedDatabaseObserver( + _pagedDataObserver = PagedDatabaseObserver( pagedTable: Profile.self, pageSize: BlockedContactsViewModel.pageSize, idColumn: .id, @@ -63,12 +72,13 @@ public class BlockedContactsViewModel { ), onChangeUnsorted: { [weak self] updatedData, updatedPageInfo in PagedData.processAndTriggerUpdates( - updatedData: self?.process(data: updatedData, for: updatedPageInfo), - currentDataRetriever: { self?.contactData }, - onDataChange: self?.onContactChange, - onUnobservedDataChange: { updatedData, changeset in - self?.unobservedContactDataChanges = (updatedData, changeset) - } + updatedData: self?.process(data: updatedData, for: updatedPageInfo) + .mapToSessionTableViewData(for: self), + currentDataRetriever: { self?.tableData }, + onDataChange: { updatedData, changeset in + self?.contactDataSubject.send((updatedData, changeset)) + }, + onUnobservedDataChange: { _, _ in } ) } ) @@ -76,59 +86,80 @@ public class BlockedContactsViewModel { // Run the initial query on a background thread so we don't block the push transition DispatchQueue.global(qos: .userInitiated).async { [weak self] in // The `.pageBefore` will query from a `0` offset loading the first page - self?.pagedDataObserver?.load(.pageBefore) + self?._pagedDataObserver?.load(.pageBefore) } } // MARK: - Contact Data - public private(set) var selectedContactIds: Set = [] - public private(set) var unobservedContactDataChanges: ([SectionModel], StagedChangeset<[SectionModel]>)? - public private(set) var contactData: [SectionModel] = [] - public private(set) var pagedDataObserver: PagedDatabaseObserver? - - public var onContactChange: (([SectionModel], StagedChangeset<[SectionModel]>) -> ())? { - didSet { - // When starting to observe interaction changes we want to trigger a UI update just in case the - // data was changed while we weren't observing - if let unobservedContactDataChanges: ([SectionModel], StagedChangeset<[SectionModel]>) = self.unobservedContactDataChanges { - onContactChange?(unobservedContactDataChanges.0 , unobservedContactDataChanges.1) - self.unobservedContactDataChanges = nil - } - } + override var title: String { "CONVERSATION_SETTINGS_BLOCKED_CONTACTS_TITLE".localized() } + override var emptyStateTextPublisher: AnyPublisher { + Just("CONVERSATION_SETTINGS_BLOCKED_CONTACTS_EMPTY_STATE".localized()) + .eraseToAnyPublisher() } - private func process(data: [DataModel], for pageInfo: PagedData.PageInfo) -> [SectionModel] { - // Update the 'selectedContactIds' to only include selected contacts which are within the - // data (ie. handle profile deletions) - let profileIds: Set = data.map { $0.id }.asSet() - selectedContactIds = selectedContactIds.intersection(profileIds) - + private let contactDataSubject: CurrentValueSubject<([SectionModel], StagedChangeset<[SectionModel]>), Never> = CurrentValueSubject(([], StagedChangeset())) + private let selectedContactIdsSubject: CurrentValueSubject, Never> = CurrentValueSubject([]) + private var _pagedDataObserver: PagedDatabaseObserver? + public override var pagedDataObserver: TransactionObserver? { _pagedDataObserver } + + public override var observableTableData: ObservableData { _observableTableData } + + private lazy var _observableTableData: ObservableData = contactDataSubject + .setFailureType(to: Error.self) + .eraseToAnyPublisher() + + override var footerButtonInfo: AnyPublisher { + selectedContactIdsSubject + .prepend([]) + .map { selectedContactIds in + SessionButton.Info( + style: .destructive, + title: "CONVERSATION_SETTINGS_BLOCKED_CONTACTS_UNBLOCK".localized(), + isEnabled: !selectedContactIds.isEmpty, + onTap: { [weak self] in self?.unblockTapped() } + ) + } + .eraseToAnyPublisher() + } + + // MARK: - Functions + + override func loadPageAfter() { _pagedDataObserver?.load(.pageAfter) } + + private func process( + data: [DataModel], + for pageInfo: PagedData.PageInfo + ) -> [SectionModel] { return [ [ SectionModel( section: .contacts, elements: data .sorted { lhs, rhs -> Bool in - lhs.profile.displayName() > rhs.profile.displayName() + lhs.profile.displayName() < rhs.profile.displayName() } - .map { model -> SessionCell.Info in + .map { [weak self] model -> SessionCell.Info in SessionCell.Info( id: model.profile, - leftAccessory: .profile(model.profile.id, model.profile), + leftAccessory: .profile(id: model.profile.id, profile: model.profile), title: model.profile.displayName(), rightAccessory: .radio( - isSelected: { [weak self] in - self?.selectedContactIds.contains(model.profile.id) == true + isSelected: { + self?.selectedContactIdsSubject.value.contains(model.profile.id) == true } ), - onTap: { [weak self] in - guard self?.selectedContactIds.contains(model.profile.id) == true else { - self?.selectedContactIds.insert(model.profile.id) - return + onTap: { + var updatedSelectedIds: Set = (self?.selectedContactIdsSubject.value ?? []) + + if !updatedSelectedIds.contains(model.profile.id) { + updatedSelectedIds.insert(model.profile.id) + } + else { + updatedSelectedIds.remove(model.profile.id) } - self?.selectedContactIds.remove(model.profile.id) + self?.selectedContactIdsSubject.send(updatedSelectedIds) } ) } @@ -210,7 +241,7 @@ public class BlockedContactsViewModel { confirmTitle: "CONVERSATION_SETTINGS_BLOCKED_CONTACTS_UNBLOCK_CONFIRMATION_ACTON".localized(), confirmStyle: .danger, cancelStyle: .alert_text - ) { _ in + ) { [weak self] _ in // Unblock the contacts Storage.shared.write { db in _ = try Contact @@ -220,6 +251,8 @@ public class BlockedContactsViewModel { // Force a config sync try MessageSender.syncConfiguration(db, forceSyncNow: true).sinkUntilComplete() } + + self?.selectedContactIdsSubject.send([]) } ) self.transitionToScreen(confirmationModal, transitionType: .present) @@ -242,8 +275,8 @@ public class BlockedContactsViewModel { static func query( filterSQL: SQL, orderSQL: SQL - ) -> (([Int64]) -> AdaptedFetchRequest>) { - return { rowIds -> AdaptedFetchRequest> in + ) -> (([Int64]) -> any FetchRequest) { + return { rowIds -> any FetchRequest in let profile: TypedTableAlias = TypedTableAlias() /// **Note:** The `numColumnsBeforeProfile` value **MUST** match the number of fields before diff --git a/Session/Settings/ConversationSettingsViewModel.swift b/Session/Settings/ConversationSettingsViewModel.swift index c8b56ec10..2786de2c0 100644 --- a/Session/Settings/ConversationSettingsViewModel.swift +++ b/Session/Settings/ConversationSettingsViewModel.swift @@ -26,7 +26,7 @@ class ConversationSettingsViewModel: SessionTableViewModel [SectionModel] in return [ SectionModel( @@ -92,10 +89,14 @@ class ConversationSettingsViewModel: SessionTableViewModel [SectionModel] in return [ SectionModel( @@ -50,7 +47,7 @@ class HelpViewModel: SessionTableViewModel Void + private let onImagePicked: (UIImage?, String?) -> Void + + // MARK: - Initialization + + init( + onTransition: @escaping (UIViewController, TransitionType) -> Void, + onImagePicked: @escaping (UIImage?, String?) -> Void + ) { + self.onTransition = onTransition + self.onImagePicked = onImagePicked + } + + // MARK: - UIImagePickerControllerDelegate + + func imagePickerControllerDidCancel(_ picker: UIImagePickerController) { + picker.dismiss(animated: true) + } + + func imagePickerController(_ picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [UIImagePickerController.InfoKey : Any]) { + guard + let imageUrl: URL = info[.imageURL] as? URL, + let rawAvatar: UIImage = info[.originalImage] as? UIImage + else { + picker.presentingViewController?.dismiss(animated: true) + return + } + + picker.presentingViewController?.dismiss(animated: true) { [weak self] in + // Check if the user selected an animated image (if so then don't crop, just + // set the avatar directly + guard + let type: Any = try? imageUrl.resourceValues(forKeys: [.typeIdentifierKey]) + .allValues + .first, + let typeString: String = type as? String, + MIMETypeUtil.supportedAnimatedImageUTITypes().contains(typeString) + else { + let viewController: CropScaleImageViewController = CropScaleImageViewController( + srcImage: rawAvatar, + successCompletion: { resultImage in + self?.onImagePicked(resultImage, nil) + } + ) + self?.onTransition(viewController, .present) + return + } + + self?.onImagePicked(nil, imageUrl.path) + } + } +} diff --git a/Session/Settings/NotificationContentViewModel.swift b/Session/Settings/NotificationContentViewModel.swift index c32e79dad..f4b97fbe1 100644 --- a/Session/Settings/NotificationContentViewModel.swift +++ b/Session/Settings/NotificationContentViewModel.swift @@ -31,10 +31,7 @@ class NotificationContentViewModel: SessionTableViewModel [SectionModel] in let currentSelection: Preferences.NotificationPreviewType? = db[.preferencesNotificationPreviewType] .defaulting(to: .defaultPreviewType) @@ -73,10 +70,5 @@ class NotificationContentViewModel: SessionTableViewModel [SectionModel] in let notificationSound: Preferences.Sound = db[.defaultNotificationSound] .defaulting(to: Preferences.Sound.defaultNotificationSound) @@ -72,9 +70,9 @@ class NotificationSettingsViewModel: SessionTableViewModel [SectionModel] in self?.storedSelection = try { guard let threadId: String = self?.threadId else { @@ -150,12 +147,9 @@ class NotificationSoundViewModel: SessionTableViewModel [SectionModel] in return [ SectionModel( @@ -128,34 +125,40 @@ class PrivacySettingsViewModel: SessionTableViewModel = { - isEditing - .map { isEditing in (isEditing ? .editing : .standard) } + Publishers + .CombineLatest( + isEditing + .map { isEditing in isEditing }, + textChanged + .handleEvents( + receiveOutput: { [weak self] value, _ in + self?.editedDisplayName = value + } + ) + .filter { _ in false } + .prepend((nil, .profileName)) + ) + .map { isEditing, _ -> NavState in (isEditing ? .editing : .standard) } .removeDuplicates() .prepend(.standard) // Initial value .eraseToAnyPublisher() @@ -176,10 +220,7 @@ class SettingsViewModel: SessionTableViewModel [SectionModel] in + private lazy var _observableTableData: ObservableData = ValueObservation + .trackingConstantRegion { [weak self] db -> [SectionModel] in let userPublicKey: String = getUserHexEncodedPublicKey(db) let profile: Profile = Profile.fetchOrCreateCurrentUser(db) @@ -198,38 +239,82 @@ class SettingsViewModel: SessionTableViewModel { Just(VersionFooterView()) @@ -383,26 +469,30 @@ class SettingsViewModel: SessionTableViewModel.SectionModel private let viewModel: SessionTableViewModel - private var hasLoadedInitialSettingsData: Bool = false + private var hasLoadedInitialTableData: Bool = false + private var isLoadingMore: Bool = false + private var isAutoLoadingNextPage: Bool = false + private var viewHasAppeared: Bool = false private var dataStreamJustFailed: Bool = false private var dataChangeCancellable: AnyCancellable? private var disposables: Set = Set() + private var onFooterTap: (() -> ())? public var viewModelType: AnyObject.Type { return type(of: viewModel) } @@ -32,7 +36,6 @@ class SessionTableViewController) { self.viewModel = viewModel + Storage.shared.addObserver(viewModel.pagedDataObserver) + super.init(nibName: nil, bundle: nil) } @@ -74,6 +116,9 @@ class SessionTableViewController, + initialLoad: Bool = false + ) { // Ensure the first load runs without animations (if we don't do this the cells will animate // in from a frame of CGRect.zero) - guard hasLoadedInitialSettingsData else { - hasLoadedInitialSettingsData = true - UIView.performWithoutAnimation { handleSettingsUpdates(updatedData, initialLoad: true) } + guard hasLoadedInitialTableData else { + hasLoadedInitialTableData = true + UIView.performWithoutAnimation { + handleDataUpdates(updatedData, changeset: changeset, initialLoad: true) + } return } + // Show the empty state if there is no data + let itemCount: Int = updatedData + .map { $0.elements.count } + .reduce(0, +) + emptyStateLabel.isHidden = (itemCount > 0) + + CATransaction.begin() + CATransaction.setCompletionBlock { [weak self] in + // Complete page loading + self?.isLoadingMore = false + self?.autoLoadNextPageIfNeeded() + } + // Reload the table content (animate changes after the first load) tableView.reload( - using: StagedChangeset(source: viewModel.settingsData, target: updatedData), + using: changeset, deleteSectionsAnimation: .none, insertSectionsAnimation: .none, reloadSectionsAnimation: .none, - deleteRowsAnimation: .bottom, - insertRowsAnimation: .none, - reloadRowsAnimation: .none, + deleteRowsAnimation: .fade, + insertRowsAnimation: .fade, + reloadRowsAnimation: .fade, interrupt: { $0.changeCount > 100 } // Prevent too many changes from causing performance issues ) { [weak self] updatedData in - self?.viewModel.updateSettings(updatedData) + self?.viewModel.updateTableData(updatedData) + } + + CATransaction.commit() + } + + private func autoLoadNextPageIfNeeded() { + guard !self.isAutoLoadingNextPage && !self.isLoadingMore else { return } + + self.isAutoLoadingNextPage = true + + DispatchQueue.main.asyncAfter(deadline: .now() + PagedData.autoLoadNextPageDelay) { [weak self] in + self?.isAutoLoadingNextPage = false + + // Note: We sort the headers as we want to prioritise loading newer pages over older ones + let sections: [(Section, CGRect)] = (self?.viewModel.tableData + .enumerated() + .map { index, section in + (section.model, (self?.tableView.rectForHeader(inSection: index) ?? .zero)) + }) + .defaulting(to: []) + let shouldLoadMore: Bool = sections + .contains { section, headerRect in + section.style == .loadMore && + headerRect != .zero && + (self?.tableView.bounds.contains(headerRect) == true) + } + + guard shouldLoadMore else { return } + + self?.isLoadingMore = true + + DispatchQueue.global(qos: .userInitiated).async { [weak self] in + self?.viewModel.loadPageAfter() + } } } @@ -188,18 +304,27 @@ class SessionTableViewController Int { - return self.viewModel.settingsData.count + return self.viewModel.tableData.count } func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { - return self.viewModel.settingsData[section].elements.count + return self.viewModel.tableData[section].elements.count } func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { - let section: SectionModel = viewModel.settingsData[indexPath.section] + let section: SectionModel = viewModel.tableData[indexPath.section] let info: SessionCell.Info = section.elements[indexPath.row] + let cell: SessionCell = tableView.dequeue(type: SessionCell.self, for: indexPath) + cell.update(with: info) + cell.update( + isEditing: (self.isEditing || (info.title?.interaction == .alwaysEditing)), + becomeFirstResponder: false, + animated: false + ) + cell.textPublisher + .sink(receiveValue: { [weak self] text in + self?.viewModel.textChanged(text, for: info.id) + }) + .store(in: &cell.disposables) - switch info.leftAccessory { - case .threadInfo(let threadViewModel, let style, let avatarTapped, let titleTapped, let titleChanged): - let cell: SessionAvatarCell = tableView.dequeue(type: SessionAvatarCell.self, for: indexPath) - cell.update( - threadViewModel: threadViewModel, - style: style, - viewController: self - ) - cell.update(isEditing: self.isEditing, animated: false) - - cell.profilePictureTapPublisher - .filter { _ in threadViewModel.threadVariant == .contact } - .sink(receiveValue: { _ in avatarTapped?() }) - .store(in: &cell.disposables) - - cell.displayNameTapPublisher - .filter { _ in threadViewModel.threadVariant == .contact } - .sink(receiveValue: { _ in titleTapped?() }) - .store(in: &cell.disposables) - - cell.textPublisher - .sink(receiveValue: { text in titleChanged?(text) }) - .store(in: &cell.disposables) - - return cell - - default: - let cell: SessionCell = tableView.dequeue(type: SessionCell.self, for: indexPath) - cell.update( - with: info, - style: .rounded, - position: Position.with(indexPath.row, count: section.elements.count) - ) - cell.update(isEditing: self.isEditing, animated: false) - - return cell - } + return cell } func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? { - let section: SectionModel = viewModel.settingsData[section] + let section: SectionModel = viewModel.tableData[section] + let result: SessionHeaderView = tableView.dequeueHeaderFooterView(type: SessionHeaderView.self) + result.update( + title: section.model.title, + style: section.model.style + ) - switch section.model.style { - case .none: - return UIView() - - case .padding, .title: - let result: SessionHeaderView = tableView.dequeueHeaderFooterView(type: SessionHeaderView.self) - result.update( - title: section.model.title, - hasSeparator: (section.elements.first?.shouldHaveBackground != false) - ) - - return result - } + return result } // MARK: - UITableViewDelegate func tableView(_ tableView: UITableView, heightForHeaderInSection section: Int) -> CGFloat { - let section: SectionModel = viewModel.settingsData[section] - - switch section.model.style { - case .none: return 0 - case .padding, .title: return UITableView.automaticDimension - } + return viewModel.tableData[section].model.style.height } func tableView(_ tableView: UITableView, estimatedHeightForRowAt indexPath: IndexPath) -> CGFloat { @@ -397,11 +525,28 @@ class SessionTableViewController CGFloat { return UITableView.automaticDimension } + + func tableView(_ tableView: UITableView, willDisplayHeaderView view: UIView, forSection section: Int) { + guard self.hasLoadedInitialTableData && self.viewHasAppeared && !self.isLoadingMore else { return } + + let section: SectionModel = self.viewModel.tableData[section] + + switch section.model.style { + case .loadMore: + self.isLoadingMore = true + + DispatchQueue.global(qos: .userInitiated).async { [weak self] in + self?.viewModel.loadPageAfter() + } + + default: break + } + } func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { tableView.deselectRow(at: indexPath, animated: true) - let section: SectionModel = self.viewModel.settingsData[indexPath.section] + let section: SectionModel = self.viewModel.tableData[indexPath.section] let info: SessionCell.Info = section.elements[indexPath.row] // Do nothing if the item is disabled @@ -414,10 +559,10 @@ class SessionTableViewController Void = { [weak self, weak tappedView] in - info.onTap?(tappedView) + info.onTap?() + info.onTapView?(tappedView) self?.manuallyReload(indexPath: indexPath, section: section, info: info) // Update the old selection as well @@ -463,10 +609,6 @@ class SessionTableViewController { typealias SectionModel = ArraySection> - typealias ObservableData = AnyPublisher<[SectionModel], Error> + typealias ObservableData = AnyPublisher<([SectionModel], StagedChangeset<[SectionModel]>), Error> // MARK: - Input @@ -18,6 +18,9 @@ class SessionTableViewModel = _isEditing .removeDuplicates() .shareReplay(1) + private let _textChanged: PassthroughSubject<(text: String?, item: SettingItem), Never> = PassthroughSubject() + lazy var textChanged: AnyPublisher<(text: String?, item: SettingItem), Never> = _textChanged + .eraseToAnyPublisher() // MARK: - Navigation @@ -37,15 +40,25 @@ class SessionTableViewModel { Just(nil).eraseToAnyPublisher() } + open var emptyStateTextPublisher: AnyPublisher { Just(nil).eraseToAnyPublisher() } - func updateSettings(_ updatedSettings: [SectionModel]) { + fileprivate var hasEmittedInitialData: Bool = false + public private(set) var tableData: [SectionModel] = [] + open var observableTableData: ObservableData { preconditionFailure("abstract class - override in subclass") } + open var pagedDataObserver: TransactionObserver? { nil } + open var footerView: AnyPublisher { Just(nil).eraseToAnyPublisher() } + open var footerButtonInfo: AnyPublisher { + Just(nil).eraseToAnyPublisher() + } + + func updateTableData(_ updatedData: [SectionModel]) { + self.tableData = updatedData + } + + func loadPageBefore() { preconditionFailure("abstract class - override in subclass") } + func loadPageAfter() { preconditionFailure("abstract class - override in subclass") } // MARK: - Functions @@ -53,6 +66,10 @@ class SessionTableViewModel( + for viewModel: SessionTableViewModel? + ) -> [ArraySection>] where Element == ArraySection> { + // Update the data to include the proper position for each element + return self.map { section in + ArraySection( + model: section.model, + elements: section.elements.enumerated().map { index, element in + element.updatedPosition(for: index, count: section.elements.count) + } + ) + } + } +} + +extension AnyPublisher { + func mapToSessionTableViewData( + for viewModel: SessionTableViewModel + ) -> AnyPublisher<(Output, StagedChangeset), Failure> where Output == [ArraySection>] { + return self + .map { [weak viewModel] updatedData -> (Output, StagedChangeset) in + let updatedDataWithPositions: Output = updatedData + .mapToSessionTableViewData(for: viewModel) + + // Generate an updated changeset + let changeset = StagedChangeset( + source: (viewModel?.tableData ?? []), + target: updatedDataWithPositions + ) + + return (updatedDataWithPositions, changeset) + } + .filter { [weak viewModel] _, changeset in + viewModel?.hasEmittedInitialData == false || // Always emit at least once + !changeset.isEmpty // Do nothing if there were no changes + } + .handleEvents(receiveOutput: { [weak viewModel] _ in + viewModel?.hasEmittedInitialData = true + }) + .eraseToAnyPublisher() + } +} diff --git a/Session/Shared/Types/DismissType.swift b/Session/Shared/Types/DismissType.swift index 494813082..0f8dbcd1c 100644 --- a/Session/Shared/Types/DismissType.swift +++ b/Session/Shared/Types/DismissType.swift @@ -10,6 +10,9 @@ public enum DismissType { /// This will only trigger a `popViewController` call (if the screen was presented it'll do nothing) case pop + /// This will only trigger a `popToRootViewController` call (if the screen was presented it'll do nothing) + case popToRoot + /// This will only trigger a `dismiss` call (if the screen was pushed to a presented navigation controller it'll dismiss /// the navigation controller, otherwise this will do nothing) case dismiss diff --git a/Session/Shared/Types/SessionCell+Accessibility.swift b/Session/Shared/Types/SessionCell+Accessibility.swift new file mode 100644 index 000000000..a9957c6df --- /dev/null +++ b/Session/Shared/Types/SessionCell+Accessibility.swift @@ -0,0 +1,18 @@ +// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved. + +import Foundation + +public extension SessionCell { + struct Accessibility: Hashable, Equatable { + let identifier: String? + let label: String? + + public init( + identifier: String? = nil, + label: String? = nil + ) { + self.identifier = identifier + self.label = label + } + } +} diff --git a/Session/Shared/Types/SessionCell+Accessory.swift b/Session/Shared/Types/SessionCell+Accessory.swift index fb0f08fe8..98a1da94f 100644 --- a/Session/Shared/Types/SessionCell+Accessory.swift +++ b/Session/Shared/Types/SessionCell+Accessory.swift @@ -12,45 +12,76 @@ extension SessionCell { UIImage?, size: IconSize, customTint: ThemeValue?, - shouldFill: Bool + shouldFill: Bool, + accessibility: SessionCell.Accessibility? ) case iconAsync( size: IconSize, customTint: ThemeValue?, shouldFill: Bool, + accessibility: SessionCell.Accessibility?, setter: (UIImageView) -> Void ) - case toggle(DataSource) - case dropDown(DataSource) + case toggle( + DataSource, + accessibility: SessionCell.Accessibility? + ) + case dropDown( + DataSource, + accessibility: SessionCell.Accessibility? + ) case radio( size: RadioSize, isSelected: () -> Bool, - storedSelection: Bool + storedSelection: Bool, + accessibility: SessionCell.Accessibility? ) - case highlightingBackgroundLabel(title: String) - case profile(String, Profile?) - case customView(viewGenerator: () -> UIView) - case threadInfo( - threadViewModel: SessionThreadViewModel, - style: ThreadInfoStyle = ThreadInfoStyle(), - avatarTapped: (() -> Void)? = nil, - titleTapped: (() -> Void)? = nil, - titleChanged: ((String) -> Void)? = nil + case highlightingBackgroundLabel( + title: String, + accessibility: SessionCell.Accessibility? + ) + case profile( + id: String, + size: IconSize, + threadVariant: SessionThread.Variant, + customImageData: Data?, + profile: Profile?, + additionalProfile: Profile?, + cornerIcon: UIImage?, + accessibility: SessionCell.Accessibility? + ) + + case search( + placeholder: String, + accessibility: SessionCell.Accessibility?, + searchTermChanged: (String?) -> Void + ) + case button( + style: SessionButton.Style, + title: String, + accessibility: SessionCell.Accessibility?, + run: (SessionButton?) -> Void + ) + case customView( + hashValue: AnyHashable, + viewGenerator: () -> UIView ) // MARK: - Convenience Vatiables var shouldFitToEdge: Bool { switch self { - case .icon(_, _, _, let shouldFill), .iconAsync(_, _, let shouldFill, _): return shouldFill + case .icon(_, _, _, let shouldFill, _), .iconAsync(_, _, let shouldFill, _, _): + return shouldFill default: return false } } var currentBoolValue: Bool { switch self { - case .toggle(let dataSource), .dropDown(let dataSource): return dataSource.currentBoolValue + case .toggle(let dataSource, _), .dropDown(let dataSource, _): return dataSource.currentBoolValue + case .radio(_, let isSelected, _, _): return isSelected() default: return false } } @@ -59,90 +90,166 @@ extension SessionCell { public func hash(into hasher: inout Hasher) { switch self { - case .icon(let image, let size, let customTint, let shouldFill): + case .icon(let image, let size, let customTint, let shouldFill, let accessibility): image.hash(into: &hasher) size.hash(into: &hasher) customTint.hash(into: &hasher) shouldFill.hash(into: &hasher) + accessibility.hash(into: &hasher) - case .iconAsync(let size, let customTint, let shouldFill, _): + case .iconAsync(let size, let customTint, let shouldFill, let accessibility, _): size.hash(into: &hasher) customTint.hash(into: &hasher) shouldFill.hash(into: &hasher) + accessibility.hash(into: &hasher) - case .toggle(let dataSource): + case .toggle(let dataSource, let accessibility): dataSource.hash(into: &hasher) + accessibility.hash(into: &hasher) - case .dropDown(let dataSource): + case .dropDown(let dataSource, let accessibility): dataSource.hash(into: &hasher) + accessibility.hash(into: &hasher) - case .radio(let size, let isSelected, let storedSelection): + case .radio(let size, let isSelected, let storedSelection, let accessibility): size.hash(into: &hasher) isSelected().hash(into: &hasher) storedSelection.hash(into: &hasher) + accessibility.hash(into: &hasher) - case .highlightingBackgroundLabel(let title): + case .highlightingBackgroundLabel(let title, let accessibility): title.hash(into: &hasher) + accessibility.hash(into: &hasher) - case .profile(let profileId, let profile): + case .profile( + let profileId, + let size, + let threadVariant, + let customImageData, + let profile, + let additionalProfile, + let cornerIcon, + let accessibility + ): profileId.hash(into: &hasher) + size.hash(into: &hasher) + threadVariant.hash(into: &hasher) + customImageData.hash(into: &hasher) profile.hash(into: &hasher) + additionalProfile.hash(into: &hasher) + cornerIcon.hash(into: &hasher) + accessibility.hash(into: &hasher) - case .customView: break - - case .threadInfo(let threadViewModel, let style, _, _, _): - threadViewModel.hash(into: &hasher) + case .search(let placeholder, let accessibility, _): + placeholder.hash(into: &hasher) + accessibility.hash(into: &hasher) + + case .button(let style, let title, let accessibility, _): style.hash(into: &hasher) + title.hash(into: &hasher) + accessibility.hash(into: &hasher) + + case .customView(let hashValue, _): + hashValue.hash(into: &hasher) } } public static func == (lhs: Accessory, rhs: Accessory) -> Bool { switch (lhs, rhs) { - case (.icon(let lhsImage, let lhsSize, let lhsCustomTint, let lhsShouldFill), .icon(let rhsImage, let rhsSize, let rhsCustomTint, let rhsShouldFill)): + case (.icon(let lhsImage, let lhsSize, let lhsCustomTint, let lhsShouldFill, let lhsAccessibility), .icon(let rhsImage, let rhsSize, let rhsCustomTint, let rhsShouldFill, let rhsAccessibility)): return ( lhsImage == rhsImage && lhsSize == rhsSize && lhsCustomTint == rhsCustomTint && - lhsShouldFill == rhsShouldFill + lhsShouldFill == rhsShouldFill && + lhsAccessibility == rhsAccessibility ) - case (.iconAsync(let lhsSize, let lhsCustomTint, let lhsShouldFill, _), .iconAsync(let rhsSize, let rhsCustomTint, let rhsShouldFill, _)): + case (.iconAsync(let lhsSize, let lhsCustomTint, let lhsShouldFill, let lhsAccessibility, _), .iconAsync(let rhsSize, let rhsCustomTint, let rhsShouldFill, let rhsAccessibility, _)): return ( lhsSize == rhsSize && lhsCustomTint == rhsCustomTint && - lhsShouldFill == rhsShouldFill + lhsShouldFill == rhsShouldFill && + lhsAccessibility == rhsAccessibility ) - case (.toggle(let lhsDataSource), .toggle(let rhsDataSource)): - return (lhsDataSource == rhsDataSource) + case (.toggle(let lhsDataSource, let lhsAccessibility), .toggle(let rhsDataSource, let rhsAccessibility)): + return ( + lhsDataSource == rhsDataSource && + lhsAccessibility == rhsAccessibility + ) - case (.dropDown(let lhsDataSource), .dropDown(let rhsDataSource)): - return (lhsDataSource == rhsDataSource) + case (.dropDown(let lhsDataSource, let lhsAccessibility), .dropDown(let rhsDataSource, let rhsAccessibility)): + return ( + lhsDataSource == rhsDataSource && + lhsAccessibility == rhsAccessibility + ) - case (.radio(let lhsSize, let lhsIsSelected, let lhsStoredSelection), .radio(let rhsSize, let rhsIsSelected, let rhsStoredSelection)): + case (.radio(let lhsSize, let lhsIsSelected, let lhsStoredSelection, let lhsAccessibility), .radio(let rhsSize, let rhsIsSelected, let rhsStoredSelection, let rhsAccessibility)): return ( lhsSize == rhsSize && lhsIsSelected() == rhsIsSelected() && - lhsStoredSelection == rhsStoredSelection + lhsStoredSelection == rhsStoredSelection && + lhsAccessibility == rhsAccessibility ) - case (.highlightingBackgroundLabel(let lhsTitle), .highlightingBackgroundLabel(let rhsTitle)): - return (lhsTitle == rhsTitle) + case (.highlightingBackgroundLabel(let lhsTitle, let lhsAccessibility), .highlightingBackgroundLabel(let rhsTitle, let rhsAccessibility)): + return ( + lhsTitle == rhsTitle && + lhsAccessibility == rhsAccessibility + ) - case (.profile(let lhsProfileId, let lhsProfile), .profile(let rhsProfileId, let rhsProfile)): + case ( + .profile( + let lhsProfileId, + let lhsSize, + let lhsThreadVariant, + let lhsProfile, + let lhsAdditionalProfile, + let lhsCustomImageData, + let lhsCornerIcon, + let lhsAccessibility + ), + .profile( + let rhsProfileId, + let rhsSize, + let rhsThreadVariant, + let rhsProfile, + let rhsAdditionalProfile, + let rhsCustomImageData, + let rhsCornerIcon, + let rhsAccessibility + ) + ): return ( lhsProfileId == rhsProfileId && - lhsProfile == rhsProfile + lhsSize == rhsSize && + lhsThreadVariant == rhsThreadVariant && + lhsProfile == rhsProfile && + lhsAdditionalProfile == rhsAdditionalProfile && + lhsCustomImageData == rhsCustomImageData && + lhsCornerIcon == rhsCornerIcon && + lhsAccessibility == rhsAccessibility ) - case (.customView, .customView): return false - - case (.threadInfo(let lhsThreadViewModel, let lhsStyle, _, _, _), .threadInfo(let rhsThreadViewModel, let rhsStyle, _, _, _)): + case (.search(let lhsPlaceholder, let lhsAccessibility, _), .search(let rhsPlaceholder, let rhsAccessibility, _)): return ( - lhsThreadViewModel == rhsThreadViewModel && - lhsStyle == rhsStyle + lhsPlaceholder == rhsPlaceholder && + lhsAccessibility == rhsAccessibility ) - + + case (.button(let lhsStyle, let lhsTitle, let lhsAccessibility, _), .button(let rhsStyle, let rhsTitle, let rhsAccessibility, _)): + return ( + lhsStyle == rhsStyle && + lhsTitle == rhsTitle && + lhsAccessibility == rhsAccessibility + ) + + case (.customView(let lhsHashValue, _), .customView(let rhsHashValue, _)): + return ( + lhsHashValue.hashValue == rhsHashValue.hashValue + ) + default: return false } } @@ -157,59 +264,121 @@ extension SessionCell.Accessory { // MARK: - .icon Variants public static func icon(_ image: UIImage?) -> SessionCell.Accessory { - return .icon(image, size: .medium, customTint: nil, shouldFill: false) + return .icon(image, size: .medium, customTint: nil, shouldFill: false, accessibility: nil) } public static func icon(_ image: UIImage?, customTint: ThemeValue) -> SessionCell.Accessory { - return .icon(image, size: .medium, customTint: customTint, shouldFill: false) + return .icon(image, size: .medium, customTint: customTint, shouldFill: false, accessibility: nil) } public static func icon(_ image: UIImage?, size: IconSize) -> SessionCell.Accessory { - return .icon(image, size: size, customTint: nil, shouldFill: false) + return .icon(image, size: size, customTint: nil, shouldFill: false, accessibility: nil) } public static func icon(_ image: UIImage?, size: IconSize, customTint: ThemeValue) -> SessionCell.Accessory { - return .icon(image, size: size, customTint: customTint, shouldFill: false) + return .icon(image, size: size, customTint: customTint, shouldFill: false, accessibility: nil) } public static func icon(_ image: UIImage?, shouldFill: Bool) -> SessionCell.Accessory { - return .icon(image, size: .medium, customTint: nil, shouldFill: shouldFill) + return .icon(image, size: .medium, customTint: nil, shouldFill: shouldFill, accessibility: nil) + } + + public static func icon(_ image: UIImage?, accessibility: SessionCell.Accessibility) -> SessionCell.Accessory { + return .icon(image, size: .medium, customTint: nil, shouldFill: false, accessibility: accessibility) } // MARK: - .iconAsync Variants public static func iconAsync(_ setter: @escaping (UIImageView) -> Void) -> SessionCell.Accessory { - return .iconAsync(size: .medium, customTint: nil, shouldFill: false, setter: setter) + return .iconAsync(size: .medium, customTint: nil, shouldFill: false, accessibility: nil, setter: setter) } public static func iconAsync(customTint: ThemeValue, _ setter: @escaping (UIImageView) -> Void) -> SessionCell.Accessory { - return .iconAsync(size: .medium, customTint: customTint, shouldFill: false, setter: setter) + return .iconAsync(size: .medium, customTint: customTint, shouldFill: false, accessibility: nil, setter: setter) } public static func iconAsync(size: IconSize, setter: @escaping (UIImageView) -> Void) -> SessionCell.Accessory { - return .iconAsync(size: size, customTint: nil, shouldFill: false, setter: setter) + return .iconAsync(size: size, customTint: nil, shouldFill: false, accessibility: nil, setter: setter) } public static func iconAsync(shouldFill: Bool, setter: @escaping (UIImageView) -> Void) -> SessionCell.Accessory { - return .iconAsync(size: .medium, customTint: nil, shouldFill: shouldFill, setter: setter) + return .iconAsync(size: .medium, customTint: nil, shouldFill: shouldFill, accessibility: nil, setter: setter) } public static func iconAsync(size: IconSize, customTint: ThemeValue, setter: @escaping (UIImageView) -> Void) -> SessionCell.Accessory { - return .iconAsync(size: size, customTint: customTint, shouldFill: false, setter: setter) + return .iconAsync(size: size, customTint: customTint, shouldFill: false, accessibility: nil, setter: setter) } public static func iconAsync(size: IconSize, shouldFill: Bool, setter: @escaping (UIImageView) -> Void) -> SessionCell.Accessory { - return .iconAsync(size: size, customTint: nil, shouldFill: shouldFill, setter: setter) + return .iconAsync(size: size, customTint: nil, shouldFill: shouldFill, accessibility: nil, setter: setter) + } + + // MARK: - .toggle Variants + + public static func toggle(_ dataSource: DataSource) -> SessionCell.Accessory { + return .toggle(dataSource, accessibility: nil) + } + + // MARK: - .dropDown Variants + + public static func dropDown(_ dataSource: DataSource) -> SessionCell.Accessory { + return .dropDown(dataSource, accessibility: nil) } // MARK: - .radio Variants public static func radio(isSelected: @escaping () -> Bool) -> SessionCell.Accessory { - return .radio(size: .medium, isSelected: isSelected, storedSelection: false) + return .radio(size: .medium, isSelected: isSelected, storedSelection: false, accessibility: nil) } public static func radio(isSelected: @escaping () -> Bool, storedSelection: Bool) -> SessionCell.Accessory { - return .radio(size: .medium, isSelected: isSelected, storedSelection: storedSelection) + return .radio(size: .medium, isSelected: isSelected, storedSelection: storedSelection, accessibility: nil) + } + + // MARK: - .highlightingBackgroundLabel Variants + + public static func highlightingBackgroundLabel(title: String) -> SessionCell.Accessory { + return .highlightingBackgroundLabel(title: title, accessibility: nil) + } + + // MARK: - .profile Variants + + public static func profile(id: String, profile: Profile?) -> SessionCell.Accessory { + return .profile( + id: id, + size: .veryLarge, + threadVariant: .contact, + customImageData: nil, + profile: profile, + additionalProfile: nil, + cornerIcon: nil, + accessibility: nil + ) + } + + public static func profile(id: String, size: IconSize, profile: Profile?) -> SessionCell.Accessory { + return .profile( + id: id, + size: size, + threadVariant: .contact, + customImageData: nil, + profile: profile, + additionalProfile: nil, + cornerIcon: nil, + accessibility: nil + ) + } + + // MARK: - .search Variants + + public static func search(placeholder: String, searchTermChanged: @escaping (String?) -> Void) -> SessionCell.Accessory { + return .search(placeholder: placeholder, accessibility: nil, searchTermChanged: searchTermChanged) + } + + // MARK: - .button Variants + + public static func button(style: SessionButton.Style, title: String, run: @escaping (SessionButton?) -> Void) -> SessionCell.Accessory { + return .button(style: style, title: title, accessibility: nil, run: run) } } @@ -293,42 +462,3 @@ extension SessionCell.Accessory { } } } - -// MARK: - SessionCell.Accessory.ThreadInfoStyle - -extension SessionCell.Accessory { - public struct ThreadInfoStyle: Hashable, Equatable { - public enum Style: Hashable, Equatable { - case small - case monoSmall - case monoLarge - } - - public struct Action: Hashable, Equatable { - let title: String - let run: (SessionButton?) -> () - - public func hash(into hasher: inout Hasher) { - title.hash(into: &hasher) - } - - public static func == (lhs: Action, rhs: Action) -> Bool { - return (lhs.title == rhs.title) - } - } - - public let separatorTitle: String? - public let descriptionStyle: Style - public let descriptionActions: [Action] - - public init( - separatorTitle: String? = nil, - descriptionStyle: Style = .monoSmall, - descriptionActions: [Action] = [] - ) { - self.separatorTitle = separatorTitle - self.descriptionStyle = descriptionStyle - self.descriptionActions = descriptionActions - } - } -} diff --git a/Session/Shared/Types/SessionCell+ExtraAction.swift b/Session/Shared/Types/SessionCell+ExtraAction.swift deleted file mode 100644 index 64ee0428c..000000000 --- a/Session/Shared/Types/SessionCell+ExtraAction.swift +++ /dev/null @@ -1,20 +0,0 @@ -// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved. - -import Foundation - -extension SessionCell { - struct ExtraAction: Hashable, Equatable { - let title: String - let onTap: (() -> Void) - - // MARK: - Conformance - - public func hash(into hasher: inout Hasher) { - title.hash(into: &hasher) - } - - static func == (lhs: SessionCell.ExtraAction, rhs: SessionCell.ExtraAction) -> Bool { - return (lhs.title == rhs.title) - } - } -} diff --git a/Session/Shared/Types/SessionCell+Info.swift b/Session/Shared/Types/SessionCell+Info.swift index e342f7789..35cd764ac 100644 --- a/Session/Shared/Types/SessionCell+Info.swift +++ b/Session/Shared/Types/SessionCell+Info.swift @@ -7,21 +7,17 @@ import SessionUIKit extension SessionCell { public struct Info: Equatable, Hashable, Differentiable { let id: ID + let position: Position let leftAccessory: SessionCell.Accessory? - let title: String - let subtitle: String? - let subtitleExtraViewGenerator: (() -> UIView)? - let tintColor: ThemeValue + let title: TextInfo? + let subtitle: TextInfo? let rightAccessory: SessionCell.Accessory? - let extraAction: SessionCell.ExtraAction? + let styling: StyleInfo let isEnabled: Bool - let shouldHaveBackground: Bool - let accessibilityIdentifier: String? - let accessibilityLabel: String? - let leftAccessoryAccessibilityLabel: String? - let rightAccessoryAccessibilityLabel: String? + let accessibility: SessionCell.Accessibility? let confirmationInfo: ConfirmationModal.Info? - let onTap: ((UIView?) -> Void)? + let onTap: (() -> Void)? + let onTapView: ((UIView?) -> Void)? var currentBoolValue: Bool { return ( @@ -34,74 +30,30 @@ extension SessionCell { init( id: ID, + position: Position = .individual, leftAccessory: SessionCell.Accessory? = nil, - title: String, - subtitle: String? = nil, - subtitleExtraViewGenerator: (() -> UIView)? = nil, - tintColor: ThemeValue = .textPrimary, + title: SessionCell.TextInfo? = nil, + subtitle: SessionCell.TextInfo? = nil, rightAccessory: SessionCell.Accessory? = nil, - extraAction: SessionCell.ExtraAction? = nil, + styling: StyleInfo = StyleInfo(), isEnabled: Bool = true, - shouldHaveBackground: Bool = true, - accessibilityIdentifier: String? = nil, - accessibilityLabel: String? = nil, - leftAccessoryAccessibilityLabel: String? = nil, - rightAccessoryAccessibilityLabel: String? = nil, + accessibility: SessionCell.Accessibility? = nil, confirmationInfo: ConfirmationModal.Info? = nil, - onTap: ((UIView?) -> Void)? + onTap: (() -> Void)? = nil, + onTapView: ((UIView?) -> Void)? = nil ) { self.id = id + self.position = position self.leftAccessory = leftAccessory self.title = title self.subtitle = subtitle - self.subtitleExtraViewGenerator = subtitleExtraViewGenerator - self.tintColor = tintColor self.rightAccessory = rightAccessory - self.extraAction = extraAction + self.styling = styling self.isEnabled = isEnabled - self.shouldHaveBackground = shouldHaveBackground - self.accessibilityIdentifier = accessibilityIdentifier - self.accessibilityLabel = accessibilityLabel - self.leftAccessoryAccessibilityLabel = leftAccessoryAccessibilityLabel - self.rightAccessoryAccessibilityLabel = rightAccessoryAccessibilityLabel + self.accessibility = accessibility self.confirmationInfo = confirmationInfo self.onTap = onTap - } - - init( - id: ID, - leftAccessory: SessionCell.Accessory? = nil, - title: String, - subtitle: String? = nil, - subtitleExtraViewGenerator: (() -> UIView)? = nil, - tintColor: ThemeValue = .textPrimary, - rightAccessory: SessionCell.Accessory? = nil, - extraAction: SessionCell.ExtraAction? = nil, - isEnabled: Bool = true, - shouldHaveBackground: Bool = true, - accessibilityIdentifier: String? = nil, - accessibilityLabel: String? = nil, - leftAccessoryAccessibilityLabel: String? = nil, - rightAccessoryAccessibilityLabel: String? = nil, - confirmationInfo: ConfirmationModal.Info? = nil, - onTap: (() -> Void)? = nil - ) { - self.id = id - self.leftAccessory = leftAccessory - self.title = title - self.subtitle = subtitle - self.subtitleExtraViewGenerator = subtitleExtraViewGenerator - self.tintColor = tintColor - self.rightAccessory = rightAccessory - self.extraAction = extraAction - self.isEnabled = isEnabled - self.shouldHaveBackground = shouldHaveBackground - self.accessibilityIdentifier = accessibilityIdentifier - self.accessibilityLabel = accessibilityLabel - self.leftAccessoryAccessibilityLabel = leftAccessoryAccessibilityLabel - self.rightAccessoryAccessibilityLabel = rightAccessoryAccessibilityLabel - self.confirmationInfo = confirmationInfo - self.onTap = (onTap != nil ? { _ in onTap?() } : nil) + self.onTapView = onTapView } // MARK: - Conformance @@ -110,37 +62,190 @@ extension SessionCell { public func hash(into hasher: inout Hasher) { id.hash(into: &hasher) + position.hash(into: &hasher) leftAccessory.hash(into: &hasher) title.hash(into: &hasher) subtitle.hash(into: &hasher) - tintColor.hash(into: &hasher) rightAccessory.hash(into: &hasher) - extraAction.hash(into: &hasher) + styling.hash(into: &hasher) isEnabled.hash(into: &hasher) - shouldHaveBackground.hash(into: &hasher) - accessibilityIdentifier.hash(into: &hasher) - accessibilityLabel.hash(into: &hasher) - leftAccessoryAccessibilityLabel.hash(into: &hasher) - rightAccessoryAccessibilityLabel.hash(into: &hasher) + accessibility.hash(into: &hasher) confirmationInfo.hash(into: &hasher) } public static func == (lhs: Info, rhs: Info) -> Bool { return ( lhs.id == rhs.id && + lhs.position == rhs.position && lhs.leftAccessory == rhs.leftAccessory && lhs.title == rhs.title && lhs.subtitle == rhs.subtitle && - lhs.tintColor == rhs.tintColor && lhs.rightAccessory == rhs.rightAccessory && - lhs.extraAction == rhs.extraAction && + lhs.styling == rhs.styling && lhs.isEnabled == rhs.isEnabled && - lhs.shouldHaveBackground == rhs.shouldHaveBackground && - lhs.accessibilityIdentifier == rhs.accessibilityIdentifier && - lhs.accessibilityLabel == rhs.accessibilityLabel && - lhs.leftAccessoryAccessibilityLabel == rhs.leftAccessoryAccessibilityLabel && - lhs.rightAccessoryAccessibilityLabel == rhs.rightAccessoryAccessibilityLabel + lhs.accessibility == rhs.accessibility + ) + } + + // MARK: - Convenience + + public func updatedPosition(for index: Int, count: Int) -> Info { + return Info( + id: id, + position: Position.with(index, count: count), + leftAccessory: leftAccessory, + title: title, + subtitle: subtitle, + rightAccessory: rightAccessory, + styling: styling, + isEnabled: isEnabled, + accessibility: accessibility, + confirmationInfo: confirmationInfo, + onTap: onTap, + onTapView: onTapView ) } } } + +// MARK: - Convenience Initializers + +public extension SessionCell.Info { + // Accessory, () -> Void + + init( + id: ID, + position: Position = .individual, + accessory: SessionCell.Accessory, + styling: SessionCell.StyleInfo = SessionCell.StyleInfo(), + isEnabled: Bool = true, + accessibility: SessionCell.Accessibility? = nil, + confirmationInfo: ConfirmationModal.Info? = nil, + onTap: (() -> Void)? = nil + ) { + self.id = id + self.position = position + self.leftAccessory = accessory + self.title = nil + self.subtitle = nil + self.rightAccessory = nil + self.styling = styling + self.isEnabled = isEnabled + self.accessibility = accessibility + self.confirmationInfo = confirmationInfo + self.onTap = onTap + self.onTapView = nil + } + + // leftAccessory, rightAccessory + + init( + id: ID, + position: Position = .individual, + leftAccessory: SessionCell.Accessory, + rightAccessory: SessionCell.Accessory, + styling: SessionCell.StyleInfo = SessionCell.StyleInfo(), + isEnabled: Bool = true, + accessibility: SessionCell.Accessibility? = nil, + confirmationInfo: ConfirmationModal.Info? = nil + ) { + self.id = id + self.position = position + self.leftAccessory = leftAccessory + self.title = nil + self.subtitle = nil + self.rightAccessory = rightAccessory + self.styling = styling + self.isEnabled = isEnabled + self.accessibility = accessibility + self.confirmationInfo = confirmationInfo + self.onTap = nil + self.onTapView = nil + } + + // String, () -> Void + + init( + id: ID, + position: Position = .individual, + leftAccessory: SessionCell.Accessory? = nil, + title: String, + rightAccessory: SessionCell.Accessory? = nil, + styling: SessionCell.StyleInfo = SessionCell.StyleInfo(), + isEnabled: Bool = true, + accessibility: SessionCell.Accessibility? = nil, + confirmationInfo: ConfirmationModal.Info? = nil, + onTap: (() -> Void)? = nil + ) { + self.id = id + self.position = position + self.leftAccessory = leftAccessory + self.title = SessionCell.TextInfo(title, font: .title) + self.subtitle = nil + self.rightAccessory = rightAccessory + self.styling = styling + self.isEnabled = isEnabled + self.accessibility = accessibility + self.confirmationInfo = confirmationInfo + self.onTap = onTap + self.onTapView = nil + } + + // TextInfo, () -> Void + + init( + id: ID, + position: Position = .individual, + leftAccessory: SessionCell.Accessory? = nil, + title: SessionCell.TextInfo, + rightAccessory: SessionCell.Accessory? = nil, + styling: SessionCell.StyleInfo = SessionCell.StyleInfo(), + isEnabled: Bool = true, + accessibility: SessionCell.Accessibility? = nil, + confirmationInfo: ConfirmationModal.Info? = nil, + onTap: (() -> Void)? = nil + ) { + self.id = id + self.position = position + self.leftAccessory = leftAccessory + self.title = title + self.subtitle = nil + self.rightAccessory = rightAccessory + self.styling = styling + self.isEnabled = isEnabled + self.accessibility = accessibility + self.confirmationInfo = confirmationInfo + self.onTap = onTap + self.onTapView = nil + } + + // String, String?, () -> Void + + init( + id: ID, + position: Position = .individual, + leftAccessory: SessionCell.Accessory? = nil, + title: String, + subtitle: String?, + rightAccessory: SessionCell.Accessory? = nil, + styling: SessionCell.StyleInfo = SessionCell.StyleInfo(), + isEnabled: Bool = true, + accessibility: SessionCell.Accessibility? = nil, + confirmationInfo: ConfirmationModal.Info? = nil, + onTap: (() -> Void)? = nil, + onTapView: ((UIView?) -> Void)? = nil + ) { + self.id = id + self.position = position + self.leftAccessory = leftAccessory + self.title = SessionCell.TextInfo(title, font: .title) + self.subtitle = SessionCell.TextInfo(subtitle, font: .subtitle) + self.rightAccessory = rightAccessory + self.styling = styling + self.isEnabled = isEnabled + self.accessibility = accessibility + self.confirmationInfo = confirmationInfo + self.onTap = onTap + self.onTapView = onTapView + } +} diff --git a/Session/Shared/Types/SessionCell+Styling.swift b/Session/Shared/Types/SessionCell+Styling.swift new file mode 100644 index 000000000..948cd1631 --- /dev/null +++ b/Session/Shared/Types/SessionCell+Styling.swift @@ -0,0 +1,170 @@ +// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved. + +import Foundation +import SessionUIKit + +// MARK: - Main Types + +public extension SessionCell { + struct TextInfo: Hashable, Equatable { + public enum Interaction: Hashable, Equatable { + case none + case editable + case copy + case alwaysEditing + } + + let text: String? + let textAlignment: NSTextAlignment + let editingPlaceholder: String? + let interaction: Interaction + let extraViewGenerator: (() -> UIView)? + + private let fontStyle: FontStyle + var font: UIFont { fontStyle.font } + + init( + _ text: String?, + font: FontStyle, + alignment: NSTextAlignment = .left, + editingPlaceholder: String? = nil, + interaction: Interaction = .none, + extraViewGenerator: (() -> UIView)? = nil + ) { + self.text = text + self.fontStyle = font + self.textAlignment = alignment + self.editingPlaceholder = editingPlaceholder + self.interaction = interaction + self.extraViewGenerator = extraViewGenerator + } + + // MARK: - Conformance + + public func hash(into hasher: inout Hasher) { + text.hash(into: &hasher) + fontStyle.hash(into: &hasher) + textAlignment.hash(into: &hasher) + interaction.hash(into: &hasher) + editingPlaceholder.hash(into: &hasher) + } + + public static func == (lhs: TextInfo, rhs: TextInfo) -> Bool { + return ( + lhs.text == rhs.text && + lhs.fontStyle == rhs.fontStyle && + lhs.textAlignment == rhs.textAlignment && + lhs.interaction == rhs.interaction && + lhs.editingPlaceholder == rhs.editingPlaceholder + ) + } + } + + struct StyleInfo: Equatable, Hashable { + let tintColor: ThemeValue + let alignment: SessionCell.Alignment + let allowedSeparators: Separators + let customPadding: Padding? + let backgroundStyle: SessionCell.BackgroundStyle + + public init( + tintColor: ThemeValue = .textPrimary, + alignment: SessionCell.Alignment = .leading, + allowedSeparators: Separators = [.top, .bottom], + customPadding: Padding? = nil, + backgroundStyle: SessionCell.BackgroundStyle = .rounded + ) { + self.tintColor = tintColor + self.alignment = alignment + self.allowedSeparators = allowedSeparators + self.customPadding = customPadding + self.backgroundStyle = backgroundStyle + } + } +} + +// MARK: - Child Types + +public extension SessionCell { + enum FontStyle: Hashable, Equatable { + case title + case titleLarge + + case subtitle + case subtitleBold + + case monoSmall + case monoLarge + + var font: UIFont { + switch self { + case .title: return .boldSystemFont(ofSize: 16) + case .titleLarge: return .systemFont(ofSize: Values.veryLargeFontSize, weight: .medium) + + case .subtitle: return .systemFont(ofSize: 14) + case .subtitleBold: return .boldSystemFont(ofSize: 14) + + case .monoSmall: return Fonts.spaceMono(ofSize: Values.smallFontSize) + case .monoLarge: return Fonts.spaceMono( + ofSize: (isIPhone5OrSmaller ? Values.mediumFontSize : Values.largeFontSize) + ) + } + } + } + + enum Alignment: Equatable, Hashable { + case leading + case centerHugging + } + + enum BackgroundStyle: Equatable, Hashable { + case rounded + case edgeToEdge + case noBackground + } + + struct Separators: OptionSet, Equatable, Hashable { + public let rawValue: Int8 + + public init(rawValue: Int8) { + self.rawValue = rawValue + } + + public static let top: Separators = Separators(rawValue: 1 << 0) + public static let bottom: Separators = Separators(rawValue: 1 << 1) + } + + struct Padding: Equatable, Hashable { + let top: CGFloat? + let leading: CGFloat? + let trailing: CGFloat? + let bottom: CGFloat? + let interItem: CGFloat? + + init( + top: CGFloat? = nil, + leading: CGFloat? = nil, + trailing: CGFloat? = nil, + bottom: CGFloat? = nil, + interItem: CGFloat? = nil + ) { + self.top = top + self.leading = leading + self.trailing = trailing + self.bottom = bottom + self.interItem = interItem + } + } +} + +// MARK: - ExpressibleByStringLiteral + +extension SessionCell.TextInfo: ExpressibleByStringLiteral, ExpressibleByExtendedGraphemeClusterLiteral, ExpressibleByUnicodeScalarLiteral { + public init(stringLiteral value: String) { + self = SessionCell.TextInfo(value, font: .title) + } + + public init(unicodeScalarLiteral value: Character) { + self = SessionCell.TextInfo(String(value), font: .title) + } +} diff --git a/Session/Shared/Types/SessionTableSection.swift b/Session/Shared/Types/SessionTableSection.swift index b0f117269..023e69f5f 100644 --- a/Session/Shared/Types/SessionTableSection.swift +++ b/Session/Shared/Types/SessionTableSection.swift @@ -2,6 +2,7 @@ import Foundation import DifferenceKit +import SessionUIKit protocol SessionTableSection: Differentiable { var title: String? { get } @@ -13,8 +14,36 @@ extension SessionTableSection { var style: SessionTableSectionStyle { .none } } -public enum SessionTableSectionStyle: Differentiable { +public enum SessionTableSectionStyle: Equatable, Hashable, Differentiable { case none - case title + case titleRoundedContent + case titleEdgeToEdgeContent + case titleNoBackgroundContent + case titleSeparator case padding + case loadMore + + var height: CGFloat { + switch self { + case .none: return 0 + case .titleRoundedContent, .titleEdgeToEdgeContent, .titleNoBackgroundContent: + return UITableView.automaticDimension + + case .titleSeparator: return Separator.height + case .padding: return Values.smallSpacing + case .loadMore: return 40 + } + } + + /// These values should always be consistent with the padding in `SessionCell` to ensure the text lines up + var edgePadding: CGFloat { + switch self { + case .titleRoundedContent, .titleNoBackgroundContent: + // Align to the start of the text in the cell + return (Values.largeSpacing + Values.mediumSpacing) + + case .titleEdgeToEdgeContent, .titleSeparator: return Values.largeSpacing + case .none, .padding, .loadMore: return 0 + } + } } diff --git a/Session/Shared/UserSelectionVC.swift b/Session/Shared/UserSelectionVC.swift index 0c920f101..17a5633cf 100644 --- a/Session/Shared/UserSelectionVC.swift +++ b/Session/Shared/UserSelectionVC.swift @@ -68,15 +68,15 @@ final class UserSelectionVC: BaseVC, UITableViewDataSource, UITableViewDelegate cell.update( with: SessionCell.Info( id: profile, - leftAccessory: .profile(profile.id, profile), + position: Position.with(indexPath.row, count: users.count), + leftAccessory: .profile(id: profile.id, profile: profile), title: profile.displayName(), rightAccessory: .radio(isSelected: { [weak self] in self?.selectedUsers.contains(profile.id) == true }), - accessibilityIdentifier: "Contact" - ), - style: .edgeToEdge, - position: Position.with(indexPath.row, count: users.count) + styling: SessionCell.StyleInfo(backgroundStyle: .edgeToEdge), + accessibility: SessionCell.Accessibility(identifier: "Contact") + ) ) return cell diff --git a/Session/Shared/Views/SessionAvatarCell.swift b/Session/Shared/Views/SessionAvatarCell.swift deleted file mode 100644 index c27d16ca7..000000000 --- a/Session/Shared/Views/SessionAvatarCell.swift +++ /dev/null @@ -1,308 +0,0 @@ -// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved. - -import UIKit -import Combine -import SessionUIKit -import SessionMessagingKit -import SessionUtilitiesKit -import SignalUtilitiesKit - -class SessionAvatarCell: UITableViewCell { - var disposables: Set = Set() - private var originalInputValue: String? - - // MARK: - Initialization - - override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { - super.init(style: style, reuseIdentifier: reuseIdentifier) - - setupViewHierarchy() - } - - required init?(coder: NSCoder) { - super.init(coder: coder) - - setupViewHierarchy() - } - - // MARK: - UI - - private let stackView: UIStackView = { - let stackView: UIStackView = UIStackView() - stackView.translatesAutoresizingMaskIntoConstraints = false - stackView.axis = .vertical - stackView.spacing = Values.mediumSpacing - stackView.alignment = .center - stackView.distribution = .equalSpacing - - let horizontalSpacing: CGFloat = (UIScreen.main.bounds.size.height < 568 ? - Values.largeSpacing : - Values.veryLargeSpacing - ) - stackView.layoutMargins = UIEdgeInsets( - top: Values.mediumSpacing, - leading: horizontalSpacing, - bottom: Values.largeSpacing, - trailing: horizontalSpacing - ) - stackView.isLayoutMarginsRelativeArrangement = true - - return stackView - }() - - fileprivate let profilePictureView: ProfilePictureView = { - let view: ProfilePictureView = ProfilePictureView() - view.accessibilityLabel = "Profile picture" - view.isAccessibilityElement = true - view.translatesAutoresizingMaskIntoConstraints = false - view.size = Values.largeProfilePictureSize - - return view - }() - - fileprivate let displayNameContainer: UIView = { - let view: UIView = UIView() - view.translatesAutoresizingMaskIntoConstraints = false - view.accessibilityLabel = "Username" - view.isAccessibilityElement = true - - return view - }() - - private lazy var displayNameLabel: UILabel = { - let label: UILabel = UILabel() - label.isAccessibilityElement = true - label.translatesAutoresizingMaskIntoConstraints = false - label.font = .ows_mediumFont(withSize: Values.veryLargeFontSize) - label.themeTextColor = .textPrimary - label.textAlignment = .center - label.lineBreakMode = .byTruncatingTail - label.numberOfLines = 0 - - return label - }() - - fileprivate let displayNameTextField: UITextField = { - let textField: TextField = TextField(placeholder: "Enter a name", usesDefaultHeight: false) - textField.translatesAutoresizingMaskIntoConstraints = false - textField.textAlignment = .center - textField.accessibilityIdentifier = "Nickname" - textField.accessibilityLabel = "Nickname" - textField.isAccessibilityElement = true - textField.alpha = 0 - - return textField - }() - - private let descriptionSeparator: Separator = { - let result: Separator = Separator() - result.isHidden = true - - return result - }() - - private let descriptionLabel: SRCopyableLabel = { - let label: SRCopyableLabel = SRCopyableLabel() - label.accessibilityLabel = "Session ID" - label.translatesAutoresizingMaskIntoConstraints = false - label.themeTextColor = .textPrimary - label.textAlignment = .center - label.lineBreakMode = .byCharWrapping - label.numberOfLines = 0 - - return label - }() - - private let descriptionActionStackView: UIStackView = { - let stackView: UIStackView = UIStackView() - stackView.translatesAutoresizingMaskIntoConstraints = false - stackView.axis = .horizontal - stackView.alignment = .center - stackView.distribution = .fillEqually - stackView.spacing = (UIDevice.current.isIPad ? Values.iPadButtonSpacing : Values.mediumSpacing) - - return stackView - }() - - private func setupViewHierarchy() { - self.themeBackgroundColor = nil - self.selectedBackgroundView = UIView() - - contentView.addSubview(stackView) - - stackView.addArrangedSubview(profilePictureView) - stackView.addArrangedSubview(displayNameContainer) - stackView.addArrangedSubview(descriptionSeparator) - stackView.addArrangedSubview(descriptionLabel) - stackView.addArrangedSubview(descriptionActionStackView) - - displayNameContainer.addSubview(displayNameLabel) - displayNameContainer.addSubview(displayNameTextField) - - setupLayout() - } - - // MARK: - Layout - - private func setupLayout() { - stackView.pin(to: contentView) - - profilePictureView.set(.width, to: profilePictureView.size) - profilePictureView.set(.height, to: profilePictureView.size) - - displayNameLabel.pin(to: displayNameContainer) - displayNameTextField.center(in: displayNameContainer) - displayNameTextField.widthAnchor - .constraint( - lessThanOrEqualTo: stackView.widthAnchor, - constant: -(stackView.layoutMargins.left + stackView.layoutMargins.right) - ) - .isActive = true - - descriptionSeparator.set( - .width, - to: .width, - of: stackView, - withOffset: -(stackView.layoutMargins.left + stackView.layoutMargins.right) - ) - descriptionActionStackView.set( - .width, - to: .width, - of: stackView, - withOffset: -(stackView.layoutMargins.left + stackView.layoutMargins.right) - ) - } - - // MARK: - Content - - override func prepareForReuse() { - super.prepareForReuse() - - self.disposables = Set() - self.originalInputValue = nil - self.displayNameLabel.text = nil - self.displayNameTextField.text = nil - self.descriptionLabel.font = .ows_lightFont(withSize: Values.smallFontSize) - self.descriptionLabel.text = nil - - self.descriptionSeparator.isHidden = true - self.descriptionActionStackView.arrangedSubviews.forEach { $0.removeFromSuperview() } - } - - func update( - threadViewModel: SessionThreadViewModel, - style: SessionCell.Accessory.ThreadInfoStyle, - viewController: UIViewController - ) { - profilePictureView.update( - publicKey: threadViewModel.threadId, - profile: threadViewModel.profile, - additionalProfile: threadViewModel.additionalProfile, - threadVariant: threadViewModel.threadVariant, - openGroupProfilePictureData: threadViewModel.openGroupProfilePictureData, - useFallbackPicture: ( - threadViewModel.threadVariant == .openGroup && - threadViewModel.openGroupProfilePictureData == nil - ), - showMultiAvatarForClosedGroup: true - ) - - originalInputValue = threadViewModel.profile?.nickname - displayNameLabel.text = { - guard !threadViewModel.threadIsNoteToSelf else { - guard let profile: Profile = threadViewModel.profile else { - return Profile.truncated(id: threadViewModel.threadId, truncating: .middle) - } - - return profile.displayName() - } - - return threadViewModel.displayName - }() - descriptionLabel.font = { - switch style.descriptionStyle { - case .small: return .ows_lightFont(withSize: Values.smallFontSize) - case .monoSmall: return Fonts.spaceMono(ofSize: Values.smallFontSize) - case .monoLarge: return Fonts.spaceMono( - ofSize: (isIPhone5OrSmaller ? Values.mediumFontSize : Values.largeFontSize) - ) - } - }() - descriptionLabel.text = threadViewModel.threadId - descriptionLabel.isHidden = (threadViewModel.threadVariant != .contact) - descriptionLabel.isUserInteractionEnabled = ( - threadViewModel.threadVariant == .contact || - threadViewModel.threadVariant == .openGroup - ) - displayNameTextField.text = threadViewModel.profile?.nickname - descriptionSeparator.update(title: style.separatorTitle) - descriptionSeparator.isHidden = (style.separatorTitle == nil) - - if (UIDevice.current.isIPad) { - descriptionActionStackView.addArrangedSubview(UIView.hStretchingSpacer()) - } - - style.descriptionActions.forEach { action in - let result: SessionButton = SessionButton(style: .bordered, size: .medium) - result.setTitle(action.title, for: UIControl.State.normal) - result.tapPublisher - .receive(on: DispatchQueue.main) - .sink(receiveValue: { [weak result] _ in action.run(result) }) - .store(in: &self.disposables) - - descriptionActionStackView.addArrangedSubview(result) - } - - if (UIDevice.current.isIPad) { - descriptionActionStackView.addArrangedSubview(UIView.hStretchingSpacer()) - } - descriptionActionStackView.isHidden = style.descriptionActions.isEmpty - } - - func update(isEditing: Bool, animated: Bool) { - let changes = { [weak self] in - self?.displayNameLabel.alpha = (isEditing ? 0 : 1) - self?.displayNameTextField.alpha = (isEditing ? 1 : 0) - } - let completion: (Bool) -> Void = { [weak self] complete in - self?.displayNameTextField.text = self?.originalInputValue - } - - if animated { - UIView.animate(withDuration: 0.25, animations: changes, completion: completion) - } - else { - changes() - completion(true) - } - - if isEditing { - displayNameTextField.becomeFirstResponder() - } - else { - displayNameTextField.resignFirstResponder() - } - } -} - -// MARK: - Compose - -extension CombineCompatible where Self: SessionAvatarCell { - var textPublisher: AnyPublisher { - return self.displayNameTextField.publisher(for: .editingChanged) - .map { textField -> String in (textField.text ?? "") } - .eraseToAnyPublisher() - } - - var displayNameTapPublisher: AnyPublisher { - return self.displayNameContainer.tapPublisher - .map { _ in () } - .eraseToAnyPublisher() - } - - var profilePictureTapPublisher: AnyPublisher { - return self.profilePictureView.tapPublisher - .map { _ in () } - .eraseToAnyPublisher() - } -} diff --git a/Session/Shared/Views/SessionCell+AccessoryView.swift b/Session/Shared/Views/SessionCell+AccessoryView.swift index dffda5793..c909e6041 100644 --- a/Session/Shared/Views/SessionCell+AccessoryView.swift +++ b/Session/Shared/Views/SessionCell+AccessoryView.swift @@ -7,15 +7,25 @@ import SessionUtilitiesKit import SignalUtilitiesKit extension SessionCell { - public class AccessoryView: UIView { + public class AccessoryView: UIView, UISearchBarDelegate { + // Note: We set a minimum width for the 'AccessoryView' so that the titles line up + // nicely when we have a mix of icons and switches + private static let minWidth: CGFloat = 50 + + private var onTap: ((SessionButton?) -> Void)? + private var searchTermChanged: ((String?) -> Void)? + // MARK: - UI + private lazy var minWidthConstraint: NSLayoutConstraint = self.widthAnchor + .constraint(greaterThanOrEqualToConstant: AccessoryView.minWidth) + private lazy var fixedWidthConstraint: NSLayoutConstraint = self.set(.width, to: AccessoryView.minWidth) private lazy var imageViewConstraints: [NSLayoutConstraint] = [ imageView.pin(.top, to: .top, of: self), - imageView.pin(.leading, to: .leading, of: self), - imageView.pin(.trailing, to: .trailing, of: self), imageView.pin(.bottom, to: .bottom, of: self) ] + private lazy var imageViewLeadingConstraint: NSLayoutConstraint = imageView.pin(.leading, to: .leading, of: self) + private lazy var imageViewTrailingConstraint: NSLayoutConstraint = imageView.pin(.trailing, to: .trailing, of: self) private lazy var imageViewWidthConstraint: NSLayoutConstraint = imageView.set(.width, to: 0) private lazy var imageViewHeightConstraint: NSLayoutConstraint = imageView.set(.height, to: 0) private lazy var toggleSwitchConstraints: [NSLayoutConstraint] = [ @@ -26,8 +36,8 @@ extension SessionCell { ] private lazy var dropDownStackViewConstraints: [NSLayoutConstraint] = [ dropDownStackView.pin(.top, to: .top, of: self), - dropDownStackView.pin(.leading, to: .leading, of: self), - dropDownStackView.pin(.trailing, to: .trailing, of: self), + dropDownStackView.pin(.leading, to: .leading, of: self, withInset: Values.smallSpacing), + dropDownStackView.pin(.trailing, to: .trailing, of: self, withInset: -Values.smallSpacing), dropDownStackView.pin(.bottom, to: .bottom, of: self) ] private lazy var radioViewWidthConstraint: NSLayoutConstraint = radioView.set(.width, to: 0) @@ -36,22 +46,35 @@ extension SessionCell { private lazy var radioBorderViewHeightConstraint: NSLayoutConstraint = radioBorderView.set(.height, to: 0) private lazy var radioBorderViewConstraints: [NSLayoutConstraint] = [ radioBorderView.pin(.top, to: .top, of: self), - radioBorderView.pin(.leading, to: .leading, of: self), - radioBorderView.pin(.trailing, to: .trailing, of: self), + radioBorderView.center(.horizontal, in: self), radioBorderView.pin(.bottom, to: .bottom, of: self) ] private lazy var highlightingBackgroundLabelConstraints: [NSLayoutConstraint] = [ highlightingBackgroundLabel.pin(.top, to: .top, of: self), - highlightingBackgroundLabel.pin(.leading, to: .leading, of: self), - highlightingBackgroundLabel.pin(.trailing, to: .trailing, of: self), + highlightingBackgroundLabel.pin(.leading, to: .leading, of: self, withInset: Values.smallSpacing), + highlightingBackgroundLabel.pin(.trailing, to: .trailing, of: self, withInset: -Values.smallSpacing), highlightingBackgroundLabel.pin(.bottom, to: .bottom, of: self) ] + private lazy var profilePictureViewLeadingConstraint: NSLayoutConstraint = profilePictureView.pin(.leading, to: .leading, of: self) + private lazy var profilePictureViewTrailingConstraint: NSLayoutConstraint = profilePictureView.pin(.trailing, to: .trailing, of: self) private lazy var profilePictureViewConstraints: [NSLayoutConstraint] = [ profilePictureView.pin(.top, to: .top, of: self), - profilePictureView.pin(.leading, to: .leading, of: self), - profilePictureView.pin(.trailing, to: .trailing, of: self), profilePictureView.pin(.bottom, to: .bottom, of: self) ] + private lazy var profilePictureViewWidthConstraint: NSLayoutConstraint = profilePictureView.set(.width, to: 0) + private lazy var profilePictureViewHeightConstraint: NSLayoutConstraint = profilePictureView.set(.height, to: 0) + private lazy var searchBarConstraints: [NSLayoutConstraint] = [ + searchBar.pin(.top, to: .top, of: self), + searchBar.pin(.leading, to: .leading, of: self, withInset: -8), // Removing default inset + searchBar.pin(.trailing, to: .trailing, of: self, withInset: 8), // Removing default inset + searchBar.pin(.bottom, to: .bottom, of: self) + ] + private lazy var buttonConstraints: [NSLayoutConstraint] = [ + button.pin(.top, to: .top, of: self), + button.pin(.leading, to: .leading, of: self), + button.pin(.trailing, to: .trailing, of: self), + button.pin(.bottom, to: .bottom, of: self) + ] private let imageView: UIImageView = { let result: UIImageView = UIImageView() @@ -143,10 +166,45 @@ extension SessionCell { private lazy var profilePictureView: ProfilePictureView = { let result: ProfilePictureView = ProfilePictureView() result.translatesAutoresizingMaskIntoConstraints = false - result.size = Values.smallProfilePictureSize result.isHidden = true - result.set(.width, to: Values.smallProfilePictureSize) - result.set(.height, to: Values.smallProfilePictureSize) + + return result + }() + + private lazy var profileIconContainerView: UIView = { + let result: UIView = UIView() + result.translatesAutoresizingMaskIntoConstraints = false + result.themeBackgroundColor = .primary + result.isHidden = true + result.set(.width, to: 26) + result.set(.height, to: 26) + result.layer.cornerRadius = (26 / 2) + + return result + }() + + private lazy var profileIconImageView: UIImageView = { + let result: UIImageView = UIImageView() + result.translatesAutoresizingMaskIntoConstraints = false + + return result + }() + + private lazy var searchBar: UISearchBar = { + let result: ContactsSearchBar = ContactsSearchBar() + result.themeTintColor = .textPrimary + result.themeBackgroundColor = .clear + result.searchTextField.themeBackgroundColor = .backgroundSecondary + result.delegate = self + + return result + }() + + private lazy var button: SessionButton = { + let result: SessionButton = SessionButton(style: .bordered, size: .medium) + result.translatesAutoresizingMaskIntoConstraints = false + result.addTarget(self, action: #selector(buttonTapped), for: .touchUpInside) + result.isHidden = true return result }() @@ -174,18 +232,29 @@ extension SessionCell { addSubview(radioBorderView) addSubview(highlightingBackgroundLabel) addSubview(profilePictureView) + addSubview(profileIconContainerView) + addSubview(button) + addSubview(searchBar) dropDownStackView.addArrangedSubview(dropDownImageView) dropDownStackView.addArrangedSubview(dropDownLabel) radioBorderView.addSubview(radioView) radioView.center(in: radioBorderView) + + profileIconContainerView.addSubview(profileIconImageView) + + profileIconContainerView.pin(.bottom, to: .bottom, of: profilePictureView) + profileIconContainerView.pin(.trailing, to: .trailing, of: profilePictureView) + profileIconImageView.pin(to: profileIconContainerView, withInset: Values.verySmallSpacing) } // MARK: - Content func prepareForReuse() { - self.isHidden = true + isHidden = true + onTap = nil + searchTermChanged = nil imageView.image = nil imageView.themeTintColor = .textPrimary @@ -207,7 +276,16 @@ extension SessionCell { radioView.isHidden = true highlightingBackgroundLabel.isHidden = true profilePictureView.isHidden = true + profileIconContainerView.isHidden = true + button.isHidden = true + searchBar.isHidden = true + minWidthConstraint.constant = AccessoryView.minWidth + minWidthConstraint.isActive = false + fixedWidthConstraint.constant = AccessoryView.minWidth + fixedWidthConstraint.isActive = false + imageViewLeadingConstraint.isActive = false + imageViewTrailingConstraint.isActive = false imageViewWidthConstraint.isActive = false imageViewHeightConstraint.isActive = false imageViewConstraints.forEach { $0.isActive = false } @@ -219,14 +297,19 @@ extension SessionCell { radioBorderViewHeightConstraint.isActive = false radioBorderViewConstraints.forEach { $0.isActive = false } highlightingBackgroundLabelConstraints.forEach { $0.isActive = false } + profilePictureViewLeadingConstraint.isActive = false + profilePictureViewTrailingConstraint.isActive = false + profilePictureViewWidthConstraint.isActive = false + profilePictureViewHeightConstraint.isActive = false profilePictureViewConstraints.forEach { $0.isActive = false } + searchBarConstraints.forEach { $0.isActive = false } + buttonConstraints.forEach { $0.isActive = false } } public func update( with accessory: Accessory?, tintColor: ThemeValue, - isEnabled: Bool, - accessibilityLabel: String? + isEnabled: Bool ) { guard let accessory: Accessory = accessory else { return } @@ -234,8 +317,9 @@ extension SessionCell { self.isHidden = false switch accessory { - case .icon(let image, let iconSize, let customTint, let shouldFill): - imageView.accessibilityLabel = accessibilityLabel + case .icon(let image, let iconSize, let customTint, let shouldFill, let accessibility): + imageView.accessibilityIdentifier = accessibility?.identifier + imageView.accessibilityLabel = accessibility?.label imageView.image = image imageView.themeTintColor = (customTint ?? tintColor) imageView.contentMode = (shouldFill ? .scaleAspectFill : .scaleAspectFit) @@ -244,21 +328,30 @@ extension SessionCell { switch iconSize { case .fit: imageView.sizeToFit() + fixedWidthConstraint.constant = (imageView.bounds.width + (shouldFill ? 0 : (Values.smallSpacing * 2))) + fixedWidthConstraint.isActive = true imageViewWidthConstraint.constant = imageView.bounds.width imageViewHeightConstraint.constant = imageView.bounds.height default: + fixedWidthConstraint.isActive = (iconSize.size <= fixedWidthConstraint.constant) imageViewWidthConstraint.constant = iconSize.size imageViewHeightConstraint.constant = iconSize.size } + minWidthConstraint.isActive = !fixedWidthConstraint.isActive + imageViewLeadingConstraint.constant = (shouldFill ? 0 : Values.smallSpacing) + imageViewTrailingConstraint.constant = (shouldFill ? 0 : -Values.smallSpacing) + imageViewLeadingConstraint.isActive = true + imageViewTrailingConstraint.isActive = true imageViewWidthConstraint.isActive = true imageViewHeightConstraint.isActive = true imageViewConstraints.forEach { $0.isActive = true } - case .iconAsync(let iconSize, let customTint, let shouldFill, let setter): + case .iconAsync(let iconSize, let customTint, let shouldFill, let accessibility, let setter): setter(imageView) - imageView.accessibilityLabel = accessibilityLabel + imageView.accessibilityIdentifier = accessibility?.identifier + imageView.accessibilityLabel = accessibility?.label imageView.themeTintColor = (customTint ?? tintColor) imageView.contentMode = (shouldFill ? .scaleAspectFill : .scaleAspectFit) imageView.isHidden = false @@ -266,22 +359,33 @@ extension SessionCell { switch iconSize { case .fit: imageView.sizeToFit() + fixedWidthConstraint.constant = (imageView.bounds.width + (shouldFill ? 0 : (Values.smallSpacing * 2))) + fixedWidthConstraint.isActive = true imageViewWidthConstraint.constant = imageView.bounds.width imageViewHeightConstraint.constant = imageView.bounds.height default: + fixedWidthConstraint.isActive = (iconSize.size <= fixedWidthConstraint.constant) imageViewWidthConstraint.constant = iconSize.size imageViewHeightConstraint.constant = iconSize.size } + minWidthConstraint.isActive = !fixedWidthConstraint.isActive + imageViewLeadingConstraint.constant = (shouldFill ? 0 : Values.smallSpacing) + imageViewTrailingConstraint.constant = (shouldFill ? 0 : -Values.smallSpacing) + imageViewLeadingConstraint.isActive = true + imageViewTrailingConstraint.isActive = true imageViewWidthConstraint.isActive = true imageViewHeightConstraint.isActive = true imageViewConstraints.forEach { $0.isActive = true } - case .toggle(let dataSource): - toggleSwitch.accessibilityLabel = accessibilityLabel + case .toggle(let dataSource, let accessibility): + toggleSwitch.accessibilityIdentifier = accessibility?.identifier + toggleSwitch.accessibilityLabel = accessibility?.label toggleSwitch.isHidden = false toggleSwitch.isEnabled = isEnabled + + fixedWidthConstraint.isActive = true toggleSwitchConstraints.forEach { $0.isActive = true } let newValue: Bool = dataSource.currentBoolValue @@ -290,13 +394,15 @@ extension SessionCell { toggleSwitch.setOn(newValue, animated: true) } - case .dropDown(let dataSource): - dropDownLabel.accessibilityLabel = accessibilityLabel + case .dropDown(let dataSource, let accessibility): + dropDownLabel.accessibilityIdentifier = accessibility?.identifier + dropDownLabel.accessibilityLabel = accessibility?.label dropDownLabel.text = dataSource.currentStringValue dropDownStackView.isHidden = false dropDownStackViewConstraints.forEach { $0.isActive = true } + minWidthConstraint.isActive = true - case .radio(let size, let isSelectedRetriever, let storedSelection): + case .radio(let size, let isSelectedRetriever, let storedSelection, let accessibility): let isSelected: Bool = isSelectedRetriever() let wasOldSelection: Bool = (!isSelected && storedSelection) @@ -307,7 +413,8 @@ extension SessionCell { ) radioBorderView.layer.cornerRadius = (size.borderSize / 2) - radioView.accessibilityLabel = accessibilityLabel + radioView.accessibilityIdentifier = accessibility?.identifier + radioView.accessibilityLabel = accessibility?.label radioView.alpha = (wasOldSelection ? 0.3 : 1) radioView.isHidden = (!isSelected && !storedSelection) radioView.themeBackgroundColor = (isSelected || wasOldSelection ? @@ -321,32 +428,89 @@ extension SessionCell { radioBorderViewWidthConstraint.constant = size.borderSize radioBorderViewHeightConstraint.constant = size.borderSize + fixedWidthConstraint.isActive = true radioViewWidthConstraint.isActive = true radioViewHeightConstraint.isActive = true radioBorderViewWidthConstraint.isActive = true radioBorderViewHeightConstraint.isActive = true radioBorderViewConstraints.forEach { $0.isActive = true } - case .highlightingBackgroundLabel(let title): - highlightingBackgroundLabel.accessibilityLabel = accessibilityLabel + case .highlightingBackgroundLabel(let title, let accessibility): + highlightingBackgroundLabel.accessibilityIdentifier = accessibility?.identifier + highlightingBackgroundLabel.accessibilityLabel = accessibility?.label highlightingBackgroundLabel.text = title highlightingBackgroundLabel.themeTextColor = tintColor highlightingBackgroundLabel.isHidden = false highlightingBackgroundLabelConstraints.forEach { $0.isActive = true } + minWidthConstraint.isActive = true - case .profile(let profileId, let profile): - profilePictureView.accessibilityLabel = accessibilityLabel + case .profile( + let profileId, + let profileSize, + let threadVariant, + let customImageData, + let profile, + let additionalProfile, + let cornerIcon, + let accessibility + ): + // Note: We MUST set the 'size' property before triggering the 'update' + // function or the profile picture won't layout correctly + switch profileSize { + case .fit: + profilePictureView.size = IconSize.large.size + profilePictureViewWidthConstraint.constant = IconSize.large.size + profilePictureViewHeightConstraint.constant = IconSize.large.size + + default: + profilePictureView.size = profileSize.size + profilePictureViewWidthConstraint.constant = profileSize.size + profilePictureViewHeightConstraint.constant = profileSize.size + } + + profilePictureView.accessibilityIdentifier = accessibility?.identifier + profilePictureView.accessibilityLabel = accessibility?.label profilePictureView.update( publicKey: profileId, + threadVariant: threadVariant, + customImageData: customImageData, profile: profile, - threadVariant: .contact + additionalProfile: additionalProfile ) profilePictureView.isHidden = false + profileIconContainerView.isHidden = (cornerIcon == nil) + profileIconImageView.image = cornerIcon + + fixedWidthConstraint.constant = profilePictureViewWidthConstraint.constant + fixedWidthConstraint.isActive = true + profilePictureViewLeadingConstraint.constant = (profilePictureView.size > AccessoryView.minWidth ? 0 : Values.smallSpacing) + profilePictureViewTrailingConstraint.constant = (profilePictureView.size > AccessoryView.minWidth ? 0 : -Values.smallSpacing) + profilePictureViewLeadingConstraint.isActive = true + profilePictureViewTrailingConstraint.isActive = true + profilePictureViewWidthConstraint.isActive = true + profilePictureViewHeightConstraint.isActive = true profilePictureViewConstraints.forEach { $0.isActive = true } - case .customView(let viewGenerator): + case .search(let placeholder, let accessibility, let searchTermChanged): + self.searchTermChanged = searchTermChanged + searchBar.accessibilityIdentifier = accessibility?.identifier + searchBar.accessibilityLabel = accessibility?.label + searchBar.placeholder = placeholder + searchBar.isHidden = false + searchBarConstraints.forEach { $0.isActive = true } + + case .button(let style, let title, let accessibility, let onTap): + self.onTap = onTap + button.accessibilityIdentifier = accessibility?.identifier + button.accessibilityLabel = accessibility?.label + button.setTitle(title, for: .normal) + button.setStyle(style) + button.isHidden = false + minWidthConstraint.isActive = true + buttonConstraints.forEach { $0.isActive = true } + + case .customView(_, let viewGenerator): let generatedView: UIView = viewGenerator() - generatedView.accessibilityLabel = accessibilityLabel addSubview(generatedView) generatedView.pin(.top, to: .top, of: self) @@ -354,10 +518,9 @@ extension SessionCell { generatedView.pin(.trailing, to: .trailing, of: self) generatedView.pin(.bottom, to: .bottom, of: self) - self.customView?.removeFromSuperview() // Just in case - self.customView = generatedView - - case .threadInfo: break + customView?.removeFromSuperview() // Just in case + customView = generatedView + minWidthConstraint.isActive = true } } @@ -370,6 +533,27 @@ extension SessionCell { func setSelected(_ selected: Bool, animated: Bool) { highlightingBackgroundLabel.setSelected(selected, animated: animated) } + + @objc private func buttonTapped() { + onTap?(button) + } + + // MARK: - UISearchBarDelegate + + public func searchBar(_ searchBar: UISearchBar, textDidChange searchText: String) { + searchTermChanged?(searchText) + } + + public func searchBarTextDidBeginEditing(_ searchBar: UISearchBar) { + searchBar.setShowsCancelButton(true, animated: true) + } + + public func searchBarTextDidEndEditing(_ searchBar: UISearchBar) { + searchBar.setShowsCancelButton(false, animated: true) + } + + public func searchBarCancelButtonClicked(_ searchBar: UISearchBar) { + searchBar.endEditing(true) + } } - } diff --git a/Session/Shared/Views/SessionCell.swift b/Session/Shared/Views/SessionCell.swift index e6fa6c033..d390fb1a2 100644 --- a/Session/Shared/Views/SessionCell.swift +++ b/Session/Shared/Views/SessionCell.swift @@ -1,6 +1,7 @@ // Copyright © 2022 Rangeproof Pty Ltd. All rights reserved. import UIKit +import Combine import GRDB import DifferenceKit import SessionUIKit @@ -9,17 +10,13 @@ import SessionUtilitiesKit public class SessionCell: UITableViewCell { public static let cornerRadius: CGFloat = 17 - public enum Style { - case rounded - case roundedEdgeToEdge - case edgeToEdge - } - - /// This value is here to allow the theming update callback to be released when preparing for reuse - private var instanceView: UIView = UIView() - private var position: Position? + private var isEditingTitle = false + public private(set) var interactionMode: SessionCell.TextInfo.Interaction = .none + private var shouldHighlightTitle: Bool = true + private var originalInputValue: String? + private var titleExtraView: UIView? private var subtitleExtraView: UIView? - private var onExtraActionTap: (() -> Void)? + var disposables: Set = Set() // MARK: - UI @@ -29,8 +26,18 @@ public class SessionCell: UITableViewCell { private var topSeparatorRightConstraint: NSLayoutConstraint = NSLayoutConstraint() private var botSeparatorLeftConstraint: NSLayoutConstraint = NSLayoutConstraint() private var botSeparatorRightConstraint: NSLayoutConstraint = NSLayoutConstraint() + private lazy var contentStackViewTopConstraint: NSLayoutConstraint = contentStackView.pin(.top, to: .top, of: cellBackgroundView) + private lazy var contentStackViewLeadingConstraint: NSLayoutConstraint = contentStackView.pin(.leading, to: .leading, of: cellBackgroundView) + private lazy var contentStackViewTrailingConstraint: NSLayoutConstraint = contentStackView.pin(.trailing, to: .trailing, of: cellBackgroundView) + private lazy var contentStackViewBottomConstraint: NSLayoutConstraint = contentStackView.pin(.bottom, to: .bottom, of: cellBackgroundView) + private lazy var contentStackViewHorizontalCenterConstraint: NSLayoutConstraint = contentStackView.center(.horizontal, in: cellBackgroundView) private lazy var leftAccessoryFillConstraint: NSLayoutConstraint = contentStackView.set(.height, to: .height, of: leftAccessoryView) + private lazy var titleTextFieldLeadingConstraint: NSLayoutConstraint = titleTextField.pin(.leading, to: .leading, of: cellBackgroundView) + private lazy var titleTextFieldTrailingConstraint: NSLayoutConstraint = titleTextField.pin(.trailing, to: .trailing, of: cellBackgroundView) + private lazy var titleMinHeightConstraint: NSLayoutConstraint = titleStackView.heightAnchor + .constraint(greaterThanOrEqualTo: titleTextField.heightAnchor) private lazy var rightAccessoryFillConstraint: NSLayoutConstraint = contentStackView.set(.height, to: .height, of: rightAccessoryView) + private lazy var accessoryWidthMatchConstraint: NSLayoutConstraint = leftAccessoryView.set(.width, to: .width, of: rightAccessoryView) private let cellBackgroundView: UIView = { let result: UIView = UIView() @@ -65,7 +72,6 @@ public class SessionCell: UITableViewCell { result.distribution = .fill result.alignment = .center result.spacing = Values.mediumSpacing - result.isLayoutMarginsRelativeArrangement = true return result }() @@ -89,10 +95,10 @@ public class SessionCell: UITableViewCell { return result }() - private let titleLabel: UILabel = { - let result: UILabel = UILabel() + private let titleLabel: SRCopyableLabel = { + let result: SRCopyableLabel = SRCopyableLabel() result.translatesAutoresizingMaskIntoConstraints = false - result.font = .boldSystemFont(ofSize: 15) + result.isUserInteractionEnabled = false result.themeTextColor = .textPrimary result.numberOfLines = 0 result.setCompressionResistanceHorizontalLow() @@ -101,10 +107,21 @@ public class SessionCell: UITableViewCell { return result }() - private let subtitleLabel: UILabel = { - let result: UILabel = UILabel() + fileprivate let titleTextField: UITextField = { + let textField: TextField = TextField(placeholder: "", usesDefaultHeight: false) + textField.translatesAutoresizingMaskIntoConstraints = false + textField.textAlignment = .center + textField.alpha = 0 + textField.isHidden = true + textField.set(.height, to: Values.largeButtonHeight) + + return textField + }() + + private let subtitleLabel: SRCopyableLabel = { + let result: SRCopyableLabel = SRCopyableLabel() result.translatesAutoresizingMaskIntoConstraints = false - result.font = .systemFont(ofSize: 13) + result.isUserInteractionEnabled = false result.themeTextColor = .textPrimary result.numberOfLines = 0 result.isHidden = true @@ -114,33 +131,6 @@ public class SessionCell: UITableViewCell { return result }() - private lazy var extraActionTopSpacingView: UIView = UIView.spacer(withHeight: Values.smallSpacing) - - private lazy var extraActionButton: UIButton = { - let result: UIButton = UIButton() - result.translatesAutoresizingMaskIntoConstraints = false - result.titleLabel?.font = .boldSystemFont(ofSize: Values.smallFontSize) - result.titleLabel?.numberOfLines = 0 - result.contentHorizontalAlignment = .left - result.contentEdgeInsets = UIEdgeInsets( - top: 8, - left: 0, - bottom: 0, - right: 0 - ) - result.addTarget(self, action: #selector(extraActionTapped), for: .touchUpInside) - result.isHidden = true - - ThemeManager.onThemeChange(observer: result) { [weak result] theme, _ in - switch theme.interfaceStyle { - case .light: result?.setThemeTitleColor(.textPrimary, for: .normal) - default: result?.setThemeTitleColor(.primary, for: .normal) - } - } - - return result - }() - public let rightAccessoryView: AccessoryView = { let result: AccessoryView = AccessoryView() result.isHidden = true @@ -186,8 +176,8 @@ public class SessionCell: UITableViewCell { titleStackView.addArrangedSubview(titleLabel) titleStackView.addArrangedSubview(subtitleLabel) - titleStackView.addArrangedSubview(extraActionTopSpacingView) - titleStackView.addArrangedSubview(extraActionButton) + + cellBackgroundView.addSubview(titleTextField) setupLayout() } @@ -204,7 +194,10 @@ public class SessionCell: UITableViewCell { topSeparatorLeftConstraint = topSeparator.pin(.left, to: .left, of: cellBackgroundView) topSeparatorRightConstraint = topSeparator.pin(.right, to: .right, of: cellBackgroundView) - contentStackView.pin(to: cellBackgroundView) + contentStackViewTopConstraint.isActive = true + contentStackViewBottomConstraint.isActive = true + + titleTextField.center(.vertical, in: titleLabel) botSeparatorLeftConstraint = botSeparator.pin(.left, to: .left, of: cellBackgroundView) botSeparatorRightConstraint = botSeparator.pin(.right, to: .right, of: cellBackgroundView) @@ -217,55 +210,59 @@ public class SessionCell: UITableViewCell { // Need to force the contentStackView to layout if needed as it might not have updated it's // sizing yet self.contentStackView.layoutIfNeeded() + repositionExtraView(titleExtraView, for: titleLabel) + repositionExtraView(subtitleExtraView, for: subtitleLabel) + } + + private func repositionExtraView(_ targetView: UIView?, for label: UILabel) { + guard + let targetView: UIView = targetView, + let content: String = label.text, + let font: UIFont = label.font + else { return } - // Position the 'subtitleExtraView' at the end of the last line of text - if - let subtitleExtraView: UIView = self.subtitleExtraView, - let subtitle: String = subtitleLabel.text, - let font: UIFont = subtitleLabel.font - { - let layoutManager: NSLayoutManager = NSLayoutManager() - let textStorage = NSTextStorage( - attributedString: NSAttributedString( - string: subtitle, - attributes: [ .font: font ] - ) + // Position the 'targetView' at the end of the last line of text + let layoutManager: NSLayoutManager = NSLayoutManager() + let textStorage = NSTextStorage( + attributedString: NSAttributedString( + string: content, + attributes: [ .font: font ] ) - textStorage.addLayoutManager(layoutManager) - - let textContainer: NSTextContainer = NSTextContainer( - size: CGSize( - width: subtitleLabel.bounds.size.width, - height: 999 - ) + ) + textStorage.addLayoutManager(layoutManager) + + let textContainer: NSTextContainer = NSTextContainer( + size: CGSize( + width: label.bounds.size.width, + height: 999 ) - textContainer.lineFragmentPadding = 0 - layoutManager.addTextContainer(textContainer) - - var glyphRange: NSRange = NSRange() - layoutManager.characterRange( - forGlyphRange: NSRange(location: subtitle.glyphCount - 1, length: 1), - actualGlyphRange: &glyphRange - ) - let lastGlyphRect: CGRect = layoutManager.boundingRect(forGlyphRange: glyphRange, in: textContainer) - - // Remove and re-add the 'subtitleExtraView' to clear any old constraints - subtitleExtraView.removeFromSuperview() - contentView.addSubview(subtitleExtraView) - - subtitleExtraView.pin( - .top, - to: .top, - of: subtitleLabel, - withInset: (lastGlyphRect.minY + ((lastGlyphRect.height / 2) - (subtitleExtraView.bounds.height / 2))) - ) - subtitleExtraView.pin( - .leading, - to: .leading, - of: subtitleLabel, - withInset: lastGlyphRect.maxX - ) - } + ) + textContainer.lineFragmentPadding = 0 + layoutManager.addTextContainer(textContainer) + + var glyphRange: NSRange = NSRange() + layoutManager.characterRange( + forGlyphRange: NSRange(location: content.glyphCount - 1, length: 1), + actualGlyphRange: &glyphRange + ) + let lastGlyphRect: CGRect = layoutManager.boundingRect(forGlyphRange: glyphRange, in: textContainer) + + // Remove and re-add the 'subtitleExtraView' to clear any old constraints + targetView.removeFromSuperview() + contentView.addSubview(targetView) + + targetView.pin( + .top, + to: .top, + of: label, + withInset: (lastGlyphRect.minY + ((lastGlyphRect.height / 2) - (targetView.bounds.height / 2))) + ) + targetView.pin( + .leading, + to: .leading, + of: label, + withInset: lastGlyphRect.maxX + ) } // MARK: - Content @@ -273,108 +270,185 @@ public class SessionCell: UITableViewCell { public override func prepareForReuse() { super.prepareForReuse() - self.instanceView = UIView() - self.position = nil - self.onExtraActionTap = nil - self.accessibilityIdentifier = nil + isEditingTitle = false + interactionMode = .none + shouldHighlightTitle = true + accessibilityIdentifier = nil + accessibilityLabel = nil + originalInputValue = nil + titleExtraView?.removeFromSuperview() + titleExtraView = nil + subtitleExtraView?.removeFromSuperview() + subtitleExtraView = nil + disposables = Set() + contentStackView.spacing = Values.mediumSpacing + contentStackViewLeadingConstraint.isActive = false + contentStackViewTrailingConstraint.isActive = false + contentStackViewHorizontalCenterConstraint.isActive = false + titleMinHeightConstraint.isActive = false leftAccessoryView.prepareForReuse() + leftAccessoryView.alpha = 1 leftAccessoryFillConstraint.isActive = false titleLabel.text = "" + titleLabel.textAlignment = .left titleLabel.themeTextColor = .textPrimary + titleLabel.alpha = 1 + titleTextField.text = "" + titleTextField.textAlignment = .center + titleTextField.themeTextColor = .textPrimary + titleTextField.isHidden = true + titleTextField.alpha = 0 + subtitleLabel.isUserInteractionEnabled = false subtitleLabel.text = "" subtitleLabel.themeTextColor = .textPrimary rightAccessoryView.prepareForReuse() + rightAccessoryView.alpha = 1 rightAccessoryFillConstraint.isActive = false + accessoryWidthMatchConstraint.isActive = false topSeparator.isHidden = true subtitleLabel.isHidden = true - extraActionTopSpacingView.isHidden = true - extraActionButton.setTitle("", for: .normal) - extraActionButton.isHidden = true botSeparator.isHidden = true - - subtitleExtraView?.removeFromSuperview() - subtitleExtraView = nil } - public func update( - with info: Info, - style: Style, - position: Position - ) { - self.instanceView = UIView() - self.position = position - self.subtitleExtraView = info.subtitleExtraViewGenerator?() - self.onExtraActionTap = info.extraAction?.onTap - self.accessibilityIdentifier = info.accessibilityIdentifier - self.accessibilityLabel = info.accessibilityLabel - self.isAccessibilityElement = true + public func update(with info: Info) { + interactionMode = (info.title?.interaction ?? .none) + shouldHighlightTitle = (info.title?.interaction != .copy) + titleExtraView = info.title?.extraViewGenerator?() + subtitleExtraView = info.subtitle?.extraViewGenerator?() + accessibilityIdentifier = info.accessibility?.identifier + accessibilityLabel = info.accessibility?.label + originalInputValue = info.title?.text + // Convenience Flags let leftFitToEdge: Bool = (info.leftAccessory?.shouldFitToEdge == true) let rightFitToEdge: Bool = (!leftFitToEdge && info.rightAccessory?.shouldFitToEdge == true) - leftAccessoryFillConstraint.isActive = leftFitToEdge + + // Content + contentStackView.spacing = (info.styling.customPadding?.interItem ?? Values.mediumSpacing) leftAccessoryView.update( with: info.leftAccessory, - tintColor: info.tintColor, - isEnabled: info.isEnabled, - accessibilityLabel: info.leftAccessoryAccessibilityLabel + tintColor: info.styling.tintColor, + isEnabled: info.isEnabled ) + titleStackView.isHidden = (info.title == nil && info.subtitle == nil) + titleLabel.isUserInteractionEnabled = (info.title?.interaction == .copy) + titleLabel.font = info.title?.font + titleLabel.text = info.title?.text + titleLabel.themeTextColor = info.styling.tintColor + titleLabel.textAlignment = (info.title?.textAlignment ?? .left) + titleLabel.isHidden = (info.title == nil) + titleTextField.text = info.title?.text + titleTextField.textAlignment = (info.title?.textAlignment ?? .left) + titleTextField.placeholder = info.title?.editingPlaceholder + titleTextField.isHidden = (info.title == nil) + subtitleLabel.isUserInteractionEnabled = (info.subtitle?.interaction == .copy) + subtitleLabel.font = info.subtitle?.font + subtitleLabel.text = info.subtitle?.text + subtitleLabel.themeTextColor = info.styling.tintColor + subtitleLabel.textAlignment = (info.subtitle?.textAlignment ?? .left) + subtitleLabel.isHidden = (info.subtitle == nil) rightAccessoryView.update( with: info.rightAccessory, - tintColor: info.tintColor, - isEnabled: info.isEnabled, - accessibilityLabel: info.rightAccessoryAccessibilityLabel - ) - rightAccessoryFillConstraint.isActive = rightFitToEdge - contentStackView.layoutMargins = UIEdgeInsets( - top: (leftFitToEdge || rightFitToEdge ? 0 : Values.mediumSpacing), - left: (leftFitToEdge ? 0 : Values.largeSpacing), - bottom: (leftFitToEdge || rightFitToEdge ? 0 : Values.mediumSpacing), - right: (rightFitToEdge ? 0 : Values.largeSpacing) + tintColor: info.styling.tintColor, + isEnabled: info.isEnabled ) - titleLabel.text = info.title - titleLabel.themeTextColor = info.tintColor - subtitleLabel.text = info.subtitle - subtitleLabel.themeTextColor = info.tintColor - subtitleLabel.isHidden = (info.subtitle == nil) - extraActionTopSpacingView.isHidden = (info.extraAction == nil) - extraActionButton.setTitle(info.extraAction?.title, for: .normal) - extraActionButton.isHidden = (info.extraAction == nil) + contentStackViewLeadingConstraint.isActive = (info.styling.alignment == .leading) + contentStackViewTrailingConstraint.isActive = (info.styling.alignment == .leading) + contentStackViewHorizontalCenterConstraint.constant = ((info.styling.customPadding?.leading ?? 0) + (info.styling.customPadding?.trailing ?? 0)) + contentStackViewHorizontalCenterConstraint.isActive = (info.styling.alignment == .centerHugging) + leftAccessoryFillConstraint.isActive = leftFitToEdge + rightAccessoryFillConstraint.isActive = rightFitToEdge + accessoryWidthMatchConstraint.isActive = { + switch (info.leftAccessory, info.rightAccessory) { + case (.button, .button): return true + default: return false + } + }() + titleLabel.setContentHuggingPriority( + (info.rightAccessory != nil ? .defaultLow : .required), + for: .horizontal + ) + titleLabel.setContentCompressionResistancePriority( + (info.rightAccessory != nil ? .defaultLow : .required), + for: .horizontal + ) + contentStackViewTopConstraint.constant = { + if let customPadding: CGFloat = info.styling.customPadding?.top { + return customPadding + } + + return (leftFitToEdge || rightFitToEdge ? 0 : Values.mediumSpacing) + }() + contentStackViewLeadingConstraint.constant = { + if let customPadding: CGFloat = info.styling.customPadding?.leading { + return customPadding + } + + return (leftFitToEdge ? 0 : Values.mediumSpacing) + }() + contentStackViewTrailingConstraint.constant = { + if let customPadding: CGFloat = info.styling.customPadding?.trailing { + return -customPadding + } + + return -(rightFitToEdge ? 0 : Values.mediumSpacing) + }() + contentStackViewBottomConstraint.constant = { + if let customPadding: CGFloat = info.styling.customPadding?.bottom { + return -customPadding + } + + return -(leftFitToEdge || rightFitToEdge ? 0 : Values.mediumSpacing) + }() + titleTextFieldLeadingConstraint.constant = { + guard info.styling.backgroundStyle != .noBackground else { return 0 } + + return (leftFitToEdge ? 0 : Values.mediumSpacing) + }() + titleTextFieldTrailingConstraint.constant = { + guard info.styling.backgroundStyle != .noBackground else { return 0 } + + return -(rightFitToEdge ? 0 : Values.mediumSpacing) + }() // Styling and positioning let defaultEdgePadding: CGFloat - cellBackgroundView.themeBackgroundColor = (info.shouldHaveBackground ? - .settings_tabBackground : - nil - ) - cellSelectedBackgroundView.isHidden = (!info.isEnabled || !info.shouldHaveBackground) - switch style { + switch info.styling.backgroundStyle { case .rounded: + cellBackgroundView.themeBackgroundColor = .settings_tabBackground + cellSelectedBackgroundView.isHidden = !info.isEnabled + defaultEdgePadding = Values.mediumSpacing backgroundLeftConstraint.constant = Values.largeSpacing backgroundRightConstraint.constant = -Values.largeSpacing cellBackgroundView.layer.cornerRadius = SessionCell.cornerRadius case .edgeToEdge: + cellBackgroundView.themeBackgroundColor = .settings_tabBackground + cellSelectedBackgroundView.isHidden = !info.isEnabled + defaultEdgePadding = 0 backgroundLeftConstraint.constant = 0 backgroundRightConstraint.constant = 0 cellBackgroundView.layer.cornerRadius = 0 - case .roundedEdgeToEdge: + case .noBackground: defaultEdgePadding = Values.mediumSpacing - backgroundLeftConstraint.constant = 0 - backgroundRightConstraint.constant = 0 - cellBackgroundView.layer.cornerRadius = SessionCell.cornerRadius + backgroundLeftConstraint.constant = Values.largeSpacing + backgroundRightConstraint.constant = -Values.largeSpacing + cellBackgroundView.themeBackgroundColor = nil + cellBackgroundView.layer.cornerRadius = 0 + cellSelectedBackgroundView.isHidden = true } let fittedEdgePadding: CGFloat = { func targetSize(accessory: Accessory?) -> CGFloat { switch accessory { - case .icon(_, let iconSize, _, _), .iconAsync(let iconSize, _, _, _): + case .icon(_, let iconSize, _, _, _), .iconAsync(let iconSize, _, _, _, _): return iconSize.size default: return defaultEdgePadding @@ -394,43 +468,103 @@ public class SessionCell: UITableViewCell { botSeparatorLeftConstraint.constant = (leftFitToEdge ? fittedEdgePadding : defaultEdgePadding) botSeparatorRightConstraint.constant = (rightFitToEdge ? -fittedEdgePadding : -defaultEdgePadding) - switch position { + switch info.position { case .top: cellBackgroundView.layer.maskedCorners = [.layerMinXMinYCorner, .layerMaxXMinYCorner] - topSeparator.isHidden = (style != .edgeToEdge) - botSeparator.isHidden = false + topSeparator.isHidden = ( + !info.styling.allowedSeparators.contains(.top) || + info.styling.backgroundStyle != .edgeToEdge + ) + botSeparator.isHidden = ( + !info.styling.allowedSeparators.contains(.bottom) || + info.styling.backgroundStyle == .noBackground + ) case .middle: cellBackgroundView.layer.maskedCorners = [] topSeparator.isHidden = true - botSeparator.isHidden = false + botSeparator.isHidden = ( + !info.styling.allowedSeparators.contains(.bottom) || + info.styling.backgroundStyle == .noBackground + ) case .bottom: cellBackgroundView.layer.maskedCorners = [.layerMinXMaxYCorner, .layerMaxXMaxYCorner] - topSeparator.isHidden = false - botSeparator.isHidden = (style != .edgeToEdge) + topSeparator.isHidden = true + botSeparator.isHidden = ( + !info.styling.allowedSeparators.contains(.bottom) || + info.styling.backgroundStyle != .edgeToEdge + ) case .individual: cellBackgroundView.layer.maskedCorners = [ .layerMinXMinYCorner, .layerMaxXMinYCorner, .layerMinXMaxYCorner, .layerMaxXMaxYCorner ] - topSeparator.isHidden = (style != .edgeToEdge) - botSeparator.isHidden = (style != .edgeToEdge) + topSeparator.isHidden = ( + !info.styling.allowedSeparators.contains(.top) || + info.styling.backgroundStyle != .edgeToEdge + ) + botSeparator.isHidden = ( + !info.styling.allowedSeparators.contains(.bottom) || + info.styling.backgroundStyle != .edgeToEdge + ) } } - public func update(isEditing: Bool, animated: Bool) {} + public func update(isEditing: Bool, becomeFirstResponder: Bool, animated: Bool) { + // Note: We set 'isUserInteractionEnabled' based on the 'info.isEditable' flag + // so can use that to determine whether this element can become editable + guard interactionMode == .editable || interactionMode == .alwaysEditing else { return } + + self.isEditingTitle = isEditing + + let changes = { [weak self] in + self?.titleLabel.alpha = (isEditing ? 0 : 1) + self?.titleTextField.alpha = (isEditing ? 1 : 0) + self?.leftAccessoryView.alpha = (isEditing ? 0 : 1) + self?.rightAccessoryView.alpha = (isEditing ? 0 : 1) + self?.titleMinHeightConstraint.isActive = isEditing + } + let completion: (Bool) -> Void = { [weak self] complete in + self?.titleTextField.text = self?.originalInputValue + } + + if animated { + UIView.animate(withDuration: 0.25, animations: changes, completion: completion) + } + else { + changes() + completion(true) + } + + if isEditing && becomeFirstResponder { + titleTextField.becomeFirstResponder() + } + else if !isEditing { + titleTextField.resignFirstResponder() + } + } // MARK: - Interaction public override func setHighlighted(_ highlighted: Bool, animated: Bool) { super.setHighlighted(highlighted, animated: animated) + // When editing disable the highlighted state changes (would result in UI elements + // reappearing otherwise) + guard !self.isEditingTitle else { return } + // If the 'cellSelectedBackgroundView' is hidden then there is no background so we // should update the titleLabel to indicate the highlighted state - if cellSelectedBackgroundView.isHidden { - titleLabel.alpha = (highlighted ? 0.8 : 1) + if cellSelectedBackgroundView.isHidden && shouldHighlightTitle { + // Note: We delay the "unhighlight" of the titleLabel so that the transition doesn't + // conflict with the transition into edit mode + DispatchQueue.main.asyncAfter(deadline: .now() + .milliseconds(10)) { [weak self] in + guard self?.isEditingTitle == false else { return } + + self?.titleLabel.alpha = (highlighted ? 0.8 : 1) + } } cellSelectedBackgroundView.alpha = (highlighted ? 1 : 0) @@ -440,12 +574,18 @@ public class SessionCell: UITableViewCell { public override func setSelected(_ selected: Bool, animated: Bool) { super.setSelected(selected, animated: animated) - + leftAccessoryView.setSelected(selected, animated: animated) rightAccessoryView.setSelected(selected, animated: animated) } - - @objc private func extraActionTapped() { - onExtraActionTap?() +} + +// MARK: - Compose + +extension CombineCompatible where Self: SessionCell { + var textPublisher: AnyPublisher { + return self.titleTextField.publisher(for: .editingChanged) + .map { textField -> String in (textField.text ?? "") } + .eraseToAnyPublisher() } } diff --git a/Session/Shared/Views/SessionHeaderView.swift b/Session/Shared/Views/SessionHeaderView.swift index bcb0fb768..4cb54f798 100644 --- a/Session/Shared/Views/SessionHeaderView.swift +++ b/Session/Shared/Views/SessionHeaderView.swift @@ -4,34 +4,44 @@ import UIKit import SessionUIKit class SessionHeaderView: UITableViewHeaderFooterView { - private lazy var emptyHeightConstraint: NSLayoutConstraint = self.heightAnchor - .constraint(equalToConstant: (Values.verySmallSpacing * 2)) - private lazy var filledHeightConstraint: NSLayoutConstraint = self.heightAnchor - .constraint(greaterThanOrEqualToConstant: Values.mediumSpacing) - // MARK: - UI - private let stackView: UIStackView = { - let result: UIStackView = UIStackView() - result.translatesAutoresizingMaskIntoConstraints = false - result.axis = .vertical - result.distribution = .fill - result.alignment = .fill - result.isLayoutMarginsRelativeArrangement = true - - return result - }() + private lazy var titleLabelConstraints: [NSLayoutConstraint] = [ + titleLabel.pin(.top, to: .top, of: self, withInset: Values.mediumSpacing), + titleLabel.pin(.bottom, to: .bottom, of: self, withInset: -Values.mediumSpacing) + ] + private lazy var titleLabelLeadingConstraint: NSLayoutConstraint = titleLabel.pin(.leading, to: .leading, of: self) + private lazy var titleLabelTrailingConstraint: NSLayoutConstraint = titleLabel.pin(.trailing, to: .trailing, of: self) + private lazy var titleSeparatorLeadingConstraint: NSLayoutConstraint = titleSeparator.pin(.leading, to: .leading, of: self) + private lazy var titleSeparatorTrailingConstraint: NSLayoutConstraint = titleSeparator.pin(.trailing, to: .trailing, of: self) private let titleLabel: UILabel = { let result: UILabel = UILabel() result.translatesAutoresizingMaskIntoConstraints = false result.font = .systemFont(ofSize: Values.mediumFontSize) result.themeTextColor = .textSecondary + result.isHidden = true return result }() - private let separator: UIView = UIView.separator() + private let titleSeparator: Separator = { + let result: Separator = Separator() + result.isHidden = true + + return result + }() + + private let loadingIndicator: UIActivityIndicatorView = { + let result: UIActivityIndicatorView = UIActivityIndicatorView(style: .medium) + result.themeTintColor = .textPrimary + result.alpha = 0.5 + result.startAnimating() + result.hidesWhenStopped = true + result.isHidden = true + + return result + }() // MARK: - Initialization @@ -41,10 +51,9 @@ class SessionHeaderView: UITableViewHeaderFooterView { self.backgroundView = UIView() self.backgroundView?.themeBackgroundColor = .backgroundPrimary - addSubview(stackView) - addSubview(separator) - - stackView.addArrangedSubview(titleLabel) + addSubview(titleLabel) + addSubview(titleSeparator) + addSubview(loadingIndicator) setupLayout() } @@ -54,42 +63,59 @@ class SessionHeaderView: UITableViewHeaderFooterView { } private func setupLayout() { - stackView.pin(to: self) + titleLabel.pin(.top, to: .top, of: self, withInset: Values.mediumSpacing) + titleLabel.pin(.bottom, to: .bottom, of: self, withInset: Values.mediumSpacing) + titleLabel.center(.vertical, in: self) - separator.pin(.left, to: .left, of: self) - separator.pin(.right, to: .right, of: self) - separator.pin(.bottom, to: .bottom, of: self) + titleSeparator.center(.vertical, in: self) + loadingIndicator.center(in: self) } // MARK: - Content + override func prepareForReuse() { + super.prepareForReuse() + + titleLabel.isHidden = true + titleSeparator.isHidden = true + loadingIndicator.isHidden = true + + titleLabelLeadingConstraint.isActive = false + titleLabelTrailingConstraint.isActive = false + titleLabelConstraints.forEach { $0.isActive = false } + + titleSeparator.center(.vertical, in: self) + titleSeparatorLeadingConstraint.isActive = false + titleSeparatorTrailingConstraint.isActive = false + } + public func update( - style: SessionCell.Style = .rounded, title: String?, - hasSeparator: Bool + style: SessionTableSectionStyle = .titleRoundedContent ) { let titleIsEmpty: Bool = (title ?? "").isEmpty - let edgePadding: CGFloat = { - switch style { - case .rounded: - // Align to the start of the text in the cell - return (Values.largeSpacing + Values.mediumSpacing) - - case .edgeToEdge, .roundedEdgeToEdge: return Values.largeSpacing - } - }() - titleLabel.text = title - titleLabel.isHidden = titleIsEmpty - stackView.layoutMargins = UIEdgeInsets( - top: (titleIsEmpty ? Values.verySmallSpacing : Values.mediumSpacing), - left: edgePadding, - bottom: (titleIsEmpty ? Values.verySmallSpacing : Values.mediumSpacing), - right: edgePadding - ) - emptyHeightConstraint.isActive = titleIsEmpty - filledHeightConstraint.isActive = !titleIsEmpty - separator.isHidden = (style == .rounded || !hasSeparator) + switch style { + case .titleRoundedContent, .titleEdgeToEdgeContent, .titleNoBackgroundContent: + titleLabel.text = title + titleLabel.isHidden = titleIsEmpty + titleLabelLeadingConstraint.constant = style.edgePadding + titleLabelTrailingConstraint.constant = -style.edgePadding + titleLabelLeadingConstraint.isActive = !titleIsEmpty + titleLabelTrailingConstraint.isActive = !titleIsEmpty + titleLabelConstraints.forEach { $0.isActive = true } + + case .titleSeparator: + titleSeparator.update(title: title) + titleSeparator.isHidden = false + titleSeparatorLeadingConstraint.constant = style.edgePadding + titleSeparatorTrailingConstraint.constant = -style.edgePadding + titleSeparatorLeadingConstraint.isActive = !titleIsEmpty + titleSeparatorTrailingConstraint.isActive = !titleIsEmpty + + case .none, .padding: break + case .loadMore: loadingIndicator.isHidden = false + } self.layoutIfNeeded() } diff --git a/Session/Utilities/BackgroundPoller.swift b/Session/Utilities/BackgroundPoller.swift index c618c40b1..5b5745127 100644 --- a/Session/Utilities/BackgroundPoller.swift +++ b/Session/Utilities/BackgroundPoller.swift @@ -110,22 +110,14 @@ public final class BackgroundPoller { .eraseToAnyPublisher() } - let promises: [Promise] = jobsToRun.map { job -> Promise in - let (promise, seal) = Promise.pending() - - // Note: In the background we just want jobs to fail silently - MessageReceiveJob.run( - job, - queue: DispatchQueue.main, - success: { _, _ in seal.fulfill(()) }, - failure: { _, _, _ in seal.fulfill(()) }, - deferred: { _ in seal.fulfill(()) } - ) - - return promise - } - - return when(fulfilled: promises) + return ClosedGroupPoller.poll( + namespaces: ClosedGroupPoller.namespaces, + from: snode, + for: groupPublicKey, + on: DispatchQueue.main, + calledFromBackgroundPoller: true, + isBackgroundPollValid: { BackgroundPoller.isValid } + ) } .eraseToAnyPublisher() } diff --git a/SessionMessagingKit/Common Networking/Header.swift b/SessionMessagingKit/Common Networking/Header.swift deleted file mode 100644 index 6c33e41a3..000000000 --- a/SessionMessagingKit/Common Networking/Header.swift +++ /dev/null @@ -1,22 +0,0 @@ -// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved. - -import Foundation - -enum Header: String { - case authorization = "Authorization" - case contentType = "Content-Type" - case contentDisposition = "Content-Disposition" - - case sogsPubKey = "X-SOGS-Pubkey" - case sogsNonce = "X-SOGS-Nonce" - case sogsTimestamp = "X-SOGS-Timestamp" - case sogsSignature = "X-SOGS-Signature" -} - -// MARK: - Convenience - -extension Dictionary where Key == Header, Value == String { - func toHTTPHeaders() -> [String: String] { - return self.reduce(into: [:]) { result, next in result[next.key.rawValue] = next.value } - } -} diff --git a/SessionMessagingKit/Common Networking/QueryParam.swift b/SessionMessagingKit/Common Networking/QueryParam.swift deleted file mode 100644 index d50ffbab5..000000000 --- a/SessionMessagingKit/Common Networking/QueryParam.swift +++ /dev/null @@ -1,15 +0,0 @@ -// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved. - -import Foundation - -enum QueryParam: String { - case publicKey = "public_key" - case fromServerId = "from_server_id" - - case required = "required" - case limit // For messages - number between 1 and 256 (default is 100) - case platform // For file server session version check - case updateTypes = "t" // String indicating the types of updates that the client supports - - case reactors = "reactors" -} diff --git a/SessionMessagingKit/Database/Models/ClosedGroup.swift b/SessionMessagingKit/Database/Models/ClosedGroup.swift index 9016643c3..085744805 100644 --- a/SessionMessagingKit/Database/Models/ClosedGroup.swift +++ b/SessionMessagingKit/Database/Models/ClosedGroup.swift @@ -92,16 +92,6 @@ public extension ClosedGroup { // MARK: - Convenience public extension ClosedGroup { - func asProfile() -> Profile { - return Profile( - id: threadId, - name: name, - profilePictureUrl: groupImageUrl, - profilePictureFileName: groupImageFileName, - profileEncryptionKey: groupImageEncryptionKey - ) - } - static func removeKeysAndUnsubscribe( _ db: Database? = nil, threadId: String, diff --git a/SessionMessagingKit/File Server/Types/FSEndpoint.swift b/SessionMessagingKit/File Server/Types/FSEndpoint.swift index 5e242bea8..edc555f4f 100644 --- a/SessionMessagingKit/File Server/Types/FSEndpoint.swift +++ b/SessionMessagingKit/File Server/Types/FSEndpoint.swift @@ -1,6 +1,7 @@ // Copyright © 2022 Rangeproof Pty Ltd. All rights reserved. import Foundation +import SessionUtilitiesKit extension FileServerAPI { public enum Endpoint: EndpointType { @@ -8,7 +9,7 @@ extension FileServerAPI { case fileIndividual(fileId: String) case sessionVersion - var path: String { + public var path: String { switch self { case .file: return "file" case .fileIndividual(let fileId): return "file/\(fileId)" diff --git a/SessionMessagingKit/Messages/Control Messages/ClosedGroupControlMessage.swift b/SessionMessagingKit/Messages/Control Messages/ClosedGroupControlMessage.swift index ec17d153e..4331ba7a4 100644 --- a/SessionMessagingKit/Messages/Control Messages/ClosedGroupControlMessage.swift +++ b/SessionMessagingKit/Messages/Control Messages/ClosedGroupControlMessage.swift @@ -338,8 +338,6 @@ public final class ClosedGroupControlMessage: ControlMessage { let contentProto = SNProtoContent.builder() let dataMessageProto = SNProtoDataMessage.builder() dataMessageProto.setClosedGroupControlMessage(try closedGroupControlMessage.build()) - // Group context - try setGroupContextIfNeeded(db, on: dataMessageProto) contentProto.setDataMessage(try dataMessageProto.build()) return try contentProto.build() } catch { diff --git a/SessionMessagingKit/Messages/Control Messages/ExpirationTimerUpdate.swift b/SessionMessagingKit/Messages/Control Messages/ExpirationTimerUpdate.swift index 3d7aadda5..1d8de57df 100644 --- a/SessionMessagingKit/Messages/Control Messages/ExpirationTimerUpdate.swift +++ b/SessionMessagingKit/Messages/Control Messages/ExpirationTimerUpdate.swift @@ -77,13 +77,6 @@ public final class ExpirationTimerUpdate: ControlMessage { dataMessageProto.setFlags(UInt32(SNProtoDataMessage.SNProtoDataMessageFlags.expirationTimerUpdate.rawValue)) dataMessageProto.setExpireTimer(duration) if let syncTarget = syncTarget { dataMessageProto.setSyncTarget(syncTarget) } - // Group context - do { - try setGroupContextIfNeeded(db, on: dataMessageProto) - } catch { - SNLog("Couldn't construct expiration timer update proto from: \(self).") - return nil - } let contentProto = SNProtoContent.builder() do { contentProto.setDataMessage(try dataMessageProto.build()) diff --git a/SessionMessagingKit/Messages/Message+Destination.swift b/SessionMessagingKit/Messages/Message+Destination.swift index 9694b2670..40211e90f 100644 --- a/SessionMessagingKit/Messages/Message+Destination.swift +++ b/SessionMessagingKit/Messages/Message+Destination.swift @@ -24,6 +24,13 @@ public extension Message { ) case openGroupInbox(server: String, openGroupPublicKey: String, blindedPublicKey: String) + var namespace: SnodeAPI.Namespace { + switch self { + case .contact(_, let namespace), .closedGroup(_, let namespace): return namespace + default: preconditionFailure("Attepted to retrieve namespace for invalid destination") + } + } + public static func from( _ db: Database, thread: SessionThread, diff --git a/SessionMessagingKit/Messages/Message.swift b/SessionMessagingKit/Messages/Message.swift index 6d8c63a64..dc31d502e 100644 --- a/SessionMessagingKit/Messages/Message.swift +++ b/SessionMessagingKit/Messages/Message.swift @@ -63,18 +63,6 @@ public class Message: Codable { public func toProto(_ db: Database) -> SNProtoContent? { preconditionFailure("toProto(_:) is abstract and must be overridden.") } - - public func setGroupContextIfNeeded(_ db: Database, on dataMessage: SNProtoDataMessage.SNProtoDataMessageBuilder) throws { - guard - let threadId: String = threadId, - (try? ClosedGroup.exists(db, id: threadId)) == true, - let legacyGroupId: Data = "\(SMKLegacy.closedGroupIdPrefix)\(threadId)".data(using: .utf8) - else { return } - - // Android needs a group context or it'll interpret the message as a one-to-one message - let groupProto = SNProtoGroupContext.builder(id: legacyGroupId, type: .deliver) - dataMessage.setGroup(try groupProto.build()) - } } // MARK: - Message Parsing/Processing diff --git a/SessionMessagingKit/Messages/Visible Messages/VisibleMessage.swift b/SessionMessagingKit/Messages/Visible Messages/VisibleMessage.swift index e48da7f33..8bd727094 100644 --- a/SessionMessagingKit/Messages/Visible Messages/VisibleMessage.swift +++ b/SessionMessagingKit/Messages/Visible Messages/VisibleMessage.swift @@ -158,7 +158,7 @@ public final class VisibleMessage: Message { // Attachments - let attachments: [Attachment]? = try? Attachment.fetchAll(db, ids: self.attachmentIds) + let attachments: [Attachment]? = try? Attachment.fetchAll(db, ids: self.attachmentIds) let attachmentProtos = (attachments ?? []).compactMap { $0.buildProto() } dataMessage.setAttachments(attachmentProtos) @@ -175,14 +175,6 @@ public final class VisibleMessage: Message { dataMessage.setReaction(reactionProto) } - // Group context - do { - try setGroupContextIfNeeded(db, on: dataMessage) - } catch { - SNLog("Couldn't construct visible message proto from: \(self).") - return nil - } - // Sync target if let syncTarget = syncTarget { dataMessage.setSyncTarget(syncTarget) diff --git a/SessionMessagingKit/Open Groups/Models/BatchRequestInfo.swift b/SessionMessagingKit/Open Groups/Models/BatchRequestInfo.swift deleted file mode 100644 index fb3ac4e41..000000000 --- a/SessionMessagingKit/Open Groups/Models/BatchRequestInfo.swift +++ /dev/null @@ -1,174 +0,0 @@ -// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved. - -import Foundation -import PromiseKit -import SessionUtilitiesKit -import SessionSnodeKit - -extension OpenGroupAPI { - // MARK: - BatchSubRequest - - struct BatchSubRequest: Encodable { - enum CodingKeys: String, CodingKey { - case method - case path - case headers - case json - case b64 - case bytes - } - - let method: HTTP.Verb - let path: String - let headers: [String: String]? - - /// The `jsonBodyEncoder` is used to avoid having to make `BatchSubRequest` a generic type (haven't found a good way - /// to keep `BatchSubRequest` encodable using protocols unfortunately so need this work around) - private let jsonBodyEncoder: ((inout KeyedEncodingContainer, CodingKeys) throws -> ())? - private let b64: String? - private let bytes: [UInt8]? - - init(request: Request) { - self.method = request.method - self.path = request.urlPathAndParamsString - self.headers = (request.headers.isEmpty ? nil : request.headers.toHTTPHeaders()) - - // Note: Need to differentiate between JSON, b64 string and bytes body values to ensure they are - // encoded correctly so the server knows how to handle them - switch request.body { - case let bodyString as String: - self.jsonBodyEncoder = nil - self.b64 = bodyString - self.bytes = nil - - case let bodyBytes as [UInt8]: - self.jsonBodyEncoder = nil - self.b64 = nil - self.bytes = bodyBytes - - default: - self.jsonBodyEncoder = { [body = request.body] container, key in - try container.encodeIfPresent(body, forKey: key) - } - self.b64 = nil - self.bytes = nil - } - } - - func encode(to encoder: Encoder) throws { - var container: KeyedEncodingContainer = encoder.container(keyedBy: CodingKeys.self) - - try container.encode(method, forKey: .method) - try container.encode(path, forKey: .path) - try container.encodeIfPresent(headers, forKey: .headers) - try jsonBodyEncoder?(&container, .json) - try container.encodeIfPresent(b64, forKey: .b64) - try container.encodeIfPresent(bytes, forKey: .bytes) - } - } - - // MARK: - BatchSubResponse - - struct BatchSubResponse: Codable { - /// The numeric http response code (e.g. 200 for success) - let code: Int32 - - /// This should always include the content type of the request - let headers: [String: String] - - /// The body of the request; will be plain json if content-type is `application/json`, otherwise it will be base64 encoded data - let body: T? - - /// A flag to indicate that there was a body but it failed to parse - let failedToParseBody: Bool - } - - // MARK: - BatchRequestInfo - - struct BatchRequestInfo: BatchRequestInfoType { - let request: Request - let responseType: Codable.Type - - var endpoint: Endpoint { request.endpoint } - - init(request: Request, responseType: R.Type) { - self.request = request - self.responseType = BatchSubResponse.self - } - - init(request: Request) { - self.init( - request: request, - responseType: NoResponse.self - ) - } - - func toSubRequest() -> BatchSubRequest { - return BatchSubRequest(request: request) - } - } - - // MARK: - BatchRequest - - typealias BatchRequest = [BatchSubRequest] - typealias BatchResponseTypes = [Codable.Type] - typealias BatchResponse = [(OnionRequestResponseInfoType, Codable?)] -} - -extension OpenGroupAPI.BatchSubResponse { - init(from decoder: Decoder) throws { - let container: KeyedDecodingContainer = try decoder.container(keyedBy: CodingKeys.self) - let body: T? = try? container.decode(T.self, forKey: .body) - - self = OpenGroupAPI.BatchSubResponse( - code: try container.decode(Int32.self, forKey: .code), - headers: try container.decode([String: String].self, forKey: .headers), - body: body, - failedToParseBody: (body == nil && T.self != NoResponse.self && !(T.self is ExpressibleByNilLiteral.Type)) - ) - } -} - -// MARK: - BatchRequestInfoType - -/// This protocol is designed to erase the types from `BatchRequestInfo` so multiple types can be used -/// in arrays when doing `/batch` and `/sequence` requests -protocol BatchRequestInfoType { - var responseType: Codable.Type { get } - var endpoint: OpenGroupAPI.Endpoint { get } - - func toSubRequest() -> OpenGroupAPI.BatchSubRequest -} - -// MARK: - Convenience - -public extension Decodable { - static func decoded(from data: Data, using dependencies: Dependencies = Dependencies()) throws -> Self { - return try data.decoded(as: Self.self, using: dependencies) - } -} - -extension Promise where T == (OnionRequestResponseInfoType, Data?) { - func decoded(as types: OpenGroupAPI.BatchResponseTypes, on queue: DispatchQueue? = nil, using dependencies: Dependencies = Dependencies()) -> Promise { - self.map(on: queue) { responseInfo, maybeData -> OpenGroupAPI.BatchResponse in - // Need to split the data into an array of data so each item can be Decoded correctly - guard let data: Data = maybeData else { throw HTTP.Error.parsingFailed } - guard let jsonObject: Any = try? JSONSerialization.jsonObject(with: data, options: [.fragmentsAllowed]) else { - throw HTTP.Error.parsingFailed - } - guard let anyArray: [Any] = jsonObject as? [Any] else { throw HTTP.Error.parsingFailed } - - let dataArray: [Data] = anyArray.compactMap { try? JSONSerialization.data(withJSONObject: $0) } - guard dataArray.count == types.count else { throw HTTP.Error.parsingFailed } - - do { - return try zip(dataArray, types) - .map { data, type in try type.decoded(from: data, using: dependencies) } - .map { data in (responseInfo, data) } - } - catch { - throw HTTP.Error.parsingFailed - } - } - } -} diff --git a/SessionMessagingKit/Open Groups/Models/SOGSMessage.swift b/SessionMessagingKit/Open Groups/Models/SOGSMessage.swift index 8ce774b31..5bbccaf02 100644 --- a/SessionMessagingKit/Open Groups/Models/SOGSMessage.swift +++ b/SessionMessagingKit/Open Groups/Models/SOGSMessage.swift @@ -67,10 +67,10 @@ extension OpenGroupAPI.Message { // If we have data and a signature (ie. the message isn't a deletion) then validate the signature if let base64EncodedData: String = maybeBase64EncodedData, let base64EncodedSignature: String = maybeBase64EncodedSignature { guard let sender: String = maybeSender, let data = Data(base64Encoded: base64EncodedData), let signature = Data(base64Encoded: base64EncodedSignature) else { - throw HTTP.Error.parsingFailed + throw HTTPError.parsingFailed } guard let dependencies: SMKDependencies = decoder.userInfo[Dependencies.userInfoKey] as? SMKDependencies else { - throw HTTP.Error.parsingFailed + throw HTTPError.parsingFailed } // Verify the signature based on the SessionId.Prefix type @@ -80,18 +80,18 @@ extension OpenGroupAPI.Message { case .blinded: guard dependencies.sign.verify(message: data.bytes, publicKey: publicKey.bytes, signature: signature.bytes) else { SNLog("Ignoring message with invalid signature.") - throw HTTP.Error.parsingFailed + throw HTTPError.parsingFailed } case .standard, .unblinded: guard (try? dependencies.ed25519.verifySignature(signature, publicKey: publicKey, data: data)) == true else { SNLog("Ignoring message with invalid signature.") - throw HTTP.Error.parsingFailed + throw HTTPError.parsingFailed } case .none: SNLog("Ignoring message with invalid sender.") - throw HTTP.Error.parsingFailed + throw HTTPError.parsingFailed } } diff --git a/SessionMessagingKit/Open Groups/OpenGroupAPI.swift b/SessionMessagingKit/Open Groups/OpenGroupAPI.swift index 1261d47b3..f485c57bb 100644 --- a/SessionMessagingKit/Open Groups/OpenGroupAPI.swift +++ b/SessionMessagingKit/Open Groups/OpenGroupAPI.swift @@ -54,8 +54,8 @@ public enum OpenGroupAPI { .defaulting(to: []) // Generate the requests - let requestResponseType: [BatchRequestInfoType] = [ - BatchRequestInfo( + let requestResponseType: [BatchRequest.Info] = [ + BatchRequest.Info( request: Request( server: server, endpoint: .capabilities @@ -71,7 +71,7 @@ public enum OpenGroupAPI { .filter(OpenGroup.Columns.roomToken != "") .fetchAll(db)) .defaulting(to: []) - .flatMap { openGroup -> [BatchRequestInfoType] in + .flatMap { openGroup -> [BatchRequest.Info] in let shouldRetrieveRecentMessages: Bool = ( openGroup.sequenceNumber == 0 || ( // If it's the first poll for this launch and it's been longer than @@ -83,14 +83,14 @@ public enum OpenGroupAPI { ) return [ - BatchRequestInfo( + BatchRequest.Info( request: Request( server: server, endpoint: .roomPollInfo(openGroup.roomToken, openGroup.infoUpdates) ), responseType: RoomPollInfo.self ), - BatchRequestInfo( + BatchRequest.Info( request: Request( server: server, endpoint: (shouldRetrieveRecentMessages ? @@ -113,7 +113,7 @@ public enum OpenGroupAPI { !capabilities.contains(.blind) ? [] : [ // Inbox - BatchRequestInfo( + BatchRequest.Info( request: Request( server: server, endpoint: (lastInboxMessageId == 0 ? @@ -125,7 +125,7 @@ public enum OpenGroupAPI { ), // Outbox - BatchRequestInfo( + BatchRequest.Info( request: Request( server: server, endpoint: (lastOutboxMessageId == 0 ? @@ -151,7 +151,7 @@ public enum OpenGroupAPI { private static func batch( _ db: Database, server: String, - requests: [BatchRequestInfoType], + requests: [BatchRequest.Info], using dependencies: SMKDependencies = SMKDependencies() ) -> AnyPublisher<[Endpoint: (ResponseInfoType, Codable?)], Error> { let responseTypes = requests.map { $0.responseType } @@ -163,7 +163,7 @@ public enum OpenGroupAPI { method: .post, server: server, endpoint: Endpoint.batch, - body: requestBody + body: BatchRequest(requests: requests) ), using: dependencies ) @@ -183,7 +183,7 @@ public enum OpenGroupAPI { private static func sequence( _ db: Database, server: String, - requests: [BatchRequestInfoType], + requests: [BatchRequest.Info], using dependencies: SMKDependencies = SMKDependencies() ) -> AnyPublisher<[Endpoint: (ResponseInfoType, Codable?)], Error> { let responseTypes = requests.map { $0.responseType } @@ -195,7 +195,7 @@ public enum OpenGroupAPI { method: .post, server: server, endpoint: Endpoint.sequence, - body: requestBody + body: BatchRequest(requests: requests) ), using: dependencies ) @@ -315,7 +315,7 @@ public enum OpenGroupAPI { ) -> AnyPublisher<(capabilities: (info: ResponseInfoType, data: Capabilities), room: (info: ResponseInfoType, data: Room)), Error> { let requestResponseType: [BatchRequest.Info] = [ // Get the latest capabilities for the server (in case it's a new server or the cached ones are stale) - BatchRequestInfo( + BatchRequest.Info( request: Request( server: server, endpoint: .capabilities @@ -324,7 +324,7 @@ public enum OpenGroupAPI { ), // And the room info - BatchRequestInfo( + BatchRequest.Info( request: Request( server: server, endpoint: .room(roomToken) @@ -351,13 +351,13 @@ public enum OpenGroupAPI { } }) .map { _, value in value } - let maybeRoom: (info: OnionRequestResponseInfoType, data: Room?)? = maybeRoomResponse - .map { info, data in (info, (data as? BatchSubResponse)?.body) } + let maybeRoom: (info: ResponseInfoType, data: Room?)? = maybeRoomResponse + .map { info, data in (info, (data as? HTTP.BatchSubResponse)?.body) } guard - let capabilitiesInfo: OnionRequestResponseInfoType = maybeCapabilities?.info, + let capabilitiesInfo: ResponseInfoType = maybeCapabilities?.info, let capabilities: Capabilities = maybeCapabilities?.data, - let roomInfo: OnionRequestResponseInfoType = maybeRoom?.info, + let roomInfo: ResponseInfoType = maybeRoom?.info, let room: Room = maybeRoom?.data else { return Fail(error: HTTPError.parsingFailed) @@ -383,7 +383,7 @@ public enum OpenGroupAPI { ) -> AnyPublisher<(capabilities: (info: ResponseInfoType, data: Capabilities), rooms: (info: ResponseInfoType, data: [Room])), Error> { let requestResponseType: [BatchRequest.Info] = [ // Get the latest capabilities for the server (in case it's a new server or the cached ones are stale) - BatchRequestInfo( + BatchRequest.Info( request: Request( server: server, endpoint: .capabilities @@ -392,7 +392,7 @@ public enum OpenGroupAPI { ), // And the room info - BatchRequestInfo( + BatchRequest.Info( request: Request( server: server, endpoint: .rooms @@ -419,13 +419,13 @@ public enum OpenGroupAPI { } }) .map { _, value in value } - let maybeRooms: (info: OnionRequestResponseInfoType, data: [Room]?)? = maybeRoomResponse - .map { info, data in (info, (data as? BatchSubResponse<[Room]>)?.body) } + let maybeRooms: (info: ResponseInfoType, data: [Room]?)? = maybeRoomResponse + .map { info, data in (info, (data as? HTTP.BatchSubResponse<[Room]>)?.body) } guard - let capabilitiesInfo: OnionRequestResponseInfoType = maybeCapabilities?.info, + let capabilitiesInfo: ResponseInfoType = maybeCapabilities?.info, let capabilities: Capabilities = maybeCapabilities?.data, - let roomsInfo: OnionRequestResponseInfoType = maybeRooms?.info, + let roomsInfo: ResponseInfoType = maybeRooms?.info, let rooms: [Room] = maybeRooms?.data else { return Fail(error: HTTPError.parsingFailed) @@ -1239,16 +1239,16 @@ public enum OpenGroupAPI { ) // Generate the requests - let requestResponseType: [BatchRequestInfoType] = [ - BatchRequestInfo( - request: Request( + let requestResponseType: [BatchRequest.Info] = [ + BatchRequest.Info( + request: Request( method: .post, server: server, endpoint: .userBan(sessionId), body: banRequestBody ) ), - BatchRequestInfo( + BatchRequest.Info( request: Request( method: .delete, server: server, @@ -1390,10 +1390,10 @@ public enum OpenGroupAPI { updatedRequest.allHTTPHeaderFields = (request.allHTTPHeaderFields ?? [:]) .updated(with: [ - Header.sogsPubKey.rawValue: signResult.publicKey, - Header.sogsTimestamp.rawValue: "\(timestamp)", - Header.sogsNonce.rawValue: nonce.base64EncodedString(), - Header.sogsSignature.rawValue: signResult.signature.toBase64() + HTTPHeader.sogsPubKey: signResult.publicKey, + HTTPHeader.sogsTimestamp: "\(timestamp)", + HTTPHeader.sogsNonce: nonce.base64EncodedString(), + HTTPHeader.sogsSignature: signResult.signature.toBase64() ]) return updatedRequest diff --git a/SessionMessagingKit/Open Groups/Types/HTTPHeader+OpenGroup.swift b/SessionMessagingKit/Open Groups/Types/HTTPHeader+OpenGroup.swift new file mode 100644 index 000000000..9b844a9bf --- /dev/null +++ b/SessionMessagingKit/Open Groups/Types/HTTPHeader+OpenGroup.swift @@ -0,0 +1,11 @@ +// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved. + +import Foundation +import SessionUtilitiesKit + +public extension HTTPHeader { + static let sogsPubKey: HTTPHeader = "X-SOGS-Pubkey" + static let sogsNonce: HTTPHeader = "X-SOGS-Nonce" + static let sogsTimestamp: HTTPHeader = "X-SOGS-Timestamp" + static let sogsSignature: HTTPHeader = "X-SOGS-Signature" +} diff --git a/SessionMessagingKit/Open Groups/Types/HTTPQueryParam+OpenGroup.swift b/SessionMessagingKit/Open Groups/Types/HTTPQueryParam+OpenGroup.swift new file mode 100644 index 000000000..eac835a7f --- /dev/null +++ b/SessionMessagingKit/Open Groups/Types/HTTPQueryParam+OpenGroup.swift @@ -0,0 +1,21 @@ +// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved. + +import Foundation +import SessionUtilitiesKit + +public extension HTTPQueryParam { + static let publicKey: HTTPQueryParam = "public_key" + static let fromServerId: HTTPQueryParam = "from_server_id" + + static let required: HTTPQueryParam = "required" + + /// For messages - number between 1 and 256 (default is 100) + static let limit: HTTPQueryParam = "limit" + + /// For file server session version check + static let platform: HTTPQueryParam = "platform" + + /// String indicating the types of updates that the client supports + static let updateTypes: HTTPQueryParam = "t" + static let reactors: HTTPQueryParam = "reactors" +} diff --git a/SessionMessagingKit/Open Groups/Types/SOGSEndpoint.swift b/SessionMessagingKit/Open Groups/Types/SOGSEndpoint.swift index 60c148595..483d2f253 100644 --- a/SessionMessagingKit/Open Groups/Types/SOGSEndpoint.swift +++ b/SessionMessagingKit/Open Groups/Types/SOGSEndpoint.swift @@ -1,6 +1,7 @@ // Copyright © 2022 Rangeproof Pty Ltd. All rights reserved. import Foundation +import SessionUtilitiesKit extension OpenGroupAPI { public enum Endpoint: EndpointType { @@ -58,7 +59,7 @@ extension OpenGroupAPI { case userUnban(String) case userModerator(String) - var path: String { + public var path: String { switch self { // Utility diff --git a/SessionMessagingKit/SMKDependencies.swift b/SessionMessagingKit/SMKDependencies.swift new file mode 100644 index 000000000..c8d9c18e0 --- /dev/null +++ b/SessionMessagingKit/SMKDependencies.swift @@ -0,0 +1,93 @@ +// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved. +import Foundation +import GRDB +import Sodium +import SessionSnodeKit +import SessionUtilitiesKit + +public class SMKDependencies: SSKDependencies { + internal var _sodium: Atomic + public var sodium: SodiumType { + get { Dependencies.getValueSettingIfNull(&_sodium) { Sodium() } } + set { _sodium.mutate { $0 = newValue } } + } + + internal var _box: Atomic + public var box: BoxType { + get { Dependencies.getValueSettingIfNull(&_box) { sodium.getBox() } } + set { _box.mutate { $0 = newValue } } + } + + internal var _genericHash: Atomic + public var genericHash: GenericHashType { + get { Dependencies.getValueSettingIfNull(&_genericHash) { sodium.getGenericHash() } } + set { _genericHash.mutate { $0 = newValue } } + } + + internal var _sign: Atomic + public var sign: SignType { + get { Dependencies.getValueSettingIfNull(&_sign) { sodium.getSign() } } + set { _sign.mutate { $0 = newValue } } + } + + internal var _aeadXChaCha20Poly1305Ietf: Atomic + public var aeadXChaCha20Poly1305Ietf: AeadXChaCha20Poly1305IetfType { + get { Dependencies.getValueSettingIfNull(&_aeadXChaCha20Poly1305Ietf) { sodium.getAeadXChaCha20Poly1305Ietf() } } + set { _aeadXChaCha20Poly1305Ietf.mutate { $0 = newValue } } + } + + internal var _ed25519: Atomic + public var ed25519: Ed25519Type { + get { Dependencies.getValueSettingIfNull(&_ed25519) { Ed25519Wrapper() } } + set { _ed25519.mutate { $0 = newValue } } + } + + internal var _nonceGenerator16: Atomic + public var nonceGenerator16: NonceGenerator16ByteType { + get { Dependencies.getValueSettingIfNull(&_nonceGenerator16) { OpenGroupAPI.NonceGenerator16Byte() } } + set { _nonceGenerator16.mutate { $0 = newValue } } + } + + internal var _nonceGenerator24: Atomic + public var nonceGenerator24: NonceGenerator24ByteType { + get { Dependencies.getValueSettingIfNull(&_nonceGenerator24) { OpenGroupAPI.NonceGenerator24Byte() } } + set { _nonceGenerator24.mutate { $0 = newValue } } + } + + // MARK: - Initialization + + public init( + onionApi: OnionRequestAPIType.Type? = nil, + generalCache: Atomic? = nil, + storage: Storage? = nil, + scheduler: ValueObservationScheduler? = nil, + sodium: SodiumType? = nil, + box: BoxType? = nil, + genericHash: GenericHashType? = nil, + sign: SignType? = nil, + aeadXChaCha20Poly1305Ietf: AeadXChaCha20Poly1305IetfType? = nil, + ed25519: Ed25519Type? = nil, + nonceGenerator16: NonceGenerator16ByteType? = nil, + nonceGenerator24: NonceGenerator24ByteType? = nil, + standardUserDefaults: UserDefaultsType? = nil, + date: Date? = nil + ) { + _sodium = Atomic(sodium) + _box = Atomic(box) + _genericHash = Atomic(genericHash) + _sign = Atomic(sign) + _aeadXChaCha20Poly1305Ietf = Atomic(aeadXChaCha20Poly1305Ietf) + _ed25519 = Atomic(ed25519) + _nonceGenerator16 = Atomic(nonceGenerator16) + _nonceGenerator24 = Atomic(nonceGenerator24) + + super.init( + onionApi: onionApi, + generalCache: generalCache, + storage: storage, + scheduler: scheduler, + standardUserDefaults: standardUserDefaults, + date: date + ) + } +} diff --git a/SessionMessagingKit/Sending & Receiving/Message Handling/MessageReceiver+ClosedGroups.swift b/SessionMessagingKit/Sending & Receiving/Message Handling/MessageReceiver+ClosedGroups.swift index a1fcb82a8..0d1a02951 100644 --- a/SessionMessagingKit/Sending & Receiving/Message Handling/MessageReceiver+ClosedGroups.swift +++ b/SessionMessagingKit/Sending & Receiving/Message Handling/MessageReceiver+ClosedGroups.swift @@ -140,7 +140,7 @@ extension MessageReceiver { ).insert(db) // Start polling - ClosedGroupPoller.shared.startPolling(for: groupPublicKey) + ClosedGroupPoller.shared.startIfNeeded(for: groupPublicKey) // Notify the PN server let _ = PushNotificationAPI.performOperation(.subscribe, for: groupPublicKey, publicKey: getUserHexEncodedPublicKey(db)) diff --git a/SessionMessagingKit/Sending & Receiving/Message Handling/MessageSender+ClosedGroups.swift b/SessionMessagingKit/Sending & Receiving/Message Handling/MessageSender+ClosedGroups.swift index f051c4750..ba6b1903c 100644 --- a/SessionMessagingKit/Sending & Receiving/Message Handling/MessageSender+ClosedGroups.swift +++ b/SessionMessagingKit/Sending & Receiving/Message Handling/MessageSender+ClosedGroups.swift @@ -83,7 +83,7 @@ extension MessageSender { .map { memberId -> MessageSender.PreparedSendData in try MessageSender.preparedSendData( db, - message: LegacyClosedGroupControlMessage( + message: ClosedGroupControlMessage( kind: .new( publicKey: Data(hex: groupPublicKey), name: name, @@ -99,7 +99,7 @@ extension MessageSender { // the 'ClosedGroup' object we created sentTimestampMs: UInt64(floor(formationTimestamp * 1000)) ), - to: .contact(publicKey: memberId), + to: .contact(publicKey: memberId, namespace: .default), interactionId: nil ) } @@ -263,7 +263,7 @@ extension MessageSender { threadId: thread.id, authorId: userPublicKey, variant: .infoClosedGroupUpdated, - body: LegacyClosedGroupControlMessage.Kind + body: ClosedGroupControlMessage.Kind .nameChange(name: name) .infoMessage(db, sender: userPublicKey), timestampMs: Int64(floor(Date().timeIntervalSince1970 * 1000)) @@ -274,7 +274,7 @@ extension MessageSender { // Send the update to the group try MessageSender.send( db, - message: LegacyClosedGroupControlMessage(kind: .nameChange(name: name)), + message: ClosedGroupControlMessage(kind: .nameChange(name: name)), interactionId: interactionId, in: thread ) @@ -493,7 +493,7 @@ extension MessageSender { preparedSendData: try MessageSender .preparedSendData( db, - message: LegacyClosedGroupControlMessage( + message: ClosedGroupControlMessage( kind: .membersRemoved( members: removedMembers.map { Data(hex: $0) } ) @@ -546,7 +546,7 @@ extension MessageSender { threadId: thread.id, authorId: userPublicKey, variant: .infoClosedGroupCurrentUserLeft, - body: LegacyClosedGroupControlMessage.Kind + body: ClosedGroupControlMessage.Kind .memberLeft .infoMessage(db, sender: userPublicKey), timestampMs: Int64(floor(Date().timeIntervalSince1970 * 1000)) @@ -561,7 +561,7 @@ extension MessageSender { sendData = try MessageSender .preparedSendData( db, - message: LegacyClosedGroupControlMessage( + message: ClosedGroupControlMessage( kind: .memberLeft ), to: try Message.Destination.from(db, thread: thread), diff --git a/SessionMessagingKit/Sending & Receiving/MessageSender+Convenience.swift b/SessionMessagingKit/Sending & Receiving/MessageSender+Convenience.swift index 1b934ead5..0aa47a509 100644 --- a/SessionMessagingKit/Sending & Receiving/MessageSender+Convenience.swift +++ b/SessionMessagingKit/Sending & Receiving/MessageSender+Convenience.swift @@ -85,8 +85,8 @@ extension MessageSender { let threadId: String = { switch destination { - case .contact(let publicKey): return publicKey - case .closedGroup(let groupPublicKey): return groupPublicKey + case .contact(let publicKey, _): return publicKey + case .closedGroup(let groupPublicKey, _): return groupPublicKey case .openGroup(let roomToken, let server, _, _, _): return OpenGroup.idFor(roomToken: roomToken, server: server) @@ -162,7 +162,10 @@ extension MessageSender { // If we don't have a userKeyPair yet then there is no need to sync the configuration // as the user doesn't exist yet (this will get triggered on the first launch of a // fresh install due to the migrations getting run) - guard Identity.userExists(db) else { + guard + Identity.userExists(db), + let ed25519SecretKey: [UInt8] = Identity.fetchUserEd25519KeyPair(db)?.secretKey + else { return Fail(error: StorageError.generic) .eraseToAnyPublisher() } @@ -205,41 +208,31 @@ extension MessageSender { to: legacyDestination, interactionId: nil ) - - when( - resolved: try userConfigMessageChanges.map { message in - try MessageSender - .sendImmediate( - db, - message: message, - to: destination, - interactionId: nil - ) + + let userConfigSendData: [PreparedSendData] = try userConfigMessageChanges + .map { message in + try MessageSender.preparedSendData( + db, + message: message, + to: destination, + interactionId: nil + ) } - ) - .done { results in - let hadError: Bool = results.contains { result in - switch result { - case .fulfilled: return false - case .rejected: return true - } - } - - guard !hadError else { - seal.reject(StorageError.generic) - return - } - - seal.fulfill(()) - } - .catch { _ in seal.reject(StorageError.generic) } - .retainUntilComplete() /// We want to avoid blocking the db write thread so we dispatch the API call to a different thread return Just(()) .setFailureType(to: Error.self) .receive(on: DispatchQueue.global(qos: .userInitiated)) - .flatMap { _ in MessageSender.sendImmediate(preparedSendData: sendData) } + .flatMap { _ -> AnyPublisher in + Publishers + .MergeMany( + ([sendData] + userConfigSendData) + .map { MessageSender.sendImmediate(preparedSendData: $0) } + ) + .collect() + .map { _ in () } + .eraseToAnyPublisher() + } .eraseToAnyPublisher() } } diff --git a/SessionMessagingKit/Sending & Receiving/MessageSender.swift b/SessionMessagingKit/Sending & Receiving/MessageSender.swift index 5b8eccd82..f60609b0d 100644 --- a/SessionMessagingKit/Sending & Receiving/MessageSender.swift +++ b/SessionMessagingKit/Sending & Receiving/MessageSender.swift @@ -23,10 +23,6 @@ public final class MessageSender { let plaintext: Data? let ciphertext: Data? - // TODO: Replace these with the target namespaces - let isClosedGroupMessage: Bool - let isConfigMessage: Bool - private init( shouldSend: Bool, message: Message?, @@ -36,9 +32,7 @@ public final class MessageSender { totalAttachmentsUploaded: Int = 0, snodeMessage: SnodeMessage?, plaintext: Data?, - ciphertext: Data?, - isClosedGroupMessage: Bool, - isConfigMessage: Bool + ciphertext: Data? ) { self.shouldSend = shouldSend @@ -51,8 +45,6 @@ public final class MessageSender { self.snodeMessage = snodeMessage self.plaintext = plaintext self.ciphertext = ciphertext - self.isClosedGroupMessage = isClosedGroupMessage - self.isConfigMessage = isConfigMessage } // The default constructor creats an instance that doesn't actually send a message @@ -68,8 +60,6 @@ public final class MessageSender { self.snodeMessage = nil self.plaintext = nil self.ciphertext = nil - self.isClosedGroupMessage = false - self.isConfigMessage = false } /// This should be used to send a message to one-to-one or closed group conversations @@ -78,9 +68,7 @@ public final class MessageSender { destination: Message.Destination, interactionId: Int64?, isSyncMessage: Bool?, - snodeMessage: SnodeMessage, - isClosedGroupMessage: Bool, - isConfigMessage: Bool + snodeMessage: SnodeMessage ) { self.shouldSend = true @@ -93,8 +81,6 @@ public final class MessageSender { self.snodeMessage = snodeMessage self.plaintext = nil self.ciphertext = nil - self.isClosedGroupMessage = isClosedGroupMessage - self.isConfigMessage = isConfigMessage } /// This should be used to send a message to open group conversations @@ -115,8 +101,6 @@ public final class MessageSender { self.snodeMessage = nil self.plaintext = plaintext self.ciphertext = nil - self.isClosedGroupMessage = false - self.isConfigMessage = false } /// This should be used to send a message to an open group inbox @@ -137,8 +121,6 @@ public final class MessageSender { self.snodeMessage = nil self.plaintext = nil self.ciphertext = ciphertext - self.isClosedGroupMessage = false - self.isConfigMessage = false } // MARK: - Mutation @@ -153,9 +135,7 @@ public final class MessageSender { totalAttachmentsUploaded: fileIds.count, snodeMessage: snodeMessage, plaintext: plaintext, - ciphertext: ciphertext, - isClosedGroupMessage: isClosedGroupMessage, - isConfigMessage: isConfigMessage + ciphertext: ciphertext ) } } @@ -333,18 +313,15 @@ public final class MessageSender { // Wrap the result let kind: SNProtoEnvelope.SNProtoEnvelopeType let senderPublicKey: String - let namespace: SnodeAPI.Namespace switch destination { - case .contact(_, let targetNamespace): + case .contact: kind = .sessionMessage senderPublicKey = "" - namespace = targetNamespace - case .closedGroup(let groupPublicKey, let targetNamespace): + case .closedGroup(let groupPublicKey, _): kind = .closedGroupMessage senderPublicKey = groupPublicKey - namespace = targetNamespace case .openGroup, .openGroupInbox: preconditionFailure() } @@ -384,9 +361,7 @@ public final class MessageSender { destination: destination, interactionId: interactionId, isSyncMessage: isSyncMessage, - snodeMessage: snodeMessage, - isClosedGroupMessage: (kind == .closedGroupMessage), - isConfigMessage: (message is ConfigurationMessage) + snodeMessage: snodeMessage ) } @@ -667,10 +642,7 @@ public final class MessageSender { return SnodeAPI .sendMessage( snodeMessage, - in: (data.isClosedGroupMessage ? - .legacyClosedGroup : - .default - ) + in: destination.namespace ) .subscribe(on: DispatchQueue.global(qos: .default)) .flatMap { result, totalCount -> AnyPublisher in @@ -1014,7 +986,7 @@ public final class MessageSender { data: try prepareSendToSnodeDestination( db, message: message, - to: .contact(publicKey: userPublicKey), + to: .contact(publicKey: userPublicKey, namespace: namespace), interactionId: interactionId, userPublicKey: userPublicKey, messageSendTimestamp: Int64(floor(Date().timeIntervalSince1970 * 1000)), diff --git a/SessionMessagingKit/Sending & Receiving/Notifications/PushNotificationAPI.swift b/SessionMessagingKit/Sending & Receiving/Notifications/PushNotificationAPI.swift index 53453fe80..735b96503 100644 --- a/SessionMessagingKit/Sending & Receiving/Notifications/PushNotificationAPI.swift +++ b/SessionMessagingKit/Sending & Receiving/Notifications/PushNotificationAPI.swift @@ -90,7 +90,7 @@ public enum PushNotificationAPI { let url = URL(string: "\(server)/unregister")! var request: URLRequest = URLRequest(url: url) request.httpMethod = "POST" - request.allHTTPHeaderFields = [ Header.contentType.rawValue: "application/json" ] + request.allHTTPHeaderFields = [ HTTPHeader.contentType: "application/json" ] request.httpBody = body return OnionRequestAPI @@ -144,7 +144,7 @@ public enum PushNotificationAPI { let url = URL(string: "\(server)/register")! var request: URLRequest = URLRequest(url: url) request.httpMethod = "POST" - request.allHTTPHeaderFields = [ Header.contentType.rawValue: "application/json" ] + request.allHTTPHeaderFields = [ HTTPHeader.contentType: "application/json" ] request.httpBody = body return Publishers @@ -227,7 +227,7 @@ public enum PushNotificationAPI { let url = URL(string: "\(server)/\(operation.endpoint)")! var request: URLRequest = URLRequest(url: url) request.httpMethod = "POST" - request.allHTTPHeaderFields = [ Header.contentType.rawValue: "application/json" ] + request.allHTTPHeaderFields = [ HTTPHeader.contentType: "application/json" ] request.httpBody = body return OnionRequestAPI @@ -272,7 +272,7 @@ public enum PushNotificationAPI { let url = URL(string: "\(server)/notify")! var request: URLRequest = URLRequest(url: url) request.httpMethod = "POST" - request.allHTTPHeaderFields = [ Header.contentType.rawValue: "application/json" ] + request.allHTTPHeaderFields = [ HTTPHeader.contentType: "application/json" ] request.httpBody = body return OnionRequestAPI diff --git a/SessionMessagingKit/Sending & Receiving/Pollers/OpenGroupPoller.swift b/SessionMessagingKit/Sending & Receiving/Pollers/OpenGroupPoller.swift index 65827d871..317c8c99d 100644 --- a/SessionMessagingKit/Sending & Receiving/Pollers/OpenGroupPoller.swift +++ b/SessionMessagingKit/Sending & Receiving/Pollers/OpenGroupPoller.swift @@ -8,7 +8,7 @@ import SessionUtilitiesKit extension OpenGroupAPI { public final class Poller { - typealias PollResponse = [OpenGroupAPI.Endpoint: (info: OnionRequestResponseInfoType, data: Codable?)] + typealias PollResponse = [OpenGroupAPI.Endpoint: (info: ResponseInfoType, data: Codable?)] private let server: String private var timer: Timer? = nil @@ -283,7 +283,7 @@ extension OpenGroupAPI { .filter { endpoint, endpointResponse in switch endpoint { case .capabilities: - guard (endpointResponse.data as? BatchSubResponse)?.body != nil else { + guard (endpointResponse.data as? HTTP.BatchSubResponse)?.body != nil else { SNLog("Open group polling failed due to invalid capability data.") return false } @@ -291,8 +291,8 @@ extension OpenGroupAPI { return true case .roomPollInfo(let roomToken, _): - guard (endpointResponse.data as? BatchSubResponse)?.body != nil else { - switch (endpointResponse.data as? BatchSubResponse)?.code { + guard (endpointResponse.data as? HTTP.BatchSubResponse)?.body != nil else { + switch (endpointResponse.data as? HTTP.BatchSubResponse)?.code { case 404: SNLog("Open group polling failed to retrieve info for unknown room '\(roomToken)'.") default: SNLog("Open group polling failed due to invalid room info data.") } @@ -303,10 +303,10 @@ extension OpenGroupAPI { case .roomMessagesRecent(let roomToken), .roomMessagesBefore(let roomToken, _), .roomMessagesSince(let roomToken, _): guard - let responseData: BatchSubResponse<[Failable]> = endpointResponse.data as? BatchSubResponse<[Failable]>, + let responseData: HTTP.BatchSubResponse<[Failable]> = endpointResponse.data as? HTTP.BatchSubResponse<[Failable]>, let responseBody: [Failable] = responseData.body else { - switch (endpointResponse.data as? BatchSubResponse<[Failable]>)?.code { + switch (endpointResponse.data as? HTTP.BatchSubResponse<[Failable]>)?.code { case 404: SNLog("Open group polling failed to retrieve messages for unknown room '\(roomToken)'.") default: SNLog("Open group polling failed due to invalid messages data.") } @@ -325,7 +325,7 @@ extension OpenGroupAPI { case .inbox, .inboxSince, .outbox, .outboxSince: guard - let responseData: BatchSubResponse<[DirectMessage]?> = endpointResponse.data as? BatchSubResponse<[DirectMessage]?>, + let responseData: HTTP.BatchSubResponse<[DirectMessage]?> = endpointResponse.data as? HTTP.BatchSubResponse<[DirectMessage]?>, !responseData.failedToParseBody else { SNLog("Open group polling failed due to invalid inbox/outbox data.") @@ -383,7 +383,7 @@ extension OpenGroupAPI { switch endpoint { case .capabilities: guard - let responseData: BatchSubResponse = endpointResponse.data as? BatchSubResponse, + let responseData: HTTP.BatchSubResponse = endpointResponse.data as? HTTP.BatchSubResponse, let responseBody: Capabilities = responseData.body else { return false } @@ -391,7 +391,7 @@ extension OpenGroupAPI { case .roomPollInfo(let roomToken, _): guard - let responseData: BatchSubResponse = endpointResponse.data as? BatchSubResponse, + let responseData: HTTP.BatchSubResponse = endpointResponse.data as? HTTP.BatchSubResponse, let responseBody: RoomPollInfo = responseData.body else { return false } guard let existingOpenGroup: OpenGroup = currentInfo?.groups.first(where: { $0.roomToken == roomToken }) else { @@ -428,7 +428,7 @@ extension OpenGroupAPI { switch endpoint { case .capabilities: guard - let responseData: BatchSubResponse = endpointResponse.data as? BatchSubResponse, + let responseData: HTTP.BatchSubResponse = endpointResponse.data as? HTTP.BatchSubResponse, let responseBody: Capabilities = responseData.body else { return } @@ -440,7 +440,7 @@ extension OpenGroupAPI { case .roomPollInfo(let roomToken, _): guard - let responseData: BatchSubResponse = endpointResponse.data as? BatchSubResponse, + let responseData: HTTP.BatchSubResponse = endpointResponse.data as? HTTP.BatchSubResponse, let responseBody: RoomPollInfo = responseData.body else { return } @@ -455,7 +455,7 @@ extension OpenGroupAPI { case .roomMessagesRecent(let roomToken), .roomMessagesBefore(let roomToken, _), .roomMessagesSince(let roomToken, _): guard - let responseData: BatchSubResponse<[Failable]> = endpointResponse.data as? BatchSubResponse<[Failable]>, + let responseData: HTTP.BatchSubResponse<[Failable]> = endpointResponse.data as? HTTP.BatchSubResponse<[Failable]>, let responseBody: [Failable] = responseData.body else { return } @@ -469,7 +469,7 @@ extension OpenGroupAPI { case .inbox, .inboxSince, .outbox, .outboxSince: guard - let responseData: BatchSubResponse<[DirectMessage]?> = endpointResponse.data as? BatchSubResponse<[DirectMessage]?>, + let responseData: HTTP.BatchSubResponse<[DirectMessage]?> = endpointResponse.data as? HTTP.BatchSubResponse<[DirectMessage]?>, !responseData.failedToParseBody else { return } diff --git a/SessionMessagingKit/Sending & Receiving/Pollers/Poller.swift b/SessionMessagingKit/Sending & Receiving/Pollers/Poller.swift index 03cf63502..8a5ab36a4 100644 --- a/SessionMessagingKit/Sending & Receiving/Pollers/Poller.swift +++ b/SessionMessagingKit/Sending & Receiving/Pollers/Poller.swift @@ -191,12 +191,12 @@ public class Poller { .setFailureType(to: Error.self) .eraseToAnyPublisher() } - + let pollerName: String = ( poller?.pollerName(for: publicKey) ?? "poller with public key \(publicKey)" ) - + // Fetch the messages return SnodeAPI.getMessages(in: namespaces, from: snode, associatedWith: publicKey) .flatMap { namespacedResults -> AnyPublisher in @@ -239,8 +239,8 @@ public class Poller { } catch { switch error { - // Ignore duplicate & selfSend message errors (and don't bother logging - // them as there will be a lot since we each service node duplicates messages) + // Ignore duplicate & selfSend message errors (and don't bother logging + // them as there will be a lot since we each service node duplicates messages) case DatabaseError.SQLITE_CONSTRAINT_UNIQUE, MessageReceiverError.duplicateMessage, MessageReceiverError.duplicateControlMessage, @@ -252,9 +252,13 @@ public class Poller { break case DatabaseError.SQLITE_ABORT: - SNLog("Failed to the database being suspended (running in background with no background task).") + // In the background ignore 'SQLITE_ABORT' (it generally means + // the BackgroundPoller has timed out + if !calledFromBackgroundPoller { + SNLog("Failed to the database being suspended (running in background with no background task).") + } break - + default: SNLog("Failed to deserialize envelope due to error: \(error).") } @@ -265,33 +269,41 @@ public class Poller { .forEach { threadId, threadMessages in messageCount += threadMessages.count - JobRunner.add( - db, - job: Job( - variant: .messageReceive, - behaviour: .runOnce, - threadId: threadId, - details: MessageReceiveJob.Details( - messages: threadMessages.map { $0.messageInfo }, - calledFromBackgroundPoller: false - ) + let jobToRun: Job? = Job( + variant: .messageReceive, + behaviour: .runOnce, + threadId: threadId, + details: MessageReceiveJob.Details( + messages: threadMessages.map { $0.messageInfo }, + calledFromBackgroundPoller: calledFromBackgroundPoller ) ) + jobsToRun = jobsToRun.appending(jobToRun) + + // If we are force-polling then add to the JobRunner so they are + // persistent and will retry on the next app run if they fail but + // don't let them auto-start + JobRunner.add(db, job: jobToRun, canStartJob: !calledFromBackgroundPoller) } + } + + // Clean up message hashes and add some logs about the poll results + if allMessagesCount == 0 && !hadValidHashUpdate { + if !calledFromBackgroundPoller { + SNLog("Received \(allMessagesCount) new message\(allMessagesCount == 1 ? "" : "s"), all duplicates - marking the hash we polled with as invalid") + } - if messageCount == 0 && !hadValidHashUpdate, let lastHash: String = lastHash { - SNLog("Received \(messages.count) new message\(messages.count == 1 ? "" : "s"), all duplicates - marking the hash we polled with as invalid") - - // Update the cached validity of the messages - try SnodeReceivedMessageInfo.handlePotentialDeletedOrInvalidHash( - db, - potentiallyInvalidHashes: [lastHash], - otherKnownValidHashes: messages.map { $0.info.hash } - ) - } - else { - SNLog("Received \(messageCount) new message\(messageCount == 1 ? "" : "s") (duplicates: \(messages.count - messageCount))") - } + // Update the cached validity of the messages + try SnodeReceivedMessageInfo.handlePotentialDeletedOrInvalidHash( + db, + potentiallyInvalidHashes: lastHashes, + otherKnownValidHashes: namespacedResults + .compactMap { $0.value.data?.messages.map { $0.info.hash } } + .reduce([], +) + ) + } + else if !calledFromBackgroundPoller { + SNLog("Received \(messageCount) new message\(messageCount == 1 ? "" : "s") in \(pollerName) (duplicates: \(allMessagesCount - messageCount))") } } diff --git a/SessionMessagingKit/Utilities/Data+Utilities.swift b/SessionMessagingKit/Utilities/Data+Utilities.swift index 967e8a263..47b3e074e 100644 --- a/SessionMessagingKit/Utilities/Data+Utilities.swift +++ b/SessionMessagingKit/Utilities/Data+Utilities.swift @@ -5,23 +5,7 @@ import SessionUtilitiesKit // MARK: - Decoding -extension Dependencies { - static let userInfoKey: CodingUserInfoKey = CodingUserInfoKey(rawValue: "io.oxen.dependencies.codingOptions")! -} - public extension Data { - func decoded(as type: T.Type, using dependencies: Dependencies = Dependencies()) throws -> T { - do { - let decoder: JSONDecoder = JSONDecoder() - decoder.userInfo = [ Dependencies.userInfoKey: dependencies ] - - return try decoder.decode(type, from: self) - } - catch { - throw HTTP.Error.parsingFailed - } - } - func removePadding() -> Data { let bytes: [UInt8] = self.bytes var paddingStart: Int = self.count diff --git a/SessionMessagingKit/Utilities/ProfileManager.swift b/SessionMessagingKit/Utilities/ProfileManager.swift index 39aa0f270..a4c671ccf 100644 --- a/SessionMessagingKit/Utilities/ProfileManager.swift +++ b/SessionMessagingKit/Utilities/ProfileManager.swift @@ -78,6 +78,13 @@ public struct ProfileManager { return data } + public static func hasProfileImageData(with fileName: String?) -> Bool { + guard let fileName: String = fileName, !fileName.isEmpty else { return false } + + return FileManager.default + .fileExists(atPath: ProfileManager.profileAvatarFilepath(filename: fileName)) + } + private static func loadProfileData(with fileName: String) -> Data? { let filePath: String = ProfileManager.profileAvatarFilepath(filename: fileName) @@ -228,7 +235,7 @@ public struct ProfileManager { return } - guard let decryptedData: Data = decryptProfileData(data: data, key: profileKeyAtStart) else { + guard let decryptedData: Data = decryptData(data: data, key: profileKeyAtStart) else { OWSLogger.warn("Avatar data for \(profile.id) could not be decrypted.") return } @@ -388,38 +395,22 @@ public struct ProfileManager { return } + // If we have no image then we should succeed (database changes happen in the callback) guard let data: Data = avatarImageData else { - // If we have no image then we need to make sure to remove it from the profile - Storage.shared.writeAsync { db in - let existingProfile: Profile = Profile.fetchOrCreateCurrentUser(db) - - OWSLogger.verbose(existingProfile.profilePictureUrl != nil ? - "Updating local profile on service with cleared avatar." : - "Updating local profile on service with no avatar." - ) - - let updatedProfile: Profile = try existingProfile - .with( - name: profileName, - profilePictureUrl: nil, - profilePictureFileName: nil, - profileEncryptionKey: (existingProfile.profilePictureUrl != nil ? - .update(newProfileKey) : - .existing - ) - ) - .saved(db) - - // Remove any cached avatar image value - if let fileName: String = existingProfile.profilePictureFileName { - profileAvatarCache.mutate { $0[fileName] = nil } + // Remove any cached avatar image value + let maybeExistingFileName: String? = Storage.shared + .read { db in + try Profile + .select(.profilePictureFileName) + .asRequest(of: String.self) + .fetchOne(db) } - - SNLog("Successfully updated service with profile.") - - try success?(db, updatedProfile) + + if let fileName: String = maybeExistingFileName { + profileAvatarCache.mutate { $0[fileName] = nil } } - return + + return success(nil, newProfileKey) } // If we have a new avatar image, we must first: @@ -447,7 +438,7 @@ public struct ProfileManager { } // Encrypt the avatar for upload - guard let encryptedAvatarData: Data = encryptProfileData(data: data, key: newProfileKey) else { + guard let encryptedAvatarData: Data = encryptData(data: data, key: newProfileKey) else { SNLog("Updating service with profile failed.") failure?(.avatarEncryptionFailed) return diff --git a/SessionShareExtension/ShareVC.swift b/SessionShareExtension/ShareNavController.swift similarity index 98% rename from SessionShareExtension/ShareVC.swift rename to SessionShareExtension/ShareNavController.swift index 47dac8d38..5edc9cedc 100644 --- a/SessionShareExtension/ShareVC.swift +++ b/SessionShareExtension/ShareNavController.swift @@ -7,7 +7,7 @@ import SignalUtilitiesKit import SessionUIKit import SignalCoreKit -final class ShareVC: UINavigationController, ShareViewDelegate { +final class ShareNavController: UINavigationController, ShareViewDelegate { private var areVersionMigrationsComplete = false public static var attachmentPrepPublisher: AnyPublisher<[SignalAttachment], Error>? @@ -183,7 +183,7 @@ final class ShareVC: UINavigationController, ShareViewDelegate { private func showMainContent() { let threadPickerVC: ThreadPickerVC = ThreadPickerVC() - threadPickerVC.shareVC = self + threadPickerVC.shareNavController = self setViewControllers([ threadPickerVC ], animated: false) @@ -427,7 +427,7 @@ final class ShareVC: UINavigationController, ShareViewDelegate { // * UTIs aren't very descriptive (there are far more MIME types than UTI types) // so in the case of file attachments we try to refine the attachment type // using the file extension. - guard let srcUtiType = ShareVC.utiType(itemProvider: itemProvider) else { + guard let srcUtiType = ShareNavController.utiType(itemProvider: itemProvider) else { let error = ShareViewControllerError.unsupportedMedia return Fail(error: error) .eraseToAnyPublisher() @@ -613,7 +613,7 @@ final class ShareVC: UINavigationController, ShareViewDelegate { Logger.debug("building DataSource with url: \(url), utiType: \(utiType)") - guard let dataSource = ShareVC.createDataSource(utiType: utiType, url: url, customFileName: loadedItem.customFileName) else { + guard let dataSource = ShareNavController.createDataSource(utiType: utiType, url: url, customFileName: loadedItem.customFileName) else { let error = ShareViewControllerError.assertionError(description: "Unable to read attachment data") return Fail(error: error) .eraseToAnyPublisher() diff --git a/SessionShareExtension/SimplifiedConversationCell.swift b/SessionShareExtension/SimplifiedConversationCell.swift index 3c2fa1e32..48fd3009d 100644 --- a/SessionShareExtension/SimplifiedConversationCell.swift +++ b/SessionShareExtension/SimplifiedConversationCell.swift @@ -92,12 +92,10 @@ final class SimplifiedConversationCell: UITableViewCell { accentLineView.alpha = (cellViewModel.threadIsBlocked == true ? 1 : 0) profilePictureView.update( publicKey: cellViewModel.threadId, - profile: cellViewModel.profile, - additionalProfile: cellViewModel.additionalProfile, threadVariant: cellViewModel.threadVariant, - openGroupProfilePictureData: cellViewModel.openGroupProfilePictureData, - useFallbackPicture: (cellViewModel.threadVariant == .openGroup && cellViewModel.openGroupProfilePictureData == nil), - showMultiAvatarForClosedGroup: true + customImageData: cellViewModel.openGroupProfilePictureData, + profile: cellViewModel.profile, + additionalProfile: cellViewModel.additionalProfile ) displayNameLabel.text = cellViewModel.displayName } diff --git a/SessionShareExtension/ThreadPickerVC.swift b/SessionShareExtension/ThreadPickerVC.swift index c5b298362..9878b4eb1 100644 --- a/SessionShareExtension/ThreadPickerVC.swift +++ b/SessionShareExtension/ThreadPickerVC.swift @@ -13,7 +13,7 @@ final class ThreadPickerVC: UIViewController, UITableViewDataSource, UITableView private var dataChangeObservable: DatabaseCancellable? private var hasLoadedInitialData: Bool = false - var shareVC: ShareVC? + var shareNavController: ShareNavController? // MARK: - Intialization @@ -182,9 +182,9 @@ final class ThreadPickerVC: UIViewController, UITableViewDataSource, UITableView messageText ) - shareVC?.dismiss(animated: true, completion: nil) + shareNavController?.dismiss(animated: true, completion: nil) - ModalActivityIndicatorViewController.present(fromViewController: shareVC!, canCancel: false, message: "vc_share_sending_message".localized()) { activityIndicator in + ModalActivityIndicatorViewController.present(fromViewController: shareNavController!, canCancel: false, message: "vc_share_sending_message".localized()) { activityIndicator in // Resume database NotificationCenter.default.post(name: Database.resumeNotification, object: self) diff --git a/SessionSnodeKit/Database/Models/Snode.swift b/SessionSnodeKit/Database/Models/Snode.swift index 9bcf6bc8c..199ef2139 100644 --- a/SessionSnodeKit/Database/Models/Snode.swift +++ b/SessionSnodeKit/Database/Models/Snode.swift @@ -60,7 +60,7 @@ extension Snode { } catch { SNLog("Failed to parse snode: \(error.localizedDescription).") - throw HTTP.Error.invalidJSON + throw HTTPError.invalidJSON } } } diff --git a/SessionSnodeKit/Database/Models/SnodeReceivedMessageInfo.swift b/SessionSnodeKit/Database/Models/SnodeReceivedMessageInfo.swift index 7addb56e5..985ef335e 100644 --- a/SessionSnodeKit/Database/Models/SnodeReceivedMessageInfo.swift +++ b/SessionSnodeKit/Database/Models/SnodeReceivedMessageInfo.swift @@ -52,18 +52,18 @@ public struct SnodeReceivedMessageInfo: Codable, FetchableRecord, MutablePersist // MARK: - Convenience public extension SnodeReceivedMessageInfo { - private static func key(for snode: Snode, publicKey: String, namespace: Int) -> String { - guard namespace != SnodeAPI.defaultNamespace else { + private static func key(for snode: Snode, publicKey: String, namespace: SnodeAPI.Namespace) -> String { + guard namespace != .default else { return "\(snode.address):\(snode.port).\(publicKey)" } - return "\(snode.address):\(snode.port).\(publicKey).\(namespace)" + return "\(snode.address):\(snode.port).\(publicKey).\(namespace.rawValue)" } init( snode: Snode, publicKey: String, - namespace: Int, + namespace: SnodeAPI.Namespace, hash: String, expirationDateMs: Int64? ) { @@ -76,15 +76,15 @@ public extension SnodeReceivedMessageInfo { // MARK: - GRDB Interactions public extension SnodeReceivedMessageInfo { - static func pruneExpiredMessageHashInfo(for snode: Snode, namespace: Int, associatedWith publicKey: String) { - // Delete any expired SnodeReceivedMessageInfo values associated to a specific node (even though - // this runs very quickly we fetch the rowIds we want to delete from a 'read' call to avoid - // blocking the write queue since this method is called very frequently) + static func pruneExpiredMessageHashInfo(for snode: Snode, namespace: SnodeAPI.Namespace, associatedWith publicKey: String) { + // Delete any expired SnodeReceivedMessageInfo values associated to a specific node (even + // though this runs very quickly we fetch the rowIds we want to delete from a 'read' call + // to avoid blocking the write queue since this method is called very frequently) let rowIds: [Int64] = Storage.shared .read { db in - // Only prune the hashes if new hashes exist for this Snode (if they don't then we don't want - // to clear out the legacy hashes) - let hasNonLegacyHash: Bool = try SnodeReceivedMessageInfo + // Only prune the hashes if new hashes exist for this Snode (if they don't then + // we don't want to clear out the legacy hashes) + let hasNonLegacyHash: Bool = SnodeReceivedMessageInfo .filter(SnodeReceivedMessageInfo.Columns.key == key(for: snode, publicKey: publicKey, namespace: namespace)) .isNotEmpty(db) @@ -111,10 +111,10 @@ public extension SnodeReceivedMessageInfo { /// This method fetches the last non-expired hash from the database for message retrieval /// - /// **Note:** This method uses a `write` instead of a read because there is a single write queue for the database and it's very common for - /// this method to be called after the hash value has been updated but before the various `read` threads have been updated, resulting in a - /// pointless fetch for data the app has already received - static func fetchLastNotExpired(for snode: Snode, namespace: Int, associatedWith publicKey: String) -> SnodeReceivedMessageInfo? { + /// **Note:** This method uses a `write` instead of a read because there is a single write queue for the database and it's + /// very common for this method to be called after the hash value has been updated but before the various `read` threads + /// have been updated, resulting in a pointless fetch for data the app has already received + static func fetchLastNotExpired(for snode: Snode, namespace: SnodeAPI.Namespace, associatedWith publicKey: String) -> SnodeReceivedMessageInfo? { return Storage.shared.read { db in let nonLegacyHash: SnodeReceivedMessageInfo? = try SnodeReceivedMessageInfo .filter( diff --git a/SessionSnodeKit/GetSnodePoolJob.swift b/SessionSnodeKit/Jobs/GetSnodePoolJob.swift similarity index 98% rename from SessionSnodeKit/GetSnodePoolJob.swift rename to SessionSnodeKit/Jobs/GetSnodePoolJob.swift index 4739bc411..71156a4c7 100644 --- a/SessionSnodeKit/GetSnodePoolJob.swift +++ b/SessionSnodeKit/Jobs/GetSnodePoolJob.swift @@ -3,6 +3,7 @@ import Foundation import Combine import GRDB +import SignalCoreKit import SessionUtilitiesKit public enum GetSnodePoolJob: JobExecutor { diff --git a/SessionSnodeKit/Models/DeleteAllBeforeRequest.swift b/SessionSnodeKit/Models/DeleteAllBeforeRequest.swift new file mode 100644 index 000000000..6f4a83ad9 --- /dev/null +++ b/SessionSnodeKit/Models/DeleteAllBeforeRequest.swift @@ -0,0 +1,81 @@ +// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved. + +import Foundation + +extension SnodeAPI { + public class DeleteAllBeforeRequest: SnodeAuthenticatedRequestBody { + enum CodingKeys: String, CodingKey { + case beforeMs = "before" + case namespace + } + + let beforeMs: UInt64 + let namespace: SnodeAPI.Namespace? + + // MARK: - Init + + public init( + beforeMs: UInt64, + namespace: SnodeAPI.Namespace?, + pubkey: String, + timestampMs: UInt64, + ed25519PublicKey: [UInt8], + ed25519SecretKey: [UInt8] + ) { + self.beforeMs = beforeMs + self.namespace = namespace + + super.init( + pubkey: pubkey, + ed25519PublicKey: ed25519PublicKey, + ed25519SecretKey: ed25519SecretKey, + timestampMs: timestampMs + ) + } + + // MARK: - Coding + + override public func encode(to encoder: Encoder) throws { + var container: KeyedEncodingContainer = encoder.container(keyedBy: CodingKeys.self) + + try container.encode(beforeMs, forKey: .beforeMs) + + // If no namespace is specified it defaults to the default namespace only (namespace + // 0), so instead in this case we want to explicitly delete from `all` namespaces + switch namespace { + case .some(let namespace): try container.encode(namespace, forKey: .namespace) + case .none: try container.encode("all", forKey: .namespace) + } + + try super.encode(to: encoder) + } + + // MARK: - Abstract Methods + + override func generateSignature() throws -> [UInt8] { + /// Ed25519 signature of `("delete_before" || namespace || before)`, signed by + /// `pubkey`. Must be base64 encoded (json) or bytes (OMQ). `namespace` is the stringified + /// version of the given non-default namespace parameter (i.e. "-42" or "all"), or the empty + /// string for the default namespace (whether explicitly given or not). + let verificationBytes: [UInt8] = SnodeAPI.Endpoint.deleteAllBefore.rawValue.bytes + .appending( + contentsOf: (namespace == nil ? + "all" : + namespace?.verificationString + )?.bytes + ) + .appending(contentsOf: "\(beforeMs)".data(using: .ascii)?.bytes) + + guard + let signatureBytes: [UInt8] = sodium.wrappedValue.sign.signature( + message: verificationBytes, + secretKey: ed25519SecretKey + ) + else { + throw SnodeAPIError.signingFailed + } + + return signatureBytes + } + } +} diff --git a/SessionSnodeKit/Models/DeleteAllBeforeResponse.swift b/SessionSnodeKit/Models/DeleteAllBeforeResponse.swift new file mode 100644 index 000000000..25e71d9ff --- /dev/null +++ b/SessionSnodeKit/Models/DeleteAllBeforeResponse.swift @@ -0,0 +1,45 @@ +// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved. + +import Foundation +import Sodium +import SessionUtilitiesKit + +public class DeleteAllBeforeResponse: SnodeRecursiveResponse { + // MARK: - Convenience + + internal func validResultMap( + userX25519PublicKey: String, + beforeMs: UInt64, + sodium: Sodium + ) -> [String: Bool] { + return swarm.reduce(into: [:]) { result, next in + guard + !next.value.failed, + let encodedSignature: Data = Data(base64Encoded: next.value.signatureBase64) + else { + 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)).") + } + else { + SNLog("Couldn't delete data from: \(next.key).") + } + return + } + + /// Signature of `( PUBKEY_HEX || BEFORE || DELETEDHASH[0] || ... || DELETEDHASH[N] )` + /// signed by the node's ed25519 pubkey. When doing a multi-namespace delete the `DELETEDHASH` + /// values are totally ordered (i.e. among all the hashes deleted regardless of namespace) + let verificationBytes: [UInt8] = userX25519PublicKey.bytes + .appending(contentsOf: "\(beforeMs)".data(using: .ascii)?.bytes) + .appending(contentsOf: next.value.deleted.joined().bytes) + + result[next.key] = sodium.sign.verify( + message: verificationBytes, + publicKey: Data(hex: next.key).bytes, + signature: encodedSignature.bytes + ) + } + } +} diff --git a/SessionSnodeKit/Models/DeleteAllMessagesRequest.swift b/SessionSnodeKit/Models/DeleteAllMessagesRequest.swift index b90ce4bcf..bd3526273 100644 --- a/SessionSnodeKit/Models/DeleteAllMessagesRequest.swift +++ b/SessionSnodeKit/Models/DeleteAllMessagesRequest.swift @@ -64,7 +64,7 @@ extension SnodeAPI { namespace?.verificationString )?.bytes ) - .appending(contentsOf: timestampMs.map { "\($0)" }?.data(using: .utf8)?.bytes) + .appending(contentsOf: timestampMs.map { "\($0)" }?.data(using: .ascii)?.bytes) guard let signatureBytes: [UInt8] = sodium.wrappedValue.sign.signature( diff --git a/SessionSnodeKit/Models/DeleteAllMessagesResponse.swift b/SessionSnodeKit/Models/DeleteAllMessagesResponse.swift new file mode 100644 index 000000000..b311e3af7 --- /dev/null +++ b/SessionSnodeKit/Models/DeleteAllMessagesResponse.swift @@ -0,0 +1,80 @@ +// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved. + +import Foundation +import Sodium +import SessionUtilitiesKit + +public class DeleteAllMessagesResponse: SnodeRecursiveResponse { + // MARK: - Convenience + + internal func validResultMap( + userX25519PublicKey: String, + timestampMs: UInt64, + sodium: Sodium + ) -> [String: Bool] { + return swarm.reduce(into: [:]) { result, next in + guard + !next.value.failed, + let encodedSignature: Data = Data(base64Encoded: next.value.signatureBase64) + else { + 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)).") + } + else { + SNLog("Couldn't delete data from: \(next.key).") + } + return + } + + /// Signature of `( PUBKEY_HEX || TIMESTAMP || DELETEDHASH[0] || ... || DELETEDHASH[N] )` + /// signed by the node's ed25519 pubkey. When doing a multi-namespace delete the `DELETEDHASH` + /// values are totally ordered (i.e. among all the hashes deleted regardless of namespace) + let verificationBytes: [UInt8] = userX25519PublicKey.bytes + .appending(contentsOf: "\(timestampMs)".data(using: .ascii)?.bytes) + .appending(contentsOf: next.value.deleted.joined().bytes) + + result[next.key] = sodium.sign.verify( + message: verificationBytes, + publicKey: Data(hex: next.key).bytes, + signature: encodedSignature.bytes + ) + } + } +} + +// MARK: - SwarmItem + +public extension DeleteAllMessagesResponse { + class SwarmItem: SnodeSwarmItem { + private enum CodingKeys: String, CodingKey { + case deleted + } + + public let deleted: [String] + public let deletedNamespaced: [String: [String]] + + // MARK: - Initialization + + required init(from decoder: Decoder) throws { + let container: KeyedDecodingContainer = try decoder.container(keyedBy: CodingKeys.self) + + if let decodedDeletedNamespaced: [String: [String]] = try? container.decode([String: [String]].self, forKey: .deleted) { + deletedNamespaced = decodedDeletedNamespaced + + /// **Note:** When doing a multi-namespace delete the `DELETEDHASH` values are totally + /// ordered (i.e. among all the hashes deleted regardless of namespace) + deleted = decodedDeletedNamespaced + .reduce(into: []) { result, next in result.append(contentsOf: next.value) } + .sorted() + } + else { + deleted = ((try? container.decode([String].self, forKey: .deleted)) ?? []) + deletedNamespaced = [:] + } + + try super.init(from: decoder) + } + } +} diff --git a/SessionSnodeKit/Models/DeleteMessagesRequest.swift b/SessionSnodeKit/Models/DeleteMessagesRequest.swift new file mode 100644 index 000000000..1210d78a3 --- /dev/null +++ b/SessionSnodeKit/Models/DeleteMessagesRequest.swift @@ -0,0 +1,70 @@ +// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved. + +import Foundation + +extension SnodeAPI { + public class DeleteMessagesRequest: SnodeAuthenticatedRequestBody { + enum CodingKeys: String, CodingKey { + case messageHashes = "messages" + case requireSuccessfulDeletion = "required" + } + + let messageHashes: [String] + let requireSuccessfulDeletion: Bool + + // MARK: - Init + + public init( + messageHashes: [String], + requireSuccessfulDeletion: Bool, + pubkey: String, + ed25519PublicKey: [UInt8], + ed25519SecretKey: [UInt8] + ) { + self.messageHashes = messageHashes + self.requireSuccessfulDeletion = requireSuccessfulDeletion + + super.init( + pubkey: pubkey, + ed25519PublicKey: ed25519PublicKey, + ed25519SecretKey: ed25519SecretKey + ) + } + + // MARK: - Coding + + override public func encode(to encoder: Encoder) throws { + var container: KeyedEncodingContainer = encoder.container(keyedBy: CodingKeys.self) + + try container.encode(messageHashes, forKey: .messageHashes) + + // Omitting the value is the same as false so omit to save data + if requireSuccessfulDeletion { + try container.encode(requireSuccessfulDeletion, forKey: .requireSuccessfulDeletion) + } + + try super.encode(to: encoder) + } + + // MARK: - Abstract Methods + + override func generateSignature() throws -> [UInt8] { + /// Ed25519 signature of `("delete" || messages...)`; this signs the value constructed + /// by concatenating "delete" and all `messages` values, using `pubkey` to sign. Must be base64 + /// encoded for json requests; binary for OMQ requests. + let verificationBytes: [UInt8] = SnodeAPI.Endpoint.deleteMessages.rawValue.bytes + .appending(contentsOf: messageHashes.joined().bytes) + + guard + let signatureBytes: [UInt8] = sodium.wrappedValue.sign.signature( + message: verificationBytes, + secretKey: ed25519SecretKey + ) + else { + throw SnodeAPIError.signingFailed + } + + return signatureBytes + } + } +} diff --git a/SessionSnodeKit/Models/DeleteMessagesResponse.swift b/SessionSnodeKit/Models/DeleteMessagesResponse.swift new file mode 100644 index 000000000..50fedb56e --- /dev/null +++ b/SessionSnodeKit/Models/DeleteMessagesResponse.swift @@ -0,0 +1,65 @@ +// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved. + +import Foundation +import Sodium +import SessionUtilitiesKit + +public class DeleteMessagesResponse: SnodeRecursiveResponse { + // MARK: - Convenience + + internal func validResultMap( + userX25519PublicKey: String, + serverHashes: [String], + sodium: Sodium + ) -> [String: Bool] { + return swarm.reduce(into: [:]) { result, next in + guard + !next.value.failed, + let encodedSignature: Data = Data(base64Encoded: next.value.signatureBase64) + else { + 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)).") + } + else { + SNLog("Couldn't delete data from: \(next.key).") + } + return + } + + /// The signature format is `( PUBKEY_HEX || RMSG[0] || ... || RMSG[N] || DMSG[0] || ... || DMSG[M] )` + let verificationBytes: [UInt8] = userX25519PublicKey.bytes + .appending(contentsOf: serverHashes.joined().bytes) + .appending(contentsOf: next.value.deleted.joined().bytes) + + result[next.key] = sodium.sign.verify( + message: verificationBytes, + publicKey: Data(hex: next.key).bytes, + signature: encodedSignature.bytes + ) + } + } +} + +// MARK: - SwarmItem + +public extension DeleteMessagesResponse { + class SwarmItem: SnodeSwarmItem { + private enum CodingKeys: String, CodingKey { + case deleted + } + + public let deleted: [String] + + // MARK: - Initialization + + required init(from decoder: Decoder) throws { + let container: KeyedDecodingContainer = try decoder.container(keyedBy: CodingKeys.self) + + deleted = try container.decode([String].self, forKey: .deleted) + + try super.init(from: decoder) + } + } +} diff --git a/SessionSnodeKit/Models/GetMessagesRequest.swift b/SessionSnodeKit/Models/GetMessagesRequest.swift index 6eeedf406..8f17d949d 100644 --- a/SessionSnodeKit/Models/GetMessagesRequest.swift +++ b/SessionSnodeKit/Models/GetMessagesRequest.swift @@ -55,7 +55,7 @@ extension SnodeAPI { /// encoded for json requests; binary for OMQ requests. let verificationBytes: [UInt8] = SnodeAPI.Endpoint.getMessages.rawValue.bytes .appending(contentsOf: namespace?.verificationString.bytes) - .appending(contentsOf: timestampMs.map { "\($0)" }?.data(using: .utf8)?.bytes) + .appending(contentsOf: timestampMs.map { "\($0)" }?.data(using: .ascii)?.bytes) guard let signatureBytes: [UInt8] = sodium.wrappedValue.sign.signature( diff --git a/SessionSnodeKit/Models/GetMessagesResponse.swift b/SessionSnodeKit/Models/GetMessagesResponse.swift new file mode 100644 index 000000000..b0aa02809 --- /dev/null +++ b/SessionSnodeKit/Models/GetMessagesResponse.swift @@ -0,0 +1,38 @@ +// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved. + +import Foundation + +public class GetMessagesResponse: SnodeResponse { + private enum CodingKeys: String, CodingKey { + case messages + case more + } + + public class RawMessage: Codable { + private enum CodingKeys: String, CodingKey { + case data + case expiration + case hash + case timestamp + } + + public let data: String + public let expiration: Int64? + public let hash: String + public let timestamp: Int64 + } + + public let messages: [RawMessage] + public let more: Bool + + // MARK: - Initialization + + required init(from decoder: Decoder) throws { + let container: KeyedDecodingContainer = try decoder.container(keyedBy: CodingKeys.self) + + messages = try container.decode([RawMessage].self, forKey: .messages) + more = try container.decode(Bool.self, forKey: .more) + + try super.init(from: decoder) + } +} diff --git a/SessionSnodeKit/Models/GetNetworkTimestampResponse.swift b/SessionSnodeKit/Models/GetNetworkTimestampResponse.swift new file mode 100644 index 000000000..71428bab9 --- /dev/null +++ b/SessionSnodeKit/Models/GetNetworkTimestampResponse.swift @@ -0,0 +1,15 @@ +// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved. + +import Foundation + +extension SnodeAPI { + public struct GetNetworkTimestampResponse: Decodable { + enum CodingKeys: String, CodingKey { + case timestamp + case version + } + + let timestamp: UInt64 + let version: [UInt64] + } +} diff --git a/SessionSnodeKit/Models/GetServiceNodesRequest.swift b/SessionSnodeKit/Models/GetServiceNodesRequest.swift new file mode 100644 index 000000000..6fae67c3e --- /dev/null +++ b/SessionSnodeKit/Models/GetServiceNodesRequest.swift @@ -0,0 +1,31 @@ +// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved. + +import Foundation + +extension SnodeAPI { + public struct GetServiceNodesRequest: Encodable { + enum CodingKeys: String, CodingKey { + case activeOnly = "active_only" + case limit + case fields + } + + let activeOnly: Bool + let limit: Int? + let fields: Fields + + public struct Fields: Encodable { + enum CodingKeys: String, CodingKey { + case publicIp = "public_ip" + case storagePort = "storage_port" + case pubkeyEd25519 = "pubkey_ed25519" + case pubkeyX25519 = "pubkey_x25519" + } + + let publicIp: Bool + let storagePort: Bool + let pubkeyEd25519: Bool + let pubkeyX25519: Bool + } + } +} diff --git a/SessionSnodeKit/Models/GetSwarmRequest.swift b/SessionSnodeKit/Models/GetSwarmRequest.swift new file mode 100644 index 000000000..86d1534b9 --- /dev/null +++ b/SessionSnodeKit/Models/GetSwarmRequest.swift @@ -0,0 +1,13 @@ +// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved. + +import Foundation + +extension SnodeAPI { + public struct GetSwarmRequest: Encodable { + enum CodingKeys: String, CodingKey { + case pubkey + } + + let pubkey: String + } +} diff --git a/SessionSnodeKit/Models/LegacyGetMessagesRequest.swift b/SessionSnodeKit/Models/LegacyGetMessagesRequest.swift new file mode 100644 index 000000000..644cdcafc --- /dev/null +++ b/SessionSnodeKit/Models/LegacyGetMessagesRequest.swift @@ -0,0 +1,28 @@ +// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved. + +import Foundation + +extension SnodeAPI { + /// This is the legacy unauthenticated message retrieval request + public struct LegacyGetMessagesRequest: Encodable { + enum CodingKeys: String, CodingKey { + case pubkey + case lastHash = "last_hash" + case namespace + } + + let pubkey: String + let lastHash: String + let namespace: SnodeAPI.Namespace? + + // MARK: - Coding + + public func encode(to encoder: Encoder) throws { + var container: KeyedEncodingContainer = encoder.container(keyedBy: CodingKeys.self) + + try container.encode(pubkey, forKey: .pubkey) + try container.encode(lastHash, forKey: .lastHash) + try container.encodeIfPresent(namespace, forKey: .namespace) + } + } +} diff --git a/SessionSnodeKit/Models/LegacySendMessageRequest.swift b/SessionSnodeKit/Models/LegacySendMessageRequest.swift new file mode 100644 index 000000000..08cfe72ef --- /dev/null +++ b/SessionSnodeKit/Models/LegacySendMessageRequest.swift @@ -0,0 +1,24 @@ +// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved. + +import Foundation + +extension SnodeAPI { + /// This is the legacy unauthenticated message store request + public struct LegacySendMessagesRequest: Encodable { + enum CodingKeys: String, CodingKey { + case namespace + } + + let message: SnodeMessage + let namespace: SnodeAPI.Namespace + + // MARK: - Coding + + public func encode(to encoder: Encoder) throws { + var container: KeyedEncodingContainer = encoder.container(keyedBy: CodingKeys.self) + + try message.encode(to: encoder) + try container.encode(namespace, forKey: .namespace) + } + } +} diff --git a/SessionSnodeKit/Models/ONSResolveRequest.swift b/SessionSnodeKit/Models/ONSResolveRequest.swift new file mode 100644 index 000000000..eaef29085 --- /dev/null +++ b/SessionSnodeKit/Models/ONSResolveRequest.swift @@ -0,0 +1,15 @@ +// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved. + +import Foundation + +extension SnodeAPI { + public struct ONSResolveRequest: Encodable { + enum CodingKeys: String, CodingKey { + case type + case base64EncodedNameHash = "name_hash" + } + + let type: Int64 + let base64EncodedNameHash: String + } +} diff --git a/SessionSnodeKit/Models/ONSResolveResponse.swift b/SessionSnodeKit/Models/ONSResolveResponse.swift new file mode 100644 index 000000000..5208bbb4c --- /dev/null +++ b/SessionSnodeKit/Models/ONSResolveResponse.swift @@ -0,0 +1,84 @@ +// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved. + +import Foundation +import Sodium +import SessionUtilitiesKit + +extension SnodeAPI { + public class ONSResolveResponse: SnodeResponse { + private struct Result: Codable { + enum CodingKeys: String, CodingKey { + case nonce + case encryptedValue = "encrypted_value" + } + + fileprivate let nonce: String? + fileprivate let encryptedValue: String + } + + enum CodingKeys: String, CodingKey { + case result + } + + private let result: Result + + // MARK: - Initialization + + required init(from decoder: Decoder) throws { + let container: KeyedDecodingContainer = try decoder.container(keyedBy: CodingKeys.self) + + result = try container.decode(Result.self, forKey: .result) + + try super.init(from: decoder) + } + + // MARK: - Convenience + + func sessionId(sodium: Sodium, nameBytes: [UInt8], nameHashBytes: [UInt8]) throws -> String { + let ciphertext: [UInt8] = Data(hex: result.encryptedValue).bytes + + // Handle old Argon2-based encryption used before HF16 + guard let hexEncodedNonce: String = result.nonce else { + let salt: [UInt8] = Data(repeating: 0, count: sodium.pwHash.SaltBytes).bytes + + guard + let key: [UInt8] = sodium.pwHash.hash( + outputLength: sodium.secretBox.KeyBytes, + passwd: nameBytes, + salt: salt, + opsLimit: sodium.pwHash.OpsLimitModerate, + memLimit: sodium.pwHash.MemLimitModerate, + alg: .Argon2ID13 + ) + else { throw SnodeAPIError.hashingFailed } + + let nonce: [UInt8] = Data(repeating: 0, count: sodium.secretBox.NonceBytes).bytes + + guard let sessionIdAsData: [UInt8] = sodium.secretBox.open(authenticatedCipherText: ciphertext, secretKey: key, nonce: nonce) else { + throw SnodeAPIError.decryptionFailed + } + + return sessionIdAsData.toHexString() + } + + let nonceBytes: [UInt8] = Data(hex: hexEncodedNonce).bytes + + // xchacha-based encryption + // key = H(name, key=H(name)) + guard let key: [UInt8] = sodium.genericHash.hash(message: nameBytes, key: nameHashBytes) else { + throw SnodeAPIError.hashingFailed + } + guard + // Should always be equal in practice + ciphertext.count >= (SessionId.byteCount + sodium.aead.xchacha20poly1305ietf.ABytes), + let sessionIdAsData = sodium.aead.xchacha20poly1305ietf.decrypt( + authenticatedCipherText: ciphertext, + secretKey: key, + nonce: nonceBytes + ) + else { throw SnodeAPIError.decryptionFailed } + + return sessionIdAsData.toHexString() + } + } +} diff --git a/SessionSnodeKit/Models/OxenDaemonRPCRequest.swift b/SessionSnodeKit/Models/OxenDaemonRPCRequest.swift new file mode 100644 index 000000000..935961963 --- /dev/null +++ b/SessionSnodeKit/Models/OxenDaemonRPCRequest.swift @@ -0,0 +1,21 @@ +// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved. + +import Foundation + +public struct OxenDaemonRPCRequest: Encodable { + private enum CodingKeys: String, CodingKey { + case endpoint + case body = "params" + } + + private let endpoint: String + private let body: T + + public init( + endpoint: SnodeAPI.Endpoint, + body: T + ) { + self.endpoint = endpoint.rawValue + self.body = body + } +} diff --git a/SessionSnodeKit/Models/RequestInfo.swift b/SessionSnodeKit/Models/RequestInfo.swift deleted file mode 100644 index 8072364df..000000000 --- a/SessionSnodeKit/Models/RequestInfo.swift +++ /dev/null @@ -1,11 +0,0 @@ -// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved. - -import Foundation - -extension OnionRequestAPI { - struct RequestInfo: Codable { - let method: String - let endpoint: String - let headers: [String: String] - } -} diff --git a/SessionSnodeKit/Models/RevokeSubkeyRequest.swift b/SessionSnodeKit/Models/RevokeSubkeyRequest.swift new file mode 100644 index 000000000..203532123 --- /dev/null +++ b/SessionSnodeKit/Models/RevokeSubkeyRequest.swift @@ -0,0 +1,60 @@ +// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved. + +import Foundation + +extension SnodeAPI { + public class RevokeSubkeyRequest: SnodeAuthenticatedRequestBody { + enum CodingKeys: String, CodingKey { + case subkeyToRevoke = "revoke_subkey" + } + + let subkeyToRevoke: String + + // MARK: - Init + + public init( + subkeyToRevoke: String, + pubkey: String, + ed25519PublicKey: [UInt8], + ed25519SecretKey: [UInt8] + ) { + self.subkeyToRevoke = subkeyToRevoke + + super.init( + pubkey: pubkey, + ed25519PublicKey: ed25519PublicKey, + ed25519SecretKey: ed25519SecretKey + ) + } + + // MARK: - Coding + + override public func encode(to encoder: Encoder) throws { + var container: KeyedEncodingContainer = encoder.container(keyedBy: CodingKeys.self) + + try container.encode(subkeyToRevoke, forKey: .subkeyToRevoke) + + try super.encode(to: encoder) + } + + // MARK: - Abstract Methods + + override func generateSignature() throws -> [UInt8] { + /// Ed25519 signature of `("revoke_subkey" || subkey)`; this signs the subkey tag, + /// using `pubkey` to sign. Must be base64 encoded for json requests; binary for OMQ requests. + let verificationBytes: [UInt8] = SnodeAPI.Endpoint.revokeSubkey.rawValue.bytes + .appending(contentsOf: subkeyToRevoke.bytes) + + guard + let signatureBytes: [UInt8] = sodium.wrappedValue.sign.signature( + message: verificationBytes, + secretKey: ed25519SecretKey + ) + else { + throw SnodeAPIError.signingFailed + } + + return signatureBytes + } + } +} diff --git a/SessionSnodeKit/Models/RevokeSubkeyResponse.swift b/SessionSnodeKit/Models/RevokeSubkeyResponse.swift new file mode 100644 index 000000000..f633fc4ed --- /dev/null +++ b/SessionSnodeKit/Models/RevokeSubkeyResponse.swift @@ -0,0 +1,43 @@ +// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved. + +import Foundation +import Sodium +import SessionUtilitiesKit + +public class RevokeSubkeyResponse: SnodeRecursiveResponse { + // MARK: - Convenience + + internal func validateResult( + userX25519PublicKey: String, + subkeyToRevoke: String, + sodium: Sodium + ) throws { + try swarm.forEach { snodePublicKey, swarmItem in + guard + !swarmItem.failed, + let encodedSignature: Data = Data(base64Encoded: swarmItem.signatureBase64) + else { + if let reason: String = swarmItem.reason, let statusCode: Int = swarmItem.code { + SNLog("Couldn't revoke subkey from: \(snodePublicKey) due to error: \(reason) (\(statusCode)).") + } + else { + SNLog("Couldn't revoke subkey from: \(snodePublicKey).") + } + return + } + + /// Signature of `( PUBKEY_HEX || SUBKEY_TAG_BYTES )` where `SUBKEY_TAG_BYTES` is the + /// requested subkey tag for revocation + let verificationBytes: [UInt8] = userX25519PublicKey.bytes + .appending(contentsOf: subkeyToRevoke.bytes) + let isValid: Bool = sodium.sign.verify( + message: verificationBytes, + publicKey: Data(hex: snodePublicKey).bytes, + signature: encodedSignature.bytes + ) + + // If the update signature is invalid then we want to fail here + guard isValid else { throw SnodeAPIError.signatureVerificationFailed } + } + } +} diff --git a/SessionSnodeKit/Models/SendMessageRequest.swift b/SessionSnodeKit/Models/SendMessageRequest.swift index 94c5ab0a9..b64748ddc 100644 --- a/SessionSnodeKit/Models/SendMessageRequest.swift +++ b/SessionSnodeKit/Models/SendMessageRequest.swift @@ -6,7 +6,7 @@ extension SnodeAPI { public class SendMessageRequest: SnodeAuthenticatedRequestBody { enum CodingKeys: String, CodingKey { case namespace - case signatureTimestamp = "timestamp"//"sig_timestamp" // TODO: Add this back once the snodes are fixed + case signatureTimestamp = "sig_timestamp" } let message: SnodeMessage @@ -60,7 +60,7 @@ extension SnodeAPI { /// option. let verificationBytes: [UInt8] = SnodeAPI.Endpoint.sendMessage.rawValue.bytes .appending(contentsOf: namespace.verificationString.bytes) - .appending(contentsOf: timestampMs.map { "\($0)" }?.data(using: .utf8)?.bytes) + .appending(contentsOf: timestampMs.map { "\($0)" }?.data(using: .ascii)?.bytes) guard let signatureBytes: [UInt8] = sodium.wrappedValue.sign.signature( diff --git a/SessionSnodeKit/Models/SendMessageResponse.swift b/SessionSnodeKit/Models/SendMessageResponse.swift new file mode 100644 index 000000000..d49dd9b55 --- /dev/null +++ b/SessionSnodeKit/Models/SendMessageResponse.swift @@ -0,0 +1,54 @@ +// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved. + +import Foundation + +public class SendMessagesResponse: SnodeRecursiveResponse { + private enum CodingKeys: String, CodingKey { + case difficulty + case hash + case swarm + } + + public let difficulty: Int64 + public let hash: String + + // MARK: - Initialization + + required init(from decoder: Decoder) throws { + let container: KeyedDecodingContainer = try decoder.container(keyedBy: CodingKeys.self) + + difficulty = try container.decode(Int64.self, forKey: .difficulty) + hash = try container.decode(String.self, forKey: .hash) + + try super.init(from: decoder) + } +} + +// MARK: - SwarmItem + +public extension SendMessagesResponse { + class SwarmItem: SnodeSwarmItem { + private enum CodingKeys: String, CodingKey { + case hash + case already + } + + public let hash: String + + /// `true` if a message with this hash was already stored + /// + /// **Note:** The `hash` is still included and signed even if this occurs + public let already: Bool + + // MARK: - Initialization + + required init(from decoder: Decoder) throws { + let container: KeyedDecodingContainer = try decoder.container(keyedBy: CodingKeys.self) + + hash = try container.decode(String.self, forKey: .hash) + already = ((try? container.decode(Bool.self, forKey: .already)) ?? false) + + try super.init(from: decoder) + } + } +} diff --git a/SessionSnodeKit/Models/SnodeAPIEndpoint.swift b/SessionSnodeKit/Models/SnodeAPIEndpoint.swift deleted file mode 100644 index 63ffd5334..000000000 --- a/SessionSnodeKit/Models/SnodeAPIEndpoint.swift +++ /dev/null @@ -1,16 +0,0 @@ -// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved. - -import Foundation - -public enum SnodeAPIEndpoint: String { - case getSwarm = "get_snodes_for_pubkey" - case getMessages = "retrieve" - case sendMessage = "store" - case deleteMessage = "delete" - case oxenDaemonRPCCall = "oxend_request" - case getInfo = "info" - case clearAllData = "delete_all" - case expire = "expire" - case batch = "batch" - case sequence = "sequence" -} diff --git a/SessionSnodeKit/Models/SnodeAuthenticatedRequestBody.swift b/SessionSnodeKit/Models/SnodeAuthenticatedRequestBody.swift new file mode 100644 index 000000000..7e349a719 --- /dev/null +++ b/SessionSnodeKit/Models/SnodeAuthenticatedRequestBody.swift @@ -0,0 +1,56 @@ +// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved. + +import Foundation +import SessionUtilitiesKit + +public class SnodeAuthenticatedRequestBody: Encodable { + private enum CodingKeys: String, CodingKey { + case pubkey + case subkey + case timestampMs = "timestamp" + case ed25519PublicKey = "pubkey_ed25519" + case signatureBase64 = "signature" + } + + private let pubkey: String + private let ed25519PublicKey: [UInt8] + internal let ed25519SecretKey: [UInt8] + private let subkey: String? + internal let timestampMs: UInt64? + + // MARK: - Initialization + + public init( + pubkey: String, + ed25519PublicKey: [UInt8], + ed25519SecretKey: [UInt8], + subkey: String? = nil, + timestampMs: UInt64? = nil + ) { + self.pubkey = pubkey + self.ed25519PublicKey = ed25519PublicKey + self.ed25519SecretKey = ed25519SecretKey + self.subkey = subkey + self.timestampMs = timestampMs + } + + // MARK: - Codable + + public func encode(to encoder: Encoder) throws { + var container: KeyedEncodingContainer = encoder.container(keyedBy: CodingKeys.self) + + // Generate the signature for the request for encoding + let signatureBase64: String = try generateSignature().toBase64() + try container.encode(pubkey, forKey: .pubkey) + try container.encodeIfPresent(subkey, forKey: .subkey) + try container.encodeIfPresent(timestampMs, forKey: .timestampMs) + try container.encode(ed25519PublicKey.toHexString(), forKey: .ed25519PublicKey) + try container.encode(signatureBase64, forKey: .signatureBase64) + } + + // MARK: - Abstract Functions + + func generateSignature() throws -> [UInt8] { + preconditionFailure("abstract class - override in subclass") + } +} diff --git a/SessionSnodeKit/SnodeMessage.swift b/SessionSnodeKit/Models/SnodeMessage.swift similarity index 98% rename from SessionSnodeKit/SnodeMessage.swift rename to SessionSnodeKit/Models/SnodeMessage.swift index dedc33c2e..fea1d5bf7 100644 --- a/SessionSnodeKit/SnodeMessage.swift +++ b/SessionSnodeKit/Models/SnodeMessage.swift @@ -5,7 +5,7 @@ import SessionUtilitiesKit public final class SnodeMessage: Codable { private enum CodingKeys: String, CodingKey { - case recipient = "pubKey" + case recipient = "pubkey" case data case ttl case timestampMs = "timestamp" diff --git a/SessionSnodeKit/Models/SnodeReceivedMessage.swift b/SessionSnodeKit/Models/SnodeReceivedMessage.swift index bf2832f5c..a4659cb78 100644 --- a/SessionSnodeKit/Models/SnodeReceivedMessage.swift +++ b/SessionSnodeKit/Models/SnodeReceivedMessage.swift @@ -11,24 +11,23 @@ public struct SnodeReceivedMessage: CustomDebugStringConvertible { public let info: SnodeReceivedMessageInfo public let data: Data - init?(snode: Snode, publicKey: String, namespace: Int, rawMessage: JSON) { - guard let hash: String = rawMessage["hash"] as? String else { return nil } - - guard - let base64EncodedString: String = rawMessage["data"] as? String, - let data: Data = Data(base64Encoded: base64EncodedString) - else { + init?( + snode: Snode, + publicKey: String, + namespace: SnodeAPI.Namespace, + rawMessage: GetMessagesResponse.RawMessage + ) { + guard let data: Data = Data(base64Encoded: rawMessage.data) else { SNLog("Failed to decode data for message: \(rawMessage).") return nil } - let expirationDateMs: Int64? = (rawMessage["expiration"] as? Int64) self.info = SnodeReceivedMessageInfo( snode: snode, publicKey: publicKey, namespace: namespace, - hash: hash, - expirationDateMs: (expirationDateMs ?? SnodeReceivedMessage.defaultExpirationSeconds) + hash: rawMessage.hash, + expirationDateMs: (rawMessage.expiration ?? SnodeReceivedMessage.defaultExpirationSeconds) ) self.data = data } diff --git a/SessionSnodeKit/Models/SnodeRecursiveResponse.swift b/SessionSnodeKit/Models/SnodeRecursiveResponse.swift new file mode 100644 index 000000000..2d4dbb1e4 --- /dev/null +++ b/SessionSnodeKit/Models/SnodeRecursiveResponse.swift @@ -0,0 +1,21 @@ +// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved. + +import Foundation + +public class SnodeRecursiveResponse: SnodeResponse { + private enum CodingKeys: String, CodingKey { + case swarm + } + + internal let swarm: [String: T] + + // MARK: - Initialization + + required init(from decoder: Decoder) throws { + let container: KeyedDecodingContainer = try decoder.container(keyedBy: CodingKeys.self) + + swarm = try container.decode([String: T].self, forKey: .swarm) + + try super.init(from: decoder) + } +} diff --git a/SessionSnodeKit/Models/SnodeRequest.swift b/SessionSnodeKit/Models/SnodeRequest.swift new file mode 100644 index 000000000..f8d777569 --- /dev/null +++ b/SessionSnodeKit/Models/SnodeRequest.swift @@ -0,0 +1,33 @@ +// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved. + +import Foundation +import SessionUtilitiesKit + +public struct SnodeRequest: Encodable { + private enum CodingKeys: String, CodingKey { + case method + case body = "params" + } + + internal let endpoint: SnodeAPI.Endpoint + internal let body: T + + // MARK: - Initialization + + public init( + endpoint: SnodeAPI.Endpoint, + body: T + ) { + self.endpoint = endpoint + self.body = body + } + + // MARK: - Codable + + public func encode(to encoder: Encoder) throws { + var container: KeyedEncodingContainer = encoder.container(keyedBy: CodingKeys.self) + + try container.encode(endpoint.rawValue, forKey: .method) + try container.encode(body, forKey: .body) + } +} diff --git a/SessionSnodeKit/Models/SnodeResponse.swift b/SessionSnodeKit/Models/SnodeResponse.swift new file mode 100644 index 000000000..cba1f63b0 --- /dev/null +++ b/SessionSnodeKit/Models/SnodeResponse.swift @@ -0,0 +1,13 @@ +// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved. + +import Foundation + +public class SnodeResponse: Codable { + private enum CodingKeys: String, CodingKey { + case hardFork = "hf" + case timeOffset = "t" + } + + internal let hardFork: [Int] + internal let timeOffset: Int64 +} diff --git a/SessionSnodeKit/Models/SnodeSwarmItem.swift b/SessionSnodeKit/Models/SnodeSwarmItem.swift new file mode 100644 index 000000000..456b0ed86 --- /dev/null +++ b/SessionSnodeKit/Models/SnodeSwarmItem.swift @@ -0,0 +1,51 @@ +// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved. + +import Foundation + +public class SnodeSwarmItem: Codable { + private enum CodingKeys: String, CodingKey { + case signatureBase64 = "signature" + + case failed + case timeout + case code + case reason + case badPeerResponse = "bad_peer_response" + case queryFailure = "query_failure" + } + + public let signatureBase64: String + + /// `true` if the request failed, possibly accompanied by one of the following: `timeout`, `code`, + /// `reason`, `badPeerResponse`, `queryFailure` + public let failed: Bool + + /// `true` if the inter-swarm request timed out + public let timeout: Bool? + + /// `X` if the inter-swarm request returned error code `X` + public let code: Int? + + /// a reason string, e.g. propagating a thrown exception messages + public let reason: String? + + /// `true` if the peer returned an unparseable response + public let badPeerResponse: Bool? + + /// `true` if the database failed to perform the query + public let queryFailure: Bool? + + // MARK: - Initialization + + public required init(from decoder: Decoder) throws { + let container: KeyedDecodingContainer = try decoder.container(keyedBy: CodingKeys.self) + + signatureBase64 = try container.decode(String.self, forKey: .signatureBase64) + failed = ((try? container.decode(Bool.self, forKey: .failed)) ?? false) + timeout = try? container.decode(Bool.self, forKey: .timeout) + code = try? container.decode(Int.self, forKey: .code) + reason = try? container.decode(String.self, forKey: .reason) + badPeerResponse = try? container.decode(Bool.self, forKey: .badPeerResponse) + queryFailure = try? container.decode(Bool.self, forKey: .queryFailure) + } +} diff --git a/SessionSnodeKit/Models/SwarmSnode.swift b/SessionSnodeKit/Models/SwarmSnode.swift index 727d79191..9ba62558d 100644 --- a/SessionSnodeKit/Models/SwarmSnode.swift +++ b/SessionSnodeKit/Models/SwarmSnode.swift @@ -40,7 +40,7 @@ extension SwarmSnode { } catch { SNLog("Failed to parse snode: \(error.localizedDescription).") - throw HTTP.Error.invalidJSON + throw HTTPError.invalidJSON } } diff --git a/SessionSnodeKit/Models/UpdateExpiryAllRequest.swift b/SessionSnodeKit/Models/UpdateExpiryAllRequest.swift new file mode 100644 index 000000000..92e5b0de6 --- /dev/null +++ b/SessionSnodeKit/Models/UpdateExpiryAllRequest.swift @@ -0,0 +1,85 @@ +// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved. + +import Foundation + +extension SnodeAPI { + public class UpdateExpiryAllRequest: SnodeAuthenticatedRequestBody { + enum CodingKeys: String, CodingKey { + case expiryMs = "expiry" + case namespace + } + + let expiryMs: UInt64 + + /// The message namespace from which to change message expiries. The request will update the expiry for + /// all messages from the specific namespace, or from all namespaces when not provided + /// + /// **Note:** If omitted when sending the request, message expiries are updated from the default namespace + /// only (namespace 0) + let namespace: SnodeAPI.Namespace? + + // MARK: - Init + + public init( + expiryMs: UInt64, + namespace: SnodeAPI.Namespace?, + pubkey: String, + ed25519PublicKey: [UInt8], + ed25519SecretKey: [UInt8] + ) { + self.expiryMs = expiryMs + self.namespace = namespace + + super.init( + pubkey: pubkey, + ed25519PublicKey: ed25519PublicKey, + ed25519SecretKey: ed25519SecretKey + ) + } + + // MARK: - Coding + + override public func encode(to encoder: Encoder) throws { + var container: KeyedEncodingContainer = encoder.container(keyedBy: CodingKeys.self) + + try container.encode(expiryMs, forKey: .expiryMs) + + // If no namespace is specified it defaults to the default namespace only (namespace + // 0), so instead in this case we want to explicitly delete from `all` namespaces + switch namespace { + case .some(let namespace): try container.encode(namespace, forKey: .namespace) + case .none: try container.encode("all", forKey: .namespace) + } + + try super.encode(to: encoder) + } + + // MARK: - Abstract Methods + + override func generateSignature() throws -> [UInt8] { + /// Ed25519 signature of `("expire_all" || namespace || expiry)`, signed by `pubkey`. Must be + /// base64 encoded (json) or bytes (OMQ). namespace should be the stringified namespace for + /// non-default namespace expiries (i.e. "42", "-99", "all"), or an empty string for the default + /// namespace (whether or not explicitly provided). + let verificationBytes: [UInt8] = SnodeAPI.Endpoint.expireAll.rawValue.bytes + .appending( + contentsOf: (namespace == nil ? + "all" : + namespace?.verificationString + )?.bytes + ) + .appending(contentsOf: "\(expiryMs)".data(using: .ascii)?.bytes) + + guard + let signatureBytes: [UInt8] = sodium.wrappedValue.sign.signature( + message: verificationBytes, + secretKey: ed25519SecretKey + ) + else { + throw SnodeAPIError.signingFailed + } + + return signatureBytes + } + } +} diff --git a/SessionSnodeKit/Models/UpdateExpiryAllResponse.swift b/SessionSnodeKit/Models/UpdateExpiryAllResponse.swift new file mode 100644 index 000000000..7deba77d5 --- /dev/null +++ b/SessionSnodeKit/Models/UpdateExpiryAllResponse.swift @@ -0,0 +1,85 @@ +// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved. + +import Foundation +import Sodium +import SessionUtilitiesKit + +public class UpdateExpiryAllResponse: SnodeRecursiveResponse { + // MARK: - Convenience + + internal func validResultMap( + userX25519PublicKey: String, + expiryMs: UInt64, + sodium: Sodium + ) throws -> [String: [String]] { + return try swarm.reduce(into: [:]) { result, next in + guard + !next.value.failed, + let encodedSignature: Data = Data(base64Encoded: next.value.signatureBase64) + else { + 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)).") + } + else { + SNLog("Couldn't update expiry from: \(next.key).") + } + return + } + + /// Signature of `( PUBKEY_HEX || EXPIRY || UPDATED[0] || ... || UPDATED[N] )` + /// signed by the node's ed25519 pubkey. When doing a multi-namespace delete the `UPDATED` + /// values are totally ordered (i.e. among all the hashes deleted regardless of namespace) + let verificationBytes: [UInt8] = userX25519PublicKey.bytes + .appending(contentsOf: "\(expiryMs)".data(using: .ascii)?.bytes) + .appending(contentsOf: next.value.updated.joined().bytes) + + let isValid: Bool = sodium.sign.verify( + message: verificationBytes, + publicKey: Data(hex: next.key).bytes, + signature: encodedSignature.bytes + ) + + // If the update signature is invalid then we want to fail here + guard isValid else { throw SnodeAPIError.signatureVerificationFailed } + + result[next.key] = next.value.updated + } + } +} + +// MARK: - SwarmItem + +public extension UpdateExpiryAllResponse { + class SwarmItem: SnodeSwarmItem { + private enum CodingKeys: String, CodingKey { + case updated + } + + public let updated: [String] + public let updatedNamespaced: [String: [String]] + + // MARK: - Initialization + + required init(from decoder: Decoder) throws { + let container: KeyedDecodingContainer = try decoder.container(keyedBy: CodingKeys.self) + + if let decodedUpdatedNamespaced: [String: [String]] = try? container.decode([String: [String]].self, forKey: .updated) { + updatedNamespaced = decodedUpdatedNamespaced + + /// **Note:** When doing a multi-namespace delete the `UPDATED` values are totally + /// ordered (i.e. among all the hashes deleted regardless of namespace) + updated = decodedUpdatedNamespaced + .reduce(into: []) { result, next in result.append(contentsOf: next.value) } + .sorted() + } + else { + updated = ((try? container.decode([String].self, forKey: .updated)) ?? []) + updatedNamespaced = [:] + } + + try super.init(from: decoder) + } + } +} diff --git a/SessionSnodeKit/Models/UpdateExpiryRequest.swift b/SessionSnodeKit/Models/UpdateExpiryRequest.swift new file mode 100644 index 000000000..4add9bd5d --- /dev/null +++ b/SessionSnodeKit/Models/UpdateExpiryRequest.swift @@ -0,0 +1,69 @@ +// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved. + +import Foundation + +extension SnodeAPI { + public class UpdateExpiryRequest: SnodeAuthenticatedRequestBody { + enum CodingKeys: String, CodingKey { + case messageHashes = "messages" + case expiryMs = "expiry" + } + + let messageHashes: [String] + let expiryMs: UInt64 + + // MARK: - Init + + public init( + messageHashes: [String], + expiryMs: UInt64, + pubkey: String, + ed25519PublicKey: [UInt8], + ed25519SecretKey: [UInt8], + subkey: String? + ) { + self.messageHashes = messageHashes + self.expiryMs = expiryMs + + super.init( + pubkey: pubkey, + ed25519PublicKey: ed25519PublicKey, + ed25519SecretKey: ed25519SecretKey, + subkey: subkey + ) + } + + // MARK: - Coding + + override public func encode(to encoder: Encoder) throws { + var container: KeyedEncodingContainer = encoder.container(keyedBy: CodingKeys.self) + + try container.encode(messageHashes, forKey: .messageHashes) + try container.encode(expiryMs, forKey: .expiryMs) + + try super.encode(to: encoder) + } + + // MARK: - Abstract Methods + + override func generateSignature() throws -> [UInt8] { + /// Ed25519 signature of `("expire" || expiry || messages[0] || ... || messages[N])` + /// where `expiry` is the expiry timestamp expressed as a string. The signature must be base64 + /// encoded (json) or bytes (bt). + let verificationBytes: [UInt8] = SnodeAPI.Endpoint.expire.rawValue.bytes + .appending(contentsOf: "\(expiryMs)".data(using: .ascii)?.bytes) + .appending(contentsOf: messageHashes.joined().bytes) + + guard + let signatureBytes: [UInt8] = sodium.wrappedValue.sign.signature( + message: verificationBytes, + secretKey: ed25519SecretKey + ) + else { + throw SnodeAPIError.signingFailed + } + + return signatureBytes + } + } +} diff --git a/SessionSnodeKit/Models/UpdateExpiryResponse.swift b/SessionSnodeKit/Models/UpdateExpiryResponse.swift new file mode 100644 index 000000000..41f7a5ede --- /dev/null +++ b/SessionSnodeKit/Models/UpdateExpiryResponse.swift @@ -0,0 +1,76 @@ +// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved. + +import Foundation +import Sodium +import SessionUtilitiesKit + +public class UpdateExpiryResponse: SnodeRecursiveResponse { + // MARK: - Convenience + + internal func validResultMap( + userX25519PublicKey: String, + messageHashes: [String], + sodium: Sodium + ) throws -> [String: (hashes: [String], expiry: UInt64)] { + return try swarm.reduce(into: [:]) { result, next in + guard + !next.value.failed, + let encodedSignature: Data = Data(base64Encoded: next.value.signatureBase64) + else { + result[next.key] = ([], 0) + + 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)).") + } + else { + SNLog("Couldn't update expiry from: \(next.key).") + } + return + } + + /// Signature of + /// `( PUBKEY_HEX || EXPIRY || RMSG[0] || ... || RMSG[N] || UMSG[0] || ... || UMSG[M] )` + /// where RMSG are the requested expiry hashes and UMSG are the actual updated hashes. The signature uses + /// the node's ed25519 pubkey. + let verificationBytes: [UInt8] = userX25519PublicKey.bytes + .appending(contentsOf: "\(next.value.expiry)".data(using: .ascii)?.bytes) + .appending(contentsOf: messageHashes.joined().bytes) + .appending(contentsOf: next.value.updated.joined().bytes) + let isValid: Bool = sodium.sign.verify( + message: verificationBytes, + publicKey: Data(hex: next.key).bytes, + signature: encodedSignature.bytes + ) + + // If the update signature is invalid then we want to fail here + guard isValid else { throw SnodeAPIError.signatureVerificationFailed } + + result[next.key] = (hashes: next.value.updated, expiry: next.value.expiry) + } + } +} + +// MARK: - SwarmItem + +public extension UpdateExpiryResponse { + class SwarmItem: SnodeSwarmItem { + private enum CodingKeys: String, CodingKey { + case updated + case expiry + } + + public let updated: [String] + public let expiry: UInt64 + + // MARK: - Initialization + + required init(from decoder: Decoder) throws { + let container: KeyedDecodingContainer = try decoder.container(keyedBy: CodingKeys.self) + + updated = ((try? container.decode([String].self, forKey: .updated)) ?? []) + expiry = try container.decode(UInt64.self, forKey: .expiry) + + try super.init(from: decoder) + } + } +} diff --git a/SessionSnodeKit/Notification+OnionRequestAPI.swift b/SessionSnodeKit/Networking/Notification+OnionRequestAPI.swift similarity index 100% rename from SessionSnodeKit/Notification+OnionRequestAPI.swift rename to SessionSnodeKit/Networking/Notification+OnionRequestAPI.swift diff --git a/SessionSnodeKit/OnionRequestAPI+Encryption.swift b/SessionSnodeKit/Networking/OnionRequestAPI+Encryption.swift similarity index 100% rename from SessionSnodeKit/OnionRequestAPI+Encryption.swift rename to SessionSnodeKit/Networking/OnionRequestAPI+Encryption.swift diff --git a/SessionSnodeKit/OnionRequestAPI.swift b/SessionSnodeKit/Networking/OnionRequestAPI.swift similarity index 89% rename from SessionSnodeKit/OnionRequestAPI.swift rename to SessionSnodeKit/Networking/OnionRequestAPI.swift index 997de4088..f3cabbee1 100644 --- a/SessionSnodeKit/OnionRequestAPI.swift +++ b/SessionSnodeKit/Networking/OnionRequestAPI.swift @@ -412,25 +412,17 @@ public enum OnionRequestAPI: OnionRequestAPIType { to snode: Snode ) -> AnyPublisher<(ResponseInfoType, Data?), Error> { /// **Note:** Currently the service nodes only support V3 Onion Requests - return sendOnionRequest(with: payload, to: OnionRequestAPIDestination.snode(snode), version: .v3) - .map { _, maybeData in - guard let data: Data = maybeData else { throw HTTP.Error.invalidResponse } - - return data - } - .recover2 { error -> Promise in - guard case OnionRequestAPIError.httpRequestFailedAtDestination(let statusCode, let data, _) = error else { - throw error - } - - throw SnodeAPI.handleError(withStatusCode: statusCode, data: data, forSnode: snode, associatedWith: publicKey) ?? error - } + return sendOnionRequest( + with: payload, + to: OnionRequestAPIDestination.snode(snode), + version: .v3 + ) } /// Sends an onion request to `server`. Builds new paths as needed. public static func sendOnionRequest( _ request: URLRequest, - to server: String, // TODO: Remove this 'server' value (unused) + to server: String, with x25519PublicKey: String ) -> AnyPublisher<(ResponseInfoType, Data?), Error> { guard let url = request.url, let host = request.url?.host else { @@ -594,78 +586,40 @@ public enum OnionRequestAPI: OnionRequestAPIType { // MARK: - Version Handling - private static func generatePayload(for request: URLRequest, with version: OnionRequestAPIVersion) -> Data? { + private static func generateV4Payload(for request: URLRequest) -> Data? { guard let url = request.url else { return nil } - switch version { - // V2 and V3 Onion Requests have the same structure - case .v2, .v3: - var rawHeaders = request.allHTTPHeaderFields ?? [:] - rawHeaders.removeValue(forKey: "User-Agent") - var headers: JSON = rawHeaders.mapValues { value in - switch value.lowercased() { - case "true": return true - case "false": return false - default: return value - } - } - - var endpoint = url.path.removingPrefix("/") - if let query = url.query { endpoint += "?\(query)" } - let bodyAsString: String - - if let body: Data = request.httpBody { - headers["Content-Type"] = "application/json" // Assume data is JSON - bodyAsString = (String(data: body, encoding: .utf8) ?? "null") - } - else { - bodyAsString = "null" - } - - let payload: JSON = [ - "body" : bodyAsString, - "endpoint" : endpoint, - "method" : request.httpMethod!, - "headers" : headers - ] - - guard let jsonData: Data = try? JSONSerialization.data(withJSONObject: payload, options: []) else { return nil } - - return jsonData - - // V4 Onion Requests have a very different structure - case .v4: - // Note: We need to remove the leading forward slash unless we are explicitly hitting a legacy - // endpoint (in which case we need it to ensure the request signing works correctly - let endpoint: String = url.path - .appending(url.query.map { value in "?\(value)" }) - - let requestInfo: RequestInfo = RequestInfo( - method: (request.httpMethod ?? "GET"), // The default (if nil) is 'GET' - endpoint: endpoint, - headers: (request.allHTTPHeaderFields ?? [:]) - .setting( - "Content-Type", - (request.httpBody == nil ? nil : - // Default to JSON if not defined - ((request.allHTTPHeaderFields ?? [:])["Content-Type"] ?? "application/json") - ) - ) - .removingValue(forKey: "User-Agent") + // Note: We need to remove the leading forward slash unless we are explicitly hitting + // a legacy endpoint (in which case we need it to ensure the request signing works + // correctly + let endpoint: String = url.path + .appending(url.query.map { value in "?\(value)" }) + + let requestInfo: HTTP.RequestInfo = HTTP.RequestInfo( + method: (request.httpMethod ?? "GET"), // The default (if nil) is 'GET' + endpoint: endpoint, + headers: (request.allHTTPHeaderFields ?? [:]) + .setting( + "Content-Type", + (request.httpBody == nil ? nil : + // Default to JSON if not defined + ((request.allHTTPHeaderFields ?? [:])["Content-Type"] ?? "application/json") + ) ) - - /// Generate the Bencoded payload in the form `l{requestInfoLength}:{requestInfo}{bodyLength}:{body}e` - guard let requestInfoData: Data = try? JSONEncoder().encode(requestInfo) else { return nil } - guard let prefixData: Data = "l\(requestInfoData.count):".data(using: .ascii), let suffixData: Data = "e".data(using: .ascii) else { - return nil - } - - if let body: Data = request.httpBody, let bodyCountData: Data = "\(body.count):".data(using: .ascii) { - return (prefixData + requestInfoData + bodyCountData + body + suffixData) - } - - return (prefixData + requestInfoData + suffixData) + .removingValue(forKey: "User-Agent") + ) + + /// Generate the Bencoded payload in the form `l{requestInfoLength}:{requestInfo}{bodyLength}:{body}e` + guard let requestInfoData: Data = try? JSONEncoder().encode(requestInfo) else { return nil } + guard let prefixData: Data = "l\(requestInfoData.count):".data(using: .ascii), let suffixData: Data = "e".data(using: .ascii) else { + return nil } + + if let body: Data = request.httpBody, let bodyCountData: Data = "\(body.count):".data(using: .ascii) { + return (prefixData + requestInfoData + bodyCountData + body + suffixData) + } + + return (prefixData + requestInfoData + suffixData) } private static func handleResponse( @@ -836,7 +790,7 @@ public enum OnionRequestAPI: OnionRequestAPIType { let infoStringEndIndex: String.Index = responseString.index(infoStringStartIndex, offsetBy: infoLength) let infoString: String = String(responseString[infoStringStartIndex.. = Atomic(false) - private static var loadedSwarms: Atomic> = Atomic([]) - private static var getSnodePoolPromise: Atomic>?> = Atomic(nil) - - /// - Note: Should only be accessed from `Threading.workQueue` to avoid race conditions. - internal static var snodeFailureCount: Atomic<[Snode: UInt]> = Atomic([:]) - /// - Note: Should only be accessed from `Threading.workQueue` to avoid race conditions. - internal static var snodePool: Atomic> = Atomic([]) - - /// The offset between the user's clock and the Service Node's clock. Used in cases where the - /// user's clock is incorrect. - /// - /// - Note: Should only be accessed from `Threading.workQueue` to avoid race conditions. - public static var clockOffset: Atomic = Atomic(0) - /// - Note: Should only be accessed from `Threading.workQueue` to avoid race conditions. - public static var swarmCache: Atomic<[String: Set]> = Atomic([:]) - - // MARK: - Namespaces - - public static let defaultNamespace = 0 - public static let closedGroupNamespace = -10 - public static let configNamespace = 5 - - // MARK: - Hardfork version - - public static var hardfork = UserDefaults.standard[.hardfork] - public static var softfork = UserDefaults.standard[.softfork] - - // MARK: - Settings - - private static let maxRetryCount: UInt = 8 - private static let minSwarmSnodeCount = 3 - private static let seedNodePool: Set = Features.useTestnet ? [ "http://public.loki.foundation:38157" ] : [ "https://storage.seed1.loki.network:4433", "https://storage.seed3.loki.network:4433", "https://public.loki.foundation:4433" ] - private static let snodeFailureThreshold = 3 - private static let targetSwarmSnodeCount = 2 - private static let minSnodePoolCount = 12 - - // MARK: Snode Pool Interaction - - private static var hasInsufficientSnodes: Bool { snodePool.wrappedValue.count < minSnodePoolCount } - - private static func loadSnodePoolIfNeeded() { - guard !hasLoadedSnodePool.wrappedValue else { return } - - let fetchedSnodePool: Set = Storage.shared - .read { db in try Snode.fetchSet(db) } - .defaulting(to: []) - - snodePool.mutate { $0 = fetchedSnodePool } - hasLoadedSnodePool.mutate { $0 = true } - } - - private static func setSnodePool(to newValue: Set, db: Database? = nil) { - snodePool.mutate { $0 = newValue } - - if let db: Database = db { - _ = try? Snode.deleteAll(db) - newValue.forEach { try? $0.save(db) } - } - else { - Storage.shared.write { db in - _ = try? Snode.deleteAll(db) - newValue.forEach { try? $0.save(db) } - } - } - } - - private static func dropSnodeFromSnodePool(_ snode: Snode) { - #if DEBUG - dispatchPrecondition(condition: .onQueue(Threading.workQueue)) - #endif - var snodePool = SnodeAPI.snodePool.wrappedValue - snodePool.remove(snode) - setSnodePool(to: snodePool) - } - - @objc public static func clearSnodePool() { - snodePool.mutate { $0.removeAll() } - - Threading.workQueue.async { - setSnodePool(to: []) - } - } - - // MARK: Swarm Interaction - private static func loadSwarmIfNeeded(for publicKey: String) { - guard !loadedSwarms.wrappedValue.contains(publicKey) else { return } - - let updatedCacheForKey: Set = Storage.shared - .read { db in try Snode.fetchSet(db, publicKey: publicKey) } - .defaulting(to: []) - - swarmCache.mutate { $0[publicKey] = updatedCacheForKey } - loadedSwarms.mutate { $0.insert(publicKey) } - } - - private static func setSwarm(to newValue: Set, for publicKey: String, persist: Bool = true) { - #if DEBUG - dispatchPrecondition(condition: .onQueue(Threading.workQueue)) - #endif - swarmCache.mutate { $0[publicKey] = newValue } - - guard persist else { return } - - Storage.shared.write { db in - try? newValue.save(db, key: publicKey) - } - } - - public static func dropSnodeFromSwarmIfNeeded(_ snode: Snode, publicKey: String) { - #if DEBUG - dispatchPrecondition(condition: .onQueue(Threading.workQueue)) - #endif - let swarmOrNil = swarmCache.wrappedValue[publicKey] - guard var swarm = swarmOrNil, let index = swarm.firstIndex(of: snode) else { return } - swarm.remove(at: index) - setSwarm(to: swarm, for: publicKey) - } - - // MARK: Internal API - - internal static func invoke(_ method: SnodeAPIEndpoint, on snode: Snode, associatedWith publicKey: String? = nil, parameters: JSON) -> Promise { - if Features.useOnionRequests { - return OnionRequestAPI - .sendOnionRequest( - to: snode, - invoking: method, - with: parameters, - associatedWith: publicKey - ) - .map2 { responseData in - guard let responseJson: JSON = try? JSONSerialization.jsonObject(with: responseData, options: [ .fragmentsAllowed ]) as? JSON else { - throw HTTP.Error.invalidJSON - } - - if let hf = responseJson["hf"] as? [Int] { - if hf[1] > softfork { - softfork = hf[1] - UserDefaults.standard[.softfork] = softfork - } - - if hf[0] > hardfork { - hardfork = hf[0] - UserDefaults.standard[.hardfork] = hardfork - softfork = hf[1] - UserDefaults.standard[.softfork] = softfork - } - } - - return responseData - } - } - else { - let url = "\(snode.address):\(snode.port)/storage_rpc/v1" - return HTTP.execute(.post, url, parameters: parameters) - .recover2 { error -> Promise in - guard case HTTP.Error.httpRequestFailed(let statusCode, let data) = error else { throw error } - throw SnodeAPI.handleError(withStatusCode: statusCode, data: data, forSnode: snode, associatedWith: publicKey) ?? error - } - } - } - - private static func getNetworkTime(from snode: Snode) -> Promise { - return invoke(.getInfo, on: snode, parameters: [:]).map2 { responseData in - guard let responseJson: JSON = try? JSONSerialization.jsonObject(with: responseData, options: [ .fragmentsAllowed ]) as? JSON else { - throw HTTP.Error.invalidJSON - } - guard let timestamp = responseJson["timestamp"] as? UInt64 else { throw HTTP.Error.invalidJSON } - return timestamp - } - } - - internal static func getRandomSnode() -> Promise { - // randomElement() uses the system's default random generator, which is cryptographically secure - return getSnodePool().map2 { $0.randomElement()! } - } - - private static func getSnodePoolFromSeedNode() -> Promise> { - let target = seedNodePool.randomElement()! - let url = "\(target)/json_rpc" - let parameters: JSON = [ - "method": "get_n_service_nodes", - "params": [ - "active_only": true, - "limit": 256, - "fields": [ - "public_ip": true, - "storage_port": true, - "pubkey_ed25519": true, - "pubkey_x25519": true - ] - ] - ] - SNLog("Populating snode pool using seed node: \(target).") - let (promise, seal) = Promise>.pending() - - Threading.workQueue.async { - attempt(maxRetryCount: 4, recoveringOn: Threading.workQueue) { - HTTP.execute(.post, url, parameters: parameters, useSeedNodeURLSession: true) - .map2 { responseData -> Set in - guard let snodePool: SnodePoolResponse = try? JSONDecoder().decode(SnodePoolResponse.self, from: responseData) else { - throw SnodeAPIError.snodePoolUpdatingFailed - } - - return snodePool.result - .serviceNodeStates - .compactMap { $0.value } - .asSet() - } - } - .done2 { snodePool in - SNLog("Got snode pool from seed node: \(target).") - seal.fulfill(snodePool) - } - .catch2 { error in - SNLog("Failed to contact seed node at: \(target).") - seal.reject(error) - } - } - - return promise - } - - private static func getSnodePoolFromSnode() -> Promise> { - var snodePool = SnodeAPI.snodePool.wrappedValue - var snodes: Set = [] - (0..<3).forEach { _ in - guard let snode = snodePool.randomElement() else { return } - - snodePool.remove(snode) - snodes.insert(snode) - } - - let snodePoolPromises: [Promise>] = snodes.map { snode in - return attempt(maxRetryCount: 4, recoveringOn: Threading.workQueue) { - // Don't specify a limit in the request. Service nodes return a shuffled - // list of nodes so if we specify a limit the 3 responses we get might have - // very little overlap. - let parameters: JSON = [ - "endpoint": "get_service_nodes", - "params": [ - "active_only": true, - "fields": [ - "public_ip": true, - "storage_port": true, - "pubkey_ed25519": true, - "pubkey_x25519": true - ] - ] - ] - - return invoke(.oxenDaemonRPCCall, on: snode, parameters: parameters) - .map2 { responseData in - guard let snodePool: SnodePoolResponse = try? JSONDecoder().decode(SnodePoolResponse.self, from: responseData) else { - throw SnodeAPIError.snodePoolUpdatingFailed - } - - return snodePool.result - .serviceNodeStates - .compactMap { $0.value } - .asSet() - } - } - } - - let promise = when(fulfilled: snodePoolPromises).map2 { results -> Set in - let result: Set = results.reduce(Set()) { prev, next in prev.intersection(next) } - - // We want the snodes to agree on at least this many snodes - guard result.count > 24 else { throw SnodeAPIError.inconsistentSnodePools } - - // Limit the snode pool size to 256 so that we don't go too long without - // refreshing it - return Set(result.prefix(256)) - } - - return promise - } - - // MARK: Public API - - public static func hasCachedSnodesInclusingExpired() -> Bool { - loadSnodePoolIfNeeded() - - return !hasInsufficientSnodes - } - - public static func getSnodePool() -> Promise> { - loadSnodePoolIfNeeded() - let now = Date() - let hasSnodePoolExpired = given(Storage.shared[.lastSnodePoolRefreshDate]) { - now.timeIntervalSince($0) > 2 * 60 * 60 - }.defaulting(to: true) - let snodePool: Set = SnodeAPI.snodePool.wrappedValue - - guard hasInsufficientSnodes || hasSnodePoolExpired else { - return Promise.value(snodePool) - } - - if let getSnodePoolPromise = getSnodePoolPromise.wrappedValue { return getSnodePoolPromise } - - let promise: Promise> - if snodePool.count < minSnodePoolCount { - promise = getSnodePoolFromSeedNode() - } - else { - promise = getSnodePoolFromSnode().recover2 { _ in - getSnodePoolFromSeedNode() - } - } - - getSnodePoolPromise.mutate { $0 = promise } - promise.map2 { snodePool -> Set in - guard !snodePool.isEmpty else { throw SnodeAPIError.snodePoolUpdatingFailed } - - return snodePool - } - - promise.then2 { snodePool -> Promise> in - let (promise, seal) = Promise>.pending() - - Storage.shared.writeAsync( - updates: { db in - db[.lastSnodePoolRefreshDate] = now - setSnodePool(to: snodePool, db: db) - }, - completion: { _, _ in - seal.fulfill(snodePool) - } - ) - - return promise - } - promise.done2 { _ in - getSnodePoolPromise.mutate { $0 = nil } - } - promise.catch2 { _ in - getSnodePoolPromise.mutate { $0 = nil } - } - - return promise - } - - public static func getSessionID(for onsName: String) -> Promise { - let validationCount = 3 - let sessionIDByteCount = 33 - // The name must be lowercased - let onsName = onsName.lowercased() - // Hash the ONS name using BLAKE2b - let nameAsData = [UInt8](onsName.data(using: String.Encoding.utf8)!) - - guard let nameHash = sodium.genericHash.hash(message: nameAsData) else { - return Promise(error: SnodeAPIError.hashingFailed) - } - - // Ask 3 different snodes for the Session ID associated with the given name hash - let base64EncodedNameHash = nameHash.toBase64() - let parameters: [String:Any] = [ - "endpoint" : "ons_resolve", - "params" : [ - "type" : 0, // type 0 means Session - "name_hash" : base64EncodedNameHash - ] - ] - let promises = (0...pending() - - when(resolved: promises).done2 { results in - var sessionIDs: [String] = [] - for result in results { - switch result { - case .rejected(let error): return seal.reject(error) - - case .fulfilled(let responseData): - guard let responseJson: JSON = try? JSONSerialization.jsonObject(with: responseData, options: [ .fragmentsAllowed ]) as? JSON else { - throw HTTP.Error.invalidJSON - } - guard - let intermediate = responseJson["result"] as? JSON, - let hexEncodedCiphertext = intermediate["encrypted_value"] as? String - else { return seal.reject(HTTP.Error.invalidJSON) } - - let ciphertext = [UInt8](Data(hex: hexEncodedCiphertext)) - let isArgon2Based = (intermediate["nonce"] == nil) - - if isArgon2Based { - // Handle old Argon2-based encryption used before HF16 - let salt = [UInt8](Data(repeating: 0, count: sodium.pwHash.SaltBytes)) - guard - let key = sodium.pwHash.hash( - outputLength: sodium.secretBox.KeyBytes, - passwd: nameAsData, - salt: salt, - opsLimit: sodium.pwHash.OpsLimitModerate, - memLimit: sodium.pwHash.MemLimitModerate, - alg: .Argon2ID13 - ) - else { return seal.reject(SnodeAPIError.hashingFailed) } - - let nonce = [UInt8](Data(repeating: 0, count: sodium.secretBox.NonceBytes)) - - guard let sessionIDAsData = sodium.secretBox.open(authenticatedCipherText: ciphertext, secretKey: key, nonce: nonce) else { - return seal.reject(SnodeAPIError.decryptionFailed) - } - - sessionIDs.append(sessionIDAsData.toHexString()) - } - else { - guard let hexEncodedNonce = intermediate["nonce"] as? String else { - return seal.reject(HTTP.Error.invalidJSON) - } - - let nonce = [UInt8](Data(hex: hexEncodedNonce)) - - // xchacha-based encryption - guard let key = sodium.genericHash.hash(message: nameAsData, key: nameHash) else { // key = H(name, key=H(name)) - return seal.reject(SnodeAPIError.hashingFailed) - } - guard ciphertext.count >= (sessionIDByteCount + sodium.aead.xchacha20poly1305ietf.ABytes) else { // Should always be equal in practice - return seal.reject(SnodeAPIError.decryptionFailed) - } - guard let sessionIDAsData = sodium.aead.xchacha20poly1305ietf.decrypt(authenticatedCipherText: ciphertext, secretKey: key, nonce: nonce) else { - return seal.reject(SnodeAPIError.decryptionFailed) - } - - sessionIDs.append(sessionIDAsData.toHexString()) - } - } - } - - guard sessionIDs.count == validationCount && Set(sessionIDs).count == 1 else { - return seal.reject(SnodeAPIError.validationFailed) - } - - seal.fulfill(sessionIDs.first!) - } - - return promise - } - - public static func getTargetSnodes(for publicKey: String) -> Promise<[Snode]> { - // shuffled() uses the system's default random generator, which is cryptographically secure - return getSwarm(for: publicKey).map2 { Array($0.shuffled().prefix(targetSwarmSnodeCount)) } - } - - public static func getSwarm(for publicKey: String) -> Promise> { - loadSwarmIfNeeded(for: publicKey) - - if let cachedSwarm = swarmCache.wrappedValue[publicKey], cachedSwarm.count >= minSwarmSnodeCount { - return Promise> { $0.fulfill(cachedSwarm) } - } - - SNLog("Getting swarm for: \((publicKey == getUserHexEncodedPublicKey()) ? "self" : publicKey).") - let parameters: [String: Any] = [ - "pubKey": (Features.useTestnet ? publicKey.removingIdPrefixIfNeeded() : publicKey) - ] - - return getRandomSnode() - .then2 { snode in - attempt(maxRetryCount: 4, recoveringOn: Threading.workQueue) { - invoke(.getSwarm, on: snode, associatedWith: publicKey, parameters: parameters) - } - } - .map2 { responseData in - let swarm = parseSnodes(from: responseData) - - setSwarm(to: swarm, for: publicKey) - return swarm - } - } - - // MARK: - Retrieve - - // Not in use until we can batch delete and store config messages - public static func getConfigMessages(from snode: Snode, associatedWith publicKey: String) -> Promise<([SnodeReceivedMessage], String?)> { - let (promise, seal) = Promise<([SnodeReceivedMessage], String?)>.pending() - - Threading.workQueue.async { - getMessagesWithAuthentication(from: snode, associatedWith: publicKey, namespace: configNamespace) - .done2 { - seal.fulfill($0) - } - .catch2 { - seal.reject($0) - } - } - - return promise - } - - public static func getMessages(from snode: Snode, associatedWith publicKey: String, authenticated: Bool = true) -> Promise<([SnodeReceivedMessage], String?)> { - let (promise, seal) = Promise<([SnodeReceivedMessage], String?)>.pending() - - Threading.workQueue.async { - let retrievePromise = (authenticated ? - getMessagesWithAuthentication(from: snode, associatedWith: publicKey, namespace: defaultNamespace) : - getMessagesUnauthenticated(from: snode, associatedWith: publicKey) - ) - - retrievePromise - .done2 { seal.fulfill($0) } - .catch2 { seal.reject($0) } - } - - return promise - } - - public static func getClosedGroupMessagesFromDefaultNamespace(from snode: Snode, associatedWith publicKey: String) -> Promise<([SnodeReceivedMessage], String?)> { - let (promise, seal) = Promise<([SnodeReceivedMessage], String?)>.pending() - - Threading.workQueue.async { - getMessagesUnauthenticated(from: snode, associatedWith: publicKey, namespace: defaultNamespace) - .done2 { seal.fulfill($0) } - .catch2 { seal.reject($0) } - } - - return promise - } - - private static func getMessagesWithAuthentication(from snode: Snode, associatedWith publicKey: String, namespace: Int) -> Promise<([SnodeReceivedMessage], String?)> { - /// **Note:** All authentication logic is only apply to 1-1 chats, the reason being that we can't currently support it yet for - /// closed groups. The Storage Server requires an ed25519 key pair, but we don't have that for our closed groups. - guard let userED25519KeyPair: Box.KeyPair = Storage.shared.read({ db in Identity.fetchUserEd25519KeyPair(db) }) else { - return Promise(error: SnodeAPIError.noKeyPair) - } - - // Get last message hash - SnodeReceivedMessageInfo.pruneExpiredMessageHashInfo(for: snode, namespace: namespace, associatedWith: publicKey) - let lastHash = SnodeReceivedMessageInfo.fetchLastNotExpired(for: snode, namespace: namespace, associatedWith: publicKey)?.hash ?? "" - - // Construct signature - let timestamp = UInt64(Int64(floor(Date().timeIntervalSince1970 * 1000)) + SnodeAPI.clockOffset.wrappedValue) - let ed25519PublicKey = userED25519KeyPair.publicKey.toHexString() - let namespaceVerificationString = (namespace == defaultNamespace ? "" : String(namespace)) - - guard - let verificationData = ("retrieve" + namespaceVerificationString + String(timestamp)).data(using: String.Encoding.utf8), - let signature = sodium.sign.signature(message: Bytes(verificationData), secretKey: userED25519KeyPair.secretKey) - else { return Promise(error: SnodeAPIError.signingFailed) } - - // Make the request - let parameters: JSON = [ - "pubKey": Features.useTestnet ? publicKey.removingIdPrefixIfNeeded() : publicKey, - "namespace": namespace, - "lastHash": lastHash, - "timestamp": timestamp, - "pubkey_ed25519": ed25519PublicKey, - "signature": signature.toBase64() - ] - - return invoke(.getMessages, on: snode, associatedWith: publicKey, parameters: parameters) - .map { responseData -> [SnodeReceivedMessage] in - guard - let responseJson: JSON = try? JSONSerialization.jsonObject(with: responseData, options: [ .fragmentsAllowed ]) as? JSON, - let rawMessages: [JSON] = responseJson["messages"] as? [JSON] - else { - return [] - } - - return rawMessages - .compactMap { rawMessage -> SnodeReceivedMessage? in - SnodeReceivedMessage( - snode: snode, - publicKey: publicKey, - namespace: namespace, - rawMessage: rawMessage - ) - } - } - .map { ($0, lastHash) } - } - - private static func getMessagesUnauthenticated( - from snode: Snode, - associatedWith publicKey: String, - namespace: Int = closedGroupNamespace - ) -> Promise<([SnodeReceivedMessage], String?)> { - // Get last message hash - SnodeReceivedMessageInfo.pruneExpiredMessageHashInfo(for: snode, namespace: namespace, associatedWith: publicKey) - let lastHash = SnodeReceivedMessageInfo.fetchLastNotExpired(for: snode, namespace: namespace, associatedWith: publicKey)?.hash ?? "" - - // Make the request - var parameters: JSON = [ - "pubKey": (Features.useTestnet ? publicKey.removingIdPrefixIfNeeded() : publicKey), - "lastHash": lastHash - ] - - // Don't include namespace if polling for 0 with no authentication - if namespace != defaultNamespace { - parameters["namespace"] = namespace - } - - return invoke(.getMessages, on: snode, associatedWith: publicKey, parameters: parameters) - .map { responseData -> [SnodeReceivedMessage] in - guard - let responseJson: JSON = try? JSONSerialization.jsonObject(with: responseData, options: [ .fragmentsAllowed ]) as? JSON, - let rawMessages: [JSON] = responseJson["messages"] as? [JSON] - else { - return [] - } - - return rawMessages - .compactMap { rawMessage -> SnodeReceivedMessage? in - SnodeReceivedMessage( - snode: snode, - publicKey: publicKey, - namespace: namespace, - rawMessage: rawMessage - ) - } - } - .map { ($0, lastHash) } - } - - // MARK: Store - - public static func sendMessage(_ message: SnodeMessage, isClosedGroupMessage: Bool, isConfigMessage: Bool) -> Promise>> { - return sendMessageUnauthenticated(message, isClosedGroupMessage: isClosedGroupMessage) - } - - // Not in use until we can batch delete and store config messages - private static func sendMessageWithAuthentication(_ message: SnodeMessage, namespace: Int) -> Promise>> { - guard - let messageData: Data = try? JSONEncoder().encode(message), - let messageJson: JSON = try? JSONSerialization.jsonObject(with: messageData, options: [ .fragmentsAllowed ]) as? JSON - else { return Promise(error: HTTP.Error.invalidJSON) } - - guard let userED25519KeyPair: Box.KeyPair = Storage.shared.read({ db in Identity.fetchUserEd25519KeyPair(db) }) else { - return Promise(error: SnodeAPIError.noKeyPair) - } - - // Construct signature - let timestamp = UInt64(Int64(floor(Date().timeIntervalSince1970 * 1000)) + SnodeAPI.clockOffset.wrappedValue) - let ed25519PublicKey = userED25519KeyPair.publicKey.toHexString() - - guard - let verificationData = ("store" + String(namespace) + String(timestamp)).data(using: String.Encoding.utf8), - let signature = sodium.sign.signature(message: Bytes(verificationData), secretKey: userED25519KeyPair.secretKey) - else { return Promise(error: SnodeAPIError.signingFailed) } - - // Make the request - let (promise, seal) = Promise>>.pending() - let publicKey = (Features.useTestnet ? message.recipient.removingIdPrefixIfNeeded() : message.recipient) - - Threading.workQueue.async { - getTargetSnodes(for: publicKey) - .map2 { targetSnodes in - var parameters: JSON = messageJson - parameters["namespace"] = namespace - parameters["sig_timestamp"] = timestamp - parameters["pubkey_ed25519"] = ed25519PublicKey - parameters["signature"] = signature.toBase64() - - return Set(targetSnodes.map { targetSnode in - attempt(maxRetryCount: maxRetryCount, recoveringOn: Threading.workQueue) { - invoke(.sendMessage, on: targetSnode, associatedWith: publicKey, parameters: parameters) - } - }) - } - .done2 { seal.fulfill($0) } - .catch2 { seal.reject($0) } - } - - return promise - } - - private static func sendMessageUnauthenticated(_ message: SnodeMessage, isClosedGroupMessage: Bool) -> Promise>> { - guard - let messageData: Data = try? JSONEncoder().encode(message), - let messageJson: JSON = try? JSONSerialization.jsonObject(with: messageData, options: [ .fragmentsAllowed ]) as? JSON - else { return Promise(error: HTTP.Error.invalidJSON) } - - let (promise, seal) = Promise>>.pending() - let publicKey = Features.useTestnet ? message.recipient.removingIdPrefixIfNeeded() : message.recipient - - Threading.workQueue.async { - getTargetSnodes(for: publicKey) - .map2 { targetSnodes in - var rawResponsePromises: Set> = Set() - var parameters: JSON = messageJson - parameters["namespace"] = (isClosedGroupMessage ? closedGroupNamespace : defaultNamespace) - - for targetSnode in targetSnodes { - let rawResponsePromise = attempt(maxRetryCount: maxRetryCount, recoveringOn: Threading.workQueue) { - invoke(.sendMessage, on: targetSnode, associatedWith: publicKey, parameters: parameters) - } - rawResponsePromises.insert(rawResponsePromise) - } - - // Send closed group messages to default namespace as well - if hardfork == 19 && softfork == 0 && isClosedGroupMessage { - parameters["namespace"] = defaultNamespace - for targetSnode in targetSnodes { - let rawResponsePromise = attempt(maxRetryCount: maxRetryCount, recoveringOn: Threading.workQueue) { - invoke(.sendMessage, on: targetSnode, associatedWith: publicKey, parameters: parameters) - } - rawResponsePromises.insert(rawResponsePromise) - } - } - - return rawResponsePromises - } - .done2 { seal.fulfill($0) } - .catch2 { seal.reject($0) } - } - - return promise - } - - // MARK: Edit - - public static func updateExpiry( - publicKey: String, - edKeyPair: Box.KeyPair, - updatedExpiryMs: UInt64, - serverHashes: [String] - ) -> Promise<[String: (hashes: [String], expiry: UInt64)]> { - let publicKey = (Features.useTestnet ? publicKey.removingIdPrefixIfNeeded() : publicKey) - - return attempt(maxRetryCount: maxRetryCount, recoveringOn: Threading.workQueue) { - getSwarm(for: publicKey) - .then2 { swarm -> Promise<[String: (hashes: [String], expiry: UInt64)]> in - // "expire" || expiry || messages[0] || ... || messages[N] - let verificationBytes = SnodeAPIEndpoint.expire.rawValue.bytes - .appending(contentsOf: "\(updatedExpiryMs)".data(using: .ascii)?.bytes) - .appending(contentsOf: serverHashes.joined().bytes) - - guard - let snode = swarm.randomElement(), - let signature = sodium.sign.signature( - message: verificationBytes, - secretKey: edKeyPair.secretKey - ) - else { - throw SnodeAPIError.signingFailed - } - - let parameters: JSON = [ - "pubkey" : publicKey, - "pubkey_ed25519" : edKeyPair.publicKey.toHexString(), - "expiry": updatedExpiryMs, - "messages": serverHashes, - "signature": signature.toBase64() - ] - - return attempt(maxRetryCount: maxRetryCount, recoveringOn: Threading.workQueue) { - invoke(.expire, on: snode, associatedWith: publicKey, parameters: parameters) - .map2 { responseData -> [String: (hashes: [String], expiry: UInt64)] in - guard let responseJson: JSON = try? JSONSerialization.jsonObject(with: responseData, options: [ .fragmentsAllowed ]) as? JSON else { - throw HTTP.Error.invalidJSON - } - guard let swarm = responseJson["swarm"] as? JSON else { throw HTTP.Error.invalidJSON } - - var result: [String: (hashes: [String], expiry: UInt64)] = [:] - - for (snodePublicKey, rawJSON) in swarm { - guard let json = rawJSON as? JSON else { throw HTTP.Error.invalidJSON } - guard (json["failed"] as? Bool ?? false) == false else { - if let reason = json["reason"] as? String, let statusCode = json["code"] as? String { - SNLog("Couldn't delete data from: \(snodePublicKey) due to error: \(reason) (\(statusCode)).") - } - else { - SNLog("Couldn't delete data from: \(snodePublicKey).") - } - result[snodePublicKey] = ([], 0) - continue - } - - guard - let hashes: [String] = json["updated"] as? [String], - let expiryApplied: UInt64 = json["expiry"] as? UInt64, - let signature: String = json["signature"] as? String - else { - throw HTTP.Error.invalidJSON - } - - // The signature format is ( PUBKEY_HEX || EXPIRY || RMSG[0] || ... || RMSG[N] || UMSG[0] || ... || UMSG[M] ) - let verificationBytes = publicKey.bytes - .appending(contentsOf: "\(expiryApplied)".data(using: .ascii)?.bytes) - .appending(contentsOf: serverHashes.joined().bytes) - .appending(contentsOf: hashes.joined().bytes) - let isValid = sodium.sign.verify( - message: verificationBytes, - publicKey: Bytes(Data(hex: snodePublicKey)), - signature: Bytes(Data(base64Encoded: signature)!) - ) - - // Ensure the signature is valid - guard isValid else { - throw SnodeAPIError.signatureVerificationFailed - } - - result[snodePublicKey] = (hashes, expiryApplied) - } - - return result - } - } - } - } - } - - // MARK: Delete - - public static func deleteMessage(publicKey: String, serverHashes: [String]) -> Promise<[String: Bool]> { - guard let userED25519KeyPair = Identity.fetchUserEd25519KeyPair() else { - return Promise(error: SnodeAPIError.noKeyPair) - } - - let publicKey = (Features.useTestnet ? publicKey.removingIdPrefixIfNeeded() : publicKey) - let userX25519PublicKey: String = getUserHexEncodedPublicKey() - - return attempt(maxRetryCount: maxRetryCount, recoveringOn: Threading.workQueue) { - getSwarm(for: publicKey) - .then2 { swarm -> Promise<[String: Bool]> in - // "delete" || messages... - let verificationBytes = SnodeAPIEndpoint.deleteMessage.rawValue.bytes - .appending(contentsOf: serverHashes.joined().bytes) - - guard - let snode = swarm.randomElement(), - let signature = sodium.sign.signature( - message: verificationBytes, - secretKey: userED25519KeyPair.secretKey - ) - else { - throw SnodeAPIError.signingFailed - } - - let parameters: JSON = [ - "pubkey" : userX25519PublicKey, - "pubkey_ed25519" : userED25519KeyPair.publicKey.toHexString(), - "messages": serverHashes, - "signature": signature.toBase64() - ] - - return attempt(maxRetryCount: maxRetryCount, recoveringOn: Threading.workQueue) { - invoke(.deleteMessage, on: snode, associatedWith: publicKey, parameters: parameters) - .map2 { responseData -> [String: Bool] in - guard let responseJson: JSON = try? JSONSerialization.jsonObject(with: responseData, options: [ .fragmentsAllowed ]) as? JSON else { - throw HTTP.Error.invalidJSON - } - guard let swarm = responseJson["swarm"] as? JSON else { throw HTTP.Error.invalidJSON } - - var result: [String: Bool] = [:] - - for (snodePublicKey, rawJSON) in swarm { - guard let json = rawJSON as? JSON else { throw HTTP.Error.invalidJSON } - - let isFailed = (json["failed"] as? Bool ?? false) - - if !isFailed { - guard - let hashes = json["deleted"] as? [String], - let signature = json["signature"] as? String - else { - throw HTTP.Error.invalidJSON - } - - // The signature format is ( PUBKEY_HEX || RMSG[0] || ... || RMSG[N] || DMSG[0] || ... || DMSG[M] ) - let verificationBytes = userX25519PublicKey.bytes - .appending(contentsOf: serverHashes.joined().bytes) - .appending(contentsOf: hashes.joined().bytes) - let isValid = sodium.sign.verify( - message: verificationBytes, - publicKey: Bytes(Data(hex: snodePublicKey)), - signature: Bytes(Data(base64Encoded: signature)!) - ) - - result[snodePublicKey] = isValid - } - else { - if let reason = json["reason"] as? String, let statusCode = json["code"] as? String { - SNLog("Couldn't delete data from: \(snodePublicKey) due to error: \(reason) (\(statusCode)).") - } - else { - SNLog("Couldn't delete data from: \(snodePublicKey).") - } - result[snodePublicKey] = false - } - } - - // If we get to here then we assume it's been deleted from at least one - // service node and as a result we need to mark the hash as invalid so - // we don't try to fetch updates since that hash going forward (if we do - // we would end up re-fetching all old messages) - Storage.shared.writeAsync { db in - try? SnodeReceivedMessageInfo.handlePotentialDeletedOrInvalidHash( - db, - potentiallyInvalidHashes: serverHashes - ) - } - - return result - } - } - } - } - } - - /// Clears all the user's data from their swarm. Returns a dictionary of snode public key to deletion confirmation. - public static func clearAllData() -> Promise<[String:Bool]> { - guard let userED25519KeyPair = Identity.fetchUserEd25519KeyPair() else { - return Promise(error: SnodeAPIError.noKeyPair) - } - - let userX25519PublicKey: String = getUserHexEncodedPublicKey() - - return attempt(maxRetryCount: maxRetryCount, recoveringOn: Threading.workQueue) { - getSwarm(for: userX25519PublicKey) - .then2 { swarm -> Promise<[String:Bool]> in - let snode = swarm.randomElement()! - - return attempt(maxRetryCount: maxRetryCount, recoveringOn: Threading.workQueue) { - getNetworkTime(from: snode).then2 { timestamp -> Promise<[String: Bool]> in - let verificationData = (SnodeAPIEndpoint.clearAllData.rawValue + String(timestamp)).data(using: String.Encoding.utf8)! - - guard let signature = sodium.sign.signature(message: Bytes(verificationData), secretKey: userED25519KeyPair.secretKey) else { - throw SnodeAPIError.signingFailed - } - - let parameters: JSON = [ - "pubkey": userX25519PublicKey, - "pubkey_ed25519": userED25519KeyPair.publicKey.toHexString(), - "timestamp": timestamp, - "signature": signature.toBase64() - ] - - return attempt(maxRetryCount: maxRetryCount, recoveringOn: Threading.workQueue) { - invoke(.clearAllData, on: snode, parameters: parameters) - .map2 { responseData -> [String: Bool] in - guard let responseJson: JSON = try? JSONSerialization.jsonObject(with: responseData, options: [ .fragmentsAllowed ]) as? JSON else { - throw HTTP.Error.invalidJSON - } - guard let swarm = responseJson["swarm"] as? JSON else { throw HTTP.Error.invalidJSON } - - var result: [String: Bool] = [:] - - for (snodePublicKey, rawJSON) in swarm { - guard let json = rawJSON as? JSON else { throw HTTP.Error.invalidJSON } - - let isFailed = json["failed"] as? Bool ?? false - - if !isFailed { - guard - let hashes = json["deleted"] as? [String], - let signature = json["signature"] as? String - else { throw HTTP.Error.invalidJSON } - - // The signature format is ( PUBKEY_HEX || TIMESTAMP || DELETEDHASH[0] || ... || DELETEDHASH[N] ) - let verificationData = [ - userX25519PublicKey, - String(timestamp), - hashes.joined() - ] - .joined() - .data(using: String.Encoding.utf8)! - let isValid = sodium.sign.verify( - message: Bytes(verificationData), - publicKey: Bytes(Data(hex: snodePublicKey)), - signature: Bytes(Data(base64Encoded: signature)!) - ) - - result[snodePublicKey] = isValid - } - else { - if let reason = json["reason"] as? String, let statusCode = json["code"] as? String { - SNLog("Couldn't delete data from: \(snodePublicKey) due to error: \(reason) (\(statusCode)).") - } else { - SNLog("Couldn't delete data from: \(snodePublicKey).") - } - - result[snodePublicKey] = false - } - } - - return result - } - } - } - } - } - } - } - - // MARK: Parsing - - // The parsing utilities below use a best attempt approach to parsing; they warn for parsing failures but don't throw exceptions. - - private static func parseSnodes(from responseData: Data) -> Set { - guard let responseJson: JSON = try? JSONSerialization.jsonObject(with: responseData, options: [ .fragmentsAllowed ]) as? JSON else { - SNLog("Failed to parse snodes from response data.") - return [] - } - guard let rawSnodes = responseJson["snodes"] as? [JSON] else { - SNLog("Failed to parse snodes from: \(responseJson).") - return [] - } - - guard let snodeData: Data = try? JSONSerialization.data(withJSONObject: rawSnodes, options: []) else { - return [] - } - - // FIXME: Hopefully at some point this different Snode structure will be deprecated and can be removed - if - let swarmSnodes: [SwarmSnode] = try? JSONDecoder().decode([Failable].self, from: snodeData).compactMap({ $0.value }), - !swarmSnodes.isEmpty - { - return swarmSnodes.map { $0.toSnode() }.asSet() - } - - return ((try? JSONDecoder().decode([Failable].self, from: snodeData)) ?? []) - .compactMap { $0.value } - .asSet() - } - - // MARK: Error Handling - - /// - Note: Should only be invoked from `Threading.workQueue` to avoid race conditions. - @discardableResult - internal static func handleError(withStatusCode statusCode: UInt, data: Data?, forSnode snode: Snode, associatedWith publicKey: String? = nil) -> Error? { - #if DEBUG - dispatchPrecondition(condition: .onQueue(Threading.workQueue)) - #endif - func handleBadSnode() { - let oldFailureCount = (SnodeAPI.snodeFailureCount.wrappedValue[snode] ?? 0) - let newFailureCount = oldFailureCount + 1 - SnodeAPI.snodeFailureCount.mutate { $0[snode] = newFailureCount } - SNLog("Couldn't reach snode at: \(snode); setting failure count to \(newFailureCount).") - if newFailureCount >= SnodeAPI.snodeFailureThreshold { - SNLog("Failure threshold reached for: \(snode); dropping it.") - if let publicKey = publicKey { - SnodeAPI.dropSnodeFromSwarmIfNeeded(snode, publicKey: publicKey) - } - SnodeAPI.dropSnodeFromSnodePool(snode) - SNLog("Snode pool count: \(snodePool.wrappedValue.count).") - SnodeAPI.snodeFailureCount.mutate { $0[snode] = 0 } - } - } - - switch statusCode { - case 500, 502, 503: - // The snode is unreachable - handleBadSnode() - - case 404: - // May caused by invalid open groups - SNLog("Can't reach the server.") - - case 406: - SNLog("The user's clock is out of sync with the service node network.") - return SnodeAPIError.clockOutOfSync - - case 421: - // The snode isn't associated with the given public key anymore - if let publicKey = publicKey { - func invalidateSwarm() { - SNLog("Invalidating swarm for: \(publicKey).") - SnodeAPI.dropSnodeFromSwarmIfNeeded(snode, publicKey: publicKey) - } - - if let data: Data = data { - let snodes = parseSnodes(from: data) - - if !snodes.isEmpty { - setSwarm(to: snodes, for: publicKey) - } - else { - invalidateSwarm() - } - } - else { - invalidateSwarm() - } - } - else { - SNLog("Got a 421 without an associated public key.") - } - - default: - handleBadSnode() - SNLog("Unhandled response code: \(statusCode).") - } - - return nil - } -} diff --git a/SessionSnodeKit/Models/OnionRequestAPIDestination.swift b/SessionSnodeKit/Types/OnionRequestAPIDestination.swift similarity index 85% rename from SessionSnodeKit/Models/OnionRequestAPIDestination.swift rename to SessionSnodeKit/Types/OnionRequestAPIDestination.swift index 235bb817e..8483ce347 100644 --- a/SessionSnodeKit/Models/OnionRequestAPIDestination.swift +++ b/SessionSnodeKit/Types/OnionRequestAPIDestination.swift @@ -2,7 +2,7 @@ import Foundation -public enum OnionRequestAPIDestination: CustomStringConvertible, Codable { +public enum OnionRequestAPIDestination: CustomStringConvertible { case snode(Snode) case server(host: String, target: String, x25519PublicKey: String, scheme: String?, port: UInt16?) diff --git a/SessionSnodeKit/Models/OnionRequestAPIError.swift b/SessionSnodeKit/Types/OnionRequestAPIError.swift similarity index 100% rename from SessionSnodeKit/Models/OnionRequestAPIError.swift rename to SessionSnodeKit/Types/OnionRequestAPIError.swift diff --git a/SessionSnodeKit/Models/OnionRequestAPIVersion.swift b/SessionSnodeKit/Types/OnionRequestAPIVersion.swift similarity index 100% rename from SessionSnodeKit/Models/OnionRequestAPIVersion.swift rename to SessionSnodeKit/Types/OnionRequestAPIVersion.swift diff --git a/SessionSnodeKit/Types/SnodeAPIEndpoint.swift b/SessionSnodeKit/Types/SnodeAPIEndpoint.swift new file mode 100644 index 000000000..ca988964e --- /dev/null +++ b/SessionSnodeKit/Types/SnodeAPIEndpoint.swift @@ -0,0 +1,33 @@ +// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved. + +import Foundation + +public extension SnodeAPI { + enum Endpoint: String { + case sendMessage = "store" + case getMessages = "retrieve" + case deleteMessages = "delete" + case deleteAll = "delete_all" + case deleteAllBefore = "delete_before" + case revokeSubkey = "revoke_subkey" + case expire = "expire" + case expireAll = "expire_all" + case batch = "batch" + case sequence = "sequence" + + case getInfo = "info" + case getSwarm = "get_snodes_for_pubkey" + + case jsonRPCCall = "json_rpc" + case oxenDaemonRPCCall = "oxend_request" + + // jsonRPCCall proxied calls + + case jsonGetNServiceNodes = "get_n_service_nodes" + + // oxenDaemonRPCCall proxied calls + + case daemonOnsResolve = "ons_resolve" + case daemonGetServiceNodes = "get_service_nodes" + } +} diff --git a/SessionSnodeKit/Models/SnodeAPIError.swift b/SessionSnodeKit/Types/SnodeAPIError.swift similarity index 100% rename from SessionSnodeKit/Models/SnodeAPIError.swift rename to SessionSnodeKit/Types/SnodeAPIError.swift diff --git a/SessionUIKit/Components/Separator.swift b/SessionUIKit/Components/Separator.swift index 178c27748..d35add8fd 100644 --- a/SessionUIKit/Components/Separator.swift +++ b/SessionUIKit/Components/Separator.swift @@ -3,7 +3,7 @@ import UIKit public final class Separator: UIView { - private static let height: CGFloat = 24 + public static let height: CGFloat = 32 // MARK: - Components @@ -25,7 +25,6 @@ public final class Separator: UIView { private lazy var titleLabel: UILabel = { let result = UILabel() - result.setContentCompressionResistancePriority(.required, for: .vertical) result.font = .systemFont(ofSize: Values.smallFontSize) result.themeTextColor = .textSecondary result.textAlignment = .center diff --git a/SessionUIKit/Components/SessionButton.swift b/SessionUIKit/Components/SessionButton.swift index 6f2c5443b..f30ca2ede 100644 --- a/SessionUIKit/Components/SessionButton.swift +++ b/SessionUIKit/Components/SessionButton.swift @@ -17,6 +17,25 @@ public final class SessionButton: UIButton { case large } + public struct Info { + public let style: Style + public let title: String + public let isEnabled: Bool + public let onTap: () -> () + + public init( + style: Style, + title: String, + isEnabled: Bool, + onTap: @escaping () -> () + ) { + self.style = style + self.title = title + self.isEnabled = isEnabled + self.onTap = onTap + } + } + private let style: Style public override var isEnabled: Bool { @@ -85,9 +104,9 @@ public final class SessionButton: UIButton { clipsToBounds = true contentEdgeInsets = UIEdgeInsets( top: 0, - left: Values.smallSpacing, + left: Values.largeSpacing, bottom: 0, - right: Values.smallSpacing + right: Values.largeSpacing ) titleLabel?.font = .boldSystemFont(ofSize: (size == .small ? Values.smallFontSize : @@ -121,12 +140,23 @@ public final class SessionButton: UIButton { }(), for: .normal ) + setThemeTitleColor( + { + switch style { + case .borderless: return .highlighted(.sessionButton_text) + case .destructiveBorderless: return .highlighted(.sessionButton_destructiveText) + case .bordered, .destructive, .filled: return nil + } + }(), + for: .highlighted + ) setThemeBackgroundColor( { switch style { - case .bordered, .borderless: return .sessionButton_background - case .destructive, .destructiveBorderless: return .sessionButton_destructiveBackground + case .bordered: return .sessionButton_background + case .destructive: return .sessionButton_destructiveBackground + case .borderless, .destructiveBorderless: return .clear case .filled: return .sessionButton_filledBackground } }(), @@ -135,8 +165,9 @@ public final class SessionButton: UIButton { setThemeBackgroundColor( { switch style { - case .bordered, .borderless: return .sessionButton_highlight - case .destructive, .destructiveBorderless: return .sessionButton_destructiveHighlight + case .bordered: return .sessionButton_highlight + case .destructive: return .sessionButton_destructiveHighlight + case .borderless, .destructiveBorderless: return nil case .filled: return .sessionButton_filledHighlight } }(), @@ -157,4 +188,10 @@ public final class SessionButton: UIButton { } }() } + + // MARK: - Functions + + public func setStyle(_ style: Style) { + setup(style: style) + } } diff --git a/SessionUIKit/Style Guide/Format.swift b/SessionUIKit/Style Guide/Format.swift new file mode 100644 index 000000000..ab25db8e5 --- /dev/null +++ b/SessionUIKit/Style Guide/Format.swift @@ -0,0 +1,32 @@ +// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved. + +import Foundation + +public enum Format { + private static let fileSizeFormatter: NumberFormatter = { + let result: NumberFormatter = NumberFormatter() + result.numberStyle = .decimal + result.minimumFractionDigits = 0 + result.maximumFractionDigits = 1 + + return result + }() + private static let oneKilobyte: Double = 1024; + private static let oneMegabyte: Double = (oneKilobyte * oneKilobyte) + + public static func fileSize(_ fileSize: UInt) -> String { + let fileSizeDouble: Double = Double(fileSize) + + switch fileSizeDouble { + case oneMegabyte...Double.greatestFiniteMagnitude: + return (Format.fileSizeFormatter + .string(from: NSNumber(floatLiteral: (fileSizeDouble / oneMegabyte)))? + .appending("MB") ?? "n/a") + + default: + return (Format.fileSizeFormatter + .string(from: NSNumber(floatLiteral: max(0.1, (fileSizeDouble / oneKilobyte))))? + .appending("KB") ?? "n/a") + } + } +} diff --git a/SessionUIKit/Style Guide/Themes/Theme+ClassicDark.swift b/SessionUIKit/Style Guide/Themes/Theme+ClassicDark.swift index 61ed9914e..988c9e49d 100644 --- a/SessionUIKit/Style Guide/Themes/Theme+ClassicDark.swift +++ b/SessionUIKit/Style Guide/Themes/Theme+ClassicDark.swift @@ -64,6 +64,7 @@ internal enum Theme_ClassicDark: ThemeColors { .solidButton_background: .classicDark3, // Settings + .settings_tertiaryAction: .primary, .settings_tabBackground: .classicDark1, // Appearance diff --git a/SessionUIKit/Style Guide/Themes/Theme+ClassicLight.swift b/SessionUIKit/Style Guide/Themes/Theme+ClassicLight.swift index d2554fc0d..ed857bce9 100644 --- a/SessionUIKit/Style Guide/Themes/Theme+ClassicLight.swift +++ b/SessionUIKit/Style Guide/Themes/Theme+ClassicLight.swift @@ -64,6 +64,7 @@ internal enum Theme_ClassicLight: ThemeColors { .solidButton_background: .classicLight3, // Settings + .settings_tertiaryAction: .classicLight0, .settings_tabBackground: .classicLight5, // AppearanceButton diff --git a/SessionUIKit/Style Guide/Themes/Theme+OceanDark.swift b/SessionUIKit/Style Guide/Themes/Theme+OceanDark.swift index 1775bfed0..eb5794531 100644 --- a/SessionUIKit/Style Guide/Themes/Theme+OceanDark.swift +++ b/SessionUIKit/Style Guide/Themes/Theme+OceanDark.swift @@ -64,6 +64,7 @@ internal enum Theme_OceanDark: ThemeColors { .solidButton_background: .oceanDark2, // Settings + .settings_tertiaryAction: .primary, .settings_tabBackground: .oceanDark1, // Appearance diff --git a/SessionUIKit/Style Guide/Themes/Theme+OceanLight.swift b/SessionUIKit/Style Guide/Themes/Theme+OceanLight.swift index c22f4fb34..cbdd87dbf 100644 --- a/SessionUIKit/Style Guide/Themes/Theme+OceanLight.swift +++ b/SessionUIKit/Style Guide/Themes/Theme+OceanLight.swift @@ -64,6 +64,7 @@ internal enum Theme_OceanLight: ThemeColors { .solidButton_background: .oceanLight5, // Settings + .settings_tertiaryAction: .oceanLight1, .settings_tabBackground: .oceanLight6, // Appearance diff --git a/SessionUIKit/Style Guide/Themes/Theme.swift b/SessionUIKit/Style Guide/Themes/Theme.swift index d81a5a3bf..f2045b034 100644 --- a/SessionUIKit/Style Guide/Themes/Theme.swift +++ b/SessionUIKit/Style Guide/Themes/Theme.swift @@ -152,6 +152,7 @@ public indirect enum ThemeValue: Hashable { case solidButton_background // Settings + case settings_tertiaryAction case settings_tabBackground // Appearance diff --git a/SessionUIKit/Types/IconSize.swift b/SessionUIKit/Types/IconSize.swift index 3735676f5..c652e63ce 100644 --- a/SessionUIKit/Types/IconSize.swift +++ b/SessionUIKit/Types/IconSize.swift @@ -4,19 +4,23 @@ import Foundation import DifferenceKit public enum IconSize: Differentiable { + case verySmall case small case medium case large case veryLarge + case extraLarge case fit public var size: CGFloat { switch self { + case .verySmall: return 12 case .small: return 20 case .medium: return 24 case .large: return 32 - case .veryLarge: return 80 + case .veryLarge: return 40 + case .extraLarge: return 80 case .fit: return 0 } } diff --git a/SessionUtilitiesKit/Database/Types/PagedDatabaseObserver.swift b/SessionUtilitiesKit/Database/Types/PagedDatabaseObserver.swift index 88ecd76cc..d052e4e11 100644 --- a/SessionUtilitiesKit/Database/Types/PagedDatabaseObserver.swift +++ b/SessionUtilitiesKit/Database/Types/PagedDatabaseObserver.swift @@ -26,7 +26,7 @@ public class PagedDatabaseObserver: TransactionObserver where private let filterSQL: SQL private let groupSQL: SQL? private let orderSQL: SQL - private let dataQuery: ([Int64]) -> AdaptedFetchRequest> + private let dataQuery: ([Int64]) -> any FetchRequest private let associatedRecords: [ErasedAssociatedRecord] private var dataCache: Atomic> = Atomic(DataCache()) @@ -45,7 +45,7 @@ public class PagedDatabaseObserver: TransactionObserver where filterSQL: SQL, groupSQL: SQL? = nil, orderSQL: SQL, - dataQuery: @escaping ([Int64]) -> AdaptedFetchRequest>, + dataQuery: @escaping ([Int64]) -> any FetchRequest, associatedRecords: [ErasedAssociatedRecord] = [], onChangeUnsorted: @escaping ([T], PagedData.PageInfo) -> () ) { @@ -463,7 +463,7 @@ public class PagedDatabaseObserver: TransactionObserver where let filterSQL: SQL = self.filterSQL let groupSQL: SQL? = self.groupSQL let orderSQL: SQL = self.orderSQL - let dataQuery: ([Int64]) -> AdaptedFetchRequest> = self.dataQuery + let dataQuery: ([Int64]) -> any FetchRequest = self.dataQuery let loadedPage: (data: [T]?, pageInfo: PagedData.PageInfo, failureCallback: (() -> ())?)? = Storage.shared.read { [weak self] db in typealias QueryInfo = (limit: Int, offset: Int, updatedCacheOffset: Int) @@ -759,34 +759,6 @@ public class PagedDatabaseObserver: TransactionObserver where // MARK: - Convenience public extension PagedDatabaseObserver { - convenience init( - pagedTable: ObservedTable.Type, - pageSize: Int, - idColumn: ObservedTable.Columns, - observedChanges: [PagedData.ObservedChanges], - joinSQL: SQL? = nil, - filterSQL: SQL, - groupSQL: SQL? = nil, - orderSQL: SQL, - dataQuery: @escaping ([Int64]) -> SQLRequest, - associatedRecords: [ErasedAssociatedRecord] = [], - onChangeUnsorted: @escaping ([T], PagedData.PageInfo) -> () - ) { - self.init( - pagedTable: pagedTable, - pageSize: pageSize, - idColumn: idColumn, - observedChanges: observedChanges, - joinSQL: joinSQL, - filterSQL: filterSQL, - groupSQL: groupSQL, - orderSQL: orderSQL, - dataQuery: { rowIds in dataQuery(rowIds).adapted { _ in ScopeAdapter([:]) } }, - associatedRecords: associatedRecords, - onChangeUnsorted: onChangeUnsorted - ) - } - func load(_ target: PagedData.PageInfo.Target) where ObservedTable.ID: SQLExpressible { self.load(target.internalTarget) } @@ -1235,7 +1207,7 @@ public class AssociatedRecord: ErasedAssociatedRecord where T: Fet public let joinToPagedType: SQL fileprivate let dataCache: Atomic> = Atomic(DataCache()) - fileprivate let dataQuery: (SQL?) -> AdaptedFetchRequest> + fileprivate let dataQuery: (SQL?) -> any FetchRequest fileprivate let associateData: (DataCache, DataCache) -> DataCache // MARK: - Initialization @@ -1243,7 +1215,7 @@ public class AssociatedRecord: ErasedAssociatedRecord where T: Fet public init( trackedAgainst: Table.Type, observedChanges: [PagedData.ObservedChanges], - dataQuery: @escaping (SQL?) -> AdaptedFetchRequest>, + dataQuery: @escaping (SQL?) -> any FetchRequest, joinToPagedType: SQL, associateData: @escaping (DataCache, DataCache) -> DataCache ) { @@ -1254,24 +1226,6 @@ public class AssociatedRecord: ErasedAssociatedRecord where T: Fet self.associateData = associateData } - public convenience init( - trackedAgainst: Table.Type, - observedChanges: [PagedData.ObservedChanges], - dataQuery: @escaping (SQL?) -> SQLRequest, - joinToPagedType: SQL, - associateData: @escaping (DataCache, DataCache) -> DataCache - ) { - self.init( - trackedAgainst: trackedAgainst, - observedChanges: observedChanges, - dataQuery: { additionalFilters in - dataQuery(additionalFilters).adapted { _ in ScopeAdapter([:]) } - }, - joinToPagedType: joinToPagedType, - associateData: associateData - ) - } - // MARK: - AssociatedRecord public func settingPagedTableName(pagedTableName: String) -> Self { diff --git a/SessionUtilitiesKit/Database/Utilities/QueryInterfaceRequest+Utilities.swift b/SessionUtilitiesKit/Database/Utilities/QueryInterfaceRequest+Utilities.swift index fa9419022..69ce03db5 100644 --- a/SessionUtilitiesKit/Database/Utilities/QueryInterfaceRequest+Utilities.swift +++ b/SessionUtilitiesKit/Database/Utilities/QueryInterfaceRequest+Utilities.swift @@ -10,7 +10,7 @@ public extension QueryInterfaceRequest { /// /// - parameter db: A database connection. /// - returns: Whether the request matches a row in the database. - func isNotEmpty(_ db: Database) throws -> Bool { + func isNotEmpty(_ db: Database) -> Bool { return ((try? SQLRequest("SELECT \(exists())").fetchOne(db)) ?? false) } } diff --git a/SessionUtilitiesKit/General/Data+Utilities.swift b/SessionUtilitiesKit/General/Data+Utilities.swift index 7db8c81da..bac5db7d9 100644 --- a/SessionUtilitiesKit/General/Data+Utilities.swift +++ b/SessionUtilitiesKit/General/Data+Utilities.swift @@ -2,7 +2,22 @@ import Foundation +public extension Dependencies { + static let userInfoKey: CodingUserInfoKey = CodingUserInfoKey(rawValue: "io.oxen.dependencies.codingOptions")! +} + public extension Data { + func decoded(as type: T.Type, using dependencies: Dependencies = Dependencies()) throws -> T { + do { + let decoder: JSONDecoder = JSONDecoder() + decoder.userInfo = [ Dependencies.userInfoKey: dependencies ] + + return try decoder.decode(type, from: self) + } + catch { + throw HTTPError.parsingFailed + } + } func removingIdPrefixIfNeeded() -> Data { var result = self diff --git a/SessionUtilitiesKit/General/SessionId.swift b/SessionUtilitiesKit/General/SessionId.swift index 7e251876e..4e892d489 100644 --- a/SessionUtilitiesKit/General/SessionId.swift +++ b/SessionUtilitiesKit/General/SessionId.swift @@ -5,6 +5,8 @@ import Sodium import Curve25519Kit public struct SessionId { + public static let byteCount: Int = 33 + public enum Prefix: String, CaseIterable { case standard = "05" // Used for identified users, open groups, etc. case blinded = "15" // Used for authentication and participants in open groups with blinding enabled diff --git a/SessionUtilitiesKit/Networking/HTTP.swift b/SessionUtilitiesKit/Networking/HTTP.swift index 5dbd54a64..33f3b2a4e 100644 --- a/SessionUtilitiesKit/Networking/HTTP.swift +++ b/SessionUtilitiesKit/Networking/HTTP.swift @@ -9,7 +9,8 @@ public enum HTTP { private static let snodeURLSession = URLSession(configuration: .ephemeral, delegate: snodeURLSessionDelegate, delegateQueue: nil) private static let snodeURLSessionDelegate = SnodeURLSessionDelegateImplementation() - // MARK: Certificates + // MARK: - Certificates + private static let storageSeed1Cert: SecCertificate = { let path = Bundle.main.path(forResource: "storage-seed-1", ofType: "der")! let data = try! Data(contentsOf: URL(fileURLWithPath: path)) @@ -28,10 +29,12 @@ public enum HTTP { return SecCertificateCreateWithData(nil, data as CFData)! }() - // MARK: Settings - public static let timeout: TimeInterval = 10 + // MARK: - Settings + + public static let defaultTimeout: TimeInterval = 10 - // MARK: Seed Node URL Session Delegate Implementation + // MARK: - Seed Node URL Session Delegate Implementation + private final class SeedNodeURLSessionDelegateImplementation : NSObject, URLSessionDelegate { func urlSession(_ session: URLSession, didReceive challenge: URLAuthenticationChallenge, completionHandler: @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void) { @@ -60,7 +63,8 @@ public enum HTTP { } } - // MARK: Snode URL Session Delegate Implementation + // MARK: - Snode URL Session Delegate Implementation + private final class SnodeURLSessionDelegateImplementation : NSObject, URLSessionDelegate { func urlSession(_ session: URLSession, didReceive challenge: URLAuthenticationChallenge, completionHandler: @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void) { diff --git a/SessionUtilitiesKit/Networking/HTTPError.swift b/SessionUtilitiesKit/Networking/HTTPError.swift new file mode 100644 index 000000000..7a3d2af08 --- /dev/null +++ b/SessionUtilitiesKit/Networking/HTTPError.swift @@ -0,0 +1,26 @@ +// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved. + +import Foundation + +public enum HTTPError: LocalizedError, Equatable { + case generic + case invalidURL + case invalidJSON + case parsingFailed + case invalidResponse + case maxFileSizeExceeded + case httpRequestFailed(statusCode: UInt, data: Data?) + case timeout + + public var errorDescription: String? { + switch self { + case .generic: return "An error occurred." + case .invalidURL: return "Invalid URL." + case .invalidJSON: return "Invalid JSON." + case .parsingFailed, .invalidResponse: return "Invalid response." + case .maxFileSizeExceeded: return "Maximum file size exceeded." + case .httpRequestFailed(let statusCode, _): return "HTTP request failed with status code: \(statusCode)." + case .timeout: return "The request timed out." + } + } +} diff --git a/SessionUtilitiesKit/Networking/HTTPHeader.swift b/SessionUtilitiesKit/Networking/HTTPHeader.swift new file mode 100644 index 000000000..1ace5ed88 --- /dev/null +++ b/SessionUtilitiesKit/Networking/HTTPHeader.swift @@ -0,0 +1,19 @@ +// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved. + +import Foundation + +public typealias HTTPHeader = String + +public extension HTTPHeader { + static let authorization: HTTPHeader = "Authorization" + static let contentType: HTTPHeader = "Content-Type" + static let contentDisposition: HTTPHeader = "Content-Disposition" +} + +// MARK: - Convenience + +public extension Dictionary where Key == HTTPHeader, Value == String { + func toHTTPHeaders() -> [String: String] { + return self.reduce(into: [:]) { result, next in result[next.key] = next.value } + } +} diff --git a/SessionUtilitiesKit/Networking/HTTPMethod.swift b/SessionUtilitiesKit/Networking/HTTPMethod.swift new file mode 100644 index 000000000..940ca4fe3 --- /dev/null +++ b/SessionUtilitiesKit/Networking/HTTPMethod.swift @@ -0,0 +1,10 @@ +// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved. + +import Foundation + +public enum HTTPMethod: String, Codable { + case get = "GET" + case put = "PUT" + case post = "POST" + case delete = "DELETE" +} diff --git a/SessionUtilitiesKit/Networking/HTTPQueryParam.swift b/SessionUtilitiesKit/Networking/HTTPQueryParam.swift new file mode 100644 index 000000000..a766bf1ed --- /dev/null +++ b/SessionUtilitiesKit/Networking/HTTPQueryParam.swift @@ -0,0 +1,5 @@ +// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved. + +import Foundation + +public typealias HTTPQueryParam = String diff --git a/SessionMessagingKit/Common Networking/Request.swift b/SessionUtilitiesKit/Networking/Request.swift similarity index 70% rename from SessionMessagingKit/Common Networking/Request.swift rename to SessionUtilitiesKit/Networking/Request.swift index f32620bb2..df2a04e78 100644 --- a/SessionMessagingKit/Common Networking/Request.swift +++ b/SessionUtilitiesKit/Networking/Request.swift @@ -1,39 +1,38 @@ import Foundation -import SessionUtilitiesKit // MARK: - Convenience Types -struct Empty: Codable {} +public struct Empty: Codable {} -typealias NoBody = Empty -typealias NoResponse = Empty +public typealias NoBody = Empty +public typealias NoResponse = Empty -protocol EndpointType: Hashable { +public protocol EndpointType: Hashable { var path: String { get } } // MARK: - Request -struct Request { - let method: HTTP.Verb - let server: String - let endpoint: Endpoint - let queryParameters: [QueryParam: String] - let headers: [Header: String] +public struct Request { + public let method: HTTPMethod + public let server: String + public let endpoint: Endpoint + public let queryParameters: [HTTPQueryParam: String] + public let headers: [HTTPHeader: String] /// This is the body value sent during the request /// /// **Warning:** The `bodyData` value should be used to when making the actual request instead of this as there /// is custom handling for certain data types - let body: T? + public let body: T? // MARK: - Initialization - init( - method: HTTP.Verb = .get, + public init( + method: HTTPMethod = .get, server: String, endpoint: Endpoint, - queryParameters: [QueryParam: String] = [:], - headers: [Header: String] = [:], + queryParameters: [HTTPQueryParam: String] = [:], + headers: [HTTPHeader: String] = [:], body: T? = nil ) { self.method = method @@ -56,7 +55,9 @@ struct Request { switch body { case let bodyString as String: // The only acceptable string body is a base64 encoded one - guard let encodedData: Data = Data(base64Encoded: bodyString) else { throw HTTP.Error.parsingFailed } + guard let encodedData: Data = Data(base64Encoded: bodyString) else { + throw HTTPError.parsingFailed + } return encodedData @@ -73,11 +74,11 @@ struct Request { // MARK: - Request Generation - var urlPathAndParamsString: String { + public var urlPathAndParamsString: String { return [ "/\(endpoint.path)", queryParameters - .map { key, value in "\(key.rawValue)=\(value)" } + .map { key, value in "\(key)=\(value)" } .joined(separator: "&") ] .compactMap { $0 } @@ -85,8 +86,8 @@ struct Request { .joined(separator: "?") } - func generateUrlRequest() throws -> URLRequest { - guard let url: URL = url else { throw HTTP.Error.invalidURL } + public func generateUrlRequest() throws -> URLRequest { + guard let url: URL = url else { throw HTTPError.invalidURL } var urlRequest: URLRequest = URLRequest(url: url) urlRequest.httpMethod = method.rawValue diff --git a/SessionUtilitiesKit/Networking/RequestInfo.swift b/SessionUtilitiesKit/Networking/RequestInfo.swift new file mode 100644 index 000000000..156cd54da --- /dev/null +++ b/SessionUtilitiesKit/Networking/RequestInfo.swift @@ -0,0 +1,21 @@ +// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved. + +import Foundation + +public extension HTTP { + struct RequestInfo: Codable { + let method: String + let endpoint: String + let headers: [String: String] + + public init( + method: String, + endpoint: String, + headers: [String: String] + ) { + self.method = method + self.endpoint = endpoint + self.headers = headers + } + } +} diff --git a/SessionSnodeKit/Models/ResponseInfo.swift b/SessionUtilitiesKit/Networking/ResponseInfo.swift similarity index 72% rename from SessionSnodeKit/Models/ResponseInfo.swift rename to SessionUtilitiesKit/Networking/ResponseInfo.swift index 80e9b5f87..9d2508c0d 100644 --- a/SessionSnodeKit/Models/ResponseInfo.swift +++ b/SessionUtilitiesKit/Networking/ResponseInfo.swift @@ -2,13 +2,13 @@ import Foundation -public protocol OnionRequestResponseInfoType: Codable { +public protocol ResponseInfoType: Codable { var code: Int { get } var headers: [String: String] { get } } -extension OnionRequestAPI { - public struct ResponseInfo: OnionRequestResponseInfoType { +public extension HTTP { + struct ResponseInfo: ResponseInfoType { public let code: Int public let headers: [String: String] @@ -18,3 +18,4 @@ extension OnionRequestAPI { } } } + diff --git a/SignalUtilitiesKit/Media Viewing & Editing/MediaMessageView.swift b/SignalUtilitiesKit/Media Viewing & Editing/MediaMessageView.swift index 904648310..9ce21e92d 100644 --- a/SignalUtilitiesKit/Media Viewing & Editing/MediaMessageView.swift +++ b/SignalUtilitiesKit/Media Viewing & Editing/MediaMessageView.swift @@ -353,7 +353,7 @@ public class MediaMessageView: UIView, OWSAudioPlayerDelegate { // Format string for file size label in call interstitial view. // Embeds: {{file size as 'N mb' or 'N kb'}}. let fileSize: UInt = attachment.dataLength - label.text = String(format: "ATTACHMENT_APPROVAL_FILE_SIZE_FORMAT".localized(), OWSFormat.formatFileSize(UInt(fileSize))) + label.text = String(format: "ATTACHMENT_APPROVAL_FILE_SIZE_FORMAT".localized(), Format.fileSize(fileSize)) label.textAlignment = .center } diff --git a/SignalUtilitiesKit/Meta/SignalUtilitiesKit.h b/SignalUtilitiesKit/Meta/SignalUtilitiesKit.h index bbaac0073..aceb514c8 100644 --- a/SignalUtilitiesKit/Meta/SignalUtilitiesKit.h +++ b/SignalUtilitiesKit/Meta/SignalUtilitiesKit.h @@ -14,7 +14,6 @@ FOUNDATION_EXPORT const unsigned char SignalUtilitiesKitVersionString[]; #import #import #import -#import #import #import #import diff --git a/SignalUtilitiesKit/Profile Pictures/ProfilePictureView.swift b/SignalUtilitiesKit/Profile Pictures/ProfilePictureView.swift index 1f87c047f..6348bbdf0 100644 --- a/SignalUtilitiesKit/Profile Pictures/ProfilePictureView.swift +++ b/SignalUtilitiesKit/Profile Pictures/ProfilePictureView.swift @@ -7,7 +7,6 @@ import SessionUIKit import SessionMessagingKit public final class ProfilePictureView: UIView { - private var hasTappableProfilePicture: Bool = false public var size: CGFloat = 0 // Constraints @@ -51,6 +50,7 @@ public final class ProfilePictureView: UIView { result.clipsToBounds = true result.themeBackgroundColor = .primary result.themeBorderColor = .backgroundPrimary + result.layer.borderWidth = 1 result.layer.cornerRadius = (Values.smallProfilePictureSize / 2) result.isHidden = true @@ -132,60 +132,29 @@ public final class ProfilePictureView: UIView { additionalProfilePlaceholderImageView.pin(.right, to: .right, of: additionalImageContainerView) additionalProfilePlaceholderImageView.pin(.bottom, to: .bottom, of: additionalImageContainerView, withInset: 5) } - - public func update( - publicKey: String = "", - profile: Profile? = nil, - additionalProfile: Profile? = nil, - threadVariant: SessionThread.Variant, - openGroupProfilePictureData: Data? = nil, - useFallbackPicture: Bool = false, - showMultiAvatarForClosedGroup: Bool = false - ) { - AssertIsOnMainThread() - guard !useFallbackPicture else { - switch self.size { - case Values.smallProfilePictureSize.. (image: UIImage?, animatedImage: YYImage?, isTappable: Bool) { - if let profile: Profile = profile, let profileData: Data = ProfileManager.profileAvatar(profile: profile) { - let format: ImageFormat = profileData.guessedImageFormat - - let image: UIImage? = (format == .gif || format == .webp ? - nil : - UIImage(data: profileData) - ) - let animatedImage: YYImage? = (format != .gif && format != .webp ? - nil : - YYImage(data: profileData) - ) - - if image != nil || animatedImage != nil { - return (image, animatedImage, true) - } - } - +// TODO: Update this to be more explicit? (or add a helper method? current code requires duplicate logic around deciding what properties should be set in what cases) + private func prepareForReuse() { + imageView.contentMode = .scaleAspectFill + imageView.isHidden = true + animatedImageView.contentMode = .scaleAspectFill + animatedImageView.isHidden = true + imageContainerView.themeBackgroundColor = .backgroundSecondary + additionalImageContainerView.isHidden = true + animatedImageView.image = nil + additionalImageView.image = nil + additionalAnimatedImageView.image = nil + additionalImageView.isHidden = true + additionalAnimatedImageView.isHidden = true + additionalProfilePlaceholderImageView.isHidden = true + } + + private func getProfilePicture( + of size: CGFloat, + for publicKey: String, + profile: Profile?, + threadVariant: SessionThread.Variant + ) -> (image: UIImage?, animatedImage: YYImage?) { + guard let profile: Profile = profile, let profileData: Data = ProfileManager.profileAvatar(profile: profile) else { return ( Identicon.generatePlaceholderIcon( seed: publicKey, @@ -193,37 +162,113 @@ public final class ProfilePictureView: UIView { .defaulting(to: publicKey), size: size ), - nil, - false + nil ) } - // Calulate the sizes (and set the additional image content) - let targetSize: CGFloat + switch profileData.guessedImageFormat { + case .gif, .webp: return (nil, YYImage(data: profileData)) + default: return (UIImage(data: profileData), nil) + } + } + + public func update( + publicKey: String, + threadVariant: SessionThread.Variant, + customImageData: Data?, + profile: Profile?, + additionalProfile: Profile? + ) { + prepareForReuse() - switch (threadVariant, showMultiAvatarForClosedGroup) { - case (.closedGroup, true): - if self.size == 40 { - targetSize = 32 - } - else if self.size == Values.largeProfilePictureSize { - targetSize = 56 - } - else { - targetSize = Values.smallProfilePictureSize + // If we are given 'customImageData' then only use that + if let customImageData: Data = customImageData { + switch customImageData.guessedImageFormat { + case .gif, .webp: + animatedImageView.image = YYImage(data: customImageData) + animatedImageView.isHidden = false + + default: + imageView.image = UIImage(data: customImageData) + imageView.isHidden = false + } + + imageViewWidthConstraint.constant = self.size + imageViewHeightConstraint.constant = self.size + imageContainerView.layer.cornerRadius = (self.size / 2) + return + } + + // Otherwise there are conversation-type-specific behaviours + switch threadVariant { + case .openGroup: + switch self.size { + case Values.smallProfilePictureSize.. UIImage? { - return (hasTappableProfilePicture ? imageView.image : nil) } } diff --git a/SignalUtilitiesKit/Utilities/OWSFormat.h b/SignalUtilitiesKit/Utilities/OWSFormat.h deleted file mode 100644 index aa0fb794d..000000000 --- a/SignalUtilitiesKit/Utilities/OWSFormat.h +++ /dev/null @@ -1,17 +0,0 @@ -// -// Copyright (c) 2017 Open Whisper Systems. All rights reserved. -// - -NS_ASSUME_NONNULL_BEGIN - -@interface OWSFormat : NSObject - -- (instancetype)init NS_UNAVAILABLE; - -+ (NSString *)formatInt:(int)value; - -+ (NSString *)formatFileSize:(unsigned long)fileSize; - -@end - -NS_ASSUME_NONNULL_END diff --git a/SignalUtilitiesKit/Utilities/OWSFormat.m b/SignalUtilitiesKit/Utilities/OWSFormat.m deleted file mode 100644 index f94660e52..000000000 --- a/SignalUtilitiesKit/Utilities/OWSFormat.m +++ /dev/null @@ -1,47 +0,0 @@ -// -// Copyright (c) 2017 Open Whisper Systems. All rights reserved. -// - -#import "OWSFormat.h" - -NS_ASSUME_NONNULL_BEGIN - -@implementation OWSFormat - -+ (NSString *)formatInt:(int)value -{ - static NSNumberFormatter *formatter = nil; - static dispatch_once_t onceToken; - dispatch_once(&onceToken, ^{ - formatter = [NSNumberFormatter new]; - formatter.numberStyle = NSNumberFormatterNoStyle; - }); - return [formatter stringFromNumber:@(value)]; -} - -+ (NSString *)formatFileSize:(unsigned long)fileSize -{ - static NSNumberFormatter *formatter = nil; - static dispatch_once_t onceToken; - dispatch_once(&onceToken, ^{ - formatter = [NSNumberFormatter new]; - formatter.numberStyle = NSNumberFormatterDecimalStyle; - }); - - const unsigned long kOneKilobyte = 1024; - const unsigned long kOneMegabyte = kOneKilobyte * kOneKilobyte; - - if (fileSize > kOneMegabyte) { - return [[formatter stringFromNumber:@((double)lround(fileSize * 100 / (CGFloat)kOneMegabyte) / 100)] - stringByAppendingString:@" MB"]; - } else if (fileSize > kOneKilobyte) { - return [[formatter stringFromNumber:@((double)lround(fileSize * 100 / (CGFloat)kOneKilobyte) / 100)] - stringByAppendingString:@" KB"]; - } else { - return [NSString stringWithFormat:@"%lu Bytes", fileSize]; - } -} - -@end - -NS_ASSUME_NONNULL_END