diff --git a/.gitmodules b/.gitmodules index b8c1e380954..94e414a92da 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,3 +1,3 @@ [submodule "LibSession-Util"] path = LibSession-Util - url = https://github.com/oxen-io/libsession-util.git + url = https://github.com/session-foundation/libsession-util.git diff --git a/LibSession-Util b/LibSession-Util index 10f15aeb1f9..50585142bea 160000 --- a/LibSession-Util +++ b/LibSession-Util @@ -1 +1 @@ -Subproject commit 10f15aeb1f9712452adf55a8d0826be6271c10e4 +Subproject commit 50585142beaa65cfb80c1dee353c5271cb46c974 diff --git a/Scripts/EmojiGenerator.swift b/Scripts/EmojiGenerator.swift index 937032a2272..519ce652208 100755 --- a/Scripts/EmojiGenerator.swift +++ b/Scripts/EmojiGenerator.swift @@ -282,8 +282,6 @@ extension EmojiGenerator { // Main enum: Create a string enum defining our enumNames equal to the baseEmoji string // e.g. case grinning = "😀" writeBlock(fileName: "Emoji.swift") { fileHandle in - fileHandle.writeLine("// swiftlint:disable all") - fileHandle.writeLine("// stringlint:disable") fileHandle.writeLine("") fileHandle.writeLine("/// A sorted representation of all available emoji") fileHandle.writeLine("enum Emoji: String, CaseIterable, Equatable {") @@ -293,7 +291,6 @@ extension EmojiGenerator { } } fileHandle.writeLine("}") - fileHandle.writeLine("// swiftlint:disable all") } } @@ -340,8 +337,6 @@ extension EmojiGenerator { // if rawValue == "😀" { self.init(baseEmoji: .grinning, skinTones: nil) } // else if rawValue == "🦻🏻" { self.init(baseEmoji: .earWithHearingAid, skinTones: [.light]) writeBlock(fileName: "EmojiWithSkinTones+String.swift") { fileHandle in - fileHandle.writeLine("// swiftlint:disable all") - fileHandle.writeLine("// stringlint:disable") fileHandle.writeLine("") fileHandle.writeLine("extension EmojiWithSkinTones {") fileHandle.indent { @@ -409,7 +404,6 @@ extension EmojiGenerator { } } fileHandle.writeLine("}") - fileHandle.writeLine("// swiftlint:disable all") } } @@ -474,8 +468,6 @@ extension EmojiGenerator { static func writeSkinToneLookupFile(from emojiModel: EmojiModel) { writeBlock(fileName: "Emoji+SkinTones.swift") { fileHandle in - fileHandle.writeLine("// swiftlint:disable all") - fileHandle.writeLine("// stringlint:disable") fileHandle.writeLine("") fileHandle.writeLine("extension Emoji {") fileHandle.indent { @@ -539,7 +531,6 @@ extension EmojiGenerator { fileHandle.writeLine("}") } fileHandle.writeLine("}") - fileHandle.writeLine("// swiftlint:disable all") } } @@ -556,8 +547,6 @@ extension EmojiGenerator { ] writeBlock(fileName: "Emoji+Category.swift") { fileHandle in - fileHandle.writeLine("// swiftlint:disable all") - fileHandle.writeLine("// stringlint:disable") fileHandle.writeLine("") fileHandle.writeLine("extension Emoji {") fileHandle.indent { @@ -664,7 +653,6 @@ extension EmojiGenerator { fileHandle.writeLine("}") } fileHandle.writeLine("}") - fileHandle.writeLine("// swiftlint:disable all") } } @@ -672,8 +660,6 @@ extension EmojiGenerator { // Name lookup: Create a computed property mapping an Emoji enum element to the raw Emoji name string // e.g. case .grinning: return "GRINNING FACE" writeBlock(fileName: "Emoji+Name.swift") { fileHandle in - fileHandle.writeLine("// swiftlint:disable all") - fileHandle.writeLine("// stringlint:disable") fileHandle.writeLine("") fileHandle.writeLine("extension Emoji {") fileHandle.indent { @@ -688,7 +674,6 @@ extension EmojiGenerator { fileHandle.writeLine("}") } fileHandle.writeLine("}") - fileHandle.writeLine("// swiftlint:disable all") } } } @@ -753,6 +738,11 @@ extension EmojiGenerator { fileHandle.writeLine("") fileHandle.writeLine("// This file is generated by EmojiGenerator.swift, do not manually edit it.") + fileHandle.writeLine("//") + fileHandle.writeLine("// swiftlint:disable all") + fileHandle.writeLine("// stringlint:disable") + fileHandle.writeLine("") + fileHandle.writeLine("import Foundation") fileHandle.writeLine("") block(fileHandle) diff --git a/Scripts/LintLocalizableStrings.swift b/Scripts/LintLocalizableStrings.swift index 95172b1d9b7..5b69e371ace 100755 --- a/Scripts/LintLocalizableStrings.swift +++ b/Scripts/LintLocalizableStrings.swift @@ -62,7 +62,7 @@ extension ProjectState { "_SharedTestUtilities/", // Exclude shared test directory "external/" // External dependencies ] - static let excludedPhrases: Set = ["", " ", " ", ",", ", ", "null", "\"", "@[0-9a-fA-F]{66}", "^[0-9A-Fa-f]+$", "/"] + static let excludedPhrases: Set = [ "", " ", " ", ",", ", ", "null", "\"", "@[0-9a-fA-F]{66}", "^[0-9A-Fa-f]+$", "/" ] static let excludedUnlocalizedStringLineMatching: [MatchType] = [ .prefix("#import", caseSensitive: false), .prefix("@available(", caseSensitive: false), @@ -72,9 +72,13 @@ extension ProjectState { .contains("precondition(", caseSensitive: false), .contains("preconditionFailure(", caseSensitive: false), .contains("logMessage:", caseSensitive: false), + .contains(".logging(", caseSensitive: false), .contains("owsFailDebug(", caseSensitive: false), + .contains("error: .other(", caseSensitive: false), .contains("#imageLiteral(resourceName:", caseSensitive: false), .contains("[UIImage imageNamed:", caseSensitive: false), + .contains("Image(", caseSensitive: false), + .contains("logo:", caseSensitive: false), .contains("UIFont(name:", caseSensitive: false), .contains(".dateFormat =", caseSensitive: false), .contains("accessibilityLabel =", caseSensitive: false), @@ -84,6 +88,8 @@ extension ProjectState { .contains("accessibilityLabel:", caseSensitive: false), .contains("Accessibility(identifier:", caseSensitive: false), .contains("Accessibility(label:", caseSensitive: false), + .contains(".withAccessibility(identifier:", caseSensitive: false), + .contains(".withAccessibility(label:", caseSensitive: false), .contains("NSAttributedString.Key(", caseSensitive: false), .contains("Notification.Name(", caseSensitive: false), .contains("Notification.Key(", caseSensitive: false), @@ -112,7 +118,17 @@ extension ProjectState { .contains("payload[", caseSensitive: false), .contains(".infoDictionary?[", caseSensitive: false), .contains("accessibilityId:", caseSensitive: false), + .contains("SNUIKit.localizedString(for:", caseSensitive: false), + .and( + .contains("id:", caseSensitive: false), + .previousLine(numEarlier: 1, .regex(Regex.crypto)) + ), + .and( + .contains("identifier:", caseSensitive: false), + .previousLine(numEarlier: 1, .contains("Dependencies.create", caseSensitive: false)) + ), .belowLineContaining("PreviewProvider"), + .belowLineContaining("#Preview"), .regex(Regex.logging), .regex(Regex.errorCreation), .regex(Regex.databaseTableName), @@ -309,6 +325,8 @@ enum Regex { static let imageInitialization = #/(?:UI)?Image\((?:named:)?(?:imageName:)?(?:systemName:)?.*\)/# static let variableToStringConversion = #/"\\(.*)"/# + static let crypto = #/Crypto.*\(/# + static let dynamicStringVariable = #/\{\w+\}/# /// Returns a list of strings that match regex pattern from content diff --git a/Session.xcodeproj/project.pbxproj b/Session.xcodeproj/project.pbxproj index 237522c9cf8..2854a0040e9 100644 --- a/Session.xcodeproj/project.pbxproj +++ b/Session.xcodeproj/project.pbxproj @@ -26,18 +26,12 @@ 34CF0788203E6B78005C4D61 /* ringback_tone_ansi.caf in Resources */ = {isa = PBXBuildFile; fileRef = 34CF0784203E6B77005C4D61 /* ringback_tone_ansi.caf */; }; 34CF078A203E6B78005C4D61 /* end_call_tone_cept.caf in Resources */ = {isa = PBXBuildFile; fileRef = 34CF0786203E6B78005C4D61 /* end_call_tone_cept.caf */; }; 34D1F0501F7D45A60066283D /* GifPickerCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 34D1F04F1F7D45A60066283D /* GifPickerCell.swift */; }; - 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 */; }; 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 */; }; - 451A13B11E13DED2000A50FD /* AppNotifications.swift in Sources */ = {isa = PBXBuildFile; fileRef = 451A13B01E13DED2000A50FD /* AppNotifications.swift */; }; 4520D8D51D417D8E00123472 /* Photos.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 4520D8D41D417D8E00123472 /* Photos.framework */; }; 4521C3C01F59F3BA00B4C582 /* TextFieldHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4521C3BF1F59F3BA00B4C582 /* TextFieldHelper.swift */; }; 4535186E1FC635DD00210559 /* MainInterface.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 4535186C1FC635DD00210559 /* MainInterface.storyboard */; }; 453518721FC635DD00210559 /* SessionShareExtension.appex in Embed Foundation Extensions */ = {isa = PBXBuildFile; fileRef = 453518681FC635DD00210559 /* SessionShareExtension.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; }; - 4539B5861F79348F007141FF /* PushRegistrationManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4539B5851F79348F007141FF /* PushRegistrationManager.swift */; }; 4542DF54208D40AC007B4E76 /* LoadingViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4542DF53208D40AC007B4E76 /* LoadingViewController.swift */; }; 454A84042059C787008B8C75 /* MediaTileViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 454A84032059C787008B8C75 /* MediaTileViewController.swift */; }; 455A16DD1F1FEA0000F86704 /* Metal.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 455A16DB1F1FEA0000F86704 /* Metal.framework */; settings = {ATTRIBUTES = (Weak, ); }; }; @@ -73,7 +67,6 @@ 45C0DC1B1E68FE9000E04C47 /* UIApplication+OWS.swift in Sources */ = {isa = PBXBuildFile; fileRef = 45C0DC1A1E68FE9000E04C47 /* UIApplication+OWS.swift */; }; 45C0DC1E1E69011F00E04C47 /* UIStoryboard+OWS.swift in Sources */ = {isa = PBXBuildFile; fileRef = 45C0DC1D1E69011F00E04C47 /* UIStoryboard+OWS.swift */; }; 45CB2FA81CB7146C00E1B343 /* Launch Screen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 45CB2FA71CB7146C00E1B343 /* Launch Screen.storyboard */; }; - 45CD81EF1DC030E7004C9430 /* SyncPushTokensJob.swift in Sources */ = {isa = PBXBuildFile; fileRef = 45CD81EE1DC030E7004C9430 /* SyncPushTokensJob.swift */; }; 45F32C232057297A00A300D5 /* MediaPageViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 45F32C1D205718B000A300D5 /* MediaPageViewController.swift */; }; 4C090A1B210FD9C7001FD7F9 /* HapticFeedback.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C090A1A210FD9C7001FD7F9 /* HapticFeedback.swift */; }; 4C1885D2218F8E1C00B67051 /* PhotoGridViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C1885D1218F8E1C00B67051 /* PhotoGridViewCell.swift */; }; @@ -106,18 +99,15 @@ 7B4C75CD26BB92060000AC89 /* DeletedMessageView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B4C75CC26BB92060000AC89 /* DeletedMessageView.swift */; }; 7B4EF25A2934743000CB351D /* SessionTableViewTitleView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B4EF2592934743000CB351D /* SessionTableViewTitleView.swift */; }; 7B50D64D28AC7CF80086CCEC /* silence.aiff in Resources */ = {isa = PBXBuildFile; fileRef = 7B50D64C28AC7CF80086CCEC /* silence.aiff */; }; - 7B521E0A29BFF84400C3C36A /* GroupLeavingJob.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B521E0929BFF84400C3C36A /* GroupLeavingJob.swift */; }; 7B5233C42900E90F00F8F375 /* SessionLabelCarouselView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B5233C32900E90F00F8F375 /* SessionLabelCarouselView.swift */; }; 7B5233C6290636D700F8F375 /* _018_DisappearingMessagesConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B5233C5290636D700F8F375 /* _018_DisappearingMessagesConfiguration.swift */; }; 7B5802992AAEF1B50050EEB1 /* OpenGroupInvitationView_SwiftUI.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B5802982AAEF1B50050EEB1 /* OpenGroupInvitationView_SwiftUI.swift */; }; 7B7037432834B81F000DCF35 /* ReactionContainerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B7037422834B81F000DCF35 /* ReactionContainerView.swift */; }; 7B7037452834BCC0000DCF35 /* ReactionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B7037442834BCC0000DCF35 /* ReactionView.swift */; }; 7B71A98F2925E2A600E54854 /* SessionFooterView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B71A98E2925E2A600E54854 /* SessionFooterView.swift */; }; - 7B7AD41F2A5512CA00469FB1 /* GetExpirationJob.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B7AD41E2A5512CA00469FB1 /* GetExpirationJob.swift */; }; 7B7CB18E270D066F0079FF93 /* IncomingCallBanner.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B7CB18D270D066F0079FF93 /* IncomingCallBanner.swift */; }; 7B7CB190270FB2150079FF93 /* MiniCallView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B7CB18F270FB2150079FF93 /* MiniCallView.swift */; }; 7B7CB192271508AD0079FF93 /* CallRingTonePlayer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B7CB191271508AD0079FF93 /* CallRingTonePlayer.swift */; }; - 7B7E5B522A4D024C00A8208E /* ExpirationUpdateJob.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B7E5B512A4D024C00A8208E /* ExpirationUpdateJob.swift */; }; 7B81682328A4C1210069F315 /* UpdateTypes.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B81682228A4C1210069F315 /* UpdateTypes.swift */; }; 7B81682828B310D50069F315 /* _007_HomeQueryOptimisationIndexes.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B81682728B310D50069F315 /* _007_HomeQueryOptimisationIndexes.swift */; }; 7B81682A28B6F1420069F315 /* ReactionResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B81682928B6F1420069F315 /* ReactionResponse.swift */; }; @@ -149,14 +139,12 @@ 7BAF54D027ACCEEC003D12F8 /* EmptySearchResultCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7BAF54CD27ACCEEC003D12F8 /* EmptySearchResultCell.swift */; }; 7BAF54D327ACCF01003D12F8 /* ShareAppExtensionContext.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7BAF54D127ACCF01003D12F8 /* ShareAppExtensionContext.swift */; }; 7BAF54D427ACCF01003D12F8 /* SAEScreenLockViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7BAF54D227ACCF01003D12F8 /* SAEScreenLockViewController.swift */; }; - 7BAFA1192A39669400B76CB9 /* BezierPathView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7BAFA1182A39669400B76CB9 /* BezierPathView.swift */; }; 7BAFA75A2AAEA281001DA43E /* LinkPreviewView_SwiftUI.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7BAFA7592AAEA281001DA43E /* LinkPreviewView_SwiftUI.swift */; }; 7BBBDC44286EAD2D00747E59 /* TappableLabel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7BBBDC43286EAD2D00747E59 /* TappableLabel.swift */; }; 7BBBDC462875600700747E59 /* DocumentTitleViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7BBBDC452875600700747E59 /* DocumentTitleViewController.swift */; }; 7BC01A3E241F40AB00BC7C55 /* NotificationServiceExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7BC01A3D241F40AB00BC7C55 /* NotificationServiceExtension.swift */; }; 7BC01A42241F40AB00BC7C55 /* SessionNotificationServiceExtension.appex in Embed Foundation Extensions */ = {isa = PBXBuildFile; fileRef = 7BC01A3B241F40AB00BC7C55 /* SessionNotificationServiceExtension.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; }; 7BC707F227290ACB002817AD /* SessionCallManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7BC707F127290ACB002817AD /* SessionCallManager.swift */; }; - 7BD477A827EC39F5004E2822 /* Atomic.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7BD477A727EC39F5004E2822 /* Atomic.swift */; }; 7BD687D12A5D0D1200D8E455 /* MessageInfoScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7BD687D02A5D0D1200D8E455 /* MessageInfoScreen.swift */; }; 7BD976972A776C76001B466F /* SessionCarouselView+SwiftUI.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7BE2701D2A64C11500CEB71A /* SessionCarouselView+SwiftUI.swift */; }; 7BDCFC08242186E700641C39 /* NotificationServiceExtensionContext.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7BDCFC07242186E700641C39 /* NotificationServiceExtensionContext.swift */; }; @@ -193,7 +181,6 @@ 9473386E2BDF5F3E00B9E169 /* InfoPlist.xcstrings in Resources */ = {isa = PBXBuildFile; fileRef = 9473386D2BDF5F3E00B9E169 /* InfoPlist.xcstrings */; }; 947AD6902C8968FF000B2730 /* Constants.swift in Sources */ = {isa = PBXBuildFile; fileRef = 947AD68F2C8968FF000B2730 /* Constants.swift */; }; 94B3DC172AF8592200C88531 /* QuoteView_SwiftUI.swift in Sources */ = {isa = PBXBuildFile; fileRef = 94B3DC162AF8592200C88531 /* QuoteView_SwiftUI.swift */; }; - 94C5DCB02BE88170003AA8C5 /* BezierPathView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 94C5DCAF2BE88170003AA8C5 /* BezierPathView.swift */; }; 94E9BC0D2C7BFBDA006984EA /* Localization+Style.swift in Sources */ = {isa = PBXBuildFile; fileRef = 94E9BC0C2C7BFBDA006984EA /* Localization+Style.swift */; }; A11CD70D17FA230600A2D1B1 /* QuartzCore.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = A11CD70C17FA230600A2D1B1 /* QuartzCore.framework */; }; A163E8AB16F3F6AA0094D68B /* Security.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = A163E8AA16F3F6A90094D68B /* Security.framework */; }; @@ -228,7 +215,6 @@ B877E24626CA13BA0007970A /* CallVC+Camera.swift in Sources */ = {isa = PBXBuildFile; fileRef = B877E24526CA13BA0007970A /* CallVC+Camera.swift */; }; B8783E9E23EB948D00404FB8 /* UILabel+Interaction.swift in Sources */ = {isa = PBXBuildFile; fileRef = B8783E9D23EB948D00404FB8 /* UILabel+Interaction.swift */; }; B879D449247E1BE300DB3608 /* PathVC.swift in Sources */ = {isa = PBXBuildFile; fileRef = B879D448247E1BE300DB3608 /* PathVC.swift */; }; - B87EF18126377A1D00124B3C /* Features.swift in Sources */ = {isa = PBXBuildFile; fileRef = B87EF18026377A1D00124B3C /* Features.swift */; }; B8856CF7256F105E001CE70E /* OWSAudioPlayer.h in Headers */ = {isa = PBXBuildFile; fileRef = C38EF2F5255B6DBC007E1867 /* OWSAudioPlayer.h */; settings = {ATTRIBUTES = (Public, ); }; }; B8856D08256F10F1001CE70E /* DeviceSleepManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = C38EF309255B6DBE007E1867 /* DeviceSleepManager.swift */; }; B8856D11256F112A001CE70E /* OWSAudioSession.swift in Sources */ = {isa = PBXBuildFile; fileRef = C38EF281255B6D84007E1867 /* OWSAudioSession.swift */; }; @@ -236,16 +222,16 @@ B8856D72256F1421001CE70E /* OWSWindowManager.h in Headers */ = {isa = PBXBuildFile; fileRef = C38EF2FB255B6DBD007E1867 /* OWSWindowManager.h */; settings = {ATTRIBUTES = (Public, ); }; }; B8856DE6256F15F2001CE70E /* String+SSK.swift in Sources */ = {isa = PBXBuildFile; fileRef = C33FDB3F255A580C00E217F9 /* String+SSK.swift */; }; B8856E09256F1676001CE70E /* UIDevice+featureSupport.swift in Sources */ = {isa = PBXBuildFile; fileRef = C38EF237255B6D65007E1867 /* UIDevice+featureSupport.swift */; }; + B886B4A72398B23E00211ABE /* (null) in Sources */ = {isa = PBXBuildFile; }; B886B4A92398BA1500211ABE /* QRCode.swift in Sources */ = {isa = PBXBuildFile; fileRef = B886B4A82398BA1500211ABE /* QRCode.swift */; }; B88FA7F2260C3EB10049422F /* OpenGroupSuggestionGrid.swift in Sources */ = {isa = PBXBuildFile; fileRef = B88FA7F1260C3EB10049422F /* OpenGroupSuggestionGrid.swift */; }; - B88FA7FB26114EA70049422F /* Hex.swift in Sources */ = {isa = PBXBuildFile; fileRef = B88FA7FA26114EA70049422F /* Hex.swift */; }; B893063F2383961A005EAA8E /* ScanQRCodeWrapperVC.swift in Sources */ = {isa = PBXBuildFile; fileRef = B893063E2383961A005EAA8E /* ScanQRCodeWrapperVC.swift */; }; B894D0752339EDCF00B4D94D /* NukeDataModal.swift in Sources */ = {isa = PBXBuildFile; fileRef = B894D0742339EDCF00B4D94D /* NukeDataModal.swift */; }; B897621C25D201F7004F83B2 /* RoundIconButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = B897621B25D201F7004F83B2 /* RoundIconButton.swift */; }; B8B320B7258C30D70020074B /* HTMLMetadata.swift in Sources */ = {isa = PBXBuildFile; fileRef = B8B320B6258C30D70020074B /* HTMLMetadata.swift */; }; B8B558F126C4BB0600693325 /* CameraManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = B8B558F026C4BB0600693325 /* CameraManager.swift */; }; B8BB82A5238F627000BA5194 /* HomeVC.swift in Sources */ = {isa = PBXBuildFile; fileRef = B8BB82A4238F627000BA5194 /* HomeVC.swift */; }; - B8BC00C0257D90E30032E807 /* General.swift in Sources */ = {isa = PBXBuildFile; fileRef = B8BC00BF257D90E30032E807 /* General.swift */; }; + B8BC00C0257D90E30032E807 /* (null) in Sources */ = {isa = PBXBuildFile; }; B8C2B2C82563685C00551B4D /* CircleView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B8C2B2C72563685C00551B4D /* CircleView.swift */; }; B8CCF6352396005F0091D419 /* SpaceMono-Regular.ttf in Resources */ = {isa = PBXBuildFile; fileRef = B8CCF6342396005F0091D419 /* SpaceMono-Regular.ttf */; }; B8CCF63F23975CFB0091D419 /* JoinOpenGroupVC.swift in Sources */ = {isa = PBXBuildFile; fileRef = B8CCF63E23975CFB0091D419 /* JoinOpenGroupVC.swift */; }; @@ -268,14 +254,12 @@ B8F5F71A25F1B35C003BF8D4 /* MediaPlaceholderView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B8F5F71925F1B35C003BF8D4 /* MediaPlaceholderView.swift */; }; B8FF8DAE25C0D00F004D1F22 /* SessionMessagingKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = C3C2A6F025539DE700C340D1 /* SessionMessagingKit.framework */; }; B8FF8DAF25C0D00F004D1F22 /* SessionUtilitiesKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = C3C2A679255388CC00C340D1 /* SessionUtilitiesKit.framework */; }; - B8FF8EA625C11FEF004D1F22 /* IPv4.swift in Sources */ = {isa = PBXBuildFile; fileRef = B8FF8EA525C11FEF004D1F22 /* IPv4.swift */; }; B9EB5ABD1884C002007CBB57 /* MessageUI.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = B9EB5ABC1884C002007CBB57 /* MessageUI.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 */; }; C302093E25DCBF08001F572D /* MentionSelectionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C302093D25DCBF07001F572D /* MentionSelectionView.swift */; }; C31A6C5C247F2CF3001123EF /* CGRect+Utilities.swift in Sources */ = {isa = PBXBuildFile; fileRef = C31A6C5B247F2CF3001123EF /* CGRect+Utilities.swift */; }; - C31D1DE32521718E005D4DA8 /* UserSelectionVC.swift in Sources */ = {isa = PBXBuildFile; fileRef = C31D1DE22521718E005D4DA8 /* UserSelectionVC.swift */; }; C32824D325C9F9790062D0A7 /* SessionSnodeKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = C3C2A59F255385C100C340D1 /* SessionSnodeKit.framework */; }; C328250F25CA06020062D0A7 /* VoiceMessageView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C328250E25CA06020062D0A7 /* VoiceMessageView.swift */; }; C328251F25CA3A900062D0A7 /* QuoteView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C328251E25CA3A900062D0A7 /* QuoteView.swift */; }; @@ -285,23 +269,15 @@ C328255225CA64470062D0A7 /* ContextMenuVC+ActionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C328255125CA64470062D0A7 /* ContextMenuVC+ActionView.swift */; }; C32C598A256D0664003C73A2 /* SNProtoEnvelope+Conversion.swift in Sources */ = {isa = PBXBuildFile; fileRef = C38EEF09255B49A8007E1867 /* SNProtoEnvelope+Conversion.swift */; }; C32C599E256DB02B003C73A2 /* TypingIndicators.swift in Sources */ = {isa = PBXBuildFile; fileRef = C33FDA87255A57FC00E217F9 /* TypingIndicators.swift */; }; - C32C5A24256DB7DB003C73A2 /* SNUserDefaults.swift in Sources */ = {isa = PBXBuildFile; fileRef = C33FDB6B255A580F00E217F9 /* SNUserDefaults.swift */; }; - C32C5A48256DB8F0003C73A2 /* BuildConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = C33FDAA8255A57FF00E217F9 /* BuildConfiguration.swift */; }; - C32C5A88256DBCF9003C73A2 /* MessageReceiver+ClosedGroups.swift in Sources */ = {isa = PBXBuildFile; fileRef = C32C5A87256DBCF9003C73A2 /* MessageReceiver+ClosedGroups.swift */; }; - C32C5DBF256DD743003C73A2 /* ClosedGroupPoller.swift in Sources */ = {isa = PBXBuildFile; fileRef = C33FDB34255A580B00E217F9 /* ClosedGroupPoller.swift */; }; - C32C5DC9256DD935003C73A2 /* ProxiedContentDownloader.swift in Sources */ = {isa = PBXBuildFile; fileRef = C33FDAF2255A580500E217F9 /* ProxiedContentDownloader.swift */; }; + C32C5A88256DBCF9003C73A2 /* MessageReceiver+LegacyClosedGroups.swift in Sources */ = {isa = PBXBuildFile; fileRef = C32C5A87256DBCF9003C73A2 /* MessageReceiver+LegacyClosedGroups.swift */; }; C32C5DD2256DD9E5003C73A2 /* LRUCache.swift in Sources */ = {isa = PBXBuildFile; fileRef = C33FDAFD255A580600E217F9 /* LRUCache.swift */; }; - C32C5DDB256DD9FF003C73A2 /* ContentProxy.swift in Sources */ = {isa = PBXBuildFile; fileRef = C33FDB68255A580F00E217F9 /* ContentProxy.swift */; }; C32C5E0C256DDAFA003C73A2 /* NSRegularExpression+SSK.swift in Sources */ = {isa = PBXBuildFile; fileRef = C33FDA7A255A57FB00E217F9 /* NSRegularExpression+SSK.swift */; }; - C32C600F256E07F5003C73A2 /* NSUserDefaults+OWS.m in Sources */ = {isa = PBXBuildFile; fileRef = C33FDB77255A581000E217F9 /* NSUserDefaults+OWS.m */; }; - C32C6018256E07F9003C73A2 /* NSUserDefaults+OWS.h in Headers */ = {isa = PBXBuildFile; fileRef = C33FDB51255A580D00E217F9 /* NSUserDefaults+OWS.h */; settings = {ATTRIBUTES = (Public, ); }; }; C33100282559000A00070591 /* UIView+Utilities.swift in Sources */ = {isa = PBXBuildFile; fileRef = C33100272559000A00070591 /* UIView+Utilities.swift */; }; C331FF1F2558F9D300070591 /* SessionUIKit.h in Headers */ = {isa = PBXBuildFile; fileRef = C331FF1D2558F9D300070591 /* SessionUIKit.h */; settings = {ATTRIBUTES = (Public, ); }; }; C331FF222558F9D300070591 /* SessionUIKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = C331FF1B2558F9D300070591 /* SessionUIKit.framework */; }; C331FF232558F9D300070591 /* SessionUIKit.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = C331FF1B2558F9D300070591 /* SessionUIKit.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; C331FF972558FA6B00070591 /* Fonts.swift in Sources */ = {isa = PBXBuildFile; fileRef = B8BB82BD2394D4CE00BA5194 /* Fonts.swift */; }; C331FF9A2558FA6B00070591 /* Values.swift in Sources */ = {isa = PBXBuildFile; fileRef = B8BB82A1238F356100BA5194 /* Values.swift */; }; - C331FFB82558FA8D00070591 /* DeviceUtilities.swift in Sources */ = {isa = PBXBuildFile; fileRef = B8544E3023D16CA500299F14 /* DeviceUtilities.swift */; }; C331FFB92558FA8D00070591 /* UIView+Constraints.swift in Sources */ = {isa = PBXBuildFile; fileRef = B885D5F52334A32100EE0D8E /* UIView+Constraints.swift */; }; C331FFE02558FB0000070591 /* SearchBar.swift in Sources */ = {isa = PBXBuildFile; fileRef = B8BB82B02390C37000BA5194 /* SearchBar.swift */; }; C331FFE32558FB0000070591 /* TabBar.swift in Sources */ = {isa = PBXBuildFile; fileRef = B8CCF638239721E20091D419 /* TabBar.swift */; }; @@ -311,24 +287,15 @@ C331FFE92558FB0000070591 /* Separator.swift in Sources */ = {isa = PBXBuildFile; fileRef = B8BB82B82394911B00BA5194 /* Separator.swift */; }; C331FFF32558FF0300070591 /* PathStatusView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B879D44A247E1D9200DB3608 /* PathStatusView.swift */; }; C331FFFE2558FF3B00070591 /* FullConversationCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = B8BB82AA238F669C00BA5194 /* FullConversationCell.swift */; }; - C33FD9AF255A548A00E217F9 /* SignalUtilitiesKit.h in Headers */ = {isa = PBXBuildFile; fileRef = C33FD9AD255A548A00E217F9 /* SignalUtilitiesKit.h */; settings = {ATTRIBUTES = (Public, ); }; }; C33FD9B3255A548A00E217F9 /* SignalUtilitiesKit.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = C33FD9AB255A548A00E217F9 /* SignalUtilitiesKit.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; C33FD9C2255A54EF00E217F9 /* SessionMessagingKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = C3C2A6F025539DE700C340D1 /* SessionMessagingKit.framework */; }; C33FD9C4255A54EF00E217F9 /* SessionSnodeKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = C3C2A59F255385C100C340D1 /* SessionSnodeKit.framework */; }; C33FD9C5255A54EF00E217F9 /* SessionUtilitiesKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = C3C2A679255388CC00C340D1 /* SessionUtilitiesKit.framework */; }; C33FDC58255A582000E217F9 /* ReverseDispatchQueue.swift in Sources */ = {isa = PBXBuildFile; fileRef = C33FDA9E255A57FF00E217F9 /* ReverseDispatchQueue.swift */; }; - C33FDC98255A582000E217F9 /* SwiftSingletons.swift in Sources */ = {isa = PBXBuildFile; fileRef = C33FDADE255A580400E217F9 /* SwiftSingletons.swift */; }; - C33FDD03255A582000E217F9 /* WeakTimer.swift in Sources */ = {isa = PBXBuildFile; fileRef = C33FDB49255A580C00E217F9 /* WeakTimer.swift */; }; - C33FDD23255A582000E217F9 /* FeatureFlags.swift in Sources */ = {isa = PBXBuildFile; fileRef = C33FDB69255A580F00E217F9 /* FeatureFlags.swift */; }; - C33FDD3A255A582000E217F9 /* Notification+Loki.swift in Sources */ = {isa = PBXBuildFile; fileRef = C33FDB80255A581100E217F9 /* Notification+Loki.swift */; }; - C33FDD49255A582000E217F9 /* ParamParser.swift in Sources */ = {isa = PBXBuildFile; fileRef = C33FDB8F255A581200E217F9 /* ParamParser.swift */; }; C33FDD8D255A582000E217F9 /* OWSSignalAddress.swift in Sources */ = {isa = PBXBuildFile; fileRef = C33FDBD3255A581800E217F9 /* OWSSignalAddress.swift */; }; C3402FE52559036600EA6424 /* SessionUIKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = C331FF1B2558F9D300070591 /* SessionUIKit.framework */; }; C34C8F7423A7830B00D82669 /* SpaceMono-Bold.ttf in Resources */ = {isa = PBXBuildFile; fileRef = C34C8F7323A7830A00D82669 /* SpaceMono-Bold.ttf */; }; - C352A2FF25574B6300338F3E /* MessageSendJob.swift in Sources */ = {isa = PBXBuildFile; fileRef = C352A2FE25574B6300338F3E /* MessageSendJob.swift */; }; - C352A35B2557824E00338F3E /* AttachmentUploadJob.swift in Sources */ = {isa = PBXBuildFile; fileRef = C352A35A2557824E00338F3E /* AttachmentUploadJob.swift */; }; - C352A36D2557858E00338F3E /* NSTimer+Proxying.m in Sources */ = {isa = PBXBuildFile; fileRef = C352A36C2557858D00338F3E /* NSTimer+Proxying.m */; }; - C352A3772557864000338F3E /* NSTimer+Proxying.h in Headers */ = {isa = PBXBuildFile; fileRef = C352A3762557859C00338F3E /* NSTimer+Proxying.h */; settings = {ATTRIBUTES = (Public, ); }; }; + C352A2FF25574B6300338F3E /* (null) in Sources */ = {isa = PBXBuildFile; }; C3548F0824456AB6009433A8 /* UIView+Wrapping.swift in Sources */ = {isa = PBXBuildFile; fileRef = C3548F0724456AB6009433A8 /* UIView+Wrapping.swift */; }; C354E75A23FE2A7600CE22E3 /* BaseVC.swift in Sources */ = {isa = PBXBuildFile; fileRef = C354E75923FE2A7600CE22E3 /* BaseVC.swift */; }; C35D0DB525AE5F1200B6BF49 /* UIEdgeInsets.swift in Sources */ = {isa = PBXBuildFile; fileRef = C35D0DB425AE5F1200B6BF49 /* UIEdgeInsets.swift */; }; @@ -338,17 +305,14 @@ C379DCF4256735770002D4EB /* VisibleMessage+Attachment.swift in Sources */ = {isa = PBXBuildFile; fileRef = C379DCF3256735770002D4EB /* VisibleMessage+Attachment.swift */; }; C37F5414255BAFA7002AEA92 /* SignalUtilitiesKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = C33FD9AB255A548A00E217F9 /* SignalUtilitiesKit.framework */; }; C37F54DC255BB84A002AEA92 /* SessionSnodeKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = C3C2A59F255385C100C340D1 /* SessionSnodeKit.framework */; }; - C38D5E8D2575011E00B6A65C /* MessageSender+ClosedGroups.swift in Sources */ = {isa = PBXBuildFile; fileRef = C38D5E8C2575011E00B6A65C /* MessageSender+ClosedGroups.swift */; }; + C38D5E8D2575011E00B6A65C /* MessageSender+LegacyClosedGroups.swift in Sources */ = {isa = PBXBuildFile; fileRef = C38D5E8C2575011E00B6A65C /* MessageSender+LegacyClosedGroups.swift */; }; C38EF00C255B61CC007E1867 /* SignalUtilitiesKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = C33FD9AB255A548A00E217F9 /* SignalUtilitiesKit.framework */; }; C38EF22B255B6D5D007E1867 /* ShareViewDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = C38EF226255B6D5D007E1867 /* ShareViewDelegate.swift */; }; C38EF24D255B6D67007E1867 /* UIView+OWS.swift in Sources */ = {isa = PBXBuildFile; fileRef = C38EF240255B6D67007E1867 /* UIView+OWS.swift */; }; C38EF24E255B6D67007E1867 /* Collection+OWS.swift in Sources */ = {isa = PBXBuildFile; fileRef = C38EF241255B6D67007E1867 /* Collection+OWS.swift */; }; C38EF2B3255B6D9C007E1867 /* UIViewController+Utilities.swift in Sources */ = {isa = PBXBuildFile; fileRef = C38EF2B1255B6D9C007E1867 /* UIViewController+Utilities.swift */; }; C38EF30C255B6DBF007E1867 /* ScreenLock.swift in Sources */ = {isa = PBXBuildFile; fileRef = C38EF2E2255B6DB9007E1867 /* ScreenLock.swift */; }; - C38EF31C255B6DBF007E1867 /* Searcher.swift in Sources */ = {isa = PBXBuildFile; fileRef = C38EF2F2255B6DBC007E1867 /* Searcher.swift */; }; - C38EF324255B6DBF007E1867 /* Bench.swift in Sources */ = {isa = PBXBuildFile; fileRef = C38EF2FA255B6DBD007E1867 /* Bench.swift */; }; C38EF32E255B6DBF007E1867 /* ImageCache.swift in Sources */ = {isa = PBXBuildFile; fileRef = C38EF304255B6DBE007E1867 /* ImageCache.swift */; }; - C38EF331255B6DBF007E1867 /* UIGestureRecognizer+OWS.swift in Sources */ = {isa = PBXBuildFile; fileRef = C38EF307255B6DBE007E1867 /* UIGestureRecognizer+OWS.swift */; }; C38EF363255B6DCC007E1867 /* ModalActivityIndicatorViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = C38EF349255B6DC7007E1867 /* ModalActivityIndicatorViewController.swift */; }; C38EF372255B6DCC007E1867 /* MediaMessageView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C38EF358255B6DCC007E1867 /* MediaMessageView.swift */; }; C38EF385255B6DD2007E1867 /* AttachmentTextToolbar.swift in Sources */ = {isa = PBXBuildFile; fileRef = C38EF37C255B6DCF007E1867 /* AttachmentTextToolbar.swift */; }; @@ -379,23 +343,19 @@ C38EF3FF255B6DF7007E1867 /* TappableView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C38EF3E1255B6DF3007E1867 /* TappableView.swift */; }; C38EF400255B6DF7007E1867 /* GalleryRailView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C38EF3E2255B6DF3007E1867 /* GalleryRailView.swift */; }; C38EF405255B6DF7007E1867 /* OWSButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = C38EF3E7255B6DF5007E1867 /* OWSButton.swift */; }; - C38EF407255B6DF7007E1867 /* Toast.swift in Sources */ = {isa = PBXBuildFile; fileRef = C38EF3E9255B6DF6007E1867 /* Toast.swift */; }; C38EF48A255B7E3F007E1867 /* SessionUIKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = C331FF1B2558F9D300070591 /* SessionUIKit.framework */; }; C3A71D0B2558989C0043A11F /* MessageWrapper.swift in Sources */ = {isa = PBXBuildFile; fileRef = C3A71D0A2558989C0043A11F /* MessageWrapper.swift */; }; C3A71D1E25589AC30043A11F /* WebSocketProto.swift in Sources */ = {isa = PBXBuildFile; fileRef = C3A71D1C25589AC30043A11F /* WebSocketProto.swift */; }; C3A71D1F25589AC30043A11F /* WebSocketResources.pb.swift in Sources */ = {isa = PBXBuildFile; fileRef = C3A71D1D25589AC30043A11F /* WebSocketResources.pb.swift */; }; - C3A71F892558BA9F0043A11F /* Mnemonic.swift in Sources */ = {isa = PBXBuildFile; fileRef = C3A71F882558BA9F0043A11F /* Mnemonic.swift */; }; C3AAFFF225AE99710089E6DD /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = C3AAFFF125AE99710089E6DD /* AppDelegate.swift */; }; C3ADC66126426688005F1414 /* ShareNavController.swift in Sources */ = {isa = PBXBuildFile; fileRef = C3ADC66026426688005F1414 /* ShareNavController.swift */; }; - C3BBE0A82554D4DE0050F1E3 /* JSON.swift in Sources */ = {isa = PBXBuildFile; fileRef = C3C2A5D92553860B00C340D1 /* JSON.swift */; }; C3BBE0AA2554D4DE0050F1E3 /* Dictionary+Utilities.swift in Sources */ = {isa = PBXBuildFile; fileRef = C3C2A5D52553860A00C340D1 /* Dictionary+Utilities.swift */; }; 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, ); }; }; C3C2A5C2255385EE00C340D1 /* Configuration.swift in Sources */ = {isa = PBXBuildFile; fileRef = C3C2A5B9255385ED00C340D1 /* Configuration.swift */; }; - C3C2A5E02553860B00C340D1 /* Threading.swift in Sources */ = {isa = PBXBuildFile; fileRef = C3C2A5D42553860A00C340D1 /* Threading.swift */; }; + C3C2A5E02553860B00C340D1 /* Threading+SSK.swift in Sources */ = {isa = PBXBuildFile; fileRef = C3C2A5D42553860A00C340D1 /* Threading+SSK.swift */; }; C3C2A5E42553860B00C340D1 /* Data+Utilities.swift in Sources */ = {isa = PBXBuildFile; fileRef = C3C2A5D82553860B00C340D1 /* Data+Utilities.swift */; }; - C3C2A67D255388CC00C340D1 /* SessionUtilitiesKit.h in Headers */ = {isa = PBXBuildFile; fileRef = C3C2A67B255388CC00C340D1 /* SessionUtilitiesKit.h */; settings = {ATTRIBUTES = (Public, ); }; }; C3C2A681255388CC00C340D1 /* SessionUtilitiesKit.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = C3C2A679255388CC00C340D1 /* SessionUtilitiesKit.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; C3C2A6C62553896A00C340D1 /* SessionUtilitiesKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = C3C2A679255388CC00C340D1 /* SessionUtilitiesKit.framework */; }; C3C2A6F425539DE700C340D1 /* SessionMessagingKit.h in Headers */ = {isa = PBXBuildFile; fileRef = C3C2A6F225539DE700C340D1 /* SessionMessagingKit.h */; settings = {ATTRIBUTES = (Public, ); }; }; @@ -405,7 +365,6 @@ C3C2A74D2553A39700C340D1 /* VisibleMessage.swift in Sources */ = {isa = PBXBuildFile; fileRef = C3C2A74C2553A39700C340D1 /* VisibleMessage.swift */; }; C3C2A75F2553A3C500C340D1 /* VisibleMessage+LinkPreview.swift in Sources */ = {isa = PBXBuildFile; fileRef = C3C2A75E2553A3C500C340D1 /* VisibleMessage+LinkPreview.swift */; }; C3C2A7852553AAF300C340D1 /* SessionProtos.pb.swift in Sources */ = {isa = PBXBuildFile; fileRef = C3C2A7832553AAF300C340D1 /* SessionProtos.pb.swift */; }; - C3C2AC2E2553CBEB00C340D1 /* String+Trimming.swift in Sources */ = {isa = PBXBuildFile; fileRef = C3C2AC2D2553CBEB00C340D1 /* String+Trimming.swift */; }; C3CA3AA2255CDADA00F4C6D4 /* english.txt in Resources */ = {isa = PBXBuildFile; fileRef = C3CA3AA1255CDADA00F4C6D4 /* english.txt */; }; C3CA3AB4255CDAE600F4C6D4 /* japanese.txt in Resources */ = {isa = PBXBuildFile; fileRef = C3CA3AB3255CDAE600F4C6D4 /* japanese.txt */; }; C3CA3ABE255CDB0D00F4C6D4 /* portuguese.txt in Resources */ = {isa = PBXBuildFile; fileRef = C3CA3ABD255CDB0D00F4C6D4 /* portuguese.txt */; }; @@ -413,9 +372,6 @@ C3D0972B2510499C00F6E3E4 /* BackgroundPoller.swift in Sources */ = {isa = PBXBuildFile; fileRef = C3D0972A2510499C00F6E3E4 /* BackgroundPoller.swift */; }; C3D90A5C25773A25002C9DF5 /* SessionUtilitiesKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = C3C2A679255388CC00C340D1 /* SessionUtilitiesKit.framework */; }; C3DAB3242480CB2B00725F25 /* SRCopyableLabel.swift in Sources */ = {isa = PBXBuildFile; fileRef = C3DAB3232480CB2A00725F25 /* SRCopyableLabel.swift */; }; - 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 */; }; 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 */; }; @@ -425,12 +381,34 @@ D2AEACDC16C426DA00C364C0 /* CFNetwork.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D2AEACDB16C426DA00C364C0 /* CFNetwork.framework */; }; FC3BD9881A30A790005B96BB /* Social.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = FC3BD9871A30A790005B96BB /* Social.framework */; }; FCB11D8C1A129A76002F93FB /* CoreMedia.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = FCB11D8B1A129A76002F93FB /* CoreMedia.framework */; }; + FD0150262CA23D5B005B08A1 /* SessionUtilitiesKit.xctestplan in Resources */ = {isa = PBXBuildFile; fileRef = FD0150252CA23D5B005B08A1 /* SessionUtilitiesKit.xctestplan */; }; + FD0150282CA23DB7005B08A1 /* GRDBExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD0150272CA23DB7005B08A1 /* GRDBExtensions.swift */; }; + FD0150292CA23DB7005B08A1 /* GRDBExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD0150272CA23DB7005B08A1 /* GRDBExtensions.swift */; }; + FD01502A2CA23DB7005B08A1 /* GRDBExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD0150272CA23DB7005B08A1 /* GRDBExtensions.swift */; }; + FD01502C2CA23DB7005B08A1 /* GRDBExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD0150272CA23DB7005B08A1 /* GRDBExtensions.swift */; }; + FD0150382CA24328005B08A1 /* MockJobRunner.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD0150372CA24328005B08A1 /* MockJobRunner.swift */; }; + FD0150392CA24328005B08A1 /* MockJobRunner.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD0150372CA24328005B08A1 /* MockJobRunner.swift */; }; + FD01503A2CA24328005B08A1 /* MockJobRunner.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD0150372CA24328005B08A1 /* MockJobRunner.swift */; }; + FD01503B2CA24328005B08A1 /* MockJobRunner.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD0150372CA24328005B08A1 /* MockJobRunner.swift */; }; + FD0150402CA2433D005B08A1 /* BencodeDecoderSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD01503D2CA2433D005B08A1 /* BencodeDecoderSpec.swift */; }; + FD0150412CA2433D005B08A1 /* BencodeEncoderSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD01503E2CA2433D005B08A1 /* BencodeEncoderSpec.swift */; }; + FD0150422CA2433D005B08A1 /* VersionSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD01503F2CA2433D005B08A1 /* VersionSpec.swift */; }; + FD0150452CA243BB005B08A1 /* LibSessionUtilSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD0150442CA243BB005B08A1 /* LibSessionUtilSpec.swift */; }; + FD0150462CA243BB005B08A1 /* LibSessionSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD0150432CA243BB005B08A1 /* LibSessionSpec.swift */; }; + FD0150482CA243CB005B08A1 /* Mock.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD0150472CA243CB005B08A1 /* Mock.swift */; }; + FD0150492CA243CB005B08A1 /* Mock.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD0150472CA243CB005B08A1 /* Mock.swift */; }; + FD01504B2CA243CB005B08A1 /* Mock.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD0150472CA243CB005B08A1 /* Mock.swift */; }; + FD01504C2CA243CB005B08A1 /* Mock.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD0150472CA243CB005B08A1 /* Mock.swift */; }; + FD01504E2CA243E7005B08A1 /* TypeConversionUtilitiesSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD01504D2CA243E7005B08A1 /* TypeConversionUtilitiesSpec.swift */; }; + FD0150502CA24468005B08A1 /* SessionSnodeKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = C3C2A59F255385C100C340D1 /* SessionSnodeKit.framework */; platformFilter = ios; }; + FD0150522CA2446D005B08A1 /* Quick in Frameworks */ = {isa = PBXBuildFile; productRef = FD0150512CA2446D005B08A1 /* Quick */; }; + FD0150542CA24471005B08A1 /* Nimble in Frameworks */ = {isa = PBXBuildFile; productRef = FD0150532CA24471005B08A1 /* Nimble */; }; FD0150582CA27DF3005B08A1 /* ScrollableLabel.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD0150572CA27DEE005B08A1 /* ScrollableLabel.swift */; }; + FD02CC122C367762009AB976 /* Request+PushNotificationAPI.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD02CC112C367761009AB976 /* Request+PushNotificationAPI.swift */; }; + FD02CC142C3677E6009AB976 /* Request+OpenGroupAPI.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD02CC132C3677E6009AB976 /* Request+OpenGroupAPI.swift */; }; + FD02CC162C3681EF009AB976 /* RevokeSubaccountResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD02CC152C3681EF009AB976 /* RevokeSubaccountResponse.swift */; }; FD0606BF2BC8C10200C3816E /* _005_AddJobUniqueHash.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD0606BE2BC8C10200C3816E /* _005_AddJobUniqueHash.swift */; }; FD0606C32BCE13ED00C3816E /* MessageRequestFooterView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD0606C22BCE13ED00C3816E /* MessageRequestFooterView.swift */; }; - FD078E4827E02561000769AF /* CommonMockedExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD078E4727E02561000769AF /* CommonMockedExtensions.swift */; }; - FD078E4927E02576000769AF /* CommonMockedExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD078E4727E02561000769AF /* CommonMockedExtensions.swift */; }; - FD078E4D27E17156000769AF /* MockOGMCache.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD078E4C27E17156000769AF /* MockOGMCache.swift */; }; FD078E5427E197CA000769AF /* OpenGroupManagerSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDC2909D27D85751005DAE71 /* OpenGroupManagerSpec.swift */; }; FD0969F92A69FFE700C5C365 /* Mocked.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD0969F82A69FFE700C5C365 /* Mocked.swift */; }; FD0969FA2A6A00B000C5C365 /* Mocked.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD0969F82A69FFE700C5C365 /* Mocked.swift */; }; @@ -439,9 +417,6 @@ FD09796E27FA6D0000936362 /* Contact.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD09796D27FA6D0000936362 /* Contact.swift */; }; FD09797027FA6FF300936362 /* Profile.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD09796F27FA6FF300936362 /* Profile.swift */; }; FD09797227FAA2F500936362 /* Optional+Utilities.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD09797127FAA2F500936362 /* Optional+Utilities.swift */; }; - FD09797527FAB64300936362 /* ProfileManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD09797327FAB3E200936362 /* ProfileManager.swift */; }; - FD09797727FAB7A600936362 /* Data+Image.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD09797627FAB7A600936362 /* Data+Image.swift */; }; - FD09797927FAB7E800936362 /* ImageFormat.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD09797827FAB7E800936362 /* ImageFormat.swift */; }; FD09797D27FBDB2000936362 /* Notification+Utilities.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD09797C27FBDB2000936362 /* Notification+Utilities.swift */; }; FD09798127FCFEE800936362 /* SessionThread.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD09798027FCFEE800936362 /* SessionThread.swift */; }; FD09798327FD1A1500936362 /* ClosedGroup.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD09798227FD1A1500936362 /* ClosedGroup.swift */; }; @@ -456,13 +431,22 @@ FD09B7E328865FDA00ED0B66 /* HighlightMentionBackgroundView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD09B7E228865FDA00ED0B66 /* HighlightMentionBackgroundView.swift */; }; FD09B7E5288670BB00ED0B66 /* _008_EmojiReacts.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD09B7E4288670BB00ED0B66 /* _008_EmojiReacts.swift */; }; FD09B7E7288670FD00ED0B66 /* Reaction.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD09B7E6288670FD00ED0B66 /* Reaction.swift */; }; - FD09C5E2282212B3000CE219 /* JobDependencies.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD09C5E1282212B3000CE219 /* JobDependencies.swift */; }; FD09C5E628260FF9000CE219 /* MediaGalleryViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD09C5E528260FF9000CE219 /* MediaGalleryViewModel.swift */; }; FD09C5E828264937000CE219 /* MediaDetailViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD09C5E728264937000CE219 /* MediaDetailViewController.swift */; }; FD09C5EA282A1BB2000CE219 /* ThreadTypingIndicator.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD09C5E9282A1BB2000CE219 /* ThreadTypingIndicator.swift */; }; FD09C5EC282B8F18000CE219 /* AttachmentError.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD09C5EB282B8F17000CE219 /* AttachmentError.swift */; }; FD0B77B029B69A65009169BA /* TopBannerController.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD0B77AF29B69A65009169BA /* TopBannerController.swift */; }; FD0B77B229B82B7A009169BA /* ArrayUtilitiesSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD0B77B129B82B7A009169BA /* ArrayUtilitiesSpec.swift */; }; + FD0E353C2AB9880B006A81F7 /* AppVersion.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD0E353A2AB98773006A81F7 /* AppVersion.swift */; }; + FD10AF0A2AF319EE007709E5 /* DeveloperSettingsViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD10AF092AF319EE007709E5 /* DeveloperSettingsViewModel.swift */; }; + FD10AF0C2AF32B9A007709E5 /* SessionListViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD10AF0B2AF32B9A007709E5 /* SessionListViewModel.swift */; }; + FD10AF122AF85D11007709E5 /* Feature+ServiceNetwork.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD10AF112AF85D11007709E5 /* Feature+ServiceNetwork.swift */; }; + FD11E2292CA4D12C001BAF58 /* YYImage in Frameworks */ = {isa = PBXBuildFile; productRef = FD6A395B2C2D10C700762359 /* YYImage */; }; + FD11E22A2CA4D12C001BAF58 /* YYImage in Frameworks */ = {isa = PBXBuildFile; productRef = FD6A394E2C2D060C00762359 /* YYImage */; }; + FD11E22B2CA4D12C001BAF58 /* YYImage in Frameworks */ = {isa = PBXBuildFile; productRef = FD6A395B2C2D10C700762359 /* YYImage */; }; + FD11E22C2CA4D12C001BAF58 /* YYImage in Frameworks */ = {isa = PBXBuildFile; productRef = FD6A39682C2D283A00762359 /* YYImage */; }; + FD11E22D2CA4D12C001BAF58 /* DifferenceKit in Frameworks */ = {isa = PBXBuildFile; productRef = FD2286782C38D4FF00BC06F7 /* DifferenceKit */; }; + FD11E22E2CA4D12C001BAF58 /* WebRTC in Frameworks */ = {isa = PBXBuildFile; productRef = FDEF57292C3CF50B00131302 /* WebRTC */; }; FD12A83D2AD63BCC00EEBA0D /* EditableState.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD12A83C2AD63BCC00EEBA0D /* EditableState.swift */; }; FD12A83F2AD63BDF00EEBA0D /* Navigatable.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD12A83E2AD63BDF00EEBA0D /* Navigatable.swift */; }; FD12A8412AD63BEA00EEBA0D /* NavigatableState.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD12A8402AD63BEA00EEBA0D /* NavigatableState.swift */; }; @@ -470,7 +454,6 @@ FD12A8452AD63C2200EEBA0D /* TableDataState.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD12A8442AD63C2200EEBA0D /* TableDataState.swift */; }; FD12A8472AD63C3400EEBA0D /* PagedObservationSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD12A8462AD63C3400EEBA0D /* PagedObservationSource.swift */; }; FD12A8492AD63C4700EEBA0D /* SessionNavItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD12A8482AD63C4700EEBA0D /* SessionNavItem.swift */; }; - FD12A84B2AD6458800EEBA0D /* DifferenceKit+Utilities.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD12A84A2AD6458800EEBA0D /* DifferenceKit+Utilities.swift */; }; FD16AB5B2A1DD7CA0083D849 /* PlaceholderIcon.swift in Sources */ = {isa = PBXBuildFile; fileRef = C38EF2A3255B6D93007E1867 /* PlaceholderIcon.swift */; }; FD16AB5F2A1DD98F0083D849 /* ProfilePictureView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C38EF2A4255B6D93007E1867 /* ProfilePictureView.swift */; }; FD16AB612A1DD9B60083D849 /* ProfilePictureView+Convenience.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD16AB602A1DD9B60083D849 /* ProfilePictureView+Convenience.swift */; }; @@ -482,7 +465,6 @@ FD17D7AE27F41C4300122BE0 /* SnodeReceivedMessageInfo.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD17D7AD27F41C4300122BE0 /* SnodeReceivedMessageInfo.swift */; }; FD17D7B827F51ECA00122BE0 /* Migration.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD17D7B727F51ECA00122BE0 /* Migration.swift */; }; FD17D7BA27F51F2100122BE0 /* TargetMigrations.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD17D7B927F51F2100122BE0 /* TargetMigrations.swift */; }; - FD17D7BF27F51F8200122BE0 /* ColumnExpressible.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD17D7BE27F51F8200122BE0 /* ColumnExpressible.swift */; }; FD17D7C127F5200100122BE0 /* TypedTableDefinition.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD17D7C027F5200100122BE0 /* TypedTableDefinition.swift */; }; FD17D7C327F5204C00122BE0 /* Database+Utilities.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD17D7C227F5204C00122BE0 /* Database+Utilities.swift */; }; FD17D7C527F5206300122BE0 /* ColumnDefinition+Utilities.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD17D7C427F5206300122BE0 /* ColumnDefinition+Utilities.swift */; }; @@ -491,38 +473,97 @@ FD17D7CD27F546FF00122BE0 /* Setting.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD17D7CC27F546FF00122BE0 /* Setting.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 */; }; + FD19363C2ACA3134004BCF0F /* ResponseInfo+SnodeAPI.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD19363B2ACA3134004BCF0F /* ResponseInfo+SnodeAPI.swift */; }; FD19363F2ACA66DE004BCF0F /* DatabaseSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD19363E2ACA66DE004BCF0F /* DatabaseSpec.swift */; }; - FD1936412ACA7BD8004BCF0F /* Result+Utilities.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD1936402ACA7BD8004BCF0F /* Result+Utilities.swift */; }; FD1A94FB2900D1C2000D73D3 /* PersistableRecord+Utilities.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD1A94FA2900D1C2000D73D3 /* PersistableRecord+Utilities.swift */; }; FD1A94FE2900D2EA000D73D3 /* PersistableRecordUtilitiesSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD1A94FD2900D2EA000D73D3 /* PersistableRecordUtilitiesSpec.swift */; }; FD1C98E4282E3C5B00B76F9E /* UINavigationBar+Utilities.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD1C98E3282E3C5B00B76F9E /* UINavigationBar+Utilities.swift */; }; - FD1D732A2A85AA2000E3F410 /* Setting+Utilities.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD1D73292A85AA2000E3F410 /* Setting+Utilities.swift */; }; FD1D732E2A86114600E3F410 /* _015_BlockCommunityMessageRequests.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD1D732D2A86114600E3F410 /* _015_BlockCommunityMessageRequests.swift */; }; FD1F9C9F2A862BE60050F671 /* MigrationRequirement.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD1F9C9E2A862BE60050F671 /* MigrationRequirement.swift */; }; - FD22724B2C326E75004D8A6C /* CustomArgSummaryDescribable+SMK.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD22724A2C326E75004D8A6C /* CustomArgSummaryDescribable+SMK.swift */; }; - FD22724F2C327BCA004D8A6C /* SSKMockedExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD22724D2C327BA5004D8A6C /* SSKMockedExtensions.swift */; }; + FD22726B2C32911C004D8A6C /* SendReadReceiptsJob.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD2272532C32911A004D8A6C /* SendReadReceiptsJob.swift */; }; + FD22726C2C32911C004D8A6C /* GroupLeavingJob.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD2272542C32911A004D8A6C /* GroupLeavingJob.swift */; }; + FD22726D2C32911C004D8A6C /* CheckForAppUpdatesJob.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD2272552C32911A004D8A6C /* CheckForAppUpdatesJob.swift */; }; + FD22726E2C32911C004D8A6C /* FailedMessageSendsJob.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD2272562C32911A004D8A6C /* FailedMessageSendsJob.swift */; }; + FD22726F2C32911C004D8A6C /* ProcessPendingGroupMemberRemovalsJob.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD2272572C32911A004D8A6C /* ProcessPendingGroupMemberRemovalsJob.swift */; }; + FD2272702C32911C004D8A6C /* DisappearingMessagesJob.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD2272582C32911A004D8A6C /* DisappearingMessagesJob.swift */; }; + FD2272712C32911C004D8A6C /* MessageReceiveJob.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD2272592C32911A004D8A6C /* MessageReceiveJob.swift */; }; + FD2272722C32911C004D8A6C /* FailedAttachmentDownloadsJob.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD22725A2C32911A004D8A6C /* FailedAttachmentDownloadsJob.swift */; }; + FD2272732C32911C004D8A6C /* ConfigurationSyncJob.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD22725B2C32911B004D8A6C /* ConfigurationSyncJob.swift */; }; + FD2272742C32911C004D8A6C /* ConfigMessageReceiveJob.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD22725C2C32911B004D8A6C /* ConfigMessageReceiveJob.swift */; }; + FD2272752C32911C004D8A6C /* RetrieveDefaultOpenGroupRoomsJob.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD22725D2C32911B004D8A6C /* RetrieveDefaultOpenGroupRoomsJob.swift */; }; + FD2272762C32911C004D8A6C /* ExpirationUpdateJob.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD22725E2C32911B004D8A6C /* ExpirationUpdateJob.swift */; }; + FD2272772C32911C004D8A6C /* AttachmentUploadJob.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD22725F2C32911B004D8A6C /* AttachmentUploadJob.swift */; }; + FD2272782C32911C004D8A6C /* AttachmentDownloadJob.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD2272602C32911B004D8A6C /* AttachmentDownloadJob.swift */; }; + FD2272792C32911C004D8A6C /* DisplayPictureDownloadJob.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD2272612C32911B004D8A6C /* DisplayPictureDownloadJob.swift */; }; + FD22727A2C32911C004D8A6C /* GroupInviteMemberJob.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD2272622C32911B004D8A6C /* GroupInviteMemberJob.swift */; }; + FD22727B2C32911C004D8A6C /* MessageSendJob.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD2272632C32911B004D8A6C /* MessageSendJob.swift */; }; + FD22727C2C32911C004D8A6C /* GroupPromoteMemberJob.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD2272642C32911B004D8A6C /* GroupPromoteMemberJob.swift */; }; + FD22727D2C32911C004D8A6C /* NotifyPushServerJob.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD2272652C32911B004D8A6C /* NotifyPushServerJob.swift */; }; + FD22727E2C32911C004D8A6C /* GarbageCollectionJob.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD2272662C32911B004D8A6C /* GarbageCollectionJob.swift */; }; + FD22727F2C32911C004D8A6C /* GetExpirationJob.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD2272672C32911B004D8A6C /* GetExpirationJob.swift */; }; + FD2272812C32911C004D8A6C /* UpdateProfilePictureJob.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD22726A2C32911C004D8A6C /* UpdateProfilePictureJob.swift */; }; + FD2272832C337830004D8A6C /* GroupPoller.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD2272822C337830004D8A6C /* GroupPoller.swift */; }; + FD2272A92C33E337004D8A6C /* ContentProxy.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD2272952C33E335004D8A6C /* ContentProxy.swift */; }; + FD2272AA2C33E337004D8A6C /* UpdatableTimestamp.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD2272962C33E335004D8A6C /* UpdatableTimestamp.swift */; }; + FD2272AB2C33E337004D8A6C /* ValidatableResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD2272972C33E335004D8A6C /* ValidatableResponse.swift */; }; + FD2272AC2C33E337004D8A6C /* IPv4.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD2272982C33E336004D8A6C /* IPv4.swift */; }; + FD2272AD2C33E337004D8A6C /* Network.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD2272992C33E336004D8A6C /* Network.swift */; }; + FD2272AE2C33E337004D8A6C /* HTTPQueryParam.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD22729A2C33E336004D8A6C /* HTTPQueryParam.swift */; }; + FD2272AF2C33E337004D8A6C /* JSON.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD22729B2C33E336004D8A6C /* JSON.swift */; }; + FD2272B02C33E337004D8A6C /* NetworkError.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD22729C2C33E336004D8A6C /* NetworkError.swift */; }; + FD2272B12C33E337004D8A6C /* ProxiedContentDownloader.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD22729D2C33E336004D8A6C /* ProxiedContentDownloader.swift */; }; + FD2272B22C33E337004D8A6C /* PreparedRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD22729E2C33E336004D8A6C /* PreparedRequest.swift */; }; + FD2272B32C33E337004D8A6C /* BatchRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD22729F2C33E336004D8A6C /* BatchRequest.swift */; }; + FD2272B42C33E337004D8A6C /* SwarmDrainBehaviour.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD2272A02C33E336004D8A6C /* SwarmDrainBehaviour.swift */; }; + FD2272B52C33E337004D8A6C /* BatchResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD2272A12C33E336004D8A6C /* BatchResponse.swift */; }; + FD2272B72C33E337004D8A6C /* Request.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD2272A32C33E337004D8A6C /* Request.swift */; }; + FD2272B92C33E337004D8A6C /* ResponseInfo.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD2272A52C33E337004D8A6C /* ResponseInfo.swift */; }; + FD2272BA2C33E337004D8A6C /* HTTPHeader.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD2272A62C33E337004D8A6C /* HTTPHeader.swift */; }; + FD2272BB2C33E337004D8A6C /* HTTPMethod.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD2272A72C33E337004D8A6C /* HTTPMethod.swift */; }; + FD2272BC2C33E337004D8A6C /* URLResponse+Utilities.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD2272A82C33E337004D8A6C /* URLResponse+Utilities.swift */; }; + FD2272BE2C34B710004D8A6C /* Publisher+Utilities.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD2272BD2C34B710004D8A6C /* Publisher+Utilities.swift */; }; + FD2272C42C34E9AA004D8A6C /* BencodeResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDD383702AFDD0E1001367F2 /* BencodeResponse.swift */; }; + FD2272C72C34EAF5004D8A6C /* ColumnExpressible.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD17D7BE27F51F8200122BE0 /* ColumnExpressible.swift */; }; + FD2272C82C34EB0A004D8A6C /* Job.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDF0B73F280402C4004C14C5 /* Job.swift */; }; + FD2272D02C34EBD0004D8A6C /* FileManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD3FAB682AF1ADCA00DC5421 /* FileManager.swift */; }; + FD2272D12C34EBD6004D8A6C /* JSONDecoder+Utilities.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD47E0AE2AA692F400A55E41 /* JSONDecoder+Utilities.swift */; }; + FD2272D42C34ECE1004D8A6C /* BencodeEncoder.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD2272D32C34ECE1004D8A6C /* BencodeEncoder.swift */; }; + FD2272D62C34ED6A004D8A6C /* RetryWithDependencies.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD83DCDC2A739D350065FFAE /* RetryWithDependencies.swift */; }; + FD2272D82C34EDE7004D8A6C /* SnodeAPIEndpoint.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD2272D72C34EDE6004D8A6C /* SnodeAPIEndpoint.swift */; }; + FD2272D92C34EED6004D8A6C /* _001_ThemePreferences.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD37E9F828A5F14A003AE748 /* _001_ThemePreferences.swift */; }; + FD2272DD2C34EFFA004D8A6C /* AppSetup.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD2272DC2C34EFFA004D8A6C /* AppSetup.swift */; }; + FD2272DE2C34F11F004D8A6C /* _001_ThemePreferences.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD37E9F828A5F14A003AE748 /* _001_ThemePreferences.swift */; }; + FD2272E02C3502BE004D8A6C /* Setting+Theme.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD2272DF2C3502BE004D8A6C /* Setting+Theme.swift */; }; + FD2272E42C35134B004D8A6C /* Data+Utilities.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD2272E32C35134B004D8A6C /* Data+Utilities.swift */; }; + FD2272E62C351378004D8A6C /* SUIKImageFormat.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD2272E52C351378004D8A6C /* SUIKImageFormat.swift */; }; + FD2272EA2C351CA7004D8A6C /* Threading.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD2272E92C351CA7004D8A6C /* Threading.swift */; }; + FD2272EC2C352155004D8A6C /* Feature.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD2272EB2C352155004D8A6C /* Feature.swift */; }; + FD2272EE2C3521D6004D8A6C /* FeatureConfig.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD2272ED2C3521D6004D8A6C /* FeatureConfig.swift */; }; + FD2272F02C352200004D8A6C /* General.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD2272EF2C352200004D8A6C /* General.swift */; }; + FD2272FA2C352D8E004D8A6C /* LibSession+SharedGroup.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD2272F12C352D8D004D8A6C /* LibSession+SharedGroup.swift */; }; + FD2272FB2C352D8E004D8A6C /* LibSession+UserGroups.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD2272F22C352D8D004D8A6C /* LibSession+UserGroups.swift */; }; + FD2272FC2C352D8E004D8A6C /* LibSession+Contacts.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD2272F32C352D8D004D8A6C /* LibSession+Contacts.swift */; }; + FD2272FD2C352D8E004D8A6C /* LibSession+ConvoInfoVolatile.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD2272F42C352D8D004D8A6C /* LibSession+ConvoInfoVolatile.swift */; }; + FD2272FE2C352D8E004D8A6C /* LibSession+GroupMembers.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD2272F52C352D8D004D8A6C /* LibSession+GroupMembers.swift */; }; + FD2272FF2C352D8E004D8A6C /* LibSession+UserProfile.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD2272F62C352D8D004D8A6C /* LibSession+UserProfile.swift */; }; + FD2273002C352D8E004D8A6C /* LibSession+GroupKeys.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD2272F72C352D8D004D8A6C /* LibSession+GroupKeys.swift */; }; + FD2273012C352D8E004D8A6C /* LibSession+Shared.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD2272F82C352D8E004D8A6C /* LibSession+Shared.swift */; }; + FD2273022C352D8E004D8A6C /* LibSession+GroupInfo.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD2272F92C352D8E004D8A6C /* LibSession+GroupInfo.swift */; }; + FD2273082C353109004D8A6C /* DisplayPictureManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD2273072C353109004D8A6C /* DisplayPictureManager.swift */; }; + FD2286682C37DA3B00BC06F7 /* LibSession+Networking.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD7F74692BAB8A6D006DDFD8 /* LibSession+Networking.swift */; }; + FD2286692C37DA5500BC06F7 /* PollerType.swift in Sources */ = {isa = PBXBuildFile; fileRef = C33FDB3A255A580B00E217F9 /* PollerType.swift */; }; FD22866F2C38D42300BC06F7 /* DifferenceKit in Frameworks */ = {isa = PBXBuildFile; productRef = FD22866E2C38D42300BC06F7 /* DifferenceKit */; }; FD2286712C38D43000BC06F7 /* DifferenceKit in Frameworks */ = {isa = PBXBuildFile; productRef = FD2286702C38D43000BC06F7 /* DifferenceKit */; }; FD2286732C38D43900BC06F7 /* DifferenceKit in Frameworks */ = {isa = PBXBuildFile; productRef = FD2286722C38D43900BC06F7 /* DifferenceKit */; }; FD2286752C38D4DD00BC06F7 /* DifferenceKit in Frameworks */ = {isa = PBXBuildFile; productRef = FD2286742C38D4DD00BC06F7 /* DifferenceKit */; }; - FD2286792C38D4FF00BC06F7 /* DifferenceKit in Frameworks */ = {isa = PBXBuildFile; productRef = FD2286782C38D4FF00BC06F7 /* DifferenceKit */; }; - FD23CE1B2A651E6D0000B97C /* Network.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD23CE1A2A651E6D0000B97C /* Network.swift */; }; - FD23CE242A675C440000B97C /* Crypto+SessionMessagingKit.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD23CE232A675C440000B97C /* Crypto+SessionMessagingKit.swift */; }; FD23CE262A676B5B0000B97C /* DependenciesSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD23CE252A676B5B0000B97C /* DependenciesSpec.swift */; }; FD23CE282A67755C0000B97C /* MockCrypto.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD23CE272A67755C0000B97C /* MockCrypto.swift */; }; FD23CE292A6775650000B97C /* MockCrypto.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD23CE272A67755C0000B97C /* MockCrypto.swift */; }; FD23CE2A2A6775660000B97C /* MockCrypto.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD23CE272A67755C0000B97C /* MockCrypto.swift */; }; - FD23CE2C2A678DF80000B97C /* MockCaches.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD23CE2B2A678DF80000B97C /* MockCaches.swift */; }; - FD23CE2D2A678E1E0000B97C /* MockCaches.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD23CE2B2A678DF80000B97C /* MockCaches.swift */; }; - FD23CE2E2A678E1E0000B97C /* MockCaches.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD23CE2B2A678DF80000B97C /* MockCaches.swift */; }; - FD23CE302A67B8820000B97C /* Caches.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD23CE2F2A67B8820000B97C /* Caches.swift */; }; FD23CE332A67C4D90000B97C /* MockNetwork.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD23CE312A67C38D0000B97C /* MockNetwork.swift */; }; FD23CE342A67C4D90000B97C /* MockNetwork.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD23CE312A67C38D0000B97C /* MockNetwork.swift */; }; - FD23CE352A67C4DA0000B97C /* MockNetwork.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD23CE312A67C38D0000B97C /* MockNetwork.swift */; }; - FD23EA5C28ED00F80058676E /* Mock.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDC290A527D860CE005DAE71 /* Mock.swift */; }; FD23EA5D28ED00FA0058676E /* TestConstants.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD83B9BD27CF2243005E1583 /* TestConstants.swift */; }; FD23EA5E28ED00FD0058676E /* NimbleExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDC290A727D9B46D005DAE71 /* NimbleExtensions.swift */; }; - FD23EA5F28ED00FF0058676E /* CommonMockedExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD078E4727E02561000769AF /* CommonMockedExtensions.swift */; }; FD23EA6128ED0B260058676E /* CombineExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD23EA6028ED0B260058676E /* CombineExtensions.swift */; }; FD23EA6228ED0B260058676E /* CombineExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD23EA6028ED0B260058676E /* CombineExtensions.swift */; }; FD23EA6328ED0B260058676E /* CombineExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD23EA6028ED0B260058676E /* CombineExtensions.swift */; }; @@ -533,38 +574,56 @@ FD245C54285065E000B966DD /* ThumbnailService.swift in Sources */ = {isa = PBXBuildFile; fileRef = C33FDAF1255A580500E217F9 /* ThumbnailService.swift */; }; FD245C55285065E500B966DD /* OpenGroupManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = C3DB66AB260ACA42001EFC55 /* OpenGroupManager.swift */; }; FD245C56285065EA00B966DD /* SNProto.swift in Sources */ = {isa = PBXBuildFile; fileRef = C3C2A7822553AAF200C340D1 /* SNProto.swift */; }; - FD245C57285065F100B966DD /* Poller.swift in Sources */ = {isa = PBXBuildFile; fileRef = C33FDB3A255A580B00E217F9 /* Poller.swift */; }; FD245C59285065FC00B966DD /* ControlMessage.swift in Sources */ = {isa = PBXBuildFile; fileRef = C3C2A7702553A41E00C340D1 /* ControlMessage.swift */; }; FD245C5A2850660100B966DD /* LinkPreviewDraft.swift in Sources */ = {isa = PBXBuildFile; fileRef = C33FDBA8255A581500E217F9 /* LinkPreviewDraft.swift */; }; FD245C5B2850660500B966DD /* ReadReceipt.swift in Sources */ = {isa = PBXBuildFile; fileRef = C300A5BC2554B00D00555489 /* ReadReceipt.swift */; }; FD245C5D2850660F00B966DD /* OWSAudioPlayer.m in Sources */ = {isa = PBXBuildFile; fileRef = C38EF2F7255B6DBC007E1867 /* OWSAudioPlayer.m */; }; FD245C5F2850662200B966DD /* OWSWindowManager.m in Sources */ = {isa = PBXBuildFile; fileRef = C38EF306255B6DBE007E1867 /* OWSWindowManager.m */; }; FD245C632850664600B966DD /* Configuration.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD245C612850664300B966DD /* Configuration.swift */; }; - FD245C642850664F00B966DD /* Threading.swift in Sources */ = {isa = PBXBuildFile; fileRef = C3ECBF7A257056B700EA7FCE /* Threading.swift */; }; FD245C652850665400B966DD /* ClosedGroupControlMessage.swift in Sources */ = {isa = PBXBuildFile; fileRef = C34A977325A3E34A00852C71 /* ClosedGroupControlMessage.swift */; }; FD245C662850665900B966DD /* OpenGroupAPI.swift in Sources */ = {isa = PBXBuildFile; fileRef = B88FA7B726045D100049422F /* OpenGroupAPI.swift */; }; - FD245C672850665E00B966DD /* AttachmentDownloadJob.swift in Sources */ = {isa = PBXBuildFile; fileRef = C352A348255781F400338F3E /* AttachmentDownloadJob.swift */; }; FD245C682850666300B966DD /* Message+Destination.swift in Sources */ = {isa = PBXBuildFile; fileRef = C352A30825574D8400338F3E /* Message+Destination.swift */; }; FD245C692850666800B966DD /* ExpirationTimerUpdate.swift in Sources */ = {isa = PBXBuildFile; fileRef = C300A5E62554B07300555489 /* ExpirationTimerUpdate.swift */; }; 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 */; }; FD29598D2A43BC0B00888A17 /* Version.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD29598C2A43BC0B00888A17 /* Version.swift */; }; - FD2959902A43BE5F00888A17 /* VersionSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD29598F2A43BE5F00888A17 /* VersionSpec.swift */; }; 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 */; }; FD2AAAF128ED57B500A49611 /* SynchronousStorage.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD2AAAEF28ED57B500A49611 /* SynchronousStorage.swift */; }; FD2AAAF228ED57B500A49611 /* SynchronousStorage.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD2AAAEF28ED57B500A49611 /* SynchronousStorage.swift */; }; - FD2B4AFD294688D000AB4848 /* LibSession+Contacts.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD2B4AFC294688D000AB4848 /* LibSession+Contacts.swift */; }; - FD2B4AFF2946C93200AB4848 /* ConfigurationSyncJob.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD2B4AFE2946C93200AB4848 /* ConfigurationSyncJob.swift */; }; FD2B4B042949887A00AB4848 /* QueryInterfaceRequest+Utilities.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD2B4B032949887A00AB4848 /* QueryInterfaceRequest+Utilities.swift */; }; - FD2DD58E2C6DBEBF0073D9BE /* SSKMockedExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD22724D2C327BA5004D8A6C /* SSKMockedExtensions.swift */; }; FD2DD5902C6DD13C0073D9BE /* DifferenceKit in Frameworks */ = {isa = PBXBuildFile; productRef = FD2DD58F2C6DD13C0073D9BE /* DifferenceKit */; }; - FD3003662A25D5B300B5A5FB /* ConfigMessageReceiveJob.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD3003652A25D5B300B5A5FB /* ConfigMessageReceiveJob.swift */; }; + FD336F602CAA28CF00C0B51B /* CommonSMKMockExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD336F562CAA28CF00C0B51B /* CommonSMKMockExtensions.swift */; }; + FD336F612CAA28CF00C0B51B /* MockNotificationsManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD336F5C2CAA28CF00C0B51B /* MockNotificationsManager.swift */; }; + FD336F622CAA28CF00C0B51B /* CustomArgSummaryDescribable+SMK.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD336F572CAA28CF00C0B51B /* CustomArgSummaryDescribable+SMK.swift */; }; + FD336F632CAA28CF00C0B51B /* MockOGMCache.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD336F5D2CAA28CF00C0B51B /* MockOGMCache.swift */; }; + FD336F642CAA28CF00C0B51B /* MockCommunityPollerCache.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD336F582CAA28CF00C0B51B /* MockCommunityPollerCache.swift */; }; + FD336F652CAA28CF00C0B51B /* MockDisplayPictureCache.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD336F592CAA28CF00C0B51B /* MockDisplayPictureCache.swift */; }; + FD336F662CAA28CF00C0B51B /* MockSwarmPoller.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD336F5F2CAA28CF00C0B51B /* MockSwarmPoller.swift */; }; + FD336F672CAA28CF00C0B51B /* MockGroupPollerCache.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD336F5A2CAA28CF00C0B51B /* MockGroupPollerCache.swift */; }; + FD336F682CAA28CF00C0B51B /* MockPoller.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD336F5E2CAA28CF00C0B51B /* MockPoller.swift */; }; + FD336F692CAA28CF00C0B51B /* MockLibSessionCache.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD336F5B2CAA28CF00C0B51B /* MockLibSessionCache.swift */; }; + FD336F6C2CAA29C600C0B51B /* CommunityPollerSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD336F6B2CAA29C200C0B51B /* CommunityPollerSpec.swift */; }; + FD336F6F2CAA37CB00C0B51B /* MockCommunityPoller.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD336F6E2CAA37CB00C0B51B /* MockCommunityPoller.swift */; }; + FD336F702CABB96C00C0B51B /* BatchRequestSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD01502D2CA24310005B08A1 /* BatchRequestSpec.swift */; }; + FD336F712CABB97800C0B51B /* DestinationSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD11E22F2CA4F498001BAF58 /* DestinationSpec.swift */; }; + FD336F722CABB97800C0B51B /* BatchResponseSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD01502E2CA24310005B08A1 /* BatchResponseSpec.swift */; }; + FD336F732CABB97800C0B51B /* HeaderSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD01502F2CA24310005B08A1 /* HeaderSpec.swift */; }; + FD336F742CABB97800C0B51B /* PreparedRequestSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD0150302CA24310005B08A1 /* PreparedRequestSpec.swift */; }; + FD336F752CABB97800C0B51B /* RequestSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD0150312CA24310005B08A1 /* RequestSpec.swift */; }; + FD336F762CABB97800C0B51B /* BencodeResponseSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDD383722AFDD6D7001367F2 /* BencodeResponseSpec.swift */; }; FD3559462CC1FF200088F2A9 /* _020_AddMissingWhisperFlag.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD3559452CC1FF140088F2A9 /* _020_AddMissingWhisperFlag.swift */; }; FD368A6829DE8F9C000DBF1E /* _012_AddFTSIfNeeded.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD368A6729DE8F9B000DBF1E /* _012_AddFTSIfNeeded.swift */; }; FD368A6A29DE9E30000DBF1E /* UIContextualAction+Utilities.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD368A6929DE9E30000DBF1E /* UIContextualAction+Utilities.swift */; }; + FD3765DF2AD8F03100DC1489 /* MockSnodeAPICache.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD3765DE2AD8F03100DC1489 /* MockSnodeAPICache.swift */; }; + FD3765E02AD8F05100DC1489 /* MockSnodeAPICache.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD3765DE2AD8F03100DC1489 /* MockSnodeAPICache.swift */; }; + FD3765E22AD8F53B00DC1489 /* CommonSSKMockExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD3765E12AD8F53B00DC1489 /* CommonSSKMockExtensions.swift */; }; + FD3765E32AD8F56200DC1489 /* CommonSSKMockExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD3765E12AD8F53B00DC1489 /* CommonSSKMockExtensions.swift */; }; + FD3765E72ADE1AA300DC1489 /* UnrevokeSubaccountRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD3765E62ADE1AA300DC1489 /* UnrevokeSubaccountRequest.swift */; }; + FD3765E92ADE1AAE00DC1489 /* UnrevokeSubaccountResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD3765E82ADE1AAE00DC1489 /* UnrevokeSubaccountResponse.swift */; }; + FD3765EA2ADE37B400DC1489 /* Authentication.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD47E0AA2AA68EEA00A55E41 /* Authentication.swift */; }; + FD3765F42ADE5A0800DC1489 /* AuthenticatedRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD3765F32ADE5A0800DC1489 /* AuthenticatedRequest.swift */; }; + FD3765F62ADE5BA500DC1489 /* ServiceInfo.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD3765F52ADE5BA500DC1489 /* ServiceInfo.swift */; }; FD37E9C328A1C6F3003AE748 /* ThemeManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD37E9C228A1C6F3003AE748 /* ThemeManager.swift */; }; FD37E9C628A1D4EC003AE748 /* Theme+ClassicDark.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD37E9C528A1D4EC003AE748 /* Theme+ClassicDark.swift */; }; FD37E9C828A1D73F003AE748 /* Theme+Colors.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD37E9C728A1D73F003AE748 /* Theme+Colors.swift */; }; @@ -578,9 +637,7 @@ FD37E9D928A230F2003AE748 /* TraitObservingWindow.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD37E9D828A230F2003AE748 /* TraitObservingWindow.swift */; }; FD37E9DB28A244E9003AE748 /* ThemePreviewView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD37E9DA28A244E9003AE748 /* ThemePreviewView.swift */; }; FD37E9DD28A384EB003AE748 /* PrimaryColorSelectionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD37E9DC28A384EB003AE748 /* PrimaryColorSelectionView.swift */; }; - FD37E9EF28A5ED70003AE748 /* SessionUtilitiesKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = C3C2A679255388CC00C340D1 /* SessionUtilitiesKit.framework */; platformFilter = ios; }; FD37E9F628A5F106003AE748 /* Configuration.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD37E9F528A5F106003AE748 /* Configuration.swift */; }; - FD37E9F928A5F14A003AE748 /* _001_ThemePreferences.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD37E9F828A5F14A003AE748 /* _001_ThemePreferences.swift */; }; FD37E9FF28A5F2CD003AE748 /* Configuration.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD37E9FE28A5F2CD003AE748 /* Configuration.swift */; }; FD37EA0128A60473003AE748 /* UIKit+Theme.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD37EA0028A60473003AE748 /* UIKit+Theme.swift */; }; FD37EA0328A9FDCC003AE748 /* HelpViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD37EA0228A9FDCC003AE748 /* HelpViewModel.swift */; }; @@ -595,21 +652,40 @@ FD39353628F7C3390084DADA /* _004_FlagMessageHashAsDeletedOrInvalid.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD39353528F7C3390084DADA /* _004_FlagMessageHashAsDeletedOrInvalid.swift */; }; FD3AABE928306BBD00E5099A /* ThreadPickerViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD3AABE828306BBD00E5099A /* ThreadPickerViewModel.swift */; }; FD3C906727E416AF00CD579F /* BlindedIdLookupSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD3C906627E416AF00CD579F /* BlindedIdLookupSpec.swift */; }; - FD3C906A27E417CE00CD579F /* CryptoSMKSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD3C906927E417CE00CD579F /* CryptoSMKSpec.swift */; }; FD3E0C84283B5835002A425C /* SessionThreadViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD3E0C83283B5835002A425C /* SessionThreadViewModel.swift */; }; + FD3FAB592ADF906300DC5421 /* Profile+CurrentUser.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD3FAB582ADF906300DC5421 /* Profile+CurrentUser.swift */; }; + FD3FAB5F2AE9BC2200DC5421 /* EditGroupViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD3FAB5E2AE9BC2200DC5421 /* EditGroupViewModel.swift */; }; + FD3FAB612AEA194E00DC5421 /* UserListViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD3FAB602AEA194E00DC5421 /* UserListViewModel.swift */; }; + FD3FAB632AEB9A1500DC5421 /* ToastController.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD3FAB622AEB9A1500DC5421 /* ToastController.swift */; }; + FD3FAB672AF0C47000DC5421 /* DisplayPictureDownloadJobSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD3FAB662AF0C47000DC5421 /* DisplayPictureDownloadJobSpec.swift */; }; + FD3FAB6C2AF1B28B00DC5421 /* MockFileManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD3FAB6A2AF1B27800DC5421 /* MockFileManager.swift */; }; + FD3FAB6D2AF1B28B00DC5421 /* MockFileManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD3FAB6A2AF1B27800DC5421 /* MockFileManager.swift */; }; + FD3FAB6E2AF1B28C00DC5421 /* MockFileManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD3FAB6A2AF1B27800DC5421 /* MockFileManager.swift */; }; FD428B192B4B576F006D0888 /* AppContext.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD428B182B4B576F006D0888 /* AppContext.swift */; }; FD428B1B2B4B6098006D0888 /* Notifications+Lifecycle.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD428B1A2B4B6098006D0888 /* Notifications+Lifecycle.swift */; }; - FD428B1D2B4B6FDC006D0888 /* UIApplicationState+Utilities.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD428B1C2B4B6FDC006D0888 /* UIApplicationState+Utilities.swift */; }; FD428B1F2B4B758B006D0888 /* AppReadiness.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD428B1E2B4B758B006D0888 /* AppReadiness.swift */; }; - FD428B212B4B75EA006D0888 /* Singleton.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD428B202B4B75EA006D0888 /* Singleton.swift */; }; FD428B232B4B9969006D0888 /* _017_RebuildFTSIfNeeded_2_4_5.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD428B222B4B9969006D0888 /* _017_RebuildFTSIfNeeded_2_4_5.swift */; }; FD42F9A8285064B800A0C77D /* PushNotificationAPI.swift in Sources */ = {isa = PBXBuildFile; fileRef = C33FDBDE255A581900E217F9 /* PushNotificationAPI.swift */; }; - FD4324302999F0BC008A0213 /* ValidatableResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD43242F2999F0BC008A0213 /* ValidatableResponse.swift */; }; - FD432432299C6933008A0213 /* _011_AddPendingReadReceipts.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD432431299C6933008A0213 /* _011_AddPendingReadReceipts.swift */; }; FD432434299C6985008A0213 /* PendingReadReceipt.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD432433299C6985008A0213 /* PendingReadReceipt.swift */; }; - FD43EE9D297A5190009C87C5 /* LibSession+UserGroups.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD43EE9C297A5190009C87C5 /* LibSession+UserGroups.swift */; }; - FD43EE9F297E2EE0009C87C5 /* LibSession+ConvoInfoVolatile.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD43EE9E297E2EE0009C87C5 /* LibSession+ConvoInfoVolatile.swift */; }; + FD47E0B12AA6A05800A55E41 /* Authentication+SessionMessagingKit.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD47E0B02AA6A05800A55E41 /* Authentication+SessionMessagingKit.swift */; }; + FD47E0B52AA6D7AA00A55E41 /* Request+SnodeAPI.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD47E0B42AA6D7AA00A55E41 /* Request+SnodeAPI.swift */; }; + FD481A902CAD16F100ECC4CF /* LibSessionGroupInfoSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD481A8F2CAD16EA00ECC4CF /* LibSessionGroupInfoSpec.swift */; }; + FD481A922CAD17DE00ECC4CF /* LibSessionGroupMembersSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD481A912CAD17D900ECC4CF /* LibSessionGroupMembersSpec.swift */; }; + FD481A942CAE0AE000ECC4CF /* MockAppContext.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD481A932CAE0ADD00ECC4CF /* MockAppContext.swift */; }; + FD481A952CAE0AE000ECC4CF /* MockAppContext.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD481A932CAE0ADD00ECC4CF /* MockAppContext.swift */; }; + FD481A962CAE0AE000ECC4CF /* MockAppContext.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD481A932CAE0ADD00ECC4CF /* MockAppContext.swift */; }; + FD481A972CAE0AE000ECC4CF /* MockAppContext.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD481A932CAE0ADD00ECC4CF /* MockAppContext.swift */; }; + FD481A992CB4CAAA00ECC4CF /* MockLibSessionCache.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD336F5B2CAA28CF00C0B51B /* MockLibSessionCache.swift */; }; + FD481A9A2CB4CAE500ECC4CF /* CommonSMKMockExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD336F562CAA28CF00C0B51B /* CommonSMKMockExtensions.swift */; }; + FD481A9B2CB4CAF100ECC4CF /* CustomArgSummaryDescribable+SMK.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD336F572CAA28CF00C0B51B /* CustomArgSummaryDescribable+SMK.swift */; }; + FD481A9C2CB4D58300ECC4CF /* MockSnodeAPICache.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD3765DE2AD8F03100DC1489 /* MockSnodeAPICache.swift */; }; + FD481AA32CB889AE00ECC4CF /* RetrieveDefaultOpenGroupRoomsJobSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD481AA22CB889A400ECC4CF /* RetrieveDefaultOpenGroupRoomsJobSpec.swift */; }; + FD49E2462B05C1D500FFBBB5 /* MockKeychain.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD49E2452B05C1D500FFBBB5 /* MockKeychain.swift */; }; + FD49E2472B05C1D500FFBBB5 /* MockKeychain.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD49E2452B05C1D500FFBBB5 /* MockKeychain.swift */; }; + FD49E2482B05C1D500FFBBB5 /* MockKeychain.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD49E2452B05C1D500FFBBB5 /* MockKeychain.swift */; }; + FD49E2492B05C1D500FFBBB5 /* MockKeychain.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD49E2452B05C1D500FFBBB5 /* MockKeychain.swift */; }; FD4B200E283492210034334B /* InsetLockableTableView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD4B200D283492210034334B /* InsetLockableTableView.swift */; }; + FD4C4E9C2B02E2A300C72199 /* DisplayPictureError.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD4C4E9B2B02E2A300C72199 /* DisplayPictureError.swift */; }; FD4C53AF2CC1D62E003B10F4 /* _021_ReworkRecipientState.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD4C53AE2CC1D61E003B10F4 /* _021_ReworkRecipientState.swift */; }; FD52090328B4680F006098F6 /* RadioButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD52090228B4680F006098F6 /* RadioButton.swift */; }; FD52090528B4915F006098F6 /* PrivacySettingsViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD52090428B4915F006098F6 /* PrivacySettingsViewModel.swift */; }; @@ -630,61 +706,31 @@ FD5D201E27B0D87C00FEA984 /* SessionId.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD5D201D27B0D87C00FEA984 /* SessionId.swift */; }; FD5E93D12C100FD70038C25A /* FileUploadResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDC4387127B5BB3B00C60D73 /* FileUploadResponse.swift */; }; FD5E93D22C12B0580038C25A /* AppVersionResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDC4383727B3863200C60D73 /* AppVersionResponse.swift */; }; - FD5E93D82C12E3B50038C25A /* FileUploadResponseSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD3C905F27E410F700CD579F /* FileUploadResponseSpec.swift */; }; + FD65318A2AA025C500DFEEAA /* TestDependencies.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD6531892AA025C500DFEEAA /* TestDependencies.swift */; }; + FD65318B2AA025C500DFEEAA /* TestDependencies.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD6531892AA025C500DFEEAA /* TestDependencies.swift */; }; + FD65318C2AA025C500DFEEAA /* TestDependencies.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD6531892AA025C500DFEEAA /* TestDependencies.swift */; }; + FD65318D2AA025C500DFEEAA /* TestDependencies.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD6531892AA025C500DFEEAA /* TestDependencies.swift */; }; + FD66CB2A2BF3449B00268FAB /* SessionSnodeKit.xctestplan in Resources */ = {isa = PBXBuildFile; fileRef = FD66CB272BF3449B00268FAB /* SessionSnodeKit.xctestplan */; }; + FD66CB2E2BF5EB0C00268FAB /* Config.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD66CB2C2BF5EB0C00268FAB /* Config.swift */; }; + FD6A38E62C2A4D8E00762359 /* GRDB in Frameworks */ = {isa = PBXBuildFile; productRef = FD6A38E52C2A4D8E00762359 /* GRDB */; }; FD6A38E92C2A630E00762359 /* CocoaLumberjackSwift in Frameworks */ = {isa = PBXBuildFile; productRef = FD6A38E82C2A630E00762359 /* CocoaLumberjackSwift */; }; FD6A38EC2C2A63B500762359 /* KeychainSwift in Frameworks */ = {isa = PBXBuildFile; productRef = FD6A38EB2C2A63B500762359 /* KeychainSwift */; }; FD6A38EF2C2A641200762359 /* DifferenceKit in Frameworks */ = {isa = PBXBuildFile; productRef = FD6A38EE2C2A641200762359 /* DifferenceKit */; }; - FD6A38F12C2A66B100762359 /* KeychainStorageType.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD6A38F02C2A66B100762359 /* KeychainStorageType.swift */; }; - FD6A38F32C2A6BBB00762359 /* Crypto+SessionUtilitiesKit.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD6A38F22C2A6BBB00762359 /* Crypto+SessionUtilitiesKit.swift */; }; - FD6A38F52C2A6BD200762359 /* Crypto.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD6A38F42C2A6BD200762359 /* Crypto.swift */; }; - FD6A38F72C2A6C0100762359 /* CryptoError.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD6A38F62C2A6C0100762359 /* CryptoError.swift */; }; - FD6A38F92C2A8AF700762359 /* DataSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD6A38F82C2A8AF700762359 /* DataSource.swift */; }; - FD6A38FE2C2A8B7E00762359 /* MediaUtils.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD6A38FD2C2A8B7E00762359 /* MediaUtils.swift */; }; - FD6A39002C2A8B9100762359 /* UTType+Utilities.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD6A38FF2C2A8B9100762359 /* UTType+Utilities.swift */; }; - FD6A39022C2A8BDE00762359 /* UIImage+Utilities.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD6A39012C2A8BDE00762359 /* UIImage+Utilities.swift */; }; - FD6A39042C2A8C0300762359 /* CGFloat+Utilities.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD6A39032C2A8C0300762359 /* CGFloat+Utilities.swift */; }; - FD6A39062C2A8C1600762359 /* CGPoint+Utilities.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD6A39052C2A8C1600762359 /* CGPoint+Utilities.swift */; }; - FD6A39082C2A8DDA00762359 /* FileSystem.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD6A39072C2A8DDA00762359 /* FileSystem.swift */; }; - FD6A390A2C2A8F2D00762359 /* FileManagerType.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD6A39092C2A8F2D00762359 /* FileManagerType.swift */; }; + FD6A38F12C2A66B100762359 /* KeychainStorage.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD6A38F02C2A66B100762359 /* KeychainStorage.swift */; }; FD6A39132C2A946A00762359 /* SwiftProtobuf in Frameworks */ = {isa = PBXBuildFile; productRef = FD6A39122C2A946A00762359 /* SwiftProtobuf */; }; - FD6A39152C2A954000762359 /* Crypto+OpenGroupAPI.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD6A39142C2A954000762359 /* Crypto+OpenGroupAPI.swift */; }; - FD6A39172C2A99A000762359 /* BencodeDecoder.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD6A39162C2A99A000762359 /* BencodeDecoder.swift */; }; - FD6A39192C2A99AB00762359 /* BencodeEncoder.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD6A39182C2A99AB00762359 /* BencodeEncoder.swift */; }; - FD6A391B2C2A99B600762359 /* BencodeResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD6A391A2C2A99B600762359 /* BencodeResponse.swift */; }; - FD6A391D2C2A99DF00762359 /* BencodeEncoderSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD6A391C2C2A99DF00762359 /* BencodeEncoderSpec.swift */; }; - FD6A391F2C2A99EF00762359 /* BencodeResponseSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD6A391E2C2A99EF00762359 /* BencodeResponseSpec.swift */; }; FD6A39222C2AA91D00762359 /* NVActivityIndicatorView in Frameworks */ = {isa = PBXBuildFile; productRef = FD6A39212C2AA91D00762359 /* NVActivityIndicatorView */; }; - FD6A39242C2AAE4500762359 /* CGRect+Utilities.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD6A39232C2AAE4500762359 /* CGRect+Utilities.swift */; }; - FD6A39262C2AB0B500762359 /* OWSViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD6A39252C2AB0B500762359 /* OWSViewController.swift */; }; - FD6A39282C2AB2AA00762359 /* CGSize+Utilities.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD6A39272C2AB2AA00762359 /* CGSize+Utilities.swift */; }; - FD6A392A2C2AB3BD00762359 /* UIBezierPath+Utilities.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD6A39292C2AB3BD00762359 /* UIBezierPath+Utilities.swift */; }; - FD6A392C2C2AC51900762359 /* AppVersion.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD6A392B2C2AC51900762359 /* AppVersion.swift */; }; - FD6A392F2C2ACAA600762359 /* Crypto+SessionSnodeKit.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD6A392E2C2ACAA600762359 /* Crypto+SessionSnodeKit.swift */; }; FD6A39322C2AD33E00762359 /* Quick in Frameworks */ = {isa = PBXBuildFile; productRef = FD6A39312C2AD33E00762359 /* Quick */; }; FD6A39342C2AD35F00762359 /* Quick in Frameworks */ = {isa = PBXBuildFile; productRef = FD6A39332C2AD35F00762359 /* Quick */; }; - FD6A39362C2AD36400762359 /* Quick in Frameworks */ = {isa = PBXBuildFile; productRef = FD6A39352C2AD36400762359 /* Quick */; }; FD6A39382C2AD36900762359 /* Quick in Frameworks */ = {isa = PBXBuildFile; productRef = FD6A39372C2AD36900762359 /* Quick */; }; FD6A393B2C2AD3A300762359 /* Nimble in Frameworks */ = {isa = PBXBuildFile; productRef = FD6A393A2C2AD3A300762359 /* Nimble */; }; FD6A393D2C2AD3AC00762359 /* Nimble in Frameworks */ = {isa = PBXBuildFile; productRef = FD6A393C2C2AD3AC00762359 /* Nimble */; }; - FD6A393F2C2AD3B100762359 /* Nimble in Frameworks */ = {isa = PBXBuildFile; productRef = FD6A393E2C2AD3B100762359 /* Nimble */; }; FD6A39412C2AD3B600762359 /* Nimble in Frameworks */ = {isa = PBXBuildFile; productRef = FD6A39402C2AD3B600762359 /* Nimble */; }; - FD6A39432C2AD81600762359 /* BackgroundTaskManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD6A39422C2AD81600762359 /* BackgroundTaskManager.swift */; }; - FD6A39452C2B783D00762359 /* UINavigationController+Utilities.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD6A39442C2B783D00762359 /* UINavigationController+Utilities.swift */; }; - FD6A39492C2BB85A00762359 /* Crypto+Attachments.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD6A39482C2BB85A00762359 /* Crypto+Attachments.swift */; }; - FD6A394F2C2D060C00762359 /* YYImage in Frameworks */ = {isa = PBXBuildFile; productRef = FD6A394E2C2D060C00762359 /* YYImage */; }; - FD6A395C2C2D10C700762359 /* YYImage in Frameworks */ = {isa = PBXBuildFile; productRef = FD6A395B2C2D10C700762359 /* YYImage */; }; FD6A39662C2D21E400762359 /* libwebp in Frameworks */ = {isa = PBXBuildFile; productRef = FD6A39652C2D21E400762359 /* libwebp */; }; - FD6A39692C2D283A00762359 /* YYImage in Frameworks */ = {isa = PBXBuildFile; productRef = FD6A39682C2D283A00762359 /* YYImage */; }; FD6A396B2C2D284500762359 /* YYImage in Frameworks */ = {isa = PBXBuildFile; productRef = FD6A396A2C2D284500762359 /* YYImage */; }; FD6A396D2C2D284B00762359 /* YYImage in Frameworks */ = {isa = PBXBuildFile; productRef = FD6A396C2C2D284B00762359 /* YYImage */; }; FD6A396F2C2E3D4400762359 /* YYImage in Frameworks */ = {isa = PBXBuildFile; productRef = FD6A396E2C2E3D4400762359 /* YYImage */; }; - FD6A39712C2E3F5800762359 /* GRDBExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD6A39702C2E3F5800762359 /* GRDBExtensions.swift */; }; - FD6A39722C2E3F5800762359 /* GRDBExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD6A39702C2E3F5800762359 /* GRDBExtensions.swift */; }; - FD6A39732C2E3F5800762359 /* GRDBExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD6A39702C2E3F5800762359 /* GRDBExtensions.swift */; }; - FD6A39742C2E3F5800762359 /* GRDBExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD6A39702C2E3F5800762359 /* GRDBExtensions.swift */; }; - FD6A7A692818BE7300035AC1 /* RetrieveDefaultOpenGroupRoomsJob.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD6A7A682818BE7300035AC1 /* RetrieveDefaultOpenGroupRoomsJob.swift */; }; - FD6A7A6B2818C17C00035AC1 /* UpdateProfilePictureJob.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD6A7A6A2818C17C00035AC1 /* UpdateProfilePictureJob.swift */; }; FD6A7A6D2818C61500035AC1 /* _002_SetupStandardJobs.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD6A7A6C2818C61500035AC1 /* _002_SetupStandardJobs.swift */; }; + FD6D9CF92CA152B300F706A8 /* Session+SNUIKit.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDE754BB2C9B9A8E002A2623 /* Session+SNUIKit.swift */; }; FD6DF00B2ACFE40D0084BA4C /* _005_AddSnodeReveivedMessageInfoPrimaryKey.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD6DF00A2ACFE40D0084BA4C /* _005_AddSnodeReveivedMessageInfoPrimaryKey.swift */; }; FD6E4C8A2A1AEE4700C7C243 /* LegacyUnsubscribeRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD6E4C892A1AEE4700C7C243 /* LegacyUnsubscribeRequest.swift */; }; FD705A92278D051200F16121 /* ReusableView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD705A91278D051200F16121 /* ReusableView.swift */; }; @@ -693,7 +739,6 @@ FD7115F428C71EB200B47552 /* ThreadDisappearingMessagesSettingsViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD7115F328C71EB200B47552 /* ThreadDisappearingMessagesSettingsViewModel.swift */; }; FD7115F828C8151C00B47552 /* DisposableBarButtonItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD7115F728C8151C00B47552 /* DisposableBarButtonItem.swift */; }; FD7115FA28C8153400B47552 /* UIBarButtonItem+Combine.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD7115F928C8153400B47552 /* UIBarButtonItem+Combine.swift */; }; - FD7115FC28C8155800B47552 /* Publisher+Utilities.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD7115FB28C8155800B47552 /* Publisher+Utilities.swift */; }; FD7115FE28C8202D00B47552 /* ReplaySubject.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD7115FD28C8202D00B47552 /* ReplaySubject.swift */; }; FD71160028C8253500B47552 /* UIView+Combine.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD7115FF28C8253500B47552 /* UIView+Combine.swift */; }; FD71160228C8255900B47552 /* UIControl+Combine.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD71160128C8255900B47552 /* UIControl+Combine.swift */; }; @@ -703,10 +748,9 @@ FD71161A28D00E1100B47552 /* NotificationContentViewModelSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD71161928D00E1100B47552 /* NotificationContentViewModelSpec.swift */; }; FD71161C28D194FB00B47552 /* MentionInfo.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD71161B28D194FB00B47552 /* MentionInfo.swift */; }; FD71161E28D9772700B47552 /* UIViewController+OWS.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD71161D28D9772700B47552 /* UIViewController+OWS.swift */; }; - FD71162028D97ABC00B47552 /* UIImage+Tinting.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD71161F28D97ABC00B47552 /* UIImage+Tinting.swift */; }; + FD71162028D97ABC00B47552 /* UIImage+Utilities.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD71161F28D97ABC00B47552 /* UIImage+Utilities.swift */; }; FD71162228D983ED00B47552 /* QRCodeScanningViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD71162128D983ED00B47552 /* QRCodeScanningViewController.swift */; }; FD71162A28DA83DF00B47552 /* GradientView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C38EF3EE255B6DF6007E1867 /* GradientView.swift */; }; - FD71162C28E1451400B47552 /* Position.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD71162B28E1451400B47552 /* Position.swift */; }; FD71162E28E168C700B47552 /* SettingsViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD71162D28E168C700B47552 /* SettingsViewModel.swift */; }; FD71163228E2C42A00B47552 /* IconSize.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD71163128E2C42A00B47552 /* IconSize.swift */; }; FD71163728E2C50700B47552 /* SessionTableViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD37EA0628AA2CCA003AE748 /* SessionTableViewController.swift */; }; @@ -731,48 +775,19 @@ FD716E6C28505E1C00C96BF4 /* MessageRequestsViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD716E6B28505E1C00C96BF4 /* MessageRequestsViewModel.swift */; }; FD716E7128505E5200C96BF4 /* MessageRequestsCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD716E7028505E5100C96BF4 /* MessageRequestsCell.swift */; }; FD716E722850647600C96BF4 /* Data+Utilities.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD859EF127BF6BA200510D0C /* Data+Utilities.swift */; }; - FD72BD9A2BDF5EEA00CF6CF6 /* Message+Origin.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD72BD992BDF5EEA00CF6CF6 /* Message+Origin.swift */; }; + FD72BD9C2BE2F2BC00CF6CF6 /* NoopSessionCallManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD72BD9B2BE2F2BC00CF6CF6 /* NoopSessionCallManager.swift */; }; + FD72BDA12BE368C800CF6CF6 /* UIWindowLevel+Utilities.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD72BDA02BE368C800CF6CF6 /* UIWindowLevel+Utilities.swift */; }; + FD72BDA42BE3690B00CF6CF6 /* CryptoSMKSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD72BDA32BE3690B00CF6CF6 /* CryptoSMKSpec.swift */; }; + FD72BDA72BE369DC00CF6CF6 /* CryptoOpenGroupAPISpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD72BDA62BE369DC00CF6CF6 /* CryptoOpenGroupAPISpec.swift */; }; FD7692F72A53A2ED000E4B70 /* SessionThreadViewModelSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD7692F62A53A2ED000E4B70 /* SessionThreadViewModelSpec.swift */; }; 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 */; }; FD778B6429B189FF001BAC6B /* _014_GenerateInitialUserConfigDumps.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD778B6329B189FF001BAC6B /* _014_GenerateInitialUserConfigDumps.swift */; }; - FD7C37B22BBB8B1E009DEEA7 /* SessionSnodeKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = C3C2A59F255385C100C340D1 /* SessionSnodeKit.framework */; platformFilter = ios; }; - FD7C37BE2BBB8BBD009DEEA7 /* SnodeRequestSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD7C37BD2BBB8BBC009DEEA7 /* SnodeRequestSpec.swift */; }; - FD7C37BF2BBB8BDF009DEEA7 /* Mock.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDC290A527D860CE005DAE71 /* Mock.swift */; }; - FD7C37C02BBB8BE1009DEEA7 /* Mocked.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD0969F82A69FFE700C5C365 /* Mocked.swift */; }; - FD7C37C12BBB8BEA009DEEA7 /* MockCrypto.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD23CE272A67755C0000B97C /* MockCrypto.swift */; }; - FD7C37C22BBB8BED009DEEA7 /* MockCaches.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD23CE2B2A678DF80000B97C /* MockCaches.swift */; }; - FD7C37C32BBB8BF0009DEEA7 /* MockGeneralCache.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDFD645C27F273F300808CA1 /* MockGeneralCache.swift */; }; - FD7C37C42BBB8BFC009DEEA7 /* MockNetwork.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD23CE312A67C38D0000B97C /* MockNetwork.swift */; }; - FD7C37C52BBB8C01009DEEA7 /* MockJobRunner.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD96F3A629DBD43D00401309 /* MockJobRunner.swift */; }; - FD7C37C62BBB8C08009DEEA7 /* TestConstants.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD83B9BD27CF2243005E1583 /* TestConstants.swift */; }; - FD7C37C72BBB8C0E009DEEA7 /* TestExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD9DD2702A72516D00ECB68E /* TestExtensions.swift */; }; - FD7C37C82BBB8C11009DEEA7 /* NimbleExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDC290A727D9B46D005DAE71 /* NimbleExtensions.swift */; }; - FD7C37C92BBB8C1A009DEEA7 /* CombineExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD23EA6028ED0B260058676E /* CombineExtensions.swift */; }; - FD7C37CA2BBB8C1D009DEEA7 /* SynchronousStorage.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD2AAAEF28ED57B500A49611 /* SynchronousStorage.swift */; }; - FD7C37CB2BBB8D36009DEEA7 /* MockUserDefaults.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD83B9D127D59495005E1583 /* MockUserDefaults.swift */; }; FD7F74572BAA9D31006DDFD8 /* _006_DropSnodeCache.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD7F74562BAA9D31006DDFD8 /* _006_DropSnodeCache.swift */; }; FD7F745B2BAAA35E006DDFD8 /* LibSession.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD7F745A2BAAA35E006DDFD8 /* LibSession.swift */; }; - FD7F745D2BAAA38B006DDFD8 /* LibSessionError.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD7F745C2BAAA38B006DDFD8 /* LibSessionError.swift */; }; FD7F745F2BAAA3B4006DDFD8 /* TypeConversion+Utilities.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD7F745E2BAAA3B4006DDFD8 /* TypeConversion+Utilities.swift */; }; FD7F74602BAAA4C7006DDFD8 /* libSessionUtil.a in Frameworks */ = {isa = PBXBuildFile; fileRef = FD9BDDF82A5D2294005F1EBC /* libSessionUtil.a */; }; FD7F74632BAAA4CA006DDFD8 /* libSessionUtil.a in Frameworks */ = {isa = PBXBuildFile; fileRef = FD9BDDF82A5D2294005F1EBC /* libSessionUtil.a */; }; - FD7F746A2BAB8A6D006DDFD8 /* LibSession+Networking.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD7F74692BAB8A6D006DDFD8 /* LibSession+Networking.swift */; }; - FD7F746C2BB2764F006DDFD8 /* RequestTarget.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD7F746B2BB2764F006DDFD8 /* RequestTarget.swift */; }; - FD7F746E2BB2766D006DDFD8 /* PreparedRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD7F746D2BB2766D006DDFD8 /* PreparedRequest.swift */; }; - FD7F74702BB276A0006DDFD8 /* BatchRequestSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD7F746F2BB276A0006DDFD8 /* BatchRequestSpec.swift */; }; - FD7F74722BB276CC006DDFD8 /* RequestSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD7F74712BB276CC006DDFD8 /* RequestSpec.swift */; }; - FD7F74742BB276D0006DDFD8 /* HeaderSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD7F74732BB276D0006DDFD8 /* HeaderSpec.swift */; }; - FD7F74762BB276E7006DDFD8 /* PreparedRequestSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD7F74752BB276E7006DDFD8 /* PreparedRequestSpec.swift */; }; - FD7F74782BB27742006DDFD8 /* BatchRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD7F74772BB27742006DDFD8 /* BatchRequest.swift */; }; - FD7F747A2BB277E1006DDFD8 /* Request+OpenGroupAPI.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD7F74792BB277E1006DDFD8 /* Request+OpenGroupAPI.swift */; }; - FD7F747C2BB28182006DDFD8 /* PreparedRequest+OnionRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD7F747B2BB28182006DDFD8 /* PreparedRequest+OnionRequest.swift */; }; - FD7F74802BB283A9006DDFD8 /* Request+SnodeAPI.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD7F747F2BB283A9006DDFD8 /* Request+SnodeAPI.swift */; }; - FD7F74822BB283CE006DDFD8 /* UpdatableTimestamp.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD7F74812BB283CE006DDFD8 /* UpdatableTimestamp.swift */; }; - FD7F74842BB283DF006DDFD8 /* SwarmDrainBehaviour.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD7F74832BB283DF006DDFD8 /* SwarmDrainBehaviour.swift */; }; - FD7F74862BB2868E006DDFD8 /* ResponseInfo+SnodeAPI.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD7F74852BB2868E006DDFD8 /* ResponseInfo+SnodeAPI.swift */; }; - FD7F74882BB2929B006DDFD8 /* Request+PushNotificationAPI.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD7F74872BB2929B006DDFD8 /* Request+PushNotificationAPI.swift */; }; - FD7F748A2BB298A8006DDFD8 /* JSONDecoder+Utilities.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD7F74892BB298A8006DDFD8 /* JSONDecoder+Utilities.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 */; }; @@ -781,7 +796,6 @@ FD83B9C727CF3F10005E1583 /* CapabilitiesSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD83B9C627CF3F10005E1583 /* CapabilitiesSpec.swift */; }; FD83B9C927D0487A005E1583 /* SendDirectMessageResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD83B9C827D0487A005E1583 /* SendDirectMessageResponse.swift */; }; FD83B9D227D59495005E1583 /* MockUserDefaults.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD83B9D127D59495005E1583 /* MockUserDefaults.swift */; }; - FD83DCDD2A739D350065FFAE /* RetryWithDependencies.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD83DCDC2A739D350065FFAE /* RetryWithDependencies.swift */; }; FD848B8B283DC509000E298B /* PagedDatabaseObserver.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD848B8A283DC509000E298B /* PagedDatabaseObserver.swift */; }; FD848B8D283E0B26000E298B /* MessageInputTypes.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD848B8C283E0B26000E298B /* MessageInputTypes.swift */; }; FD848B8F283EF2A8000E298B /* UIScrollView+Utilities.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD848B8E283EF2A8000E298B /* UIScrollView+Utilities.swift */; }; @@ -789,49 +803,66 @@ FD848B9628422A2A000E298B /* MessageViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD848B86283B844B000E298B /* MessageViewModel.swift */; }; FD848B9828422F1A000E298B /* Date+Utilities.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD848B9728422F1A000E298B /* Date+Utilities.swift */; }; FD848B9A28442CE6000E298B /* StorageError.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD848B9928442CE6000E298B /* StorageError.swift */; }; - FD848B9C284435D7000E298B /* AppSetup.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD848B9B284435D7000E298B /* AppSetup.swift */; }; FD86FDA32BC5020600EC251B /* PrivacyInfo.xcprivacy in Resources */ = {isa = PBXBuildFile; fileRef = FD86FDA22BC5020600EC251B /* PrivacyInfo.xcprivacy */; }; FD86FDA42BC51C5400EC251B /* PrivacyInfo.xcprivacy in Resources */ = {isa = PBXBuildFile; fileRef = FD86FDA22BC5020600EC251B /* PrivacyInfo.xcprivacy */; }; FD86FDA52BC51C5500EC251B /* PrivacyInfo.xcprivacy in Resources */ = {isa = PBXBuildFile; fileRef = FD86FDA22BC5020600EC251B /* PrivacyInfo.xcprivacy */; }; FD87DCFA28B74DB300AF0F98 /* ConversationSettingsViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD87DCF928B74DB300AF0F98 /* ConversationSettingsViewModel.swift */; }; FD87DCFE28B7582C00AF0F98 /* BlockedContactsViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD87DCFD28B7582C00AF0F98 /* BlockedContactsViewModel.swift */; }; - FD87DD0428B8727D00AF0F98 /* Configuration.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD87DD0328B8727D00AF0F98 /* Configuration.swift */; }; FD8ECF7B29340FFD00C0D1BB /* LibSession+SessionMessagingKit.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD8ECF7A29340FFD00C0D1BB /* LibSession+SessionMessagingKit.swift */; }; FD8ECF7D2934293A00C0D1BB /* _013_SessionUtilChanges.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD8ECF7C2934293A00C0D1BB /* _013_SessionUtilChanges.swift */; }; FD8ECF7F2934298100C0D1BB /* ConfigDump.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD8ECF7E2934298100C0D1BB /* ConfigDump.swift */; }; - FD8ECF9029381FC200C0D1BB /* LibSession+UserProfile.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD8ECF8F29381FC200C0D1BB /* LibSession+UserProfile.swift */; }; - FD8ECF922938552800C0D1BB /* Threading.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD8ECF912938552800C0D1BB /* Threading.swift */; }; - FD8ECF94293856AF00C0D1BB /* Randomness.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD8ECF93293856AF00C0D1BB /* Randomness.swift */; }; - FD9004122818ABDC00ABAAF6 /* Job.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDF0B73F280402C4004C14C5 /* Job.swift */; }; + FD8FD7622C37B7BD001E38C7 /* Position.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD71162B28E1451400B47552 /* Position.swift */; }; + FD8FD7642C37C24A001E38C7 /* EquatableIgnoring.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD8FD7632C37C24A001E38C7 /* EquatableIgnoring.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 */; }; FD9004162818B46700ABAAF6 /* JobRunnerError.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDE77F68280F9EDA002CFC5D /* JobRunnerError.swift */; }; FD96F3A529DBC3DC00401309 /* MessageSendJobSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD96F3A429DBC3DC00401309 /* MessageSendJobSpec.swift */; }; - FD96F3A729DBD43D00401309 /* MockJobRunner.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD96F3A629DBD43D00401309 /* MockJobRunner.swift */; }; FD97B2402A3FEB050027DD57 /* ARC4RandomNumberGenerator.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD97B23F2A3FEB050027DD57 /* ARC4RandomNumberGenerator.swift */; }; FD97B2422A3FEBF30027DD57 /* UnreadMarkerCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD97B2412A3FEBF30027DD57 /* UnreadMarkerCell.swift */; }; FD9AECA52AAA9609009B3406 /* NotificationError.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD9AECA42AAA9609009B3406 /* NotificationError.swift */; }; - FD9B30F3293EA0BF008DEE3E /* BatchResponseSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD9B30F2293EA0BF008DEE3E /* BatchResponseSpec.swift */; }; FD9BDE002A5D22B7005F1EBC /* libSessionUtil.a in Frameworks */ = {isa = PBXBuildFile; fileRef = FD9BDDF82A5D2294005F1EBC /* libSessionUtil.a */; }; FD9BDE012A5D24EA005F1EBC /* SessionUIKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = C331FF1B2558F9D300070591 /* SessionUIKit.framework */; }; FD9DD2712A72516D00ECB68E /* TestExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD9DD2702A72516D00ECB68E /* TestExtensions.swift */; }; FD9DD2722A72516D00ECB68E /* TestExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD9DD2702A72516D00ECB68E /* TestExtensions.swift */; }; FD9DD2732A72516D00ECB68E /* TestExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD9DD2702A72516D00ECB68E /* TestExtensions.swift */; }; - FDA1E83929A5771A00C5C3BD /* LibSessionUtilSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDA1E83829A5771A00C5C3BD /* LibSessionUtilSpec.swift */; }; - FDA1E83B29A5F2D500C5C3BD /* LibSession+Shared.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDA1E83A29A5F2D500C5C3BD /* LibSession+Shared.swift */; }; - FDA1E83D29AC71A800C5C3BD /* LibSessionSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDA1E83C29AC71A800C5C3BD /* LibSessionSpec.swift */; }; - FDA8EAFE280E8B78002B68E5 /* FailedMessageSendsJob.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDA8EAFD280E8B78002B68E5 /* FailedMessageSendsJob.swift */; }; - FDA8EB00280E8D58002B68E5 /* FailedAttachmentDownloadsJob.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDA8EAFF280E8D58002B68E5 /* FailedAttachmentDownloadsJob.swift */; }; - FDA8EB10280F8238002B68E5 /* Codable+Utilities.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDA8EB0F280F8238002B68E5 /* Codable+Utilities.swift */; }; + FDAA16762AC28A3B00DDBF77 /* UserDefaultsType.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDAA16752AC28A3B00DDBF77 /* UserDefaultsType.swift */; }; + FDAA167B2AC28E2F00DDBF77 /* SnodeRequestSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDAA167A2AC28E2F00DDBF77 /* SnodeRequestSpec.swift */; }; + FDAA167D2AC528A200DDBF77 /* Preferences+Sound.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDAA167C2AC528A200DDBF77 /* Preferences+Sound.swift */; }; + FDAA167F2AC5290000DDBF77 /* Preferences+NotificationPreviewType.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDAA167E2AC5290000DDBF77 /* Preferences+NotificationPreviewType.swift */; }; + FDB348632BE3774000B716C2 /* BezierPathView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDB348622BE3774000B716C2 /* BezierPathView.swift */; }; + FDB3486E2BE8457F00B716C2 /* BackgroundTaskManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDB3486D2BE8457F00B716C2 /* BackgroundTaskManager.swift */; }; + FDB3487E2BE856C800B716C2 /* UIBezierPath+Utilities.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDB3487D2BE856C800B716C2 /* UIBezierPath+Utilities.swift */; }; + FDB348802BE86A4400B716C2 /* libSessionUtil.a in Frameworks */ = {isa = PBXBuildFile; fileRef = FD9BDDF82A5D2294005F1EBC /* libSessionUtil.a */; }; + FDB348892BE8705D00B716C2 /* SessionUtilitiesKit.h in Headers */ = {isa = PBXBuildFile; fileRef = FDB3486C2BE8448500B716C2 /* SessionUtilitiesKit.h */; settings = {ATTRIBUTES = (Public, ); }; }; FDB4BBC72838B91E00B7C95D /* LinkPreviewError.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDB4BBC62838B91E00B7C95D /* LinkPreviewError.swift */; }; - FDB4BBC92839BEF000B7C95D /* ProfileManagerError.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDB4BBC82839BEF000B7C95D /* ProfileManagerError.swift */; }; + FDB5DAC12A9443A5002C8721 /* MessageSender+Groups.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDB5DAC02A9443A5002C8721 /* MessageSender+Groups.swift */; }; + FDB5DAC72A9447E7002C8721 /* _022_GroupsRebuildChanges.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDB5DAC62A9447E7002C8721 /* _022_GroupsRebuildChanges.swift */; }; + FDB5DAD42A9483F3002C8721 /* GroupUpdateInviteMessage.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDB5DAD32A9483F3002C8721 /* GroupUpdateInviteMessage.swift */; }; + FDB5DADA2A95D839002C8721 /* GroupUpdateInfoChangeMessage.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDB5DAD92A95D839002C8721 /* GroupUpdateInfoChangeMessage.swift */; }; + FDB5DADC2A95D840002C8721 /* GroupUpdateMemberChangeMessage.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDB5DADB2A95D840002C8721 /* GroupUpdateMemberChangeMessage.swift */; }; + FDB5DADE2A95D847002C8721 /* GroupUpdatePromoteMessage.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDB5DADD2A95D847002C8721 /* GroupUpdatePromoteMessage.swift */; }; + FDB5DAE02A95D84D002C8721 /* GroupUpdateMemberLeftMessage.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDB5DADF2A95D84D002C8721 /* GroupUpdateMemberLeftMessage.swift */; }; + FDB5DAE22A95D8A0002C8721 /* GroupUpdateInviteResponseMessage.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDB5DAE12A95D8A0002C8721 /* GroupUpdateInviteResponseMessage.swift */; }; + FDB5DAE62A95D8B0002C8721 /* GroupUpdateDeleteMemberContentMessage.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDB5DAE52A95D8B0002C8721 /* GroupUpdateDeleteMemberContentMessage.swift */; }; + FDB5DAE82A95D96C002C8721 /* MessageReceiver+Groups.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDB5DAE72A95D96C002C8721 /* MessageReceiver+Groups.swift */; }; + FDB5DAF32A96DD4F002C8721 /* PreparedRequest+Sending.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDB5DAF22A96DD4F002C8721 /* PreparedRequest+Sending.swift */; }; + FDB5DB062A981C67002C8721 /* PreparedRequestSendingSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDB5DB052A981C67002C8721 /* PreparedRequestSendingSpec.swift */; }; + FDB5DB082A981F8B002C8721 /* Mocked.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD0969F82A69FFE700C5C365 /* Mocked.swift */; }; + FDB5DB092A981F8D002C8721 /* MockCrypto.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD23CE272A67755C0000B97C /* MockCrypto.swift */; }; + FDB5DB0B2A981F92002C8721 /* MockGeneralCache.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDFD645C27F273F300808CA1 /* MockGeneralCache.swift */; }; + FDB5DB0C2A981F96002C8721 /* MockNetwork.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD23CE312A67C38D0000B97C /* MockNetwork.swift */; }; + FDB5DB102A981FA3002C8721 /* TestConstants.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD83B9BD27CF2243005E1583 /* TestConstants.swift */; }; + FDB5DB112A981FA6002C8721 /* TestExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD9DD2702A72516D00ECB68E /* TestExtensions.swift */; }; + FDB5DB122A981FA8002C8721 /* NimbleExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDC290A727D9B46D005DAE71 /* NimbleExtensions.swift */; }; + FDB5DB142A981FAE002C8721 /* CombineExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD23EA6028ED0B260058676E /* CombineExtensions.swift */; }; + FDB5DB152A981FB0002C8721 /* SynchronousStorage.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD2AAAEF28ED57B500A49611 /* SynchronousStorage.swift */; }; FDB6A87C2AD75B7F002D4F96 /* PhotosUI.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = FDB6A87B2AD75B7F002D4F96 /* PhotosUI.framework */; }; FDB7400B28EB99A70094D718 /* TimeInterval+Utilities.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDB7400A28EB99A70094D718 /* TimeInterval+Utilities.swift */; }; FDB7400D28EBEC240094D718 /* DateHeaderCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDB7400C28EBEC240094D718 /* DateHeaderCell.swift */; }; FDBB25E32988B13800F1508E /* _004_AddJobPriority.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDBB25E22988B13800F1508E /* _004_AddJobPriority.swift */; }; FDBB25E72988BBBE00F1508E /* UIContextualAction+Theming.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDBB25E62988BBBD00F1508E /* UIContextualAction+Theming.swift */; }; + FDBEE52E2B6A18B900C143A0 /* UserDefaultsConfig.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDBEE52D2B6A18B900C143A0 /* UserDefaultsConfig.swift */; }; FDC0F0042BFECE12002CBFB9 /* TimeUnit.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDC0F0032BFECE12002CBFB9 /* TimeUnit.swift */; }; - FDC0F0082C00721A002CBFB9 /* MockOGPoller.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDC0F0072C00721A002CBFB9 /* MockOGPoller.swift */; }; FDC13D472A16E4CA007267C7 /* SubscribeRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDC13D462A16E4CA007267C7 /* SubscribeRequest.swift */; }; FDC13D492A16EC20007267C7 /* Service.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDC13D482A16EC20007267C7 /* Service.swift */; }; FDC13D4B2A16ECBA007267C7 /* SubscribeResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDC13D4A2A16ECBA007267C7 /* SubscribeResponse.swift */; }; @@ -851,10 +882,9 @@ FDC2909427D710B4005DAE71 /* SOGSEndpointSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDC2909327D710B4005DAE71 /* SOGSEndpointSpec.swift */; }; FDC2909627D71252005DAE71 /* SOGSErrorSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDC2909527D71252005DAE71 /* SOGSErrorSpec.swift */; }; FDC2909827D7129B005DAE71 /* PersonalizationSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDC2909727D7129B005DAE71 /* PersonalizationSpec.swift */; }; - FDC290A627D860CE005DAE71 /* Mock.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDC290A527D860CE005DAE71 /* Mock.swift */; }; FDC290A827D9B46D005DAE71 /* NimbleExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDC290A727D9B46D005DAE71 /* NimbleExtensions.swift */; }; FDC290A927D9B46D005DAE71 /* NimbleExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDC290A727D9B46D005DAE71 /* NimbleExtensions.swift */; }; - FDC290AA27D9B6FD005DAE71 /* Mock.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDC290A527D860CE005DAE71 /* Mock.swift */; }; + FDC383392A93411100FFD6A2 /* Setting+Utilities.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD1D73292A85AA2000E3F410 /* Setting+Utilities.swift */; }; FDC4380927B31D4E00C60D73 /* OpenGroupAPIError.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDC4380827B31D4E00C60D73 /* OpenGroupAPIError.swift */; }; FDC4381727B32EC700C60D73 /* Personalization.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDC4381627B32EC700C60D73 /* Personalization.swift */; }; FDC4382027B36ADC00C60D73 /* SOGSEndpoint.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDC4381F27B36ADC00C60D73 /* SOGSEndpoint.swift */; }; @@ -864,7 +894,6 @@ 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 */; }; FDC4386C27B4E90300C60D73 /* SessionUtilitiesKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = C3C2A679255388CC00C340D1 /* SessionUtilitiesKit.framework */; }; FDC4387827B5C35400C60D73 /* SendMessageRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDC4387727B5C35400C60D73 /* SendMessageRequest.swift */; }; FDC4389227B9FFC700C60D73 /* SessionMessagingKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = C3C2A6F025539DE700C340D1 /* SessionMessagingKit.framework */; platformFilter = ios; }; @@ -872,7 +901,6 @@ 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 */; }; - FDC438BD27BB2AB400C60D73 /* Mockable.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDC438BC27BB2AB400C60D73 /* Mockable.swift */; }; FDC438C727BB6DF000C60D73 /* DirectMessage.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDC438C627BB6DF000C60D73 /* DirectMessage.swift */; }; FDC438C927BB706500C60D73 /* SendDirectMessageRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDC438C827BB706500C60D73 /* SendDirectMessageRequest.swift */; }; FDC438CB27BB7DB100C60D73 /* UpdateMessageRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDC438CA27BB7DB100C60D73 /* UpdateMessageRequest.swift */; }; @@ -880,43 +908,96 @@ FDC498B72AC15F7D00EDD897 /* AppNotificationCategory.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDC498B62AC15F7D00EDD897 /* AppNotificationCategory.swift */; }; FDC498B92AC15FE300EDD897 /* AppNotificationAction.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDC498B82AC15FE300EDD897 /* AppNotificationAction.swift */; }; FDC498BB2AC1606C00EDD897 /* AppNotificationUserInfoKey.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDC498BA2AC1606C00EDD897 /* AppNotificationUserInfoKey.swift */; }; - FDC6D6F32860607300B04575 /* SessionEnvironment.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDF0B7542807C4BB004C14C5 /* SessionEnvironment.swift */; }; FDC6D7602862B3F600B04575 /* Dependencies.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDC6D75F2862B3F600B04575 /* Dependencies.swift */; }; FDCD2E032A41294E00964D6A /* LegacyGroupOnlyRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDCD2E022A41294E00964D6A /* LegacyGroupOnlyRequest.swift */; }; - FDCDB8DE2810F73B00352A0C /* Differentiable+Utilities.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDCDB8DD2810F73B00352A0C /* Differentiable+Utilities.swift */; }; FDCDB8E02811007F00352A0C /* HomeViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDCDB8DF2811007F00352A0C /* HomeViewModel.swift */; }; FDD20C162A09E64A003898FB /* GetExpiriesRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDD20C152A09E64A003898FB /* GetExpiriesRequest.swift */; }; FDD20C182A09E7D3003898FB /* GetExpiriesResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDD20C172A09E7D3003898FB /* GetExpiriesResponse.swift */; }; FDD20C1A2A0A03AC003898FB /* DeleteInboxResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDD20C192A0A03AC003898FB /* DeleteInboxResponse.swift */; }; FDD2506E283711D600198BDA /* DifferenceKit+Utilities.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDD2506D283711D600198BDA /* DifferenceKit+Utilities.swift */; }; - FDD250702837199200198BDA /* GarbageCollectionJob.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDD2506F2837199200198BDA /* GarbageCollectionJob.swift */; }; FDD250722837234B00198BDA /* MediaGalleryNavigationController.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDD250712837234B00198BDA /* MediaGalleryNavigationController.swift */; }; FDD82C3F2A205D0A00425F05 /* ProcessResult.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDD82C3E2A205D0A00425F05 /* ProcessResult.swift */; }; - FDDC08F229A300E800BF9681 /* TypeConversionUtilitiesSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDDC08F129A300E800BF9681 /* TypeConversionUtilitiesSpec.swift */; }; - FDDD554C2C1FC812006CBF03 /* CheckForAppUpdatesJob.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDDD554B2C1FC812006CBF03 /* CheckForAppUpdatesJob.swift */; }; FDDD554E2C1FCB77006CBF03 /* _019_ScheduleAppUpdateCheckJob.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDDD554D2C1FCB77006CBF03 /* _019_ScheduleAppUpdateCheckJob.swift */; }; FDDF074429C3E3D000E5E8B5 /* FetchRequest+Utilities.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDDF074329C3E3D000E5E8B5 /* FetchRequest+Utilities.swift */; }; FDDF074A29DAB36900E5E8B5 /* JobRunnerSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDDF074929DAB36900E5E8B5 /* JobRunnerSpec.swift */; }; - FDE049032C76A09700B6F9BB /* UIAlertAction+Utilities.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDE049022C76A09700B6F9BB /* UIAlertAction+Utilities.swift */; }; FDE125232A837E4E002DA685 /* MainAppContext.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDE125222A837E4E002DA685 /* MainAppContext.swift */; }; - FDE658A329418E2F00A33BC1 /* KeyPair.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDE658A229418E2F00A33BC1 /* KeyPair.swift */; }; + FDE519F72AB7CDC700450C53 /* Result+Utilities.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDE519F62AB7CDC700450C53 /* Result+Utilities.swift */; }; + FDE519F92AB802BB00450C53 /* Message+Origin.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDE519F82AB802BB00450C53 /* Message+Origin.swift */; }; FDE6E99829F8E63A00F93C5D /* Accessibility.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDE6E99729F8E63A00F93C5D /* Accessibility.swift */; }; + FDE7549B2C940108002A2623 /* MessageViewModel+DeletionActions.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDE7549A2C940108002A2623 /* MessageViewModel+DeletionActions.swift */; }; + FDE7549D2C9961A4002A2623 /* CommunityPoller.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDE7549C2C9961A4002A2623 /* CommunityPoller.swift */; }; + FDE754A12C9A60A6002A2623 /* Crypto+OpenGroupAPI.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDE754A02C9A60A6002A2623 /* Crypto+OpenGroupAPI.swift */; }; + FDE754A32C9A8FD1002A2623 /* SwarmPoller.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDE754A22C9A8FD1002A2623 /* SwarmPoller.swift */; }; + FDE754A82C9B964D002A2623 /* MessageReceiverGroupsSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDE754A42C9B964D002A2623 /* MessageReceiverGroupsSpec.swift */; }; + FDE754A92C9B964D002A2623 /* MessageSenderGroupsSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDE754A52C9B964D002A2623 /* MessageSenderGroupsSpec.swift */; }; + FDE754AA2C9B964D002A2623 /* MessageSenderSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDE754A62C9B964D002A2623 /* MessageSenderSpec.swift */; }; + FDE754AC2C9B967E002A2623 /* FileUploadResponseSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDE754AB2C9B967D002A2623 /* FileUploadResponseSpec.swift */; }; + FDE754B02C9B96B4002A2623 /* WebRTCSession.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDE754AD2C9B96B3002A2623 /* WebRTCSession.swift */; }; + FDE754B12C9B96B4002A2623 /* TurnServerInfo.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDE754AE2C9B96B4002A2623 /* TurnServerInfo.swift */; }; + FDE754B22C9B96B4002A2623 /* WebRTC+Utilities.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDE754AF2C9B96B4002A2623 /* WebRTC+Utilities.swift */; }; + FDE754B62C9B96BB002A2623 /* WebRTCSession+UI.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDE754B32C9B96BA002A2623 /* WebRTCSession+UI.swift */; }; + FDE754B72C9B96BB002A2623 /* WebRTCSession+MessageHandling.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDE754B42C9B96BA002A2623 /* WebRTCSession+MessageHandling.swift */; }; + FDE754B82C9B96BB002A2623 /* WebRTCSession+DataChannel.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDE754B52C9B96BB002A2623 /* WebRTCSession+DataChannel.swift */; }; + FDE754BA2C9B97B8002A2623 /* UIDevice+Utilities.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDE754B92C9B97B8002A2623 /* UIDevice+Utilities.swift */; }; + FDE754BE2C9BA16C002A2623 /* UIButtonConfiguration+Utilities.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDE754BD2C9BA16C002A2623 /* UIButtonConfiguration+Utilities.swift */; }; + FDE754C02C9BAEF6002A2623 /* Array+Utilities.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDE754BF2C9BAEF6002A2623 /* Array+Utilities.swift */; }; + FDE754C22C9BAF0C002A2623 /* Atomic.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDE754C12C9BAF0B002A2623 /* Atomic.swift */; }; + FDE754CC2C9BAF37002A2623 /* MediaUtils.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDE754C72C9BAF36002A2623 /* MediaUtils.swift */; }; + FDE754CD2C9BAF37002A2623 /* UTType+Utilities.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDE754C82C9BAF36002A2623 /* UTType+Utilities.swift */; }; + FDE754CE2C9BAF37002A2623 /* ImageFormat.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDE754C92C9BAF36002A2623 /* ImageFormat.swift */; }; + FDE754CF2C9BAF37002A2623 /* DataSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDE754CA2C9BAF37002A2623 /* DataSource.swift */; }; + FDE754D02C9BAF37002A2623 /* Data+Image.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDE754CB2C9BAF37002A2623 /* Data+Image.swift */; }; + FDE754D22C9BAF53002A2623 /* JobDependencies.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDE754D12C9BAF53002A2623 /* JobDependencies.swift */; }; + FDE754D42C9BAF6B002A2623 /* UICollectionView+ReusableView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDE754D32C9BAF6B002A2623 /* UICollectionView+ReusableView.swift */; }; + FDE754DB2C9BAF8A002A2623 /* Crypto.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDE754D52C9BAF89002A2623 /* Crypto.swift */; }; + FDE754DC2C9BAF8A002A2623 /* CryptoError.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDE754D62C9BAF89002A2623 /* CryptoError.swift */; }; + FDE754DD2C9BAF8A002A2623 /* Mnemonic.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDE754D72C9BAF89002A2623 /* Mnemonic.swift */; }; + FDE754DE2C9BAF8A002A2623 /* Crypto+SessionUtilitiesKit.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDE754D82C9BAF89002A2623 /* Crypto+SessionUtilitiesKit.swift */; }; + FDE754DF2C9BAF8A002A2623 /* KeyPair.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDE754D92C9BAF89002A2623 /* KeyPair.swift */; }; + FDE754E02C9BAF8A002A2623 /* Hex.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDE754DA2C9BAF8A002A2623 /* Hex.swift */; }; + FDE754E32C9BAFF4002A2623 /* Crypto+SessionSnodeKit.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDE754E12C9BAFF4002A2623 /* Crypto+SessionSnodeKit.swift */; }; + FDE754E52C9BB012002A2623 /* BezierPathView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDE754E42C9BB012002A2623 /* BezierPathView.swift */; }; + FDE754E72C9BB051002A2623 /* OWSViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDE754E62C9BB051002A2623 /* OWSViewController.swift */; }; + FDE754F02C9BB08B002A2623 /* Crypto+Attachments.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDE754EC2C9BB08B002A2623 /* Crypto+Attachments.swift */; }; + FDE754F12C9BB08B002A2623 /* Crypto+LibSession.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDE754ED2C9BB08B002A2623 /* Crypto+LibSession.swift */; }; + FDE754F22C9BB08B002A2623 /* Crypto+SessionMessagingKit.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDE754EE2C9BB08B002A2623 /* Crypto+SessionMessagingKit.swift */; }; + FDE754F82C9BB0B0002A2623 /* UserNotificationConfig.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDE754F32C9BB0AF002A2623 /* UserNotificationConfig.swift */; }; + FDE754F92C9BB0B0002A2623 /* NotificationActionHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDE754F42C9BB0AF002A2623 /* NotificationActionHandler.swift */; }; + FDE754FA2C9BB0B0002A2623 /* NotificationPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDE754F52C9BB0AF002A2623 /* NotificationPresenter.swift */; }; + FDE754FB2C9BB0B0002A2623 /* PushRegistrationManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDE754F62C9BB0AF002A2623 /* PushRegistrationManager.swift */; }; + FDE754FC2C9BB0B0002A2623 /* SyncPushTokensJob.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDE754F72C9BB0AF002A2623 /* SyncPushTokensJob.swift */; }; + FDE754FE2C9BB0D0002A2623 /* Threading+SMK.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDE754FD2C9BB0D0002A2623 /* Threading+SMK.swift */; }; + FDE755002C9BB0FA002A2623 /* SessionEnvironment.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDE754FF2C9BB0FA002A2623 /* SessionEnvironment.swift */; }; + FDE755022C9BB122002A2623 /* _011_AddPendingReadReceipts.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDE755012C9BB122002A2623 /* _011_AddPendingReadReceipts.swift */; }; + FDE755052C9BB4EE002A2623 /* BencodeDecoder.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDE755032C9BB4ED002A2623 /* BencodeDecoder.swift */; }; + FDE755062C9BB4EE002A2623 /* Bencode.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDE755042C9BB4ED002A2623 /* Bencode.swift */; }; + FDE7550C2C9BC135002A2623 /* Codable+Utilities.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDE755072C9BC134002A2623 /* Codable+Utilities.swift */; }; + FDE7550D2C9BC135002A2623 /* CGPoint+Utilities.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDE755082C9BC134002A2623 /* CGPoint+Utilities.swift */; }; + FDE7550E2C9BC135002A2623 /* CGFloat+Utilities.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDE755092C9BC135002A2623 /* CGFloat+Utilities.swift */; }; + FDE7550F2C9BC135002A2623 /* CGRect+Utilities.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDE7550A2C9BC135002A2623 /* CGRect+Utilities.swift */; }; + FDE755102C9BC135002A2623 /* CGSize+Utilities.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDE7550B2C9BC135002A2623 /* CGSize+Utilities.swift */; }; + FDE755182C9BC169002A2623 /* UIAlertAction+Utilities.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDE755132C9BC169002A2623 /* UIAlertAction+Utilities.swift */; }; + FDE755192C9BC169002A2623 /* UIImage+Utilities.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDE755142C9BC169002A2623 /* UIImage+Utilities.swift */; }; + FDE7551A2C9BC169002A2623 /* UIApplicationState+Utilities.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDE755152C9BC169002A2623 /* UIApplicationState+Utilities.swift */; }; + FDE7551B2C9BC169002A2623 /* UINavigationController+Utilities.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDE755162C9BC169002A2623 /* UINavigationController+Utilities.swift */; }; + FDE7551C2C9BC169002A2623 /* UIBezierPath+Utilities.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDE755172C9BC169002A2623 /* UIBezierPath+Utilities.swift */; }; + FDE755202C9BC1A6002A2623 /* CacheConfig.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDE7551F2C9BC1A6002A2623 /* CacheConfig.swift */; }; + FDE755222C9BC1BA002A2623 /* LibSessionError.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDE755212C9BC1BA002A2623 /* LibSessionError.swift */; }; + FDE755242C9BC1D1002A2623 /* Publisher+Utilities.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDE755232C9BC1D1002A2623 /* Publisher+Utilities.swift */; }; FDE77F6B280FEB28002CFC5D /* ControlMessageProcessRecord.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDE77F6A280FEB28002CFC5D /* ControlMessageProcessRecord.swift */; }; - FDED2E3C282E1B5D00B2CD2A /* UICollectionView+ReusableView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDED2E3B282E1B5D00B2CD2A /* UICollectionView+ReusableView.swift */; }; - FDEF57212C3CF03A00131302 /* WebRTCSession.swift in Sources */ = {isa = PBXBuildFile; fileRef = B8DE1FB326C22F2F0079C9CE /* WebRTCSession.swift */; }; - FDEF57222C3CF03D00131302 /* WebRTCSession+UI.swift in Sources */ = {isa = PBXBuildFile; fileRef = B806ECA026C4A7E4008BDA44 /* WebRTCSession+UI.swift */; }; - FDEF57232C3CF04300131302 /* WebRTCSession+MessageHandling.swift in Sources */ = {isa = PBXBuildFile; fileRef = B8B558FE26C4E05E00693325 /* WebRTCSession+MessageHandling.swift */; }; - FDEF57242C3CF04700131302 /* WebRTC+Utilities.swift in Sources */ = {isa = PBXBuildFile; fileRef = B8BF43B926CC95FB007828D1 /* WebRTC+Utilities.swift */; }; - FDEF57252C3CF04C00131302 /* WebRTCSession+DataChannel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7BCD116B27016062006330F1 /* WebRTCSession+DataChannel.swift */; }; - FDEF57262C3CF05F00131302 /* TurnServerInfo.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7BFD1A8B2747150E00FB91B9 /* TurnServerInfo.swift */; }; - FDEF572A2C3CF50B00131302 /* WebRTC in Frameworks */ = {isa = PBXBuildFile; productRef = FDEF57292C3CF50B00131302 /* WebRTC */; }; + FDEF57212C3CF03A00131302 /* (null) in Sources */ = {isa = PBXBuildFile; }; + FDEF57222C3CF03D00131302 /* (null) in Sources */ = {isa = PBXBuildFile; }; + FDEF57232C3CF04300131302 /* (null) in Sources */ = {isa = PBXBuildFile; }; + FDEF57242C3CF04700131302 /* (null) in Sources */ = {isa = PBXBuildFile; }; + FDEF57252C3CF04C00131302 /* (null) in Sources */ = {isa = PBXBuildFile; }; + FDEF57262C3CF05F00131302 /* (null) in Sources */ = {isa = PBXBuildFile; }; + FDEF573E2C40F2A100131302 /* GroupUpdateMemberLeftNotificationMessage.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDEF573D2C40F2A100131302 /* GroupUpdateMemberLeftNotificationMessage.swift */; }; FDEF57712C44D2D300131302 /* GeoLite2-Country-Blocks-IPv4 in Resources */ = {isa = PBXBuildFile; fileRef = FDEF57702C44D2D300131302 /* GeoLite2-Country-Blocks-IPv4 */; }; + FDF01FAD2A9ECC4200CAF969 /* SingletonConfig.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDF01FAC2A9ECC4200CAF969 /* SingletonConfig.swift */; }; FDF0B73C27FFD3D6004C14C5 /* LinkPreview.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDF0B73B27FFD3D6004C14C5 /* LinkPreview.swift */; }; FDF0B7422804EA4F004C14C5 /* _002_SetupStandardJobs.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDF0B7412804EA4F004C14C5 /* _002_SetupStandardJobs.swift */; }; - FDF0B7472804F0CE004C14C5 /* DisappearingMessagesJob.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDF0B7462804F0CE004C14C5 /* DisappearingMessagesJob.swift */; }; FDF0B74928060D13004C14C5 /* QuotedReplyModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDF0B74828060D13004C14C5 /* QuotedReplyModel.swift */; }; FDF0B74B28061F7A004C14C5 /* InteractionAttachment.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDF0B74A28061F7A004C14C5 /* InteractionAttachment.swift */; }; - FDF0B74F28079E5E004C14C5 /* SendReadReceiptsJob.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDF0B74E28079E5E004C14C5 /* SendReadReceiptsJob.swift */; }; FDF0B7512807BA56004C14C5 /* NotificationsProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDF0B7502807BA56004C14C5 /* NotificationsProtocol.swift */; }; FDF0B7582807F368004C14C5 /* MessageReceiverError.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDF0B7572807F368004C14C5 /* MessageReceiverError.swift */; }; FDF0B75A2807F3A3004C14C5 /* MessageSenderError.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDF0B7592807F3A3004C14C5 /* MessageSenderError.swift */; }; @@ -928,16 +1009,10 @@ 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 /* NetworkError.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDF8487629405906007DCAE5 /* NetworkError.swift */; }; - FDF8487B29405906007DCAE5 /* HTTPHeader.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDF8487729405906007DCAE5 /* HTTPHeader.swift */; }; - FDF8487C29405906007DCAE5 /* HTTPMethod.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDF8487829405906007DCAE5 /* HTTPMethod.swift */; }; + FDF71EA32B072C2800A8D6B5 /* LibSessionMessage.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDF71EA22B072C2800A8D6B5 /* LibSessionMessage.swift */; }; + FDF71EA52B07363500A8D6B5 /* MessageReceiver+LibSession.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDF71EA42B07363500A8D6B5 /* MessageReceiver+LibSession.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 */; }; FDF8488929405B27007DCAE5 /* Data+Utilities.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDFD645A27F26D4600808CA1 /* Data+Utilities.swift */; }; FDF8489129405C13007DCAE5 /* SnodeAPINamespace.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDF8489029405C13007DCAE5 /* SnodeAPINamespace.swift */; }; FDF8489429405C1B007DCAE5 /* SnodeAPI.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDF8489329405C1B007DCAE5 /* SnodeAPI.swift */; }; @@ -948,7 +1023,6 @@ 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 */; }; 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 */; }; @@ -969,33 +1043,24 @@ 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 */; }; + FDF848DC29405C5B007DCAE5 /* RevokeSubaccountRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDF848BA29405C5A007DCAE5 /* RevokeSubaccountRequest.swift */; }; FDF848DD29405C5B007DCAE5 /* LegacySendMessageRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDF848BB29405C5A007DCAE5 /* LegacySendMessageRequest.swift */; }; - FDF848E429405D6E007DCAE5 /* SnodeAPIEndpoint.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDF848DF29405D6E007DCAE5 /* SnodeAPIEndpoint.swift */; }; FDF848E529405D6E007DCAE5 /* SnodeAPIError.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDF848E029405D6E007DCAE5 /* SnodeAPIError.swift */; }; FDF848E629405D6E007DCAE5 /* Destination.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDF848E129405D6E007DCAE5 /* Destination.swift */; }; - FDF848EB29405E4F007DCAE5 /* Network+OnionRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDF848E829405E4E007DCAE5 /* Network+OnionRequest.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 */; }; - FDFBB74B2A1EFF4900CA7350 /* Bencode.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDFBB74A2A1EFF4900CA7350 /* Bencode.swift */; }; FDFBB74D2A1F3C4E00CA7350 /* NotificationMetadata.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDFBB74C2A1F3C4E00CA7350 /* NotificationMetadata.swift */; }; - FDFBB7542A2023EB00CA7350 /* BencodeDecoderSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDFBB7532A2023EB00CA7350 /* BencodeDecoderSpec.swift */; }; FDFC4D9A29F0C51500992FB6 /* String+Trimming.swift in Sources */ = {isa = PBXBuildFile; fileRef = C3C2A5D22553860900C340D1 /* String+Trimming.swift */; }; - FDFD645927F26C6800808CA1 /* Array+Utilities.swift in Sources */ = {isa = PBXBuildFile; fileRef = C3C2A5D12553860800C340D1 /* Array+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 */; }; FDFE75B12ABD2D2400655640 /* _016_MakeBrokenProfileTimestampsNullable.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDFE75B02ABD2D2400655640 /* _016_MakeBrokenProfileTimestampsNullable.swift */; }; - FDFE75B22ABD469500655640 /* MockJobRunner.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD96F3A629DBD43D00401309 /* MockJobRunner.swift */; }; - FDFE75B32ABD469500655640 /* MockJobRunner.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD96F3A629DBD43D00401309 /* MockJobRunner.swift */; }; FDFE75B42ABD46B600655640 /* MockUserDefaults.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD83B9D127D59495005E1583 /* MockUserDefaults.swift */; }; FDFE75B52ABD46B700655640 /* MockUserDefaults.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD83B9D127D59495005E1583 /* MockUserDefaults.swift */; }; - FDFF61D729F2600300F95FB0 /* Identity+Utilities.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDFF61D629F2600300F95FB0 /* Identity+Utilities.swift */; }; FDFF9FDF2A787F57005E0628 /* JSONEncoder+Utilities.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDFF9FDE2A787F57005E0628 /* JSONEncoder+Utilities.swift */; }; /* End PBXBuildFile section */ @@ -1112,13 +1177,6 @@ remoteGlobalIDString = C33FD9AA255A548A00E217F9; remoteInfo = SignalUtilitiesKit; }; - FD37E9F128A5ED70003AE748 /* PBXContainerItemProxy */ = { - isa = PBXContainerItemProxy; - containerPortal = D221A080169C9E5E00537ABF /* Project object */; - proxyType = 1; - remoteGlobalIDString = C3C2A678255388CC00C340D1; - remoteInfo = SessionUtilitiesKit; - }; FD71160D28D00BAE00B47552 /* PBXContainerItemProxy */ = { isa = PBXContainerItemProxy; containerPortal = D221A080169C9E5E00537ABF /* Project object */; @@ -1126,13 +1184,6 @@ remoteGlobalIDString = D221A088169C9E5E00537ABF; remoteInfo = Session; }; - FD7C37B32BBB8B1E009DEEA7 /* PBXContainerItemProxy */ = { - isa = PBXContainerItemProxy; - containerPortal = D221A080169C9E5E00537ABF /* Project object */; - proxyType = 1; - remoteGlobalIDString = C3C2A59E255385C100C340D1; - remoteInfo = SessionSnodeKit; - }; FD7F74612BAAA4C7006DDFD8 /* PBXContainerItemProxy */ = { isa = PBXContainerItemProxy; containerPortal = D221A080169C9E5E00537ABF /* Project object */; @@ -1154,6 +1205,27 @@ remoteGlobalIDString = C3C2A678255388CC00C340D1; remoteInfo = SessionUtilitiesKit; }; + FDB348812BE86A4400B716C2 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = D221A080169C9E5E00537ABF /* Project object */; + proxyType = 1; + remoteGlobalIDString = FD9BDDF72A5D2294005F1EBC; + remoteInfo = SessionUtil; + }; + FDB348842BE86A4800B716C2 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = D221A080169C9E5E00537ABF /* Project object */; + proxyType = 1; + remoteGlobalIDString = FD9BDDF72A5D2294005F1EBC; + remoteInfo = SessionUtil; + }; + FDB5DAFF2A981C43002C8721 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = D221A080169C9E5E00537ABF /* Project object */; + proxyType = 1; + remoteGlobalIDString = C3C2A59E255385C100C340D1; + remoteInfo = SessionSnodeKit; + }; FDC4389327B9FFC700C60D73 /* PBXContainerItemProxy */ = { isa = PBXContainerItemProxy; containerPortal = D221A080169C9E5E00537ABF /* Project object */; @@ -1213,27 +1285,19 @@ 34CF0784203E6B77005C4D61 /* ringback_tone_ansi.caf */ = {isa = PBXFileReference; lastKnownFileType = file; name = ringback_tone_ansi.caf; path = Session/Meta/AudioFiles/ringback_tone_ansi.caf; sourceTree = SOURCE_ROOT; }; 34CF0786203E6B78005C4D61 /* end_call_tone_cept.caf */ = {isa = PBXFileReference; lastKnownFileType = file; name = end_call_tone_cept.caf; path = Session/Meta/AudioFiles/end_call_tone_cept.caf; sourceTree = SOURCE_ROOT; }; 34D1F04F1F7D45A60066283D /* GifPickerCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GifPickerCell.swift; sourceTree = ""; }; - 34D1F0511F7E8EA30066283D /* GiphyDownloader.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GiphyDownloader.swift; sourceTree = ""; }; - 34D99CE3217509C1000AFB39 /* AppEnvironment.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppEnvironment.swift; sourceTree = ""; }; - 34F308A01ECB469700BB7697 /* OWSBezierPathView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = OWSBezierPathView.h; sourceTree = ""; }; - 34F308A11ECB469700BB7697 /* OWSBezierPathView.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = OWSBezierPathView.m; sourceTree = ""; }; 4503F1BB20470A5B00CEE724 /* classic-quiet.aifc */ = {isa = PBXFileReference; lastKnownFileType = file; path = "classic-quiet.aifc"; sourceTree = ""; }; 4503F1BC20470A5B00CEE724 /* classic.aifc */ = {isa = PBXFileReference; lastKnownFileType = file; path = classic.aifc; sourceTree = ""; }; - 450DF2081E0DD2C6003D14BE /* UserNotificationsAdaptee.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; lineEnding = 0; path = UserNotificationsAdaptee.swift; sourceTree = ""; xcLanguageSpecificationIdentifier = xcode.lang.swift; }; - 451A13B01E13DED2000A50FD /* AppNotifications.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; lineEnding = 0; path = AppNotifications.swift; sourceTree = ""; xcLanguageSpecificationIdentifier = xcode.lang.swift; }; 4520D8D41D417D8E00123472 /* Photos.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Photos.framework; path = System/Library/Frameworks/Photos.framework; sourceTree = SDKROOT; }; 4521C3BF1F59F3BA00B4C582 /* TextFieldHelper.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TextFieldHelper.swift; sourceTree = ""; }; 453518681FC635DD00210559 /* SessionShareExtension.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = SessionShareExtension.appex; sourceTree = BUILT_PRODUCTS_DIR; }; 4535186D1FC635DD00210559 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/MainInterface.storyboard; sourceTree = ""; }; 4535186F1FC635DD00210559 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; - 4539B5851F79348F007141FF /* PushRegistrationManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PushRegistrationManager.swift; sourceTree = ""; }; 4542DF53208D40AC007B4E76 /* LoadingViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoadingViewController.swift; sourceTree = ""; }; 454A84032059C787008B8C75 /* MediaTileViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MediaTileViewController.swift; sourceTree = ""; }; 455A16DB1F1FEA0000F86704 /* Metal.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Metal.framework; path = System/Library/Frameworks/Metal.framework; sourceTree = SDKROOT; }; 455A16DC1F1FEA0000F86704 /* MetalKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = MetalKit.framework; path = System/Library/Frameworks/MetalKit.framework; sourceTree = SDKROOT; }; 45847E861E4283C30080EAB3 /* Intents.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Intents.framework; path = System/Library/Frameworks/Intents.framework; sourceTree = SDKROOT; }; 45A2F004204473A3002E978A /* NewMessage.aifc */ = {isa = PBXFileReference; lastKnownFileType = file; name = NewMessage.aifc; path = Session/Meta/AudioFiles/NewMessage.aifc; sourceTree = SOURCE_ROOT; }; - 45B201741DAECBFD00C461E0 /* Signal-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Signal-Bridging-Header.h"; sourceTree = ""; }; 45B5360D206DD8BB00D61655 /* UIResponder+OWS.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIResponder+OWS.swift"; sourceTree = ""; }; 45B74A5B2044AAB300CD42F8 /* aurora-quiet.aifc */ = {isa = PBXFileReference; lastKnownFileType = file; path = "aurora-quiet.aifc"; sourceTree = ""; }; 45B74A5C2044AAB300CD42F8 /* synth-quiet.aifc */ = {isa = PBXFileReference; lastKnownFileType = file; path = "synth-quiet.aifc"; sourceTree = ""; }; @@ -1263,7 +1327,6 @@ 45C0DC1A1E68FE9000E04C47 /* UIApplication+OWS.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "UIApplication+OWS.swift"; sourceTree = ""; }; 45C0DC1D1E69011F00E04C47 /* UIStoryboard+OWS.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "UIStoryboard+OWS.swift"; sourceTree = ""; }; 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; }; 4C090A1A210FD9C7001FD7F9 /* HapticFeedback.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HapticFeedback.swift; sourceTree = ""; }; 4C1885D1218F8E1C00B67051 /* PhotoGridViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PhotoGridViewCell.swift; sourceTree = ""; }; @@ -1296,18 +1359,15 @@ 7B4C75CC26BB92060000AC89 /* DeletedMessageView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeletedMessageView.swift; sourceTree = ""; }; 7B4EF2592934743000CB351D /* SessionTableViewTitleView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SessionTableViewTitleView.swift; sourceTree = ""; }; 7B50D64C28AC7CF80086CCEC /* silence.aiff */ = {isa = PBXFileReference; lastKnownFileType = audio.aiff; path = silence.aiff; sourceTree = ""; }; - 7B521E0929BFF84400C3C36A /* GroupLeavingJob.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GroupLeavingJob.swift; sourceTree = ""; }; 7B5233C32900E90F00F8F375 /* SessionLabelCarouselView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SessionLabelCarouselView.swift; sourceTree = ""; }; 7B5233C5290636D700F8F375 /* _018_DisappearingMessagesConfiguration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = _018_DisappearingMessagesConfiguration.swift; sourceTree = ""; }; 7B5802982AAEF1B50050EEB1 /* OpenGroupInvitationView_SwiftUI.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OpenGroupInvitationView_SwiftUI.swift; sourceTree = ""; }; 7B7037422834B81F000DCF35 /* ReactionContainerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReactionContainerView.swift; sourceTree = ""; }; 7B7037442834BCC0000DCF35 /* ReactionView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReactionView.swift; sourceTree = ""; }; 7B71A98E2925E2A600E54854 /* SessionFooterView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SessionFooterView.swift; sourceTree = ""; }; - 7B7AD41E2A5512CA00469FB1 /* GetExpirationJob.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GetExpirationJob.swift; sourceTree = ""; }; 7B7CB18D270D066F0079FF93 /* IncomingCallBanner.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IncomingCallBanner.swift; sourceTree = ""; }; 7B7CB18F270FB2150079FF93 /* MiniCallView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MiniCallView.swift; sourceTree = ""; }; 7B7CB191271508AD0079FF93 /* CallRingTonePlayer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CallRingTonePlayer.swift; sourceTree = ""; }; - 7B7E5B512A4D024C00A8208E /* ExpirationUpdateJob.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ExpirationUpdateJob.swift; sourceTree = ""; }; 7B81682228A4C1210069F315 /* UpdateTypes.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UpdateTypes.swift; sourceTree = ""; }; 7B81682728B310D50069F315 /* _007_HomeQueryOptimisationIndexes.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = _007_HomeQueryOptimisationIndexes.swift; sourceTree = ""; }; 7B81682928B6F1420069F315 /* ReactionResponse.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReactionResponse.swift; sourceTree = ""; }; @@ -1340,7 +1400,6 @@ 7BAF54D127ACCF01003D12F8 /* ShareAppExtensionContext.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ShareAppExtensionContext.swift; sourceTree = ""; }; 7BAF54D227ACCF01003D12F8 /* SAEScreenLockViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SAEScreenLockViewController.swift; sourceTree = ""; }; 7BAF54D527ACD0E2003D12F8 /* ReusableView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ReusableView.swift; sourceTree = ""; }; - 7BAFA1182A39669400B76CB9 /* BezierPathView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BezierPathView.swift; sourceTree = ""; }; 7BAFA7592AAEA281001DA43E /* LinkPreviewView_SwiftUI.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LinkPreviewView_SwiftUI.swift; sourceTree = ""; }; 7BBBDC43286EAD2D00747E59 /* TappableLabel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TappableLabel.swift; sourceTree = ""; }; 7BBBDC452875600700747E59 /* DocumentTitleViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DocumentTitleViewController.swift; sourceTree = ""; }; @@ -1348,8 +1407,6 @@ 7BC01A3D241F40AB00BC7C55 /* NotificationServiceExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationServiceExtension.swift; sourceTree = ""; }; 7BC01A3F241F40AB00BC7C55 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 7BC707F127290ACB002817AD /* SessionCallManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SessionCallManager.swift; sourceTree = ""; }; - 7BCD116B27016062006330F1 /* WebRTCSession+DataChannel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "WebRTCSession+DataChannel.swift"; sourceTree = ""; }; - 7BD477A727EC39F5004E2822 /* Atomic.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Atomic.swift; sourceTree = ""; }; 7BD687D02A5D0D1200D8E455 /* MessageInfoScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageInfoScreen.swift; sourceTree = ""; }; 7BD687D22A5D283200D8E455 /* build_libSession_util.sh */ = {isa = PBXFileReference; lastKnownFileType = text.script.sh; path = build_libSession_util.sh; sourceTree = ""; }; 7BDCFC0424206E7300641C39 /* SessionNotificationServiceExtension.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = SessionNotificationServiceExtension.entitlements; sourceTree = ""; }; @@ -1358,7 +1415,6 @@ 7BF8D1FA2A70AF57005F1D6E /* SwiftUI+Theme.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "SwiftUI+Theme.swift"; sourceTree = ""; }; 7BFA8AE22831D0D4001876F3 /* ContextMenuVC+EmojiReactsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ContextMenuVC+EmojiReactsView.swift"; sourceTree = ""; }; 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 = ""; }; 9409433D2C7EB81800D9D2E0 /* WebRTCSession+Constants.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "WebRTCSession+Constants.swift"; sourceTree = ""; }; 9409433F2C7ED62300D9D2E0 /* StartupError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StartupError.swift; sourceTree = ""; }; @@ -1388,7 +1444,6 @@ 9473386D2BDF5F3E00B9E169 /* InfoPlist.xcstrings */ = {isa = PBXFileReference; lastKnownFileType = text.json.xcstrings; path = InfoPlist.xcstrings; sourceTree = ""; }; 947AD68F2C8968FF000B2730 /* Constants.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Constants.swift; sourceTree = ""; }; 94B3DC162AF8592200C88531 /* QuoteView_SwiftUI.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = QuoteView_SwiftUI.swift; sourceTree = ""; }; - 94C5DCAF2BE88170003AA8C5 /* BezierPathView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BezierPathView.swift; sourceTree = ""; }; 94E9BC0C2C7BFBDA006984EA /* Localization+Style.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Localization+Style.swift"; sourceTree = ""; }; 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; }; @@ -1403,7 +1458,6 @@ B6FE7EB61ADD62FA00A6D22F /* PushKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = PushKit.framework; path = System/Library/Frameworks/PushKit.framework; sourceTree = SDKROOT; }; B8041A9425C8FA1D003C2166 /* MediaLoaderView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MediaLoaderView.swift; sourceTree = ""; }; B8041AA625C90927003C2166 /* TypingIndicatorCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TypingIndicatorCell.swift; sourceTree = ""; }; - B806ECA026C4A7E4008BDA44 /* WebRTCSession+UI.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "WebRTCSession+UI.swift"; sourceTree = ""; }; B80A579E23DFF1F300876683 /* NewClosedGroupVC.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NewClosedGroupVC.swift; sourceTree = ""; }; B817AD9926436593009DF825 /* SimplifiedConversationCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SimplifiedConversationCell.swift; sourceTree = ""; }; B817AD9B26436F73009DF825 /* ThreadPickerVC.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ThreadPickerVC.swift; sourceTree = ""; }; @@ -1420,7 +1474,6 @@ B849789525D4A2F500D0D0B3 /* LinkPreviewView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LinkPreviewView.swift; sourceTree = ""; }; B84A89BB25DE328A0040017D /* ProfilePictureVC.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfilePictureVC.swift; sourceTree = ""; }; B85357BE23A1AE0800AAF6CD /* SeedReminderView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SeedReminderView.swift; sourceTree = ""; }; - B8544E3023D16CA500299F14 /* DeviceUtilities.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeviceUtilities.swift; sourceTree = ""; }; B8569AC225CB5D2900DBA3DB /* ConversationVC+Interaction.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ConversationVC+Interaction.swift"; sourceTree = ""; }; B8569AE225CBB19A00DBA3DB /* DocumentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DocumentView.swift; sourceTree = ""; }; B86BD08323399ACF000F5AE3 /* Modal.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Modal.swift; sourceTree = ""; }; @@ -1430,18 +1483,15 @@ B8783E9D23EB948D00404FB8 /* UILabel+Interaction.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UILabel+Interaction.swift"; sourceTree = ""; }; B879D448247E1BE300DB3608 /* PathVC.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PathVC.swift; sourceTree = ""; }; B879D44A247E1D9200DB3608 /* PathStatusView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PathStatusView.swift; sourceTree = ""; }; - B87EF18026377A1D00124B3C /* Features.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Features.swift; sourceTree = ""; }; B885D5F52334A32100EE0D8E /* UIView+Constraints.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIView+Constraints.swift"; sourceTree = ""; }; B886B4A82398BA1500211ABE /* QRCode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = QRCode.swift; sourceTree = ""; }; B88FA7B726045D100049422F /* OpenGroupAPI.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OpenGroupAPI.swift; sourceTree = ""; }; B88FA7F1260C3EB10049422F /* OpenGroupSuggestionGrid.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OpenGroupSuggestionGrid.swift; sourceTree = ""; }; - B88FA7FA26114EA70049422F /* Hex.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Hex.swift; sourceTree = ""; }; B893063E2383961A005EAA8E /* ScanQRCodeWrapperVC.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ScanQRCodeWrapperVC.swift; sourceTree = ""; }; B894D0742339EDCF00B4D94D /* NukeDataModal.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NukeDataModal.swift; sourceTree = ""; }; B897621B25D201F7004F83B2 /* RoundIconButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoundIconButton.swift; sourceTree = ""; }; B8B320B6258C30D70020074B /* HTMLMetadata.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = HTMLMetadata.swift; sourceTree = ""; }; B8B558F026C4BB0600693325 /* CameraManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CameraManager.swift; sourceTree = ""; }; - B8B558FE26C4E05E00693325 /* WebRTCSession+MessageHandling.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "WebRTCSession+MessageHandling.swift"; sourceTree = ""; }; B8B5BCEB2394D869003823C9 /* SessionButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SessionButton.swift; sourceTree = ""; }; B8BB82A1238F356100BA5194 /* Values.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Values.swift; sourceTree = ""; }; B8BB82A4238F627000BA5194 /* HomeVC.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomeVC.swift; sourceTree = ""; }; @@ -1450,8 +1500,6 @@ B8BB82B423947F2D00BA5194 /* TextField.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TextField.swift; sourceTree = ""; }; B8BB82B82394911B00BA5194 /* Separator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Separator.swift; sourceTree = ""; }; B8BB82BD2394D4CE00BA5194 /* Fonts.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Fonts.swift; sourceTree = ""; }; - B8BC00BF257D90E30032E807 /* General.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = General.swift; sourceTree = ""; }; - B8BF43B926CC95FB007828D1 /* WebRTC+Utilities.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "WebRTC+Utilities.swift"; sourceTree = ""; }; B8C2B2C72563685C00551B4D /* CircleView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CircleView.swift; sourceTree = ""; }; B8CCF6342396005F0091D419 /* SpaceMono-Regular.ttf */ = {isa = PBXFileReference; lastKnownFileType = file; path = "SpaceMono-Regular.ttf"; sourceTree = ""; }; B8CCF638239721E20091D419 /* TabBar.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TabBar.swift; sourceTree = ""; }; @@ -1461,14 +1509,12 @@ B8D0A26825E4A2C200C1835E /* Onboarding.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Onboarding.swift; sourceTree = ""; }; B8D84EA225DF745A005A043E /* LinkPreviewState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LinkPreviewState.swift; sourceTree = ""; }; B8D84ECE25E3108A005A043E /* ExpandingAttachmentsButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ExpandingAttachmentsButton.swift; sourceTree = ""; }; - B8DE1FB326C22F2F0079C9CE /* WebRTCSession.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WebRTCSession.swift; sourceTree = ""; }; B8DE1FB526C22FCB0079C9CE /* CallMessage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CallMessage.swift; sourceTree = ""; }; B8EB20ED2640F28000773E52 /* VisibleMessage+OpenGroupInvitation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "VisibleMessage+OpenGroupInvitation.swift"; sourceTree = ""; }; B8EB20EF2640F7F000773E52 /* OpenGroupInvitationView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OpenGroupInvitationView.swift; sourceTree = ""; }; B8F5F58225EC94A6003BF8D4 /* Collection+Utilities.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Collection+Utilities.swift"; sourceTree = ""; }; B8F5F60225EDE16F003BF8D4 /* DataExtractionNotification.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DataExtractionNotification.swift; sourceTree = ""; }; B8F5F71925F1B35C003BF8D4 /* MediaPlaceholderView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MediaPlaceholderView.swift; 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; }; 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 = ""; }; @@ -1478,53 +1524,32 @@ C300A5FB2554B0A000555489 /* MessageReceiver.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageReceiver.swift; sourceTree = ""; }; C302093D25DCBF07001F572D /* MentionSelectionView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MentionSelectionView.swift; sourceTree = ""; }; C31A6C5B247F2CF3001123EF /* CGRect+Utilities.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "CGRect+Utilities.swift"; sourceTree = ""; }; - C31D1DE22521718E005D4DA8 /* UserSelectionVC.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserSelectionVC.swift; sourceTree = ""; }; C328250E25CA06020062D0A7 /* VoiceMessageView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VoiceMessageView.swift; sourceTree = ""; }; C328251E25CA3A900062D0A7 /* QuoteView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = QuoteView.swift; sourceTree = ""; }; C328252F25CA55360062D0A7 /* ContextMenuWindow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContextMenuWindow.swift; sourceTree = ""; }; C328253F25CA55880062D0A7 /* ContextMenuVC.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContextMenuVC.swift; sourceTree = ""; }; C328254825CA60E60062D0A7 /* ContextMenuVC+Action.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ContextMenuVC+Action.swift"; sourceTree = ""; }; C328255125CA64470062D0A7 /* ContextMenuVC+ActionView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ContextMenuVC+ActionView.swift"; sourceTree = ""; }; - C32C5A87256DBCF9003C73A2 /* MessageReceiver+ClosedGroups.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "MessageReceiver+ClosedGroups.swift"; sourceTree = ""; }; + C32C5A87256DBCF9003C73A2 /* MessageReceiver+LegacyClosedGroups.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "MessageReceiver+LegacyClosedGroups.swift"; sourceTree = ""; }; C33100272559000A00070591 /* UIView+Utilities.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIView+Utilities.swift"; sourceTree = ""; }; C331FF1B2558F9D300070591 /* SessionUIKit.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = SessionUIKit.framework; sourceTree = BUILT_PRODUCTS_DIR; }; C331FF1D2558F9D300070591 /* SessionUIKit.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = SessionUIKit.h; sourceTree = ""; }; C331FF1E2558F9D300070591 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; C33FD9AB255A548A00E217F9 /* SignalUtilitiesKit.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = SignalUtilitiesKit.framework; sourceTree = BUILT_PRODUCTS_DIR; }; - C33FD9AD255A548A00E217F9 /* SignalUtilitiesKit.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = SignalUtilitiesKit.h; sourceTree = ""; }; C33FD9AE255A548A00E217F9 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; C33FDA7A255A57FB00E217F9 /* NSRegularExpression+SSK.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "NSRegularExpression+SSK.swift"; sourceTree = ""; }; C33FDA87255A57FC00E217F9 /* TypingIndicators.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TypingIndicators.swift; sourceTree = ""; }; C33FDA9E255A57FF00E217F9 /* ReverseDispatchQueue.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ReverseDispatchQueue.swift; sourceTree = ""; }; - C33FDAA8255A57FF00E217F9 /* BuildConfiguration.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BuildConfiguration.swift; sourceTree = ""; }; - C33FDADE255A580400E217F9 /* SwiftSingletons.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SwiftSingletons.swift; sourceTree = ""; }; C33FDAF1255A580500E217F9 /* ThumbnailService.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ThumbnailService.swift; sourceTree = ""; }; - C33FDAF2255A580500E217F9 /* ProxiedContentDownloader.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ProxiedContentDownloader.swift; sourceTree = ""; }; C33FDAFD255A580600E217F9 /* LRUCache.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LRUCache.swift; sourceTree = ""; }; - C33FDB34255A580B00E217F9 /* ClosedGroupPoller.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ClosedGroupPoller.swift; sourceTree = ""; }; - C33FDB3A255A580B00E217F9 /* Poller.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Poller.swift; sourceTree = ""; }; + C33FDB3A255A580B00E217F9 /* PollerType.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PollerType.swift; sourceTree = ""; }; C33FDB3F255A580C00E217F9 /* String+SSK.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "String+SSK.swift"; sourceTree = ""; }; - C33FDB49255A580C00E217F9 /* WeakTimer.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = WeakTimer.swift; sourceTree = ""; }; - C33FDB51255A580D00E217F9 /* NSUserDefaults+OWS.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "NSUserDefaults+OWS.h"; sourceTree = ""; }; - C33FDB68255A580F00E217F9 /* ContentProxy.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ContentProxy.swift; sourceTree = ""; }; - C33FDB69255A580F00E217F9 /* FeatureFlags.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FeatureFlags.swift; sourceTree = ""; }; - C33FDB6B255A580F00E217F9 /* SNUserDefaults.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SNUserDefaults.swift; sourceTree = ""; }; - C33FDB77255A581000E217F9 /* NSUserDefaults+OWS.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "NSUserDefaults+OWS.m"; sourceTree = ""; }; - C33FDB80255A581100E217F9 /* Notification+Loki.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "Notification+Loki.swift"; sourceTree = ""; }; - C33FDB8F255A581200E217F9 /* ParamParser.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ParamParser.swift; sourceTree = ""; }; C33FDBA8255A581500E217F9 /* LinkPreviewDraft.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LinkPreviewDraft.swift; sourceTree = ""; }; C33FDBD3255A581800E217F9 /* OWSSignalAddress.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OWSSignalAddress.swift; sourceTree = ""; }; C33FDBDE255A581900E217F9 /* PushNotificationAPI.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PushNotificationAPI.swift; sourceTree = ""; }; C34A977325A3E34A00852C71 /* ClosedGroupControlMessage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ClosedGroupControlMessage.swift; sourceTree = ""; }; C34C8F7323A7830A00D82669 /* SpaceMono-Bold.ttf */ = {isa = PBXFileReference; lastKnownFileType = file; path = "SpaceMono-Bold.ttf"; sourceTree = ""; }; - C352A2FE25574B6300338F3E /* MessageSendJob.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageSendJob.swift; sourceTree = ""; }; C352A30825574D8400338F3E /* Message+Destination.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Message+Destination.swift"; sourceTree = ""; }; - C352A31225574F5200338F3E /* MessageReceiveJob.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageReceiveJob.swift; sourceTree = ""; }; - C352A32E2557549C00338F3E /* NotifyPushServerJob.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotifyPushServerJob.swift; sourceTree = ""; }; - C352A348255781F400338F3E /* AttachmentDownloadJob.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AttachmentDownloadJob.swift; sourceTree = ""; }; - C352A35A2557824E00338F3E /* AttachmentUploadJob.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AttachmentUploadJob.swift; sourceTree = ""; }; - C352A36C2557858D00338F3E /* NSTimer+Proxying.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = "NSTimer+Proxying.m"; sourceTree = ""; }; - C352A3762557859C00338F3E /* NSTimer+Proxying.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "NSTimer+Proxying.h"; sourceTree = ""; }; C3548F0724456AB6009433A8 /* UIView+Wrapping.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIView+Wrapping.swift"; sourceTree = ""; }; C354E75923FE2A7600CE22E3 /* BaseVC.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BaseVC.swift; sourceTree = ""; }; C35D0DB425AE5F1200B6BF49 /* UIEdgeInsets.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UIEdgeInsets.swift; sourceTree = ""; }; @@ -1532,7 +1557,7 @@ C374EEEA25DA3CA70073A857 /* ConversationTitleView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConversationTitleView.swift; sourceTree = ""; }; C374EEF325DB31D40073A857 /* VoiceMessageRecordingView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VoiceMessageRecordingView.swift; sourceTree = ""; }; C379DCF3256735770002D4EB /* VisibleMessage+Attachment.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "VisibleMessage+Attachment.swift"; sourceTree = ""; }; - C38D5E8C2575011E00B6A65C /* MessageSender+ClosedGroups.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "MessageSender+ClosedGroups.swift"; sourceTree = ""; }; + C38D5E8C2575011E00B6A65C /* MessageSender+LegacyClosedGroups.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "MessageSender+LegacyClosedGroups.swift"; sourceTree = ""; }; C38EEF09255B49A8007E1867 /* SNProtoEnvelope+Conversion.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "SNProtoEnvelope+Conversion.swift"; sourceTree = ""; }; C38EF224255B6D5D007E1867 /* SignalAttachment.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = SignalAttachment.swift; path = "SessionMessagingKit/Sending & Receiving/Attachments/SignalAttachment.swift"; sourceTree = SOURCE_ROOT; }; C38EF226255B6D5D007E1867 /* ShareViewDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = ShareViewDelegate.swift; path = SignalUtilitiesKit/Utilities/ShareViewDelegate.swift; sourceTree = SOURCE_ROOT; }; @@ -1546,14 +1571,11 @@ C38EF2E2255B6DB9007E1867 /* ScreenLock.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = ScreenLock.swift; path = "SignalUtilitiesKit/Screen Lock/ScreenLock.swift"; sourceTree = SOURCE_ROOT; }; C38EF2EC255B6DBA007E1867 /* ProximityMonitoringManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = ProximityMonitoringManager.swift; path = SessionMessagingKit/Utilities/ProximityMonitoringManager.swift; sourceTree = SOURCE_ROOT; }; C38EF2EF255B6DBB007E1867 /* Weak.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = Weak.swift; path = SessionUtilitiesKit/General/Weak.swift; sourceTree = SOURCE_ROOT; }; - C38EF2F2255B6DBC007E1867 /* Searcher.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = Searcher.swift; path = SignalUtilitiesKit/Utilities/Searcher.swift; sourceTree = SOURCE_ROOT; }; C38EF2F5255B6DBC007E1867 /* OWSAudioPlayer.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = OWSAudioPlayer.h; path = SessionMessagingKit/Utilities/OWSAudioPlayer.h; sourceTree = SOURCE_ROOT; }; 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; }; C38EF304255B6DBE007E1867 /* ImageCache.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = ImageCache.swift; path = SignalUtilitiesKit/Utilities/ImageCache.swift; 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; }; C38EF349255B6DC7007E1867 /* ModalActivityIndicatorViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = ModalActivityIndicatorViewController.swift; path = "SignalUtilitiesKit/Shared View Controllers/ModalActivityIndicatorViewController.swift"; sourceTree = SOURCE_ROOT; }; C38EF358255B6DCC007E1867 /* MediaMessageView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = MediaMessageView.swift; path = "SignalUtilitiesKit/Media Viewing & Editing/MediaMessageView.swift"; sourceTree = SOURCE_ROOT; }; @@ -1585,12 +1607,10 @@ C38EF3E1255B6DF3007E1867 /* TappableView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = TappableView.swift; path = "SignalUtilitiesKit/Shared Views/TappableView.swift"; sourceTree = SOURCE_ROOT; }; C38EF3E2255B6DF3007E1867 /* GalleryRailView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = GalleryRailView.swift; path = "SignalUtilitiesKit/Shared Views/GalleryRailView.swift"; sourceTree = SOURCE_ROOT; }; C38EF3E7255B6DF5007E1867 /* OWSButton.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = OWSButton.swift; path = "SignalUtilitiesKit/Shared Views/OWSButton.swift"; sourceTree = SOURCE_ROOT; }; - C38EF3E9255B6DF6007E1867 /* Toast.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = Toast.swift; path = "SignalUtilitiesKit/Shared Views/Toast.swift"; sourceTree = SOURCE_ROOT; }; C38EF3EE255B6DF6007E1867 /* GradientView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = GradientView.swift; path = SessionUIKit/Components/GradientView.swift; sourceTree = SOURCE_ROOT; }; C3A71D0A2558989C0043A11F /* MessageWrapper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageWrapper.swift; sourceTree = ""; }; C3A71D1C25589AC30043A11F /* WebSocketProto.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = WebSocketProto.swift; sourceTree = ""; }; C3A71D1D25589AC30043A11F /* WebSocketResources.pb.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = WebSocketResources.pb.swift; sourceTree = ""; }; - C3A71F882558BA9F0043A11F /* Mnemonic.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Mnemonic.swift; sourceTree = ""; }; C3AAFFF125AE99710089E6DD /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; C3ADC66026426688005F1414 /* ShareNavController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShareNavController.swift; sourceTree = ""; }; C3BBE0C62554F1570050F1E3 /* FixedWidthInteger+BigEndian.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "FixedWidthInteger+BigEndian.swift"; sourceTree = ""; }; @@ -1599,14 +1619,11 @@ C3C2A5A2255385C100C340D1 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; C3C2A5B9255385ED00C340D1 /* Configuration.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Configuration.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 = ""; }; - C3C2A5D42553860A00C340D1 /* Threading.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Threading.swift; sourceTree = ""; }; + C3C2A5D42553860A00C340D1 /* Threading+SSK.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "Threading+SSK.swift"; sourceTree = ""; }; C3C2A5D52553860A00C340D1 /* Dictionary+Utilities.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "Dictionary+Utilities.swift"; sourceTree = ""; }; C3C2A5D82553860B00C340D1 /* Data+Utilities.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "Data+Utilities.swift"; sourceTree = ""; }; - C3C2A5D92553860B00C340D1 /* JSON.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = JSON.swift; sourceTree = ""; }; C3C2A679255388CC00C340D1 /* SessionUtilitiesKit.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = SessionUtilitiesKit.framework; sourceTree = BUILT_PRODUCTS_DIR; }; - C3C2A67B255388CC00C340D1 /* SessionUtilitiesKit.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = SessionUtilitiesKit.h; sourceTree = ""; }; C3C2A67C255388CC00C340D1 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; C3C2A6F025539DE700C340D1 /* SessionMessagingKit.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = SessionMessagingKit.framework; sourceTree = BUILT_PRODUCTS_DIR; }; C3C2A6F225539DE700C340D1 /* SessionMessagingKit.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = SessionMessagingKit.h; sourceTree = ""; }; @@ -1618,7 +1635,6 @@ C3C2A7702553A41E00C340D1 /* ControlMessage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ControlMessage.swift; sourceTree = ""; }; C3C2A7822553AAF200C340D1 /* SNProto.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SNProto.swift; sourceTree = ""; }; C3C2A7832553AAF300C340D1 /* SessionProtos.pb.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SessionProtos.pb.swift; sourceTree = ""; }; - C3C2AC2D2553CBEB00C340D1 /* String+Trimming.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "String+Trimming.swift"; sourceTree = ""; }; C3C3CF8824D8EED300E1CCE7 /* TextView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TextView.swift; sourceTree = ""; }; C3CA3AA1255CDADA00F4C6D4 /* english.txt */ = {isa = PBXFileReference; lastKnownFileType = text; path = english.txt; sourceTree = ""; }; C3CA3AB3255CDAE600F4C6D4 /* japanese.txt */ = {isa = PBXFileReference; lastKnownFileType = text; path = japanese.txt; sourceTree = ""; }; @@ -1627,10 +1643,6 @@ C3D0972A2510499C00F6E3E4 /* BackgroundPoller.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BackgroundPoller.swift; sourceTree = ""; }; C3DAB3232480CB2A00725F25 /* SRCopyableLabel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SRCopyableLabel.swift; sourceTree = ""; }; C3DB66AB260ACA42001EFC55 /* OpenGroupManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OpenGroupManager.swift; sourceTree = ""; }; - C3DB66C2260ACCE6001EFC55 /* OpenGroupPoller.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OpenGroupPoller.swift; sourceTree = ""; }; - C3E5C2F9251DBABB0040DFFC /* EditClosedGroupVC.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EditClosedGroupVC.swift; sourceTree = ""; }; - C3ECBF7A257056B700EA7FCE /* Threading.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Threading.swift; sourceTree = ""; }; - C3F0A52F255C80BC007BE2A3 /* NoopNotificationsManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NoopNotificationsManager.swift; sourceTree = ""; }; 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; }; @@ -1644,19 +1656,32 @@ E1A0AD8B16E13FDD0071E604 /* CoreFoundation.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = CoreFoundation.framework; path = System/Library/Frameworks/CoreFoundation.framework; sourceTree = SDKROOT; }; 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; }; + FD0150252CA23D5B005B08A1 /* SessionUtilitiesKit.xctestplan */ = {isa = PBXFileReference; lastKnownFileType = text; path = SessionUtilitiesKit.xctestplan; sourceTree = ""; }; + FD0150272CA23DB7005B08A1 /* GRDBExtensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GRDBExtensions.swift; sourceTree = ""; }; + FD01502D2CA24310005B08A1 /* BatchRequestSpec.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BatchRequestSpec.swift; sourceTree = ""; }; + FD01502E2CA24310005B08A1 /* BatchResponseSpec.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BatchResponseSpec.swift; sourceTree = ""; }; + FD01502F2CA24310005B08A1 /* HeaderSpec.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HeaderSpec.swift; sourceTree = ""; }; + FD0150302CA24310005B08A1 /* PreparedRequestSpec.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PreparedRequestSpec.swift; sourceTree = ""; }; + FD0150312CA24310005B08A1 /* RequestSpec.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RequestSpec.swift; sourceTree = ""; }; + FD0150372CA24328005B08A1 /* MockJobRunner.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockJobRunner.swift; sourceTree = ""; }; + FD01503D2CA2433D005B08A1 /* BencodeDecoderSpec.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BencodeDecoderSpec.swift; sourceTree = ""; }; + FD01503E2CA2433D005B08A1 /* BencodeEncoderSpec.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BencodeEncoderSpec.swift; sourceTree = ""; }; + FD01503F2CA2433D005B08A1 /* VersionSpec.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VersionSpec.swift; sourceTree = ""; }; + FD0150432CA243BB005B08A1 /* LibSessionSpec.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LibSessionSpec.swift; sourceTree = ""; }; + FD0150442CA243BB005B08A1 /* LibSessionUtilSpec.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LibSessionUtilSpec.swift; sourceTree = ""; }; + FD0150472CA243CB005B08A1 /* Mock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Mock.swift; sourceTree = ""; }; + FD01504D2CA243E7005B08A1 /* TypeConversionUtilitiesSpec.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TypeConversionUtilitiesSpec.swift; sourceTree = ""; }; FD0150572CA27DEE005B08A1 /* ScrollableLabel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ScrollableLabel.swift; sourceTree = ""; }; + FD02CC112C367761009AB976 /* Request+PushNotificationAPI.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "Request+PushNotificationAPI.swift"; sourceTree = ""; }; + FD02CC132C3677E6009AB976 /* Request+OpenGroupAPI.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "Request+OpenGroupAPI.swift"; sourceTree = ""; }; + FD02CC152C3681EF009AB976 /* RevokeSubaccountResponse.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RevokeSubaccountResponse.swift; sourceTree = ""; }; FD0606BE2BC8C10200C3816E /* _005_AddJobUniqueHash.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = _005_AddJobUniqueHash.swift; sourceTree = ""; }; FD0606C22BCE13ED00C3816E /* MessageRequestFooterView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageRequestFooterView.swift; sourceTree = ""; }; - FD078E4727E02561000769AF /* CommonMockedExtensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CommonMockedExtensions.swift; sourceTree = ""; }; - FD078E4C27E17156000769AF /* MockOGMCache.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockOGMCache.swift; sourceTree = ""; }; FD0969F82A69FFE700C5C365 /* Mocked.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Mocked.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 = ""; }; FD09797127FAA2F500936362 /* Optional+Utilities.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Optional+Utilities.swift"; sourceTree = ""; }; - FD09797327FAB3E200936362 /* ProfileManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfileManager.swift; sourceTree = ""; }; - FD09797627FAB7A600936362 /* Data+Image.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Data+Image.swift"; sourceTree = ""; }; - FD09797827FAB7E800936362 /* ImageFormat.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageFormat.swift; sourceTree = ""; }; FD09797C27FBDB2000936362 /* Notification+Utilities.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Notification+Utilities.swift"; sourceTree = ""; }; FD09798027FCFEE800936362 /* SessionThread.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SessionThread.swift; sourceTree = ""; }; FD09798227FD1A1500936362 /* ClosedGroup.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ClosedGroup.swift; sourceTree = ""; }; @@ -1671,13 +1696,17 @@ FD09B7E228865FDA00ED0B66 /* HighlightMentionBackgroundView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HighlightMentionBackgroundView.swift; sourceTree = ""; }; FD09B7E4288670BB00ED0B66 /* _008_EmojiReacts.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = _008_EmojiReacts.swift; sourceTree = ""; }; FD09B7E6288670FD00ED0B66 /* Reaction.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Reaction.swift; sourceTree = ""; }; - FD09C5E1282212B3000CE219 /* JobDependencies.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = JobDependencies.swift; sourceTree = ""; }; FD09C5E528260FF9000CE219 /* MediaGalleryViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MediaGalleryViewModel.swift; sourceTree = ""; }; FD09C5E728264937000CE219 /* MediaDetailViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MediaDetailViewController.swift; sourceTree = ""; }; FD09C5E9282A1BB2000CE219 /* ThreadTypingIndicator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ThreadTypingIndicator.swift; sourceTree = ""; }; FD09C5EB282B8F17000CE219 /* AttachmentError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AttachmentError.swift; sourceTree = ""; }; FD0B77AF29B69A65009169BA /* TopBannerController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TopBannerController.swift; sourceTree = ""; }; FD0B77B129B82B7A009169BA /* ArrayUtilitiesSpec.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ArrayUtilitiesSpec.swift; sourceTree = ""; }; + FD0E353A2AB98773006A81F7 /* AppVersion.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppVersion.swift; sourceTree = ""; }; + FD10AF092AF319EE007709E5 /* DeveloperSettingsViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeveloperSettingsViewModel.swift; sourceTree = ""; }; + FD10AF0B2AF32B9A007709E5 /* SessionListViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SessionListViewModel.swift; sourceTree = ""; }; + FD10AF112AF85D11007709E5 /* Feature+ServiceNetwork.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Feature+ServiceNetwork.swift"; sourceTree = ""; }; + FD11E22F2CA4F498001BAF58 /* DestinationSpec.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DestinationSpec.swift; sourceTree = ""; }; FD12A83C2AD63BCC00EEBA0D /* EditableState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EditableState.swift; sourceTree = ""; }; FD12A83E2AD63BDF00EEBA0D /* Navigatable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Navigatable.swift; sourceTree = ""; }; FD12A8402AD63BEA00EEBA0D /* NavigatableState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NavigatableState.swift; sourceTree = ""; }; @@ -1685,7 +1714,6 @@ FD12A8442AD63C2200EEBA0D /* TableDataState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TableDataState.swift; sourceTree = ""; }; FD12A8462AD63C3400EEBA0D /* PagedObservationSource.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PagedObservationSource.swift; sourceTree = ""; }; FD12A8482AD63C4700EEBA0D /* SessionNavItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SessionNavItem.swift; sourceTree = ""; }; - FD12A84A2AD6458800EEBA0D /* DifferenceKit+Utilities.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "DifferenceKit+Utilities.swift"; sourceTree = ""; }; FD16AB602A1DD9B60083D849 /* ProfilePictureView+Convenience.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ProfilePictureView+Convenience.swift"; sourceTree = ""; }; FD17D79527F3E04600122BE0 /* _001_InitialSetupMigration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = _001_InitialSetupMigration.swift; sourceTree = ""; }; FD17D79827F40AB800122BE0 /* _003_YDBToGRDBMigration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = _003_YDBToGRDBMigration.swift; sourceTree = ""; }; @@ -1704,36 +1732,107 @@ FD17D7CC27F546FF00122BE0 /* Setting.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Setting.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 = ""; }; + FD19363B2ACA3134004BCF0F /* ResponseInfo+SnodeAPI.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ResponseInfo+SnodeAPI.swift"; sourceTree = ""; }; FD19363E2ACA66DE004BCF0F /* DatabaseSpec.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DatabaseSpec.swift; sourceTree = ""; }; - FD1936402ACA7BD8004BCF0F /* Result+Utilities.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Result+Utilities.swift"; sourceTree = ""; }; + FD1936462ACE752F004BCF0F /* _005_AddJobUniqueHash.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = _005_AddJobUniqueHash.swift; sourceTree = ""; }; FD1A94FA2900D1C2000D73D3 /* PersistableRecord+Utilities.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "PersistableRecord+Utilities.swift"; sourceTree = ""; }; FD1A94FD2900D2EA000D73D3 /* PersistableRecordUtilitiesSpec.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PersistableRecordUtilitiesSpec.swift; sourceTree = ""; }; FD1C98E3282E3C5B00B76F9E /* UINavigationBar+Utilities.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UINavigationBar+Utilities.swift"; sourceTree = ""; }; FD1D73292A85AA2000E3F410 /* Setting+Utilities.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Setting+Utilities.swift"; sourceTree = ""; }; FD1D732D2A86114600E3F410 /* _015_BlockCommunityMessageRequests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = _015_BlockCommunityMessageRequests.swift; sourceTree = ""; }; FD1F9C9E2A862BE60050F671 /* MigrationRequirement.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MigrationRequirement.swift; sourceTree = ""; }; - FD22724A2C326E75004D8A6C /* CustomArgSummaryDescribable+SMK.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "CustomArgSummaryDescribable+SMK.swift"; sourceTree = ""; }; - FD22724D2C327BA5004D8A6C /* SSKMockedExtensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SSKMockedExtensions.swift; sourceTree = ""; }; - FD23CE1A2A651E6D0000B97C /* Network.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Network.swift; sourceTree = ""; }; - FD23CE232A675C440000B97C /* Crypto+SessionMessagingKit.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Crypto+SessionMessagingKit.swift"; sourceTree = ""; }; + FD2272532C32911A004D8A6C /* SendReadReceiptsJob.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SendReadReceiptsJob.swift; sourceTree = ""; }; + FD2272542C32911A004D8A6C /* GroupLeavingJob.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GroupLeavingJob.swift; sourceTree = ""; }; + FD2272552C32911A004D8A6C /* CheckForAppUpdatesJob.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CheckForAppUpdatesJob.swift; sourceTree = ""; }; + FD2272562C32911A004D8A6C /* FailedMessageSendsJob.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FailedMessageSendsJob.swift; sourceTree = ""; }; + FD2272572C32911A004D8A6C /* ProcessPendingGroupMemberRemovalsJob.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ProcessPendingGroupMemberRemovalsJob.swift; sourceTree = ""; }; + FD2272582C32911A004D8A6C /* DisappearingMessagesJob.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DisappearingMessagesJob.swift; sourceTree = ""; }; + FD2272592C32911A004D8A6C /* MessageReceiveJob.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MessageReceiveJob.swift; sourceTree = ""; }; + FD22725A2C32911A004D8A6C /* FailedAttachmentDownloadsJob.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FailedAttachmentDownloadsJob.swift; sourceTree = ""; }; + FD22725B2C32911B004D8A6C /* ConfigurationSyncJob.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ConfigurationSyncJob.swift; sourceTree = ""; }; + FD22725C2C32911B004D8A6C /* ConfigMessageReceiveJob.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ConfigMessageReceiveJob.swift; sourceTree = ""; }; + FD22725D2C32911B004D8A6C /* RetrieveDefaultOpenGroupRoomsJob.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RetrieveDefaultOpenGroupRoomsJob.swift; sourceTree = ""; }; + FD22725E2C32911B004D8A6C /* ExpirationUpdateJob.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ExpirationUpdateJob.swift; sourceTree = ""; }; + FD22725F2C32911B004D8A6C /* AttachmentUploadJob.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AttachmentUploadJob.swift; sourceTree = ""; }; + FD2272602C32911B004D8A6C /* AttachmentDownloadJob.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AttachmentDownloadJob.swift; sourceTree = ""; }; + FD2272612C32911B004D8A6C /* DisplayPictureDownloadJob.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DisplayPictureDownloadJob.swift; sourceTree = ""; }; + FD2272622C32911B004D8A6C /* GroupInviteMemberJob.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GroupInviteMemberJob.swift; sourceTree = ""; }; + FD2272632C32911B004D8A6C /* MessageSendJob.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MessageSendJob.swift; sourceTree = ""; }; + FD2272642C32911B004D8A6C /* GroupPromoteMemberJob.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GroupPromoteMemberJob.swift; sourceTree = ""; }; + FD2272652C32911B004D8A6C /* NotifyPushServerJob.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NotifyPushServerJob.swift; sourceTree = ""; }; + FD2272662C32911B004D8A6C /* GarbageCollectionJob.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GarbageCollectionJob.swift; sourceTree = ""; }; + FD2272672C32911B004D8A6C /* GetExpirationJob.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GetExpirationJob.swift; sourceTree = ""; }; + FD22726A2C32911C004D8A6C /* UpdateProfilePictureJob.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UpdateProfilePictureJob.swift; sourceTree = ""; }; + FD2272822C337830004D8A6C /* GroupPoller.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GroupPoller.swift; sourceTree = ""; }; + FD2272952C33E335004D8A6C /* ContentProxy.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ContentProxy.swift; sourceTree = ""; }; + FD2272962C33E335004D8A6C /* UpdatableTimestamp.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UpdatableTimestamp.swift; sourceTree = ""; }; + FD2272972C33E335004D8A6C /* ValidatableResponse.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ValidatableResponse.swift; sourceTree = ""; }; + FD2272982C33E336004D8A6C /* IPv4.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = IPv4.swift; sourceTree = ""; }; + FD2272992C33E336004D8A6C /* Network.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Network.swift; sourceTree = ""; }; + FD22729A2C33E336004D8A6C /* HTTPQueryParam.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = HTTPQueryParam.swift; sourceTree = ""; }; + FD22729B2C33E336004D8A6C /* JSON.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = JSON.swift; sourceTree = ""; }; + FD22729C2C33E336004D8A6C /* NetworkError.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NetworkError.swift; sourceTree = ""; }; + FD22729D2C33E336004D8A6C /* ProxiedContentDownloader.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ProxiedContentDownloader.swift; sourceTree = ""; }; + FD22729E2C33E336004D8A6C /* PreparedRequest.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PreparedRequest.swift; sourceTree = ""; }; + FD22729F2C33E336004D8A6C /* BatchRequest.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BatchRequest.swift; sourceTree = ""; }; + FD2272A02C33E336004D8A6C /* SwarmDrainBehaviour.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SwarmDrainBehaviour.swift; sourceTree = ""; }; + FD2272A12C33E336004D8A6C /* BatchResponse.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BatchResponse.swift; sourceTree = ""; }; + FD2272A32C33E337004D8A6C /* Request.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Request.swift; sourceTree = ""; }; + FD2272A52C33E337004D8A6C /* ResponseInfo.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ResponseInfo.swift; sourceTree = ""; }; + FD2272A62C33E337004D8A6C /* HTTPHeader.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = HTTPHeader.swift; sourceTree = ""; }; + FD2272A72C33E337004D8A6C /* HTTPMethod.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = HTTPMethod.swift; sourceTree = ""; }; + FD2272A82C33E337004D8A6C /* URLResponse+Utilities.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "URLResponse+Utilities.swift"; sourceTree = ""; }; + FD2272BD2C34B710004D8A6C /* Publisher+Utilities.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Publisher+Utilities.swift"; sourceTree = ""; }; + FD2272D32C34ECE1004D8A6C /* BencodeEncoder.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BencodeEncoder.swift; sourceTree = ""; }; + FD2272D72C34EDE6004D8A6C /* SnodeAPIEndpoint.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SnodeAPIEndpoint.swift; sourceTree = ""; }; + FD2272DC2C34EFFA004D8A6C /* AppSetup.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppSetup.swift; sourceTree = ""; }; + FD2272DF2C3502BE004D8A6C /* Setting+Theme.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Setting+Theme.swift"; sourceTree = ""; }; + FD2272E32C35134B004D8A6C /* Data+Utilities.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Data+Utilities.swift"; sourceTree = ""; }; + FD2272E52C351378004D8A6C /* SUIKImageFormat.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SUIKImageFormat.swift; sourceTree = ""; }; + FD2272E92C351CA7004D8A6C /* Threading.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Threading.swift; sourceTree = ""; }; + FD2272EB2C352155004D8A6C /* Feature.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Feature.swift; sourceTree = ""; }; + FD2272ED2C3521D6004D8A6C /* FeatureConfig.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FeatureConfig.swift; sourceTree = ""; }; + FD2272EF2C352200004D8A6C /* General.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = General.swift; sourceTree = ""; }; + FD2272F12C352D8D004D8A6C /* LibSession+SharedGroup.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "LibSession+SharedGroup.swift"; sourceTree = ""; }; + FD2272F22C352D8D004D8A6C /* LibSession+UserGroups.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "LibSession+UserGroups.swift"; sourceTree = ""; }; + FD2272F32C352D8D004D8A6C /* LibSession+Contacts.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "LibSession+Contacts.swift"; sourceTree = ""; }; + FD2272F42C352D8D004D8A6C /* LibSession+ConvoInfoVolatile.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "LibSession+ConvoInfoVolatile.swift"; sourceTree = ""; }; + FD2272F52C352D8D004D8A6C /* LibSession+GroupMembers.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "LibSession+GroupMembers.swift"; sourceTree = ""; }; + FD2272F62C352D8D004D8A6C /* LibSession+UserProfile.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "LibSession+UserProfile.swift"; sourceTree = ""; }; + FD2272F72C352D8D004D8A6C /* LibSession+GroupKeys.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "LibSession+GroupKeys.swift"; sourceTree = ""; }; + FD2272F82C352D8E004D8A6C /* LibSession+Shared.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "LibSession+Shared.swift"; sourceTree = ""; }; + FD2272F92C352D8E004D8A6C /* LibSession+GroupInfo.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "LibSession+GroupInfo.swift"; sourceTree = ""; }; + FD2273072C353109004D8A6C /* DisplayPictureManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DisplayPictureManager.swift; sourceTree = ""; }; FD23CE252A676B5B0000B97C /* DependenciesSpec.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DependenciesSpec.swift; sourceTree = ""; }; FD23CE272A67755C0000B97C /* MockCrypto.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockCrypto.swift; sourceTree = ""; }; - FD23CE2B2A678DF80000B97C /* MockCaches.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockCaches.swift; sourceTree = ""; }; - FD23CE2F2A67B8820000B97C /* Caches.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Caches.swift; sourceTree = ""; }; FD23CE312A67C38D0000B97C /* MockNetwork.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockNetwork.swift; sourceTree = ""; }; FD23EA6028ED0B260058676E /* CombineExtensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CombineExtensions.swift; sourceTree = ""; }; FD245C612850664300B966DD /* Configuration.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Configuration.swift; sourceTree = ""; }; FD28A4F527EAD44C00FF65E7 /* Storage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Storage.swift; sourceTree = ""; }; FD29598C2A43BC0B00888A17 /* Version.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Version.swift; sourceTree = ""; }; - FD29598F2A43BE5F00888A17 /* VersionSpec.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VersionSpec.swift; sourceTree = ""; }; FD2AAAEF28ED57B500A49611 /* SynchronousStorage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SynchronousStorage.swift; sourceTree = ""; }; - FD2B4AFC294688D000AB4848 /* LibSession+Contacts.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "LibSession+Contacts.swift"; sourceTree = ""; }; - FD2B4AFE2946C93200AB4848 /* ConfigurationSyncJob.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConfigurationSyncJob.swift; sourceTree = ""; }; FD2B4B032949887A00AB4848 /* QueryInterfaceRequest+Utilities.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "QueryInterfaceRequest+Utilities.swift"; sourceTree = ""; }; - FD3003652A25D5B300B5A5FB /* ConfigMessageReceiveJob.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConfigMessageReceiveJob.swift; sourceTree = ""; }; + FD336F562CAA28CF00C0B51B /* CommonSMKMockExtensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CommonSMKMockExtensions.swift; sourceTree = ""; }; + FD336F572CAA28CF00C0B51B /* CustomArgSummaryDescribable+SMK.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "CustomArgSummaryDescribable+SMK.swift"; sourceTree = ""; }; + FD336F582CAA28CF00C0B51B /* MockCommunityPollerCache.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockCommunityPollerCache.swift; sourceTree = ""; }; + FD336F592CAA28CF00C0B51B /* MockDisplayPictureCache.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockDisplayPictureCache.swift; sourceTree = ""; }; + FD336F5A2CAA28CF00C0B51B /* MockGroupPollerCache.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockGroupPollerCache.swift; sourceTree = ""; }; + FD336F5B2CAA28CF00C0B51B /* MockLibSessionCache.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockLibSessionCache.swift; sourceTree = ""; }; + FD336F5C2CAA28CF00C0B51B /* MockNotificationsManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockNotificationsManager.swift; sourceTree = ""; }; + FD336F5D2CAA28CF00C0B51B /* MockOGMCache.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockOGMCache.swift; sourceTree = ""; }; + FD336F5E2CAA28CF00C0B51B /* MockPoller.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockPoller.swift; sourceTree = ""; }; + FD336F5F2CAA28CF00C0B51B /* MockSwarmPoller.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockSwarmPoller.swift; sourceTree = ""; }; + FD336F6B2CAA29C200C0B51B /* CommunityPollerSpec.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CommunityPollerSpec.swift; sourceTree = ""; }; + FD336F6E2CAA37CB00C0B51B /* MockCommunityPoller.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockCommunityPoller.swift; sourceTree = ""; }; FD3559452CC1FF140088F2A9 /* _020_AddMissingWhisperFlag.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = _020_AddMissingWhisperFlag.swift; sourceTree = ""; }; FD368A6729DE8F9B000DBF1E /* _012_AddFTSIfNeeded.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = _012_AddFTSIfNeeded.swift; sourceTree = ""; }; FD368A6929DE9E30000DBF1E /* UIContextualAction+Utilities.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIContextualAction+Utilities.swift"; sourceTree = ""; }; + FD3765DE2AD8F03100DC1489 /* MockSnodeAPICache.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockSnodeAPICache.swift; sourceTree = ""; }; + FD3765E12AD8F53B00DC1489 /* CommonSSKMockExtensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CommonSSKMockExtensions.swift; sourceTree = ""; }; + FD3765E62ADE1AA300DC1489 /* UnrevokeSubaccountRequest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UnrevokeSubaccountRequest.swift; sourceTree = ""; }; + FD3765E82ADE1AAE00DC1489 /* UnrevokeSubaccountResponse.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UnrevokeSubaccountResponse.swift; sourceTree = ""; }; + FD3765F32ADE5A0800DC1489 /* AuthenticatedRequest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AuthenticatedRequest.swift; sourceTree = ""; }; + FD3765F52ADE5BA500DC1489 /* ServiceInfo.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ServiceInfo.swift; sourceTree = ""; }; FD37E9C228A1C6F3003AE748 /* ThemeManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ThemeManager.swift; sourceTree = ""; }; FD37E9C528A1D4EC003AE748 /* Theme+ClassicDark.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Theme+ClassicDark.swift"; sourceTree = ""; }; FD37E9C728A1D73F003AE748 /* Theme+Colors.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Theme+Colors.swift"; sourceTree = ""; }; @@ -1765,22 +1864,31 @@ 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 = ""; }; - FD3C905F27E410F700CD579F /* FileUploadResponseSpec.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FileUploadResponseSpec.swift; sourceTree = ""; }; FD3C906627E416AF00CD579F /* BlindedIdLookupSpec.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BlindedIdLookupSpec.swift; sourceTree = ""; }; - FD3C906927E417CE00CD579F /* CryptoSMKSpec.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CryptoSMKSpec.swift; sourceTree = ""; }; FD3E0C83283B5835002A425C /* SessionThreadViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SessionThreadViewModel.swift; sourceTree = ""; }; + FD3FAB582ADF906300DC5421 /* Profile+CurrentUser.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Profile+CurrentUser.swift"; sourceTree = ""; }; + FD3FAB5E2AE9BC2200DC5421 /* EditGroupViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EditGroupViewModel.swift; sourceTree = ""; }; + FD3FAB602AEA194E00DC5421 /* UserListViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserListViewModel.swift; sourceTree = ""; }; + FD3FAB622AEB9A1500DC5421 /* ToastController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ToastController.swift; sourceTree = ""; }; + FD3FAB662AF0C47000DC5421 /* DisplayPictureDownloadJobSpec.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DisplayPictureDownloadJobSpec.swift; sourceTree = ""; }; + FD3FAB682AF1ADCA00DC5421 /* FileManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FileManager.swift; sourceTree = ""; }; + FD3FAB6A2AF1B27800DC5421 /* MockFileManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockFileManager.swift; sourceTree = ""; }; FD428B182B4B576F006D0888 /* AppContext.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppContext.swift; sourceTree = ""; }; FD428B1A2B4B6098006D0888 /* Notifications+Lifecycle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Notifications+Lifecycle.swift"; sourceTree = ""; }; - FD428B1C2B4B6FDC006D0888 /* UIApplicationState+Utilities.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIApplicationState+Utilities.swift"; sourceTree = ""; }; FD428B1E2B4B758B006D0888 /* AppReadiness.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppReadiness.swift; sourceTree = ""; }; - FD428B202B4B75EA006D0888 /* Singleton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Singleton.swift; sourceTree = ""; }; FD428B222B4B9969006D0888 /* _017_RebuildFTSIfNeeded_2_4_5.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = _017_RebuildFTSIfNeeded_2_4_5.swift; sourceTree = ""; }; - FD43242F2999F0BC008A0213 /* ValidatableResponse.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ValidatableResponse.swift; sourceTree = ""; }; - FD432431299C6933008A0213 /* _011_AddPendingReadReceipts.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = _011_AddPendingReadReceipts.swift; sourceTree = ""; }; FD432433299C6985008A0213 /* PendingReadReceipt.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PendingReadReceipt.swift; sourceTree = ""; }; - FD43EE9C297A5190009C87C5 /* LibSession+UserGroups.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "LibSession+UserGroups.swift"; sourceTree = ""; }; - FD43EE9E297E2EE0009C87C5 /* LibSession+ConvoInfoVolatile.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "LibSession+ConvoInfoVolatile.swift"; sourceTree = ""; }; + FD47E0AA2AA68EEA00A55E41 /* Authentication.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Authentication.swift; sourceTree = ""; }; + FD47E0AE2AA692F400A55E41 /* JSONDecoder+Utilities.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "JSONDecoder+Utilities.swift"; sourceTree = ""; }; + FD47E0B02AA6A05800A55E41 /* Authentication+SessionMessagingKit.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Authentication+SessionMessagingKit.swift"; sourceTree = ""; }; + FD47E0B42AA6D7AA00A55E41 /* Request+SnodeAPI.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Request+SnodeAPI.swift"; sourceTree = ""; }; + FD481A8F2CAD16EA00ECC4CF /* LibSessionGroupInfoSpec.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LibSessionGroupInfoSpec.swift; sourceTree = ""; }; + FD481A912CAD17D900ECC4CF /* LibSessionGroupMembersSpec.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LibSessionGroupMembersSpec.swift; sourceTree = ""; }; + FD481A932CAE0ADD00ECC4CF /* MockAppContext.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockAppContext.swift; sourceTree = ""; }; + FD481AA22CB889A400ECC4CF /* RetrieveDefaultOpenGroupRoomsJobSpec.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RetrieveDefaultOpenGroupRoomsJobSpec.swift; sourceTree = ""; }; + FD49E2452B05C1D500FFBBB5 /* MockKeychain.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockKeychain.swift; sourceTree = ""; }; FD4B200D283492210034334B /* InsetLockableTableView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InsetLockableTableView.swift; sourceTree = ""; }; + FD4C4E9B2B02E2A300C72199 /* DisplayPictureError.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DisplayPictureError.swift; sourceTree = ""; }; FD4C53AE2CC1D61E003B10F4 /* _021_ReworkRecipientState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = _021_ReworkRecipientState.swift; sourceTree = ""; }; FD52090228B4680F006098F6 /* RadioButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RadioButton.swift; sourceTree = ""; }; FD52090428B4915F006098F6 /* PrivacySettingsViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PrivacySettingsViewModel.swift; sourceTree = ""; }; @@ -1801,36 +1909,11 @@ FD5C7308285007920029977D /* BlindedIdLookup.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BlindedIdLookup.swift; sourceTree = ""; }; FD5CE3442A3C5D96001A6DE3 /* DecryptExportedKey.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DecryptExportedKey.swift; sourceTree = ""; }; FD5D201D27B0D87C00FEA984 /* SessionId.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SessionId.swift; sourceTree = ""; }; - FD6A38F02C2A66B100762359 /* KeychainStorageType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeychainStorageType.swift; sourceTree = ""; }; - FD6A38F22C2A6BBB00762359 /* Crypto+SessionUtilitiesKit.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Crypto+SessionUtilitiesKit.swift"; sourceTree = ""; }; - FD6A38F42C2A6BD200762359 /* Crypto.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Crypto.swift; sourceTree = ""; }; - FD6A38F62C2A6C0100762359 /* CryptoError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CryptoError.swift; sourceTree = ""; }; - FD6A38F82C2A8AF700762359 /* DataSource.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DataSource.swift; sourceTree = ""; }; - FD6A38FD2C2A8B7E00762359 /* MediaUtils.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MediaUtils.swift; sourceTree = ""; }; - FD6A38FF2C2A8B9100762359 /* UTType+Utilities.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UTType+Utilities.swift"; sourceTree = ""; }; - FD6A39012C2A8BDE00762359 /* UIImage+Utilities.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIImage+Utilities.swift"; sourceTree = ""; }; - FD6A39032C2A8C0300762359 /* CGFloat+Utilities.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "CGFloat+Utilities.swift"; sourceTree = ""; }; - FD6A39052C2A8C1600762359 /* CGPoint+Utilities.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "CGPoint+Utilities.swift"; sourceTree = ""; }; - FD6A39072C2A8DDA00762359 /* FileSystem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FileSystem.swift; sourceTree = ""; }; - FD6A39092C2A8F2D00762359 /* FileManagerType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FileManagerType.swift; sourceTree = ""; }; - FD6A39142C2A954000762359 /* Crypto+OpenGroupAPI.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Crypto+OpenGroupAPI.swift"; sourceTree = ""; }; - FD6A39162C2A99A000762359 /* BencodeDecoder.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BencodeDecoder.swift; sourceTree = ""; }; - FD6A39182C2A99AB00762359 /* BencodeEncoder.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BencodeEncoder.swift; sourceTree = ""; }; - FD6A391A2C2A99B600762359 /* BencodeResponse.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BencodeResponse.swift; sourceTree = ""; }; - FD6A391C2C2A99DF00762359 /* BencodeEncoderSpec.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BencodeEncoderSpec.swift; sourceTree = ""; }; - FD6A391E2C2A99EF00762359 /* BencodeResponseSpec.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BencodeResponseSpec.swift; sourceTree = ""; }; - FD6A39232C2AAE4500762359 /* CGRect+Utilities.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "CGRect+Utilities.swift"; sourceTree = ""; }; - FD6A39252C2AB0B500762359 /* OWSViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OWSViewController.swift; sourceTree = ""; }; - FD6A39272C2AB2AA00762359 /* CGSize+Utilities.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "CGSize+Utilities.swift"; sourceTree = ""; }; - FD6A39292C2AB3BD00762359 /* UIBezierPath+Utilities.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIBezierPath+Utilities.swift"; sourceTree = ""; }; - FD6A392B2C2AC51900762359 /* AppVersion.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppVersion.swift; sourceTree = ""; }; - FD6A392E2C2ACAA600762359 /* Crypto+SessionSnodeKit.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Crypto+SessionSnodeKit.swift"; sourceTree = ""; }; - FD6A39422C2AD81600762359 /* BackgroundTaskManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BackgroundTaskManager.swift; sourceTree = ""; }; - FD6A39442C2B783D00762359 /* UINavigationController+Utilities.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UINavigationController+Utilities.swift"; sourceTree = ""; }; - FD6A39482C2BB85A00762359 /* Crypto+Attachments.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Crypto+Attachments.swift"; sourceTree = ""; }; - FD6A39702C2E3F5800762359 /* GRDBExtensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GRDBExtensions.swift; sourceTree = ""; }; - FD6A7A682818BE7300035AC1 /* RetrieveDefaultOpenGroupRoomsJob.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RetrieveDefaultOpenGroupRoomsJob.swift; sourceTree = ""; }; - FD6A7A6A2818C17C00035AC1 /* UpdateProfilePictureJob.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UpdateProfilePictureJob.swift; sourceTree = ""; }; + FD6531892AA025C500DFEEAA /* TestDependencies.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TestDependencies.swift; sourceTree = ""; }; + FD66CB272BF3449B00268FAB /* SessionSnodeKit.xctestplan */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = SessionSnodeKit.xctestplan; sourceTree = ""; }; + FD66CB2B2BF344C600268FAB /* SessionMessageKit.xctestplan */ = {isa = PBXFileReference; lastKnownFileType = text; path = SessionMessageKit.xctestplan; sourceTree = ""; }; + FD66CB2C2BF5EB0C00268FAB /* Config.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Config.swift; sourceTree = ""; }; + FD6A38F02C2A66B100762359 /* KeychainStorage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeychainStorage.swift; sourceTree = ""; }; FD6A7A6C2818C61500035AC1 /* _002_SetupStandardJobs.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = _002_SetupStandardJobs.swift; sourceTree = ""; }; FD6DF00A2ACFE40D0084BA4C /* _005_AddSnodeReveivedMessageInfoPrimaryKey.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = _005_AddSnodeReveivedMessageInfoPrimaryKey.swift; sourceTree = ""; }; FD6E4C892A1AEE4700C7C243 /* LegacyUnsubscribeRequest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LegacyUnsubscribeRequest.swift; sourceTree = ""; }; @@ -1841,7 +1924,6 @@ FD7115F328C71EB200B47552 /* ThreadDisappearingMessagesSettingsViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ThreadDisappearingMessagesSettingsViewModel.swift; sourceTree = ""; }; FD7115F728C8151C00B47552 /* DisposableBarButtonItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DisposableBarButtonItem.swift; sourceTree = ""; }; FD7115F928C8153400B47552 /* UIBarButtonItem+Combine.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIBarButtonItem+Combine.swift"; sourceTree = ""; }; - FD7115FB28C8155800B47552 /* Publisher+Utilities.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Publisher+Utilities.swift"; sourceTree = ""; }; FD7115FD28C8202D00B47552 /* ReplaySubject.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReplaySubject.swift; sourceTree = ""; }; FD7115FF28C8253500B47552 /* UIView+Combine.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIView+Combine.swift"; sourceTree = ""; }; FD71160128C8255900B47552 /* UIControl+Combine.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIControl+Combine.swift"; sourceTree = ""; }; @@ -1852,7 +1934,7 @@ FD71161928D00E1100B47552 /* NotificationContentViewModelSpec.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationContentViewModelSpec.swift; sourceTree = ""; }; FD71161B28D194FB00B47552 /* MentionInfo.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MentionInfo.swift; sourceTree = ""; }; FD71161D28D9772700B47552 /* UIViewController+OWS.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIViewController+OWS.swift"; sourceTree = ""; }; - FD71161F28D97ABC00B47552 /* UIImage+Tinting.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIImage+Tinting.swift"; sourceTree = ""; }; + FD71161F28D97ABC00B47552 /* UIImage+Utilities.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIImage+Utilities.swift"; sourceTree = ""; }; FD71162128D983ED00B47552 /* QRCodeScanningViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = QRCodeScanningViewController.swift; sourceTree = ""; }; FD71162B28E1451400B47552 /* Position.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Position.swift; sourceTree = ""; }; FD71162D28E168C700B47552 /* SettingsViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsViewModel.swift; sourceTree = ""; }; @@ -1873,32 +1955,18 @@ FD716E6B28505E1C00C96BF4 /* MessageRequestsViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MessageRequestsViewModel.swift; sourceTree = ""; }; FD716E7028505E5100C96BF4 /* MessageRequestsCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MessageRequestsCell.swift; sourceTree = ""; }; FD72BD992BDF5EEA00CF6CF6 /* Message+Origin.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Message+Origin.swift"; sourceTree = ""; }; + FD72BD9B2BE2F2BC00CF6CF6 /* NoopSessionCallManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NoopSessionCallManager.swift; sourceTree = ""; }; + FD72BDA02BE368C800CF6CF6 /* UIWindowLevel+Utilities.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIWindowLevel+Utilities.swift"; sourceTree = ""; }; + FD72BDA32BE3690B00CF6CF6 /* CryptoSMKSpec.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CryptoSMKSpec.swift; sourceTree = ""; }; + FD72BDA62BE369DC00CF6CF6 /* CryptoOpenGroupAPISpec.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CryptoOpenGroupAPISpec.swift; sourceTree = ""; }; FD7692F62A53A2ED000E4B70 /* SessionThreadViewModelSpec.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SessionThreadViewModelSpec.swift; sourceTree = ""; }; 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 = ""; }; FD778B6329B189FF001BAC6B /* _014_GenerateInitialUserConfigDumps.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = _014_GenerateInitialUserConfigDumps.swift; sourceTree = ""; }; - FD7C37AE2BBB8B1D009DEEA7 /* SessionSnodeKitTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = SessionSnodeKitTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; - FD7C37BD2BBB8BBC009DEEA7 /* SnodeRequestSpec.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SnodeRequestSpec.swift; sourceTree = ""; }; FD7F74562BAA9D31006DDFD8 /* _006_DropSnodeCache.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = _006_DropSnodeCache.swift; sourceTree = ""; }; FD7F745A2BAAA35E006DDFD8 /* LibSession.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LibSession.swift; sourceTree = ""; }; - FD7F745C2BAAA38B006DDFD8 /* LibSessionError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LibSessionError.swift; sourceTree = ""; }; FD7F745E2BAAA3B4006DDFD8 /* TypeConversion+Utilities.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "TypeConversion+Utilities.swift"; sourceTree = ""; }; FD7F74692BAB8A6D006DDFD8 /* LibSession+Networking.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "LibSession+Networking.swift"; sourceTree = ""; }; - FD7F746B2BB2764F006DDFD8 /* RequestTarget.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RequestTarget.swift; sourceTree = ""; }; - FD7F746D2BB2766D006DDFD8 /* PreparedRequest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PreparedRequest.swift; sourceTree = ""; }; - FD7F746F2BB276A0006DDFD8 /* BatchRequestSpec.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BatchRequestSpec.swift; sourceTree = ""; }; - FD7F74712BB276CC006DDFD8 /* RequestSpec.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RequestSpec.swift; sourceTree = ""; }; - FD7F74732BB276D0006DDFD8 /* HeaderSpec.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = HeaderSpec.swift; sourceTree = ""; }; - FD7F74752BB276E7006DDFD8 /* PreparedRequestSpec.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PreparedRequestSpec.swift; sourceTree = ""; }; - FD7F74772BB27742006DDFD8 /* BatchRequest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BatchRequest.swift; sourceTree = ""; }; - FD7F74792BB277E1006DDFD8 /* Request+OpenGroupAPI.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Request+OpenGroupAPI.swift"; sourceTree = ""; }; - FD7F747B2BB28182006DDFD8 /* PreparedRequest+OnionRequest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "PreparedRequest+OnionRequest.swift"; sourceTree = ""; }; - FD7F747F2BB283A9006DDFD8 /* Request+SnodeAPI.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Request+SnodeAPI.swift"; sourceTree = ""; }; - FD7F74812BB283CE006DDFD8 /* UpdatableTimestamp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UpdatableTimestamp.swift; sourceTree = ""; }; - FD7F74832BB283DF006DDFD8 /* SwarmDrainBehaviour.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SwarmDrainBehaviour.swift; sourceTree = ""; }; - FD7F74852BB2868E006DDFD8 /* ResponseInfo+SnodeAPI.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ResponseInfo+SnodeAPI.swift"; sourceTree = ""; }; - FD7F74872BB2929B006DDFD8 /* Request+PushNotificationAPI.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Request+PushNotificationAPI.swift"; sourceTree = ""; }; - FD7F74892BB298A8006DDFD8 /* JSONDecoder+Utilities.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "JSONDecoder+Utilities.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 = ""; }; @@ -1914,46 +1982,55 @@ FD848B9228420164000E298B /* UnicodeScalar+Utilities.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UnicodeScalar+Utilities.swift"; sourceTree = ""; }; FD848B9728422F1A000E298B /* Date+Utilities.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Date+Utilities.swift"; sourceTree = ""; }; FD848B9928442CE6000E298B /* StorageError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StorageError.swift; sourceTree = ""; }; - FD848B9B284435D7000E298B /* AppSetup.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppSetup.swift; sourceTree = ""; }; FD859EEF27BF207700510D0C /* SessionProtos.proto */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.protobuf; path = SessionProtos.proto; sourceTree = ""; }; FD859EF027BF207C00510D0C /* WebSocketResources.proto */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.protobuf; path = WebSocketResources.proto; sourceTree = ""; }; FD859EF127BF6BA200510D0C /* Data+Utilities.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Data+Utilities.swift"; sourceTree = ""; }; FD86FDA22BC5020600EC251B /* PrivacyInfo.xcprivacy */ = {isa = PBXFileReference; lastKnownFileType = text.xml; path = PrivacyInfo.xcprivacy; sourceTree = ""; }; FD87DCF928B74DB300AF0F98 /* ConversationSettingsViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConversationSettingsViewModel.swift; sourceTree = ""; }; FD87DCFD28B7582C00AF0F98 /* BlockedContactsViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BlockedContactsViewModel.swift; sourceTree = ""; }; - FD87DD0328B8727D00AF0F98 /* Configuration.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Configuration.swift; sourceTree = ""; }; FD8ECF7A29340FFD00C0D1BB /* LibSession+SessionMessagingKit.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "LibSession+SessionMessagingKit.swift"; sourceTree = ""; }; FD8ECF7C2934293A00C0D1BB /* _013_SessionUtilChanges.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = _013_SessionUtilChanges.swift; sourceTree = ""; }; FD8ECF7E2934298100C0D1BB /* ConfigDump.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConfigDump.swift; sourceTree = ""; }; - FD8ECF8F29381FC200C0D1BB /* LibSession+UserProfile.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "LibSession+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 = ""; }; + FD8FD7632C37C24A001E38C7 /* EquatableIgnoring.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EquatableIgnoring.swift; sourceTree = ""; }; FD9004132818AD0B00ABAAF6 /* _002_SetupStandardJobs.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = _002_SetupStandardJobs.swift; sourceTree = ""; }; FD9401CE2ABD04AC003A4834 /* TRANSLATIONS.md */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = net.daringfireball.markdown; path = TRANSLATIONS.md; sourceTree = ""; }; FD96F3A429DBC3DC00401309 /* MessageSendJobSpec.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageSendJobSpec.swift; sourceTree = ""; }; - FD96F3A629DBD43D00401309 /* MockJobRunner.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockJobRunner.swift; sourceTree = ""; }; FD97B23F2A3FEB050027DD57 /* ARC4RandomNumberGenerator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ARC4RandomNumberGenerator.swift; sourceTree = ""; }; FD97B2412A3FEBF30027DD57 /* UnreadMarkerCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UnreadMarkerCell.swift; sourceTree = ""; }; FD9AECA42AAA9609009B3406 /* NotificationError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationError.swift; sourceTree = ""; }; - FD9B30F2293EA0BF008DEE3E /* BatchResponseSpec.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BatchResponseSpec.swift; sourceTree = ""; }; FD9BDDF82A5D2294005F1EBC /* libSessionUtil.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = libSessionUtil.a; sourceTree = BUILT_PRODUCTS_DIR; }; FD9DD2702A72516D00ECB68E /* TestExtensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TestExtensions.swift; sourceTree = ""; }; - FDA1E83829A5771A00C5C3BD /* LibSessionUtilSpec.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LibSessionUtilSpec.swift; sourceTree = ""; }; - FDA1E83A29A5F2D500C5C3BD /* LibSession+Shared.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "LibSession+Shared.swift"; sourceTree = ""; }; - FDA1E83C29AC71A800C5C3BD /* LibSessionSpec.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LibSessionSpec.swift; sourceTree = ""; }; - FDA8EAFD280E8B78002B68E5 /* FailedMessageSendsJob.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FailedMessageSendsJob.swift; sourceTree = ""; }; - FDA8EAFF280E8D58002B68E5 /* FailedAttachmentDownloadsJob.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FailedAttachmentDownloadsJob.swift; sourceTree = ""; }; - FDA8EB0F280F8238002B68E5 /* Codable+Utilities.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Codable+Utilities.swift"; sourceTree = ""; }; + FDAA16752AC28A3B00DDBF77 /* UserDefaultsType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserDefaultsType.swift; sourceTree = ""; }; + FDAA167A2AC28E2F00DDBF77 /* SnodeRequestSpec.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SnodeRequestSpec.swift; sourceTree = ""; }; + FDAA167C2AC528A200DDBF77 /* Preferences+Sound.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Preferences+Sound.swift"; sourceTree = ""; }; + FDAA167E2AC5290000DDBF77 /* Preferences+NotificationPreviewType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Preferences+NotificationPreviewType.swift"; sourceTree = ""; }; FDAED05B2A7C6CE600091B25 /* MigrationRequirement.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MigrationRequirement.swift; sourceTree = ""; }; + FDB348622BE3774000B716C2 /* BezierPathView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BezierPathView.swift; sourceTree = ""; }; + FDB3486C2BE8448500B716C2 /* SessionUtilitiesKit.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = SessionUtilitiesKit.h; sourceTree = ""; }; + FDB3486D2BE8457F00B716C2 /* BackgroundTaskManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BackgroundTaskManager.swift; sourceTree = ""; }; + FDB3487D2BE856C800B716C2 /* UIBezierPath+Utilities.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIBezierPath+Utilities.swift"; sourceTree = ""; }; FDB4BBC62838B91E00B7C95D /* LinkPreviewError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LinkPreviewError.swift; sourceTree = ""; }; - FDB4BBC82839BEF000B7C95D /* ProfileManagerError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfileManagerError.swift; sourceTree = ""; }; + FDB5DAC02A9443A5002C8721 /* MessageSender+Groups.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "MessageSender+Groups.swift"; sourceTree = ""; }; + FDB5DAC62A9447E7002C8721 /* _022_GroupsRebuildChanges.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = _022_GroupsRebuildChanges.swift; sourceTree = ""; }; + FDB5DAD32A9483F3002C8721 /* GroupUpdateInviteMessage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GroupUpdateInviteMessage.swift; sourceTree = ""; }; + FDB5DAD92A95D839002C8721 /* GroupUpdateInfoChangeMessage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GroupUpdateInfoChangeMessage.swift; sourceTree = ""; }; + FDB5DADB2A95D840002C8721 /* GroupUpdateMemberChangeMessage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GroupUpdateMemberChangeMessage.swift; sourceTree = ""; }; + FDB5DADD2A95D847002C8721 /* GroupUpdatePromoteMessage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GroupUpdatePromoteMessage.swift; sourceTree = ""; }; + FDB5DADF2A95D84D002C8721 /* GroupUpdateMemberLeftMessage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GroupUpdateMemberLeftMessage.swift; sourceTree = ""; }; + FDB5DAE12A95D8A0002C8721 /* GroupUpdateInviteResponseMessage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GroupUpdateInviteResponseMessage.swift; sourceTree = ""; }; + FDB5DAE52A95D8B0002C8721 /* GroupUpdateDeleteMemberContentMessage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GroupUpdateDeleteMemberContentMessage.swift; sourceTree = ""; }; + FDB5DAE72A95D96C002C8721 /* MessageReceiver+Groups.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "MessageReceiver+Groups.swift"; sourceTree = ""; }; + FDB5DAF22A96DD4F002C8721 /* PreparedRequest+Sending.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "PreparedRequest+Sending.swift"; sourceTree = ""; }; + FDB5DAFA2A981C42002C8721 /* SessionSnodeKitTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = SessionSnodeKitTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; + FDB5DB052A981C67002C8721 /* PreparedRequestSendingSpec.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PreparedRequestSendingSpec.swift; sourceTree = ""; }; FDB6A87B2AD75B7F002D4F96 /* PhotosUI.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = PhotosUI.framework; path = System/Library/Frameworks/PhotosUI.framework; sourceTree = SDKROOT; }; FDB7400A28EB99A70094D718 /* TimeInterval+Utilities.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "TimeInterval+Utilities.swift"; sourceTree = ""; }; FDB7400C28EBEC240094D718 /* DateHeaderCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DateHeaderCell.swift; sourceTree = ""; }; FDBB25E22988B13800F1508E /* _004_AddJobPriority.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = _004_AddJobPriority.swift; sourceTree = ""; }; FDBB25E62988BBBD00F1508E /* UIContextualAction+Theming.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIContextualAction+Theming.swift"; sourceTree = ""; }; + FDBEE52D2B6A18B900C143A0 /* UserDefaultsConfig.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UserDefaultsConfig.swift; sourceTree = ""; }; FDC0F0032BFECE12002CBFB9 /* TimeUnit.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimeUnit.swift; sourceTree = ""; }; - FDC0F0072C00721A002CBFB9 /* MockOGPoller.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockOGPoller.swift; sourceTree = ""; }; + FDC0F00B2C04100E002CBFB9 /* Session.xctestplan */ = {isa = PBXFileReference; lastKnownFileType = text; name = Session.xctestplan; path = SessionTests/Session.xctestplan; sourceTree = SOURCE_ROOT; }; FDC13D462A16E4CA007267C7 /* SubscribeRequest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SubscribeRequest.swift; sourceTree = ""; }; FDC13D482A16EC20007267C7 /* Service.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Service.swift; sourceTree = ""; }; FDC13D4A2A16ECBA007267C7 /* SubscribeResponse.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SubscribeResponse.swift; sourceTree = ""; }; @@ -1973,20 +2050,17 @@ FDC2909527D71252005DAE71 /* SOGSErrorSpec.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SOGSErrorSpec.swift; sourceTree = ""; }; FDC2909727D7129B005DAE71 /* PersonalizationSpec.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PersonalizationSpec.swift; sourceTree = ""; }; FDC2909D27D85751005DAE71 /* OpenGroupManagerSpec.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OpenGroupManagerSpec.swift; sourceTree = ""; }; - FDC290A527D860CE005DAE71 /* Mock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Mock.swift; sourceTree = ""; }; FDC290A727D9B46D005DAE71 /* NimbleExtensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NimbleExtensions.swift; sourceTree = ""; }; FDC4380827B31D4E00C60D73 /* OpenGroupAPIError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OpenGroupAPIError.swift; sourceTree = ""; }; FDC4381627B32EC700C60D73 /* Personalization.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Personalization.swift; sourceTree = ""; }; FDC4381F27B36ADC00C60D73 /* SOGSEndpoint.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SOGSEndpoint.swift; sourceTree = ""; }; FDC4382E27B383AF00C60D73 /* LegacyPushServerResponse.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LegacyPushServerResponse.swift; sourceTree = ""; }; FDC4383727B3863200C60D73 /* AppVersionResponse.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppVersionResponse.swift; sourceTree = ""; }; - FDC4383D27B4708600C60D73 /* Atomic.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Atomic.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 = ""; }; 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; }; @@ -1994,9 +2068,6 @@ FDC438A327BB107F00C60D73 /* UserBanRequest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserBanRequest.swift; sourceTree = ""; }; FDC438A527BB113A00C60D73 /* UserUnbanRequest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserUnbanRequest.swift; sourceTree = ""; }; 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 = ""; }; - FDC438BC27BB2AB400C60D73 /* Mockable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Mockable.swift; sourceTree = ""; }; FDC438C627BB6DF000C60D73 /* DirectMessage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DirectMessage.swift; sourceTree = ""; }; FDC438C827BB706500C60D73 /* SendDirectMessageRequest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SendDirectMessageRequest.swift; sourceTree = ""; }; FDC438CA27BB7DB100C60D73 /* UpdateMessageRequest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UpdateMessageRequest.swift; sourceTree = ""; }; @@ -2007,29 +2078,88 @@ FDC6D75F2862B3F600B04575 /* Dependencies.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Dependencies.swift; sourceTree = ""; }; FDCCC6E82ABA7402002BBEF5 /* EmojiGenerator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EmojiGenerator.swift; sourceTree = ""; }; FDCD2E022A41294E00964D6A /* LegacyGroupOnlyRequest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LegacyGroupOnlyRequest.swift; sourceTree = ""; }; - FDCDB8DD2810F73B00352A0C /* Differentiable+Utilities.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Differentiable+Utilities.swift"; sourceTree = ""; }; FDCDB8DF2811007F00352A0C /* HomeViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomeViewModel.swift; sourceTree = ""; }; FDD20C152A09E64A003898FB /* GetExpiriesRequest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GetExpiriesRequest.swift; sourceTree = ""; }; FDD20C172A09E7D3003898FB /* GetExpiriesResponse.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GetExpiriesResponse.swift; sourceTree = ""; }; FDD20C192A0A03AC003898FB /* DeleteInboxResponse.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeleteInboxResponse.swift; sourceTree = ""; }; FDD2506D283711D600198BDA /* DifferenceKit+Utilities.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "DifferenceKit+Utilities.swift"; sourceTree = ""; }; - FDD2506F2837199200198BDA /* GarbageCollectionJob.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GarbageCollectionJob.swift; sourceTree = ""; }; FDD250712837234B00198BDA /* MediaGalleryNavigationController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MediaGalleryNavigationController.swift; sourceTree = ""; }; + FDD383702AFDD0E1001367F2 /* BencodeResponse.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BencodeResponse.swift; sourceTree = ""; }; + FDD383722AFDD6D7001367F2 /* BencodeResponseSpec.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BencodeResponseSpec.swift; sourceTree = ""; }; FDD82C3E2A205D0A00425F05 /* ProcessResult.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProcessResult.swift; sourceTree = ""; }; - FDDC08F129A300E800BF9681 /* TypeConversionUtilitiesSpec.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TypeConversionUtilitiesSpec.swift; sourceTree = ""; }; - FDDD554B2C1FC812006CBF03 /* CheckForAppUpdatesJob.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CheckForAppUpdatesJob.swift; sourceTree = ""; }; FDDD554D2C1FCB77006CBF03 /* _019_ScheduleAppUpdateCheckJob.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = _019_ScheduleAppUpdateCheckJob.swift; sourceTree = ""; }; FDDF074329C3E3D000E5E8B5 /* FetchRequest+Utilities.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "FetchRequest+Utilities.swift"; sourceTree = ""; }; FDDF074929DAB36900E5E8B5 /* JobRunnerSpec.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = JobRunnerSpec.swift; sourceTree = ""; }; - FDE049022C76A09700B6F9BB /* UIAlertAction+Utilities.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIAlertAction+Utilities.swift"; sourceTree = ""; }; FDE125222A837E4E002DA685 /* MainAppContext.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainAppContext.swift; sourceTree = ""; }; - FDE658A229418E2F00A33BC1 /* KeyPair.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = KeyPair.swift; sourceTree = ""; }; + FDE519F62AB7CDC700450C53 /* Result+Utilities.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Result+Utilities.swift"; sourceTree = ""; }; + FDE519F82AB802BB00450C53 /* Message+Origin.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Message+Origin.swift"; sourceTree = ""; }; FDE6E99729F8E63A00F93C5D /* Accessibility.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Accessibility.swift; sourceTree = ""; }; FDE7214F287E50D50093DF33 /* ProtoWrappers.py */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.script.python; path = ProtoWrappers.py; sourceTree = ""; }; FDE72150287E50D50093DF33 /* LintLocalizableStrings.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LintLocalizableStrings.swift; sourceTree = ""; }; + FDE7549A2C940108002A2623 /* MessageViewModel+DeletionActions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "MessageViewModel+DeletionActions.swift"; sourceTree = ""; }; + FDE7549C2C9961A4002A2623 /* CommunityPoller.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CommunityPoller.swift; sourceTree = ""; }; + FDE754A02C9A60A6002A2623 /* Crypto+OpenGroupAPI.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "Crypto+OpenGroupAPI.swift"; sourceTree = ""; }; + FDE754A22C9A8FD1002A2623 /* SwarmPoller.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SwarmPoller.swift; sourceTree = ""; }; + FDE754A42C9B964D002A2623 /* MessageReceiverGroupsSpec.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MessageReceiverGroupsSpec.swift; sourceTree = ""; }; + FDE754A52C9B964D002A2623 /* MessageSenderGroupsSpec.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MessageSenderGroupsSpec.swift; sourceTree = ""; }; + FDE754A62C9B964D002A2623 /* MessageSenderSpec.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MessageSenderSpec.swift; sourceTree = ""; }; + FDE754AB2C9B967D002A2623 /* FileUploadResponseSpec.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FileUploadResponseSpec.swift; sourceTree = ""; }; + FDE754AD2C9B96B3002A2623 /* WebRTCSession.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = WebRTCSession.swift; sourceTree = ""; }; + FDE754AE2C9B96B4002A2623 /* TurnServerInfo.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TurnServerInfo.swift; sourceTree = ""; }; + FDE754AF2C9B96B4002A2623 /* WebRTC+Utilities.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "WebRTC+Utilities.swift"; sourceTree = ""; }; + FDE754B32C9B96BA002A2623 /* WebRTCSession+UI.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "WebRTCSession+UI.swift"; sourceTree = ""; }; + FDE754B42C9B96BA002A2623 /* WebRTCSession+MessageHandling.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "WebRTCSession+MessageHandling.swift"; sourceTree = ""; }; + FDE754B52C9B96BB002A2623 /* WebRTCSession+DataChannel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "WebRTCSession+DataChannel.swift"; sourceTree = ""; }; + FDE754B92C9B97B8002A2623 /* UIDevice+Utilities.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "UIDevice+Utilities.swift"; sourceTree = ""; }; + FDE754BB2C9B9A8E002A2623 /* Session+SNUIKit.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Session+SNUIKit.swift"; sourceTree = ""; }; + FDE754BD2C9BA16C002A2623 /* UIButtonConfiguration+Utilities.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIButtonConfiguration+Utilities.swift"; sourceTree = ""; }; + FDE754BF2C9BAEF6002A2623 /* Array+Utilities.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "Array+Utilities.swift"; sourceTree = ""; }; + FDE754C12C9BAF0B002A2623 /* Atomic.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Atomic.swift; sourceTree = ""; }; + FDE754C72C9BAF36002A2623 /* MediaUtils.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MediaUtils.swift; sourceTree = ""; }; + FDE754C82C9BAF36002A2623 /* UTType+Utilities.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "UTType+Utilities.swift"; sourceTree = ""; }; + FDE754C92C9BAF36002A2623 /* ImageFormat.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ImageFormat.swift; sourceTree = ""; }; + FDE754CA2C9BAF37002A2623 /* DataSource.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DataSource.swift; sourceTree = ""; }; + FDE754CB2C9BAF37002A2623 /* Data+Image.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "Data+Image.swift"; sourceTree = ""; }; + FDE754D12C9BAF53002A2623 /* JobDependencies.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = JobDependencies.swift; sourceTree = ""; }; + FDE754D32C9BAF6B002A2623 /* UICollectionView+ReusableView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "UICollectionView+ReusableView.swift"; sourceTree = ""; }; + FDE754D52C9BAF89002A2623 /* Crypto.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Crypto.swift; sourceTree = ""; }; + FDE754D62C9BAF89002A2623 /* CryptoError.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CryptoError.swift; sourceTree = ""; }; + FDE754D72C9BAF89002A2623 /* Mnemonic.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Mnemonic.swift; sourceTree = ""; }; + FDE754D82C9BAF89002A2623 /* Crypto+SessionUtilitiesKit.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "Crypto+SessionUtilitiesKit.swift"; sourceTree = ""; }; + FDE754D92C9BAF89002A2623 /* KeyPair.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = KeyPair.swift; sourceTree = ""; }; + FDE754DA2C9BAF8A002A2623 /* Hex.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Hex.swift; sourceTree = ""; }; + FDE754E12C9BAFF4002A2623 /* Crypto+SessionSnodeKit.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "Crypto+SessionSnodeKit.swift"; sourceTree = ""; }; + FDE754E42C9BB012002A2623 /* BezierPathView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BezierPathView.swift; sourceTree = ""; }; + FDE754E62C9BB051002A2623 /* OWSViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OWSViewController.swift; sourceTree = ""; }; + FDE754EC2C9BB08B002A2623 /* Crypto+Attachments.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "Crypto+Attachments.swift"; sourceTree = ""; }; + FDE754ED2C9BB08B002A2623 /* Crypto+LibSession.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "Crypto+LibSession.swift"; sourceTree = ""; }; + FDE754EE2C9BB08B002A2623 /* Crypto+SessionMessagingKit.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "Crypto+SessionMessagingKit.swift"; sourceTree = ""; }; + FDE754F32C9BB0AF002A2623 /* UserNotificationConfig.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UserNotificationConfig.swift; sourceTree = ""; }; + FDE754F42C9BB0AF002A2623 /* NotificationActionHandler.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NotificationActionHandler.swift; sourceTree = ""; }; + FDE754F52C9BB0AF002A2623 /* NotificationPresenter.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NotificationPresenter.swift; sourceTree = ""; }; + FDE754F62C9BB0AF002A2623 /* PushRegistrationManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PushRegistrationManager.swift; sourceTree = ""; }; + FDE754F72C9BB0AF002A2623 /* SyncPushTokensJob.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SyncPushTokensJob.swift; sourceTree = ""; }; + FDE754FD2C9BB0D0002A2623 /* Threading+SMK.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "Threading+SMK.swift"; sourceTree = ""; }; + FDE754FF2C9BB0FA002A2623 /* SessionEnvironment.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SessionEnvironment.swift; sourceTree = ""; }; + FDE755012C9BB122002A2623 /* _011_AddPendingReadReceipts.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = _011_AddPendingReadReceipts.swift; sourceTree = ""; }; + FDE755032C9BB4ED002A2623 /* BencodeDecoder.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BencodeDecoder.swift; sourceTree = ""; }; + FDE755042C9BB4ED002A2623 /* Bencode.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Bencode.swift; sourceTree = ""; }; + FDE755072C9BC134002A2623 /* Codable+Utilities.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "Codable+Utilities.swift"; sourceTree = ""; }; + FDE755082C9BC134002A2623 /* CGPoint+Utilities.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "CGPoint+Utilities.swift"; sourceTree = ""; }; + FDE755092C9BC135002A2623 /* CGFloat+Utilities.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "CGFloat+Utilities.swift"; sourceTree = ""; }; + FDE7550A2C9BC135002A2623 /* CGRect+Utilities.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "CGRect+Utilities.swift"; sourceTree = ""; }; + FDE7550B2C9BC135002A2623 /* CGSize+Utilities.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "CGSize+Utilities.swift"; sourceTree = ""; }; + FDE755132C9BC169002A2623 /* UIAlertAction+Utilities.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "UIAlertAction+Utilities.swift"; sourceTree = ""; }; + FDE755142C9BC169002A2623 /* UIImage+Utilities.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "UIImage+Utilities.swift"; sourceTree = ""; }; + FDE755152C9BC169002A2623 /* UIApplicationState+Utilities.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "UIApplicationState+Utilities.swift"; sourceTree = ""; }; + FDE755162C9BC169002A2623 /* UINavigationController+Utilities.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "UINavigationController+Utilities.swift"; sourceTree = ""; }; + FDE755172C9BC169002A2623 /* UIBezierPath+Utilities.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "UIBezierPath+Utilities.swift"; sourceTree = ""; }; + FDE7551F2C9BC1A6002A2623 /* CacheConfig.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CacheConfig.swift; sourceTree = ""; }; + FDE755212C9BC1BA002A2623 /* LibSessionError.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LibSessionError.swift; sourceTree = ""; }; + FDE755232C9BC1D1002A2623 /* Publisher+Utilities.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "Publisher+Utilities.swift"; sourceTree = ""; }; FDE77F68280F9EDA002CFC5D /* JobRunnerError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = JobRunnerError.swift; sourceTree = ""; }; FDE77F6A280FEB28002CFC5D /* ControlMessageProcessRecord.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ControlMessageProcessRecord.swift; sourceTree = ""; }; - FDED2E3B282E1B5D00B2CD2A /* UICollectionView+ReusableView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UICollectionView+ReusableView.swift"; sourceTree = ""; }; + FDEF573D2C40F2A100131302 /* GroupUpdateMemberLeftNotificationMessage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GroupUpdateMemberLeftNotificationMessage.swift; sourceTree = ""; }; FDEF57642C44B8C200131302 /* ProcessIP2CountryData.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProcessIP2CountryData.swift; sourceTree = ""; }; FDEF57662C44C1DF00131302 /* GeoLite2-Country-Blocks-IPv4.csv */ = {isa = PBXFileReference; lastKnownFileType = text; path = "GeoLite2-Country-Blocks-IPv4.csv"; sourceTree = ""; }; FDEF57672C44C1DF00131302 /* GeoLite2-Country-Locations-de.csv */ = {isa = PBXFileReference; lastKnownFileType = text; path = "GeoLite2-Country-Locations-de.csv"; sourceTree = ""; }; @@ -2041,16 +2171,14 @@ FDEF576D2C44C1DF00131302 /* GeoLite2-Country-Locations-ru.csv */ = {isa = PBXFileReference; lastKnownFileType = text; path = "GeoLite2-Country-Locations-ru.csv"; sourceTree = ""; }; FDEF576E2C44C1DF00131302 /* GeoLite2-Country-Locations-zh-CN.csv */ = {isa = PBXFileReference; lastKnownFileType = text; path = "GeoLite2-Country-Locations-zh-CN.csv"; sourceTree = ""; }; FDEF57702C44D2D300131302 /* GeoLite2-Country-Blocks-IPv4 */ = {isa = PBXFileReference; lastKnownFileType = file; name = "GeoLite2-Country-Blocks-IPv4"; path = "Countries/GeoLite2-Country-Blocks-IPv4"; sourceTree = ""; }; + FDF01FAC2A9ECC4200CAF969 /* SingletonConfig.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SingletonConfig.swift; sourceTree = ""; }; FDF0B73B27FFD3D6004C14C5 /* LinkPreview.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LinkPreview.swift; sourceTree = ""; }; FDF0B73F280402C4004C14C5 /* Job.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Job.swift; sourceTree = ""; }; FDF0B7412804EA4F004C14C5 /* _002_SetupStandardJobs.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = _002_SetupStandardJobs.swift; sourceTree = ""; }; FDF0B7432804EF1B004C14C5 /* JobRunner.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = JobRunner.swift; sourceTree = ""; }; - FDF0B7462804F0CE004C14C5 /* DisappearingMessagesJob.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DisappearingMessagesJob.swift; sourceTree = ""; }; FDF0B74828060D13004C14C5 /* QuotedReplyModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = QuotedReplyModel.swift; sourceTree = ""; }; FDF0B74A28061F7A004C14C5 /* InteractionAttachment.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InteractionAttachment.swift; sourceTree = ""; }; - FDF0B74E28079E5E004C14C5 /* SendReadReceiptsJob.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SendReadReceiptsJob.swift; sourceTree = ""; }; FDF0B7502807BA56004C14C5 /* NotificationsProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationsProtocol.swift; sourceTree = ""; }; - FDF0B7542807C4BB004C14C5 /* SessionEnvironment.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SessionEnvironment.swift; sourceTree = ""; }; FDF0B7572807F368004C14C5 /* MessageReceiverError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageReceiverError.swift; sourceTree = ""; }; 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 = ""; }; @@ -2061,14 +2189,10 @@ 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 /* NetworkError.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NetworkError.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 = ""; }; + FDF71EA22B072C2800A8D6B5 /* LibSessionMessage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LibSessionMessage.swift; sourceTree = ""; }; + FDF71EA42B07363500A8D6B5 /* MessageReceiver+LibSession.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "MessageReceiver+LibSession.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 = ""; }; 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 = ""; }; @@ -2078,7 +2202,6 @@ 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 = ""; }; 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 = ""; }; @@ -2099,20 +2222,15 @@ 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 = ""; }; + FDF848BA29405C5A007DCAE5 /* RevokeSubaccountRequest.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RevokeSubaccountRequest.swift; sourceTree = ""; }; FDF848BB29405C5A007DCAE5 /* LegacySendMessageRequest.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LegacySendMessageRequest.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 /* Destination.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Destination.swift; sourceTree = ""; }; - FDF848E829405E4E007DCAE5 /* Network+OnionRequest.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "Network+OnionRequest.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 = ""; }; - FDFBB74A2A1EFF4900CA7350 /* Bencode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Bencode.swift; sourceTree = ""; }; FDFBB74C2A1F3C4E00CA7350 /* NotificationMetadata.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationMetadata.swift; sourceTree = ""; }; - FDFBB7532A2023EB00CA7350 /* BencodeDecoderSpec.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BencodeDecoderSpec.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 = ""; }; @@ -2120,7 +2238,6 @@ FDFDE127282D05530098B17F /* MediaPresentationContext.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MediaPresentationContext.swift; sourceTree = ""; }; FDFDE129282D056B0098B17F /* MediaZoomAnimationController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MediaZoomAnimationController.swift; sourceTree = ""; }; FDFE75B02ABD2D2400655640 /* _016_MakeBrokenProfileTimestampsNullable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = _016_MakeBrokenProfileTimestampsNullable.swift; sourceTree = ""; }; - FDFF61D629F2600300F95FB0 /* Identity+Utilities.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Identity+Utilities.swift"; sourceTree = ""; }; FDFF9FDE2A787F57005E0628 /* JSONEncoder+Utilities.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "JSONEncoder+Utilities.swift"; sourceTree = ""; }; /* End PBXFileReference section */ @@ -2160,9 +2277,8 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( - FD2286712C38D43000BC06F7 /* DifferenceKit in Frameworks */, FD6A396B2C2D284500762359 /* YYImage in Frameworks */, - FD37E9EF28A5ED70003AE748 /* SessionUtilitiesKit.framework in Frameworks */, + FD2286712C38D43000BC06F7 /* DifferenceKit in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -2185,6 +2301,7 @@ buildActionMask = 2147483647; files = ( C3C2A6C62553896A00C340D1 /* SessionUtilitiesKit.framework in Frameworks */, + FDB348802BE86A4400B716C2 /* libSessionUtil.a in Frameworks */, FD7F74602BAAA4C7006DDFD8 /* libSessionUtil.a in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; @@ -2195,6 +2312,7 @@ files = ( FD6A39662C2D21E400762359 /* libwebp in Frameworks */, FD7F74632BAAA4CA006DDFD8 /* libSessionUtil.a in Frameworks */, + FD6A38E62C2A4D8E00762359 /* GRDB in Frameworks */, FD6A38EC2C2A63B500762359 /* KeychainSwift in Frameworks */, FD6A38EF2C2A641200762359 /* DifferenceKit in Frameworks */, FD6A396D2C2D284B00762359 /* YYImage in Frameworks */, @@ -2220,6 +2338,12 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( + FD11E22E2CA4D12C001BAF58 /* WebRTC in Frameworks */, + FD11E22D2CA4D12C001BAF58 /* DifferenceKit in Frameworks */, + FD11E22C2CA4D12C001BAF58 /* YYImage in Frameworks */, + FD11E22B2CA4D12C001BAF58 /* YYImage in Frameworks */, + FD11E22A2CA4D12C001BAF58 /* YYImage in Frameworks */, + FD11E2292CA4D12C001BAF58 /* YYImage in Frameworks */, B8FF8DAE25C0D00F004D1F22 /* SessionMessagingKit.framework in Frameworks */, B8FF8DAF25C0D00F004D1F22 /* SessionUtilitiesKit.framework in Frameworks */, FDB6A87C2AD75B7F002D4F96 /* PhotosUI.framework in Frameworks */, @@ -2230,51 +2354,46 @@ 45847E871E4283C30080EAB3 /* Intents.framework in Frameworks */, 4520D8D51D417D8E00123472 /* Photos.framework in Frameworks */, B6B226971BE4B7D200860F4D /* ContactsUI.framework in Frameworks */, - FDEF572A2C3CF50B00131302 /* WebRTC in Frameworks */, 45BD60821DE9547E00A8F436 /* Contacts.framework in Frameworks */, B6FE7EB71ADD62FA00A6D22F /* PushKit.framework in Frameworks */, FC3BD9881A30A790005B96BB /* Social.framework in Frameworks */, FCB11D8C1A129A76002F93FB /* CoreMedia.framework in Frameworks */, 70377AAB1918450100CAF501 /* MobileCoreServices.framework in Frameworks */, - FD6A39692C2D283A00762359 /* YYImage in Frameworks */, B9EB5ABD1884C002007CBB57 /* MessageUI.framework in Frameworks */, 3496956021A2FC8100DCFE74 /* CloudKit.framework in Frameworks */, 76C87F19181EFCE600C4ACAB /* MediaPlayer.framework in Frameworks */, - FD6A394F2C2D060C00762359 /* YYImage in Frameworks */, A11CD70D17FA230600A2D1B1 /* QuartzCore.framework in Frameworks */, A163E8AB16F3F6AA0094D68B /* Security.framework in Frameworks */, A1C32D5117A06544000A904E /* AddressBook.framework in Frameworks */, A1C32D5017A06538000A904E /* AddressBookUI.framework in Frameworks */, D2AEACDC16C426DA00C364C0 /* CFNetwork.framework in Frameworks */, C331FF222558F9D300070591 /* SessionUIKit.framework in Frameworks */, - FD2286792C38D4FF00BC06F7 /* DifferenceKit in Frameworks */, D2179CFE16BB0B480006F3AB /* SystemConfiguration.framework in Frameworks */, D2179CFC16BB0B3A0006F3AB /* CoreTelephony.framework in Frameworks */, D221A08E169C9E5E00537ABF /* UIKit.framework in Frameworks */, - FD6A395C2C2D10C700762359 /* YYImage in Frameworks */, D221A090169C9E5E00537ABF /* Foundation.framework in Frameworks */, D221A0E8169DFFC500537ABF /* AVFoundation.framework in Frameworks */, D24B5BD5169F568C00681372 /* AudioToolbox.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; - FD71160628D00BAE00B47552 /* Frameworks */ = { + FD01504F2CA2445E005B08A1 /* Frameworks */ = { isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( - FD2DD5902C6DD13C0073D9BE /* DifferenceKit in Frameworks */, - FD6A39322C2AD33E00762359 /* Quick in Frameworks */, - FD6A393B2C2AD3A300762359 /* Nimble in Frameworks */, + FD0150542CA24471005B08A1 /* Nimble in Frameworks */, + FD0150522CA2446D005B08A1 /* Quick in Frameworks */, + FD0150502CA24468005B08A1 /* SessionSnodeKit.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; - FD7C37AB2BBB8B1D009DEEA7 /* Frameworks */ = { + FD71160628D00BAE00B47552 /* Frameworks */ = { isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( - FD6A393F2C2AD3B100762359 /* Nimble in Frameworks */, - FD6A39362C2AD36400762359 /* Quick in Frameworks */, - FD7C37B22BBB8B1E009DEEA7 /* SessionSnodeKit.framework in Frameworks */, + FD2DD5902C6DD13C0073D9BE /* DifferenceKit in Frameworks */, + FD6A39322C2AD33E00762359 /* Quick in Frameworks */, + FD6A393B2C2AD3A300762359 /* Nimble in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -2635,48 +2754,24 @@ B8A582AC258C653C00AFD84C /* Crypto */ = { isa = PBXGroup; children = ( - FD6A38F22C2A6BBB00762359 /* Crypto+SessionUtilitiesKit.swift */, - FD6A38F42C2A6BD200762359 /* Crypto.swift */, - FD6A38F62C2A6C0100762359 /* CryptoError.swift */, - B88FA7FA26114EA70049422F /* Hex.swift */, - FDE658A229418E2F00A33BC1 /* KeyPair.swift */, - C3A71F882558BA9F0043A11F /* Mnemonic.swift */, + FDE754D52C9BAF89002A2623 /* Crypto.swift */, + FDE754D82C9BAF89002A2623 /* Crypto+SessionUtilitiesKit.swift */, + FDE754D62C9BAF89002A2623 /* CryptoError.swift */, + FDE754DA2C9BAF8A002A2623 /* Hex.swift */, + FDE754D92C9BAF89002A2623 /* KeyPair.swift */, + FDE754D72C9BAF89002A2623 /* Mnemonic.swift */, ); path = Crypto; sourceTree = ""; }; - B8A582AE258C65D000AFD84C /* Networking */ = { - isa = PBXGroup; - children = ( - C33FDB68255A580F00E217F9 /* ContentProxy.swift */, - FDF8487729405906007DCAE5 /* HTTPHeader.swift */, - FDF8487829405906007DCAE5 /* HTTPMethod.swift */, - FDF8487529405906007DCAE5 /* HTTPQueryParam.swift */, - B8FF8EA525C11FEF004D1F22 /* IPv4.swift */, - C3C2A5D92553860B00C340D1 /* JSON.swift */, - FDF8488529405A60007DCAE5 /* Request.swift */, - FDC438B027BB159600C60D73 /* RequestInfo.swift */, - FD7F746B2BB2764F006DDFD8 /* RequestTarget.swift */, - FD7F746D2BB2766D006DDFD8 /* PreparedRequest.swift */, - FD7F74772BB27742006DDFD8 /* BatchRequest.swift */, - FDF8488229405A12007DCAE5 /* BatchResponse.swift */, - FDC438B227BB15B400C60D73 /* ResponseInfo.swift */, - C33FDAF2255A580500E217F9 /* ProxiedContentDownloader.swift */, - FDF8487629405906007DCAE5 /* NetworkError.swift */, - FD23CE1A2A651E6D0000B97C /* Network.swift */, - FDF848EE294067E4007DCAE5 /* URLResponse+Utilities.swift */, - ); - path = Networking; - sourceTree = ""; - }; B8A582AF258C665E00AFD84C /* Media */ = { isa = PBXGroup; children = ( - FD09797627FAB7A600936362 /* Data+Image.swift */, - FD6A38F82C2A8AF700762359 /* DataSource.swift */, - FD09797827FAB7E800936362 /* ImageFormat.swift */, - FD6A38FD2C2A8B7E00762359 /* MediaUtils.swift */, - FD6A38FF2C2A8B9100762359 /* UTType+Utilities.swift */, + FDE754CB2C9BAF37002A2623 /* Data+Image.swift */, + FDE754CA2C9BAF37002A2623 /* DataSource.swift */, + FDE754C92C9BAF36002A2623 /* ImageFormat.swift */, + FDE754C72C9BAF36002A2623 /* MediaUtils.swift */, + FDE754C82C9BAF36002A2623 /* UTType+Utilities.swift */, ); path = Media; sourceTree = ""; @@ -2684,39 +2779,30 @@ B8A582B0258C66C900AFD84C /* General */ = { isa = PBXGroup; children = ( - 947AD68F2C8968FF000B2730 /* Constants.swift */, FD428B182B4B576F006D0888 /* AppContext.swift */, - 7BD477A727EC39F5004E2822 /* Atomic.swift */, + FDE754BF2C9BAEF6002A2623 /* Array+Utilities.swift */, + FDE754C12C9BAF0B002A2623 /* Atomic.swift */, + FD47E0AA2AA68EEA00A55E41 /* Authentication.swift */, + 947AD68F2C8968FF000B2730 /* Constants.swift */, FDC438CC27BC641200C60D73 /* Set+Utilities.swift */, 7BAF54D527ACD0E2003D12F8 /* ReusableView.swift */, - C3C2A5D12553860800C340D1 /* Array+Utilities.swift */, - FDC4383D27B4708600C60D73 /* Atomic.swift */, - C33FDAA8255A57FF00E217F9 /* BuildConfiguration.swift */, B8F5F58225EC94A6003BF8D4 /* Collection+Utilities.swift */, - FD23CE2F2A67B8820000B97C /* Caches.swift */, FDFD645A27F26D4600808CA1 /* Data+Utilities.swift */, - FDC6D75F2862B3F600B04575 /* Dependencies.swift */, C3C2A5D52553860A00C340D1 /* Dictionary+Utilities.swift */, - B87EF18026377A1D00124B3C /* Features.swift */, - FD6A39072C2A8DDA00762359 /* FileSystem.swift */, - B8BC00BF257D90E30032E807 /* General.swift */, + FD2272EB2C352155004D8A6C /* Feature.swift */, + FD10AF112AF85D11007709E5 /* Feature+ServiceNetwork.swift */, + FD2272EF2C352200004D8A6C /* General.swift */, C3C2A5CE2553860700C340D1 /* Logging.swift */, C33FDAFD255A580600E217F9 /* LRUCache.swift */, C33FDA7A255A57FB00E217F9 /* NSRegularExpression+SSK.swift */, - C352A3762557859C00338F3E /* NSTimer+Proxying.h */, - C352A36C2557858D00338F3E /* NSTimer+Proxying.m */, 7B1D74AF27C365960030B423 /* Timer+MainThread.swift */, - C33FDB51255A580D00E217F9 /* NSUserDefaults+OWS.h */, - C33FDB77255A581000E217F9 /* NSUserDefaults+OWS.m */, FD705A91278D051200F16121 /* ReusableView.swift */, FD17D7AF27F4225C00122BE0 /* Set+Utilities.swift */, - C33FDB6B255A580F00E217F9 /* SNUserDefaults.swift */, C33FDB3F255A580C00E217F9 /* String+SSK.swift */, - C3C2AC2D2553CBEB00C340D1 /* String+Trimming.swift */, FD7728952849E7E90018502F /* String+Utilities.swift */, FDB7400A28EB99A70094D718 /* TimeInterval+Utilities.swift */, C38EF237255B6D65007E1867 /* UIDevice+featureSupport.swift */, - FDED2E3B282E1B5D00B2CD2A /* UICollectionView+ReusableView.swift */, + FDE754D32C9BAF6B002A2623 /* UICollectionView+ReusableView.swift */, FD7728972849E8110018502F /* UITableView+ReusableView.swift */, C35D0DB425AE5F1200B6BF49 /* UIEdgeInsets.swift */, FD848B9228420164000E298B /* UnicodeScalar+Utilities.swift */, @@ -2725,7 +2811,7 @@ 7B7CB191271508AD0079FF93 /* CallRingTonePlayer.swift */, 7B0EFDED274F598600FFAAE7 /* TimestampUtils.swift */, FD428B1A2B4B6098006D0888 /* Notifications+Lifecycle.swift */, - FD428B202B4B75EA006D0888 /* Singleton.swift */, + FD2272DF2C3502BE004D8A6C /* Setting+Theme.swift */, 943C6D832B86B5F1004ACE64 /* Localization.swift */, ); path = General; @@ -2748,22 +2834,20 @@ B8CCF63B239757C10091D419 /* Shared */ = { isa = PBXGroup; children = ( - 9422569E2C23F8FE00C0FDBF /* ScanQRCodeScreen.swift */, FD71164128E2C83500B47552 /* Types */, FD71164028E2C83000B47552 /* Views */, - 4CA46F4B219CCC630038ABDE /* CaptionView.swift */, - 94C5DCAF2BE88170003AA8C5 /* BezierPathView.swift */, - 34F308A01ECB469700BB7697 /* OWSBezierPathView.h */, - 34F308A11ECB469700BB7697 /* OWSBezierPathView.m */, - 7BAFA1182A39669400B76CB9 /* BezierPathView.swift */, C354E75923FE2A7600CE22E3 /* BaseVC.swift */, + FDE754E42C9BB012002A2623 /* BezierPathView.swift */, + 4CA46F4B219CCC630038ABDE /* CaptionView.swift */, B8BB82AA238F669C00BA5194 /* FullConversationCell.swift */, + 7B81FB582AB01AA8002FB267 /* LoadingIndicatorView.swift */, 4542DF53208D40AC007B4E76 /* LoadingViewController.swift */, FD71162128D983ED00B47552 /* QRCodeScanningViewController.swift */, + 9422569E2C23F8FE00C0FDBF /* ScanQRCodeScreen.swift */, B893063E2383961A005EAA8E /* ScanQRCodeWrapperVC.swift */, - C31D1DE22521718E005D4DA8 /* UserSelectionVC.swift */, FD52090828B59411006098F6 /* ScreenLockUI.swift */, FD37EA0828AA2D27003AE748 /* SessionTableViewModel.swift */, + FD10AF0B2AF32B9A007709E5 /* SessionListViewModel.swift */, FD37EA0628AA2CCA003AE748 /* SessionTableViewController.swift */, 7BE2701D2A64C11500CEB71A /* SessionCarouselView+SwiftUI.swift */, 7B4EF2592934743000CB351D /* SessionTableViewTitleView.swift */, @@ -2771,7 +2855,7 @@ 7B2561C329874851005C086C /* SessionCarouselView+Info.swift */, 7B3A3933298882D6002FE4AC /* SessionCarouselViewDelegate.swift */, 7B8914762A7CAAE200A4C627 /* SessionHostingViewController.swift */, - 7B81FB582AB01AA8002FB267 /* LoadingIndicatorView.swift */, + FD3FAB602AEA194E00DC5421 /* UserListViewModel.swift */, ); path = Shared; sourceTree = ""; @@ -2783,6 +2867,7 @@ FD716E692850327900C96BF4 /* EndCallMode.swift */, FD716E6528502EE200C96BF4 /* CurrentCallProtocol.swift */, FD716E6328502DDD00C96BF4 /* CallManagerProtocol.swift */, + FD72BD9B2BE2F2BC00CF6CF6 /* NoopSessionCallManager.swift */, ); path = Calls; sourceTree = ""; @@ -2803,6 +2888,8 @@ C300A5C72554B03900555489 /* Control Messages */, C3C2A74325539EB700C340D1 /* Message.swift */, C352A30825574D8400338F3E /* Message+Destination.swift */, + FDE519F82AB802BB00450C53 /* Message+Origin.swift */, + FDF71EA22B072C2800A8D6B5 /* LibSessionMessage.swift */, 943C6D812B75E061004ACE64 /* Message+DisappearingMessages.swift */, FD72BD992BDF5EEA00CF6CF6 /* Message+Origin.swift */, ); @@ -2826,6 +2913,7 @@ C300A5C72554B03900555489 /* Control Messages */ = { isa = PBXGroup; children = ( + FDB5DAD22A9483D4002C8721 /* Group Update Messages */, C3C2A7702553A41E00C340D1 /* ControlMessage.swift */, B8DE1FB526C22FCB0079C9CE /* CallMessage.swift */, C34A977325A3E34A00852C71 /* ClosedGroupControlMessage.swift */, @@ -2898,14 +2986,6 @@ path = "Context Menu"; sourceTree = ""; }; - C32B405424A961E1001117B5 /* Dependencies */ = { - isa = PBXGroup; - children = ( - C3DAB3232480CB2A00725F25 /* SRCopyableLabel.swift */, - ); - path = Dependencies; - sourceTree = ""; - }; C32C5995256DAF85003C73A2 /* Typing Indicators */ = { isa = PBXGroup; children = ( @@ -2917,10 +2997,11 @@ C32C59F8256DB5A6003C73A2 /* Pollers */ = { isa = PBXGroup; children = ( - C33FDB3A255A580B00E217F9 /* Poller.swift */, - C33FDB34255A580B00E217F9 /* ClosedGroupPoller.swift */, - C3DB66C2260ACCE6001EFC55 /* OpenGroupPoller.swift */, + C33FDB3A255A580B00E217F9 /* PollerType.swift */, + FDE754A22C9A8FD1002A2623 /* SwarmPoller.swift */, FDF848F629414477007DCAE5 /* CurrentUserPoller.swift */, + FD2272822C337830004D8A6C /* GroupPoller.swift */, + FDE7549C2C9961A4002A2623 /* CommunityPoller.swift */, ); path = Pollers; sourceTree = ""; @@ -2956,7 +3037,6 @@ isa = PBXGroup; children = ( C331FF422558F9E400070591 /* Meta */, - FD37E9F428A5F0FB003AE748 /* Database */, C331FFCC2558FAF300070591 /* Components */, C331FF5E2558FA0F00070591 /* Style Guide */, FD71163028E2C41900B47552 /* Types */, @@ -2990,12 +3070,14 @@ C331FFAE2558FA7700070591 /* Utilities */ = { isa = PBXGroup; children = ( - B8544E3023D16CA500299F14 /* DeviceUtilities.swift */, + FD2272E32C35134B004D8A6C /* Data+Utilities.swift */, + FDE754BD2C9BA16C002A2623 /* UIButtonConfiguration+Utilities.swift */, FD37E9D628A20B5D003AE748 /* UIColor+Utilities.swift */, B885D5F52334A32100EE0D8E /* UIView+Constraints.swift */, C33100272559000A00070591 /* UIView+Utilities.swift */, - FD71161F28D97ABC00B47552 /* UIImage+Tinting.swift */, + FD71161F28D97ABC00B47552 /* UIImage+Utilities.swift */, FDBB25E62988BBBD00F1508E /* UIContextualAction+Theming.swift */, + FDE754B92C9B97B8002A2623 /* UIDevice+Utilities.swift */, 7BA1E0E72A8087DB00123D0D /* SwiftUI+Utilities.swift */, 9422EE2A2B8C3A97004C740D /* String+Utilities.swift */, 94E9BC0C2C7BFBDA006984EA /* Localization+Style.swift */, @@ -3024,6 +3106,8 @@ FD0150572CA27DEE005B08A1 /* ScrollableLabel.swift */, FD71165A28E6DDBC00B47552 /* StyledNavigationController.swift */, FD0B77AF29B69A65009169BA /* TopBannerController.swift */, + FD3FAB622AEB9A1500DC5421 /* ToastController.swift */, + FDB348622BE3774000B716C2 /* BezierPathView.swift */, ); path = Components; sourceTree = ""; @@ -3037,7 +3121,6 @@ C3851CD225624B060061EEB0 /* Shared Views */, C360970125AD22D3008B62B2 /* Shared View Controllers */, C3CA3B11255CF17200F4C6D4 /* Utilities */, - FD87DD0328B8727D00AF0F98 /* Configuration.swift */, ); path = SignalUtilitiesKit; sourceTree = ""; @@ -3045,7 +3128,6 @@ C33FD9B7255A54A300E217F9 /* Meta */ = { isa = PBXGroup; children = ( - C33FD9AD255A548A00E217F9 /* SignalUtilitiesKit.h */, C33FD9AE255A548A00E217F9 /* Info.plist */, ); path = Meta; @@ -3054,7 +3136,28 @@ C352A2F325574B3300338F3E /* Jobs */ = { isa = PBXGroup; children = ( - FDF0B7452804F0A8004C14C5 /* Types */, + FD2272602C32911B004D8A6C /* AttachmentDownloadJob.swift */, + FD22725F2C32911B004D8A6C /* AttachmentUploadJob.swift */, + FD2272552C32911A004D8A6C /* CheckForAppUpdatesJob.swift */, + FD22725C2C32911B004D8A6C /* ConfigMessageReceiveJob.swift */, + FD22725B2C32911B004D8A6C /* ConfigurationSyncJob.swift */, + FD2272582C32911A004D8A6C /* DisappearingMessagesJob.swift */, + FD2272612C32911B004D8A6C /* DisplayPictureDownloadJob.swift */, + FD22725E2C32911B004D8A6C /* ExpirationUpdateJob.swift */, + FD22725A2C32911A004D8A6C /* FailedAttachmentDownloadsJob.swift */, + FD2272562C32911A004D8A6C /* FailedMessageSendsJob.swift */, + FD2272662C32911B004D8A6C /* GarbageCollectionJob.swift */, + FD2272672C32911B004D8A6C /* GetExpirationJob.swift */, + FD2272622C32911B004D8A6C /* GroupInviteMemberJob.swift */, + FD2272542C32911A004D8A6C /* GroupLeavingJob.swift */, + FD2272642C32911B004D8A6C /* GroupPromoteMemberJob.swift */, + FD2272592C32911A004D8A6C /* MessageReceiveJob.swift */, + FD2272632C32911B004D8A6C /* MessageSendJob.swift */, + FD2272652C32911B004D8A6C /* NotifyPushServerJob.swift */, + FD2272572C32911A004D8A6C /* ProcessPendingGroupMemberRemovalsJob.swift */, + FD22725D2C32911B004D8A6C /* RetrieveDefaultOpenGroupRoomsJob.swift */, + FD2272532C32911A004D8A6C /* SendReadReceiptsJob.swift */, + FD22726A2C32911C004D8A6C /* UpdateProfilePictureJob.swift */, ); path = Jobs; sourceTree = ""; @@ -3101,6 +3204,7 @@ FD37E9CB28A1E578003AE748 /* AppearanceViewController.swift */, FD37EA0228A9FDCC003AE748 /* HelpViewModel.swift */, B86BD08523399CEF000F5AE3 /* SeedModal.swift */, + FD10AF092AF319EE007709E5 /* DeveloperSettingsViewModel.swift */, B894D0742339EDCF00B4D94D /* NukeDataModal.swift */, FDF848F229413DB0007DCAE5 /* ImagePickerHandler.swift */, ); @@ -3129,7 +3233,7 @@ isa = PBXGroup; children = ( B80A579E23DFF1F300876683 /* NewClosedGroupVC.swift */, - C3E5C2F9251DBABB0040DFFC /* EditClosedGroupVC.swift */, + FD3FAB5E2AE9BC2200DC5421 /* EditGroupViewModel.swift */, ); path = "Closed Groups"; sourceTree = ""; @@ -3138,7 +3242,6 @@ isa = PBXGroup; children = ( 3430FE171F7751D4000EC51B /* GiphyAPI.swift */, - 34D1F0511F7E8EA30066283D /* GiphyDownloader.swift */, 34D1F04F1F7D45A60066283D /* GifPickerCell.swift */, 34BECE2F1F7ABCF800D7438D /* GifPickerLayout.swift */, 34BECE2D1F7ABCE000D7438D /* GifPickerViewController.swift */, @@ -3176,10 +3279,11 @@ isa = PBXGroup; children = ( FDC498B52AC15F6D00EDD897 /* Types */, - 4539B5851F79348F007141FF /* PushRegistrationManager.swift */, - 45CD81EE1DC030E7004C9430 /* SyncPushTokensJob.swift */, - 451A13B01E13DED2000A50FD /* AppNotifications.swift */, - 450DF2081E0DD2C6003D14BE /* UserNotificationsAdaptee.swift */, + FDE754F42C9BB0AF002A2623 /* NotificationActionHandler.swift */, + FDE754F52C9BB0AF002A2623 /* NotificationPresenter.swift */, + FDE754F62C9BB0AF002A2623 /* PushRegistrationManager.swift */, + FDE754F72C9BB0AF002A2623 /* SyncPushTokensJob.swift */, + FDE754F32C9BB0AF002A2623 /* UserNotificationConfig.swift */, ); path = Notifications; sourceTree = ""; @@ -3208,7 +3312,7 @@ isa = PBXGroup; children = ( C38EF349255B6DC7007E1867 /* ModalActivityIndicatorViewController.swift */, - FD6A39252C2AB0B500762359 /* OWSViewController.swift */, + FDE754E62C9BB051002A2623 /* OWSViewController.swift */, ); path = "Shared View Controllers"; sourceTree = ""; @@ -3265,7 +3369,6 @@ C38EF383255B6DD1007E1867 /* ApprovalRailCellView.swift */, C38EF3E7255B6DF5007E1867 /* OWSButton.swift */, C38EF3DB255B6DF1007E1867 /* OWSLayerView.swift */, - C38EF3E9255B6DF6007E1867 /* Toast.swift */, C38EF3E2255B6DF3007E1867 /* GalleryRailView.swift */, C38EF3E1255B6DF3007E1867 /* TappableView.swift */, ); @@ -3288,25 +3391,28 @@ isa = PBXGroup; children = ( FD428B1E2B4B758B006D0888 /* AppReadiness.swift */, + FD47E0B02AA6A05800A55E41 /* Authentication+SessionMessagingKit.swift */, FD859EF127BF6BA200510D0C /* Data+Utilities.swift */, C38EF309255B6DBE007E1867 /* DeviceSleepManager.swift */, - FDF0B7542807C4BB004C14C5 /* SessionEnvironment.swift */, + FD4C4E9B2B02E2A300C72199 /* DisplayPictureError.swift */, + FD2273072C353109004D8A6C /* DisplayPictureManager.swift */, C3BBE0C62554F1570050F1E3 /* FixedWidthInteger+BigEndian.swift */, C3A71D0A2558989C0043A11F /* MessageWrapper.swift */, C38EF2F5255B6DBC007E1867 /* OWSAudioPlayer.h */, C38EF2F7255B6DBC007E1867 /* OWSAudioPlayer.m */, C38EF281255B6D84007E1867 /* OWSAudioSession.swift */, FDF0B75D280AAF35004C14C5 /* Preferences.swift */, + FDAA167E2AC5290000DDBF77 /* Preferences+NotificationPreviewType.swift */, + FDAA167C2AC528A200DDBF77 /* Preferences+Sound.swift */, C38EF2FB255B6DBD007E1867 /* OWSWindowManager.h */, C38EF306255B6DBE007E1867 /* OWSWindowManager.m */, - FD09797327FAB3E200936362 /* ProfileManager.swift */, - FDB4BBC82839BEF000B7C95D /* ProfileManagerError.swift */, + FD3FAB582ADF906300DC5421 /* Profile+CurrentUser.swift */, + FD16AB602A1DD9B60083D849 /* ProfilePictureView+Convenience.swift */, C38EF2EC255B6DBA007E1867 /* ProximityMonitoringManager.swift */, + FDE754FF2C9BB0FA002A2623 /* SessionEnvironment.swift */, C38EEF09255B49A8007E1867 /* SNProtoEnvelope+Conversion.swift */, - FDC4386827B4E6B700C60D73 /* String+Utlities.swift */, - FD16AB602A1DD9B60083D849 /* ProfilePictureView+Convenience.swift */, - C3ECBF7A257056B700EA7FCE /* Threading.swift */, - FDFF61D629F2600300F95FB0 /* Identity+Utilities.swift */, + FDE754FD2C9BB0D0002A2623 /* Threading+SMK.swift */, + FD72BDA02BE368C800CF6CF6 /* UIWindowLevel+Utilities.swift */, ); path = Utilities; sourceTree = ""; @@ -3315,11 +3421,11 @@ isa = PBXGroup; children = ( C3C2A5B0255385C700C340D1 /* Meta */, - FD6A392D2C2ACA9500762359 /* Crypto */, + FDE754E22C9BAFF4002A2623 /* Crypto */, FD17D79D27F40CAA00122BE0 /* Database */, FDF8489929405C5A007DCAE5 /* Models */, FDF8488F29405C13007DCAE5 /* Types */, - FDF8489229405C1B007DCAE5 /* Networking */, + FD2272842C33E28D004D8A6C /* SnodeAPI */, FD7F74682BAB8A5D006DDFD8 /* LibSession */, C3C2A5CD255385F300C340D1 /* Utilities */, C3C2A5B9255385ED00C340D1 /* Configuration.swift */, @@ -3340,8 +3446,11 @@ isa = PBXGroup; children = ( C3C2A5D82553860B00C340D1 /* Data+Utilities.swift */, + FD2272BD2C34B710004D8A6C /* Publisher+Utilities.swift */, + FD83DCDC2A739D350065FFAE /* RetryWithDependencies.swift */, C3C2A5D22553860900C340D1 /* String+Trimming.swift */, - C3C2A5D42553860A00C340D1 /* Threading.swift */, + C3C2A5D42553860A00C340D1 /* Threading+SSK.swift */, + FD2272A82C33E337004D8A6C /* URLResponse+Utilities.swift */, ); path = Utilities; sourceTree = ""; @@ -3353,11 +3462,12 @@ FD7115F528C8150600B47552 /* Combine */, B8A582AC258C653C00AFD84C /* Crypto */, B8A582AB258C64E800AFD84C /* Database */, + FDF01FAE2A9ED0C800CAF969 /* Dependency Injection */, B8A582B0258C66C900AFD84C /* General */, FD9004102818ABB000ABAAF6 /* JobRunner */, B8A582AF258C665E00AFD84C /* Media */, - B8A582AE258C65D000AFD84C /* Networking */, FD7F74582BAAA349006DDFD8 /* LibSession */, + FD2272D22C34ECBB004D8A6C /* Types */, FD09796527F6B0A800936362 /* Utilities */, FD37E9FE28A5F2CD003AE748 /* Configuration.swift */, ); @@ -3367,7 +3477,7 @@ C3C2A68B255388D500C340D1 /* Meta */ = { isa = PBXGroup; children = ( - C3C2A67B255388CC00C340D1 /* SessionUtilitiesKit.h */, + FDB3486C2BE8448500B716C2 /* SessionUtilitiesKit.h */, C3C2A67C255388CC00C340D1 /* Info.plist */, ); path = Meta; @@ -3378,8 +3488,8 @@ children = ( C3C2A7802553AA6300C340D1 /* Protos */, C3C2A70A25539DF900C340D1 /* Meta */, - FD6A39462C2BA2C000762359 /* Crypto */, B8DE1FB226C22F1F0079C9CE /* Calls */, + FDE754EF2C9BB08B002A2623 /* Crypto */, C32C5BCB256DC818003C73A2 /* Database */, C300A5BB2554AFFB00555489 /* Messages */, C352A2F325574B3300338F3E /* Jobs */, @@ -3437,28 +3547,18 @@ C3CA3B11255CF17200F4C6D4 /* Utilities */ = { isa = PBXGroup; children = ( - FD848B9B284435D7000E298B /* AppSetup.swift */, - FDCDB8DD2810F73B00352A0C /* Differentiable+Utilities.swift */, + FD2272DC2C34EFFA004D8A6C /* AppSetup.swift */, C38EF3DC255B6DF1007E1867 /* DirectionalPanGestureRecognizer.swift */, C38EF240255B6D67007E1867 /* UIView+OWS.swift */, FD71161D28D9772700B47552 /* UIViewController+OWS.swift */, C38EF2B1255B6D9C007E1867 /* UIViewController+Utilities.swift */, - C38EF307255B6DBE007E1867 /* UIGestureRecognizer+OWS.swift */, C33FDBD3255A581800E217F9 /* OWSSignalAddress.swift */, - FD6A392B2C2AC51900762359 /* AppVersion.swift */, C38EF304255B6DBE007E1867 /* ImageCache.swift */, - C38EF2F2255B6DBC007E1867 /* Searcher.swift */, - C3F0A52F255C80BC007BE2A3 /* NoopNotificationsManager.swift */, - C33FDB69255A580F00E217F9 /* FeatureFlags.swift */, - C33FDB80255A581100E217F9 /* Notification+Loki.swift */, - C33FDB8F255A581200E217F9 /* ParamParser.swift */, - C33FDADE255A580400E217F9 /* SwiftSingletons.swift */, - C33FDB49255A580C00E217F9 /* WeakTimer.swift */, C33FDA9E255A57FF00E217F9 /* ReverseDispatchQueue.swift */, C38EF241255B6D67007E1867 /* Collection+OWS.swift */, C38EF3AE255B6DE5007E1867 /* OrderedDictionary.swift */, C38EF226255B6D5D007E1867 /* ShareViewDelegate.swift */, - C38EF2FA255B6DBD007E1867 /* Bench.swift */, + FDB3487D2BE856C800B716C2 /* UIBezierPath+Utilities.swift */, ); path = Utilities; sourceTree = ""; @@ -3476,8 +3576,8 @@ isa = PBXGroup; children = ( C3AAFFF125AE99710089E6DD /* AppDelegate.swift */, + FDE754BB2C9B9A8E002A2623 /* Session+SNUIKit.swift */, 9409433F2C7ED62300D9D2E0 /* StartupError.swift */, - 34D99CE3217509C1000AFB39 /* AppEnvironment.swift */, 7BFD1A952747689000FB91B9 /* TurnServers */, B8FF8E6025C10D8B004D1F22 /* Countries */, 34330A581E7875FB00DF2FB9 /* Fonts */, @@ -3489,7 +3589,6 @@ B67EBF5C19194AC60084CCFD /* Settings.bundle */, B657DDC91911A40500F45B0C /* Signal.entitlements */, FDF2220A2818F38D000A4995 /* SessionApp.swift */, - 45B201741DAECBFD00C461E0 /* Signal-Bridging-Header.h */, D221A095169C9E5E00537ABF /* Session-Info.plist */, 4C63CBFF210A620B003AE45C /* SignalTSan.supp */, 4C6F527B20FFE8400097DEEE /* SignalUBSan.supp */, @@ -3513,7 +3612,7 @@ FD83B9BC27CF2215005E1583 /* _SharedTestUtilities */, FD71160A28D00BAE00B47552 /* SessionTests */, FDC4388F27B9FFC700C60D73 /* SessionMessagingKitTests */, - FD7C37AF2BBB8B1E009DEEA7 /* SessionSnodeKitTests */, + FDB5DAFB2A981C43002C8721 /* SessionSnodeKitTests */, FD83B9B027CF200A005E1583 /* SessionUtilitiesKitTests */, FDE7214E287E50D50093DF33 /* Scripts */, D221A08C169C9E5E00537ABF /* Frameworks */, @@ -3536,7 +3635,7 @@ FD83B9AF27CF200A005E1583 /* SessionUtilitiesKitTests.xctest */, FD71160928D00BAE00B47552 /* SessionTests.xctest */, FD9BDDF82A5D2294005F1EBC /* libSessionUtil.a */, - FD7C37AE2BBB8B1D009DEEA7 /* SessionSnodeKitTests.xctest */, + FDB5DAFA2A981C42002C8721 /* SessionSnodeKitTests.xctest */, ); name = Products; sourceTree = ""; @@ -3585,7 +3684,7 @@ B8B558ED26C4B55F00693325 /* Calls */, C360969C25AD18BA008B62B2 /* Closed Groups */, B835246C25C38AA20089A44F /* Conversations */, - C32B405424A961E1001117B5 /* Dependencies */, + FD37E9F428A5F0FB003AE748 /* Database */, C360968E25AD16E8008B62B2 /* Home */, C36096BA25AD1B14008B62B2 /* Media Viewing & Editing */, C36096BB25AD1BBB008B62B2 /* Notifications */, @@ -3602,38 +3701,28 @@ FD09796527F6B0A800936362 /* Utilities */ = { isa = PBXGroup; children = ( - FD6A39422C2AD81600762359 /* BackgroundTaskManager.swift */, - FDFBB74A2A1EFF4900CA7350 /* Bencode.swift */, - FD6A39162C2A99A000762359 /* BencodeDecoder.swift */, - FD6A39182C2A99AB00762359 /* BencodeEncoder.swift */, - FD6A391A2C2A99B600762359 /* BencodeResponse.swift */, FD97B23F2A3FEB050027DD57 /* ARC4RandomNumberGenerator.swift */, - FD6A39032C2A8C0300762359 /* CGFloat+Utilities.swift */, - FD6A39052C2A8C1600762359 /* CGPoint+Utilities.swift */, - FD6A39232C2AAE4500762359 /* CGRect+Utilities.swift */, - FD6A39272C2AB2AA00762359 /* CGSize+Utilities.swift */, - FDA8EB0F280F8238002B68E5 /* Codable+Utilities.swift */, - FD12A84A2AD6458800EEBA0D /* DifferenceKit+Utilities.swift */, + FDE755092C9BC135002A2623 /* CGFloat+Utilities.swift */, + FDE755082C9BC134002A2623 /* CGPoint+Utilities.swift */, + FDE7550A2C9BC135002A2623 /* CGRect+Utilities.swift */, + FDE7550B2C9BC135002A2623 /* CGSize+Utilities.swift */, + FDE755072C9BC134002A2623 /* Codable+Utilities.swift */, + FD47E0AE2AA692F400A55E41 /* JSONDecoder+Utilities.swift */, + FDE519F62AB7CDC700450C53 /* Result+Utilities.swift */, FD559DF42A7368CB00C7C62A /* DispatchQueue+Utilities.swift */, FD09796A27F6C67500936362 /* Failable.swift */, - FD6A39092C2A8F2D00762359 /* FileManagerType.swift */, - FD7F74892BB298A8006DDFD8 /* JSONDecoder+Utilities.swift */, FDFF9FDE2A787F57005E0628 /* JSONEncoder+Utilities.swift */, - FD6A38F02C2A66B100762359 /* KeychainStorageType.swift */, FD09797127FAA2F500936362 /* Optional+Utilities.swift */, FDC289462C881A3800020BC2 /* MutableIdentifiable.swift */, FD09797C27FBDB2000936362 /* Notification+Utilities.swift */, FDF222082818D2B0000A4995 /* NSAttributedString+Utilities.swift */, FDC0F0032BFECE12002CBFB9 /* TimeUnit.swift */, - FD8ECF912938552800C0D1BB /* Threading.swift */, - FD8ECF93293856AF00C0D1BB /* Randomness.swift */, - FD1936402ACA7BD8004BCF0F /* Result+Utilities.swift */, + FDE755132C9BC169002A2623 /* UIAlertAction+Utilities.swift */, + FDE755152C9BC169002A2623 /* UIApplicationState+Utilities.swift */, + FDE755172C9BC169002A2623 /* UIBezierPath+Utilities.swift */, + FDE755142C9BC169002A2623 /* UIImage+Utilities.swift */, + FDE755162C9BC169002A2623 /* UINavigationController+Utilities.swift */, FD29598C2A43BC0B00888A17 /* Version.swift */, - FDE049022C76A09700B6F9BB /* UIAlertAction+Utilities.swift */, - FD428B1C2B4B6FDC006D0888 /* UIApplicationState+Utilities.swift */, - FD6A39292C2AB3BD00762359 /* UIBezierPath+Utilities.swift */, - FD6A39012C2A8BDE00762359 /* UIImage+Utilities.swift */, - FD6A39442C2B783D00762359 /* UINavigationController+Utilities.swift */, ); path = Utilities; sourceTree = ""; @@ -3678,7 +3767,7 @@ FD09B7E4288670BB00ED0B66 /* _008_EmojiReacts.swift */, 7BAA7B6528D2DE4700AE1489 /* _009_OpenGroupPermission.swift */, FD7115F128C6CB3900B47552 /* _010_AddThreadIdToFTS.swift */, - FD432431299C6933008A0213 /* _011_AddPendingReadReceipts.swift */, + FDE755012C9BB122002A2623 /* _011_AddPendingReadReceipts.swift */, FD368A6729DE8F9B000DBF1E /* _012_AddFTSIfNeeded.swift */, FD8ECF7C2934293A00C0D1BB /* _013_SessionUtilChanges.swift */, FD778B6329B189FF001BAC6B /* _014_GenerateInitialUserConfigDumps.swift */, @@ -3689,6 +3778,7 @@ FDDD554D2C1FCB77006CBF03 /* _019_ScheduleAppUpdateCheckJob.swift */, FD3559452CC1FF140088F2A9 /* _020_AddMissingWhisperFlag.swift */, FD4C53AE2CC1D61E003B10F4 /* _021_ReworkRecipientState.swift */, + FDB5DAC62A9447E7002C8721 /* _022_GroupsRebuildChanges.swift */, ); path = Migrations; sourceTree = ""; @@ -3762,6 +3852,7 @@ FD9004132818AD0B00ABAAF6 /* _002_SetupStandardJobs.swift */, FD17D7E627F6A16700122BE0 /* _003_YDBToGRDBMigration.swift */, FDBB25E22988B13800F1508E /* _004_AddJobPriority.swift */, + FD1936462ACE752F004BCF0F /* _005_AddJobUniqueHash.swift */, FD0606BE2BC8C10200C3816E /* _005_AddJobUniqueHash.swift */, ); path = Migrations; @@ -3772,7 +3863,7 @@ children = ( FD17D7E427F6A09900122BE0 /* Identity.swift */, FDF0B73F280402C4004C14C5 /* Job.swift */, - FD09C5E1282212B3000CE219 /* JobDependencies.swift */, + FDE754D12C9BAF53002A2623 /* JobDependencies.swift */, FD17D7CC27F546FF00122BE0 /* Setting.swift */, ); path = Models; @@ -3794,18 +3885,55 @@ path = Utilities; sourceTree = ""; }; - FD22724C2C327B97004D8A6C /* _TestUtilities */ = { + FD2272842C33E28D004D8A6C /* SnodeAPI */ = { + isa = PBXGroup; + children = ( + FDF8489329405C1B007DCAE5 /* SnodeAPI.swift */, + FD2272D72C34EDE6004D8A6C /* SnodeAPIEndpoint.swift */, + FDF848E029405D6E007DCAE5 /* SnodeAPIError.swift */, + FDF8489029405C13007DCAE5 /* SnodeAPINamespace.swift */, + FD47E0B42AA6D7AA00A55E41 /* Request+SnodeAPI.swift */, + FD19363B2ACA3134004BCF0F /* ResponseInfo+SnodeAPI.swift */, + ); + path = SnodeAPI; + sourceTree = ""; + }; + FD2272C52C34E9D1004D8A6C /* Types */ = { isa = PBXGroup; children = ( - FD22724D2C327BA5004D8A6C /* SSKMockedExtensions.swift */, + FD01502D2CA24310005B08A1 /* BatchRequestSpec.swift */, + FD01502E2CA24310005B08A1 /* BatchResponseSpec.swift */, + FDD383722AFDD6D7001367F2 /* BencodeResponseSpec.swift */, + FD11E22F2CA4F498001BAF58 /* DestinationSpec.swift */, + FD01502F2CA24310005B08A1 /* HeaderSpec.swift */, + FD0150302CA24310005B08A1 /* PreparedRequestSpec.swift */, + FDB5DB052A981C67002C8721 /* PreparedRequestSendingSpec.swift */, + FD0150312CA24310005B08A1 /* RequestSpec.swift */, ); - path = _TestUtilities; + path = Types; + sourceTree = ""; + }; + FD2272D22C34ECBB004D8A6C /* Types */ = { + isa = PBXGroup; + children = ( + FD0E353A2AB98773006A81F7 /* AppVersion.swift */, + FDB3486D2BE8457F00B716C2 /* BackgroundTaskManager.swift */, + FDE755042C9BB4ED002A2623 /* Bencode.swift */, + FDE755032C9BB4ED002A2623 /* BencodeDecoder.swift */, + FD2272D32C34ECE1004D8A6C /* BencodeEncoder.swift */, + FD3FAB682AF1ADCA00DC5421 /* FileManager.swift */, + FD6A38F02C2A66B100762359 /* KeychainStorage.swift */, + FD8FD7632C37C24A001E38C7 /* EquatableIgnoring.swift */, + FD2272E92C351CA7004D8A6C /* Threading.swift */, + FDAA16752AC28A3B00DDBF77 /* UserDefaultsType.swift */, + ); + path = Types; sourceTree = ""; }; FD23CE202A661CE80000B97C /* Crypto */ = { isa = PBXGroup; children = ( - FD6A39142C2A954000762359 /* Crypto+OpenGroupAPI.swift */, + FDE754A02C9A60A6002A2623 /* Crypto+OpenGroupAPI.swift */, ); path = Crypto; sourceTree = ""; @@ -3813,12 +3941,30 @@ FD2B4B022949886900AB4848 /* Database */ = { isa = PBXGroup; children = ( - FD1D73292A85AA2000E3F410 /* Setting+Utilities.swift */, FD2B4B032949887A00AB4848 /* QueryInterfaceRequest+Utilities.swift */, + FD1D73292A85AA2000E3F410 /* Setting+Utilities.swift */, ); path = Database; sourceTree = ""; }; + FD336F6A2CAA29BC00C0B51B /* Pollers */ = { + isa = PBXGroup; + children = ( + FD336F6B2CAA29C200C0B51B /* CommunityPollerSpec.swift */, + ); + path = Pollers; + sourceTree = ""; + }; + FD3765DD2AD8F02300DC1489 /* _TestUtilities */ = { + isa = PBXGroup; + children = ( + FD23CE312A67C38D0000B97C /* MockNetwork.swift */, + FD3765DE2AD8F03100DC1489 /* MockSnodeAPICache.swift */, + FD3765E12AD8F53B00DC1489 /* CommonSSKMockExtensions.swift */, + ); + path = _TestUtilities; + sourceTree = ""; + }; FD37E9C428A1C701003AE748 /* Themes */ = { isa = PBXGroup; children = ( @@ -3886,40 +4032,25 @@ path = Contacts; sourceTree = ""; }; - FD3C906827E417B100CD579F /* Utilities */ = { - isa = PBXGroup; - children = ( - FD3C906927E417CE00CD579F /* CryptoSMKSpec.swift */, - ); - path = Utilities; - sourceTree = ""; - }; FD3E0C82283B581F002A425C /* Shared Models */ = { isa = PBXGroup; children = ( + FD71162B28E1451400B47552 /* Position.swift */, + FD71161B28D194FB00B47552 /* MentionInfo.swift */, FD848B8C283E0B26000E298B /* MessageInputTypes.swift */, FD848B86283B844B000E298B /* MessageViewModel.swift */, + FDE7549A2C940108002A2623 /* MessageViewModel+DeletionActions.swift */, FD3E0C83283B5835002A425C /* SessionThreadViewModel.swift */, - FD71161B28D194FB00B47552 /* MentionInfo.swift */, ); path = "Shared Models"; sourceTree = ""; }; - FD6A392D2C2ACA9500762359 /* Crypto */ = { + FD66CB2D2BF5EB0C00268FAB /* Types */ = { isa = PBXGroup; children = ( - FD6A392E2C2ACAA600762359 /* Crypto+SessionSnodeKit.swift */, + FD66CB2C2BF5EB0C00268FAB /* Config.swift */, ); - path = Crypto; - sourceTree = ""; - }; - FD6A39462C2BA2C000762359 /* Crypto */ = { - isa = PBXGroup; - children = ( - FD6A39482C2BB85A00762359 /* Crypto+Attachments.swift */, - FD23CE232A675C440000B97C /* Crypto+SessionMessagingKit.swift */, - ); - path = Crypto; + path = Types; sourceTree = ""; }; FD7115F528C8150600B47552 /* Combine */ = { @@ -3927,8 +4058,7 @@ children = ( FD7115F628C8150D00B47552 /* Disposable Views */, FD7115FD28C8202D00B47552 /* ReplaySubject.swift */, - FD83DCDC2A739D350065FFAE /* RetryWithDependencies.swift */, - FD7115FB28C8155800B47552 /* Publisher+Utilities.swift */, + FDE755232C9BC1D1002A2623 /* Publisher+Utilities.swift */, FD71160128C8255900B47552 /* UIControl+Combine.swift */, FD7115F928C8153400B47552 /* UIBarButtonItem+Combine.swift */, FD7115FF28C8253500B47552 /* UIView+Combine.swift */, @@ -3947,6 +4077,7 @@ FD71160A28D00BAE00B47552 /* SessionTests */ = { isa = PBXGroup; children = ( + FDC0F00B2C04100E002CBFB9 /* Session.xctestplan */, FD71161228D00D5300B47552 /* Conversations */, FD19363D2ACA66CF004BCF0F /* Database */, FD71161828D00E0100B47552 /* Settings */, @@ -3982,9 +4113,9 @@ FD71163028E2C41900B47552 /* Types */ = { isa = PBXGroup; children = ( - FD71162B28E1451400B47552 /* Position.swift */, - FD71163128E2C42A00B47552 /* IconSize.swift */, FDE6E99729F8E63A00F93C5D /* Accessibility.swift */, + FD71163128E2C42A00B47552 /* IconSize.swift */, + FD2272E52C351378004D8A6C /* SUIKImageFormat.swift */, ); path = Types; sourceTree = ""; @@ -3998,6 +4129,7 @@ FD71164728E2CE8700B47552 /* SessionCell+AccessoryView.swift */, FD71164528E2CC1300B47552 /* SessionHighlightingBackgroundLabel.swift */, 7B71A98E2925E2A600E54854 /* SessionFooterView.swift */, + C3DAB3232480CB2A00725F25 /* SRCopyableLabel.swift */, ); path = Views; sourceTree = ""; @@ -4030,6 +4162,22 @@ path = Views; sourceTree = ""; }; + FD72BDA22BE368FA00CF6CF6 /* Crypto */ = { + isa = PBXGroup; + children = ( + FD72BDA32BE3690B00CF6CF6 /* CryptoSMKSpec.swift */, + ); + path = Crypto; + sourceTree = ""; + }; + FD72BDA52BE369B600CF6CF6 /* Crypto */ = { + isa = PBXGroup; + children = ( + FD72BDA62BE369DC00CF6CF6 /* CryptoOpenGroupAPISpec.swift */, + ); + path = Crypto; + sourceTree = ""; + }; FD7692F52A53A2C7000E4B70 /* Shared Models */ = { isa = PBXGroup; children = ( @@ -4041,7 +4189,8 @@ FD7728A1284F0DF50018502F /* Message Handling */ = { isa = PBXGroup; children = ( - C38D5E8C2575011E00B6A65C /* MessageSender+ClosedGroups.swift */, + FDB5DAC02A9443A5002C8721 /* MessageSender+Groups.swift */, + C38D5E8C2575011E00B6A65C /* MessageSender+LegacyClosedGroups.swift */, FD5C72F6284F0E560029977D /* MessageReceiver+ReadReceipts.swift */, FD5C72F8284F0E880029977D /* MessageReceiver+TypingIndicators.swift */, FD5C72FA284F0EA10029977D /* MessageReceiver+DataExtractionNotification.swift */, @@ -4049,36 +4198,20 @@ FD5C7300284F0F7A0029977D /* MessageReceiver+UnsendRequests.swift */, FD5C7302284F0FA50029977D /* MessageReceiver+Calls.swift */, FD5C7304284F0FF30029977D /* MessageReceiver+VisibleMessages.swift */, - C32C5A87256DBCF9003C73A2 /* MessageReceiver+ClosedGroups.swift */, + FDB5DAE72A95D96C002C8721 /* MessageReceiver+Groups.swift */, + C32C5A87256DBCF9003C73A2 /* MessageReceiver+LegacyClosedGroups.swift */, + FDF71EA42B07363500A8D6B5 /* MessageReceiver+LibSession.swift */, FD5C7306284F103B0029977D /* MessageReceiver+MessageRequests.swift */, ); path = "Message Handling"; sourceTree = ""; }; - FD7C37AF2BBB8B1E009DEEA7 /* SessionSnodeKitTests */ = { - isa = PBXGroup; - children = ( - FD22724C2C327B97004D8A6C /* _TestUtilities */, - FD7C37BC2BBB8BB1009DEEA7 /* Models */, - ); - path = SessionSnodeKitTests; - sourceTree = ""; - }; - FD7C37BC2BBB8BB1009DEEA7 /* Models */ = { - isa = PBXGroup; - children = ( - FD3C905F27E410F700CD579F /* FileUploadResponseSpec.swift */, - FD7C37BD2BBB8BBC009DEEA7 /* SnodeRequestSpec.swift */, - ); - path = Models; - sourceTree = ""; - }; FD7F74582BAAA349006DDFD8 /* LibSession */ = { isa = PBXGroup; children = ( FD7F74592BAAA352006DDFD8 /* Utilities */, FD7F745A2BAAA35E006DDFD8 /* LibSession.swift */, - FD7F745C2BAAA38B006DDFD8 /* LibSessionError.swift */, + FDE755212C9BC1BA002A2623 /* LibSessionError.swift */, ); path = LibSession; sourceTree = ""; @@ -4102,10 +4235,10 @@ FD83B9B027CF200A005E1583 /* SessionUtilitiesKitTests */ = { isa = PBXGroup; children = ( + FD0150252CA23D5B005B08A1 /* SessionUtilitiesKit.xctestplan */, FD37EA1228AB3F60003AE748 /* Database */, FD83B9B927CF20A5005E1583 /* General */, FDDF074829DAB35200E5E8B5 /* JobRunner */, - FD9B30F1293EA0AF008DEE3E /* Networking */, FDC289482C881C5500020BC2 /* LibSession */, FDFBB7522A2023DE00CA7350 /* Utilities */, ); @@ -4125,20 +4258,21 @@ FD83B9BC27CF2215005E1583 /* _SharedTestUtilities */ = { isa = PBXGroup; children = ( - FDC290A527D860CE005DAE71 /* Mock.swift */, + FD0150472CA243CB005B08A1 /* Mock.swift */, FD0969F82A69FFE700C5C365 /* Mocked.swift */, + FD481A932CAE0ADD00ECC4CF /* MockAppContext.swift */, FD23CE272A67755C0000B97C /* MockCrypto.swift */, - FD23CE2B2A678DF80000B97C /* MockCaches.swift */, + FD3FAB6A2AF1B27800DC5421 /* MockFileManager.swift */, FDFD645C27F273F300808CA1 /* MockGeneralCache.swift */, FD83B9D127D59495005E1583 /* MockUserDefaults.swift */, - FD23CE312A67C38D0000B97C /* MockNetwork.swift */, - FD96F3A629DBD43D00401309 /* MockJobRunner.swift */, + FD49E2452B05C1D500FFBBB5 /* MockKeychain.swift */, + FD0150372CA24328005B08A1 /* MockJobRunner.swift */, FD83B9BD27CF2243005E1583 /* TestConstants.swift */, + FD6531892AA025C500DFEEAA /* TestDependencies.swift */, FD9DD2702A72516D00ECB68E /* TestExtensions.swift */, FDC290A727D9B46D005DAE71 /* NimbleExtensions.swift */, - FD078E4727E02561000769AF /* CommonMockedExtensions.swift */, FD23EA6028ED0B260058676E /* CombineExtensions.swift */, - FD6A39702C2E3F5800762359 /* GRDBExtensions.swift */, + FD0150272CA23DB7005B08A1 /* GRDBExtensions.swift */, FD2AAAEF28ED57B500A49611 /* SynchronousStorage.swift */, ); path = _SharedTestUtilities; @@ -4164,6 +4298,7 @@ children = ( FD2B4B022949886900AB4848 /* Database */, FD8ECF8E29381FB200C0D1BB /* Config Handling */, + FD66CB2D2BF5EB0C00268FAB /* Types */, FD8ECF7A29340FFD00C0D1BB /* LibSession+SessionMessagingKit.swift */, ); path = LibSession; @@ -4172,8 +4307,10 @@ FD8ECF802934385900C0D1BB /* LibSession */ = { isa = PBXGroup; children = ( - FDA1E83829A5771A00C5C3BD /* LibSessionUtilSpec.swift */, - FDA1E83C29AC71A800C5C3BD /* LibSessionSpec.swift */, + FD0150432CA243BB005B08A1 /* LibSessionSpec.swift */, + FD481A8F2CAD16EA00ECC4CF /* LibSessionGroupInfoSpec.swift */, + FD481A912CAD17D900ECC4CF /* LibSessionGroupMembersSpec.swift */, + FD0150442CA243BB005B08A1 /* LibSessionUtilSpec.swift */, ); path = LibSession; sourceTree = ""; @@ -4181,11 +4318,15 @@ FD8ECF8E29381FB200C0D1BB /* Config Handling */ = { isa = PBXGroup; children = ( - FD2B4AFC294688D000AB4848 /* LibSession+Contacts.swift */, - FD43EE9E297E2EE0009C87C5 /* LibSession+ConvoInfoVolatile.swift */, - FDA1E83A29A5F2D500C5C3BD /* LibSession+Shared.swift */, - FD43EE9C297A5190009C87C5 /* LibSession+UserGroups.swift */, - FD8ECF8F29381FC200C0D1BB /* LibSession+UserProfile.swift */, + FD2272F32C352D8D004D8A6C /* LibSession+Contacts.swift */, + FD2272F42C352D8D004D8A6C /* LibSession+ConvoInfoVolatile.swift */, + FD2272F92C352D8E004D8A6C /* LibSession+GroupInfo.swift */, + FD2272F72C352D8D004D8A6C /* LibSession+GroupKeys.swift */, + FD2272F52C352D8D004D8A6C /* LibSession+GroupMembers.swift */, + FD2272F82C352D8E004D8A6C /* LibSession+Shared.swift */, + FD2272F12C352D8D004D8A6C /* LibSession+SharedGroup.swift */, + FD2272F22C352D8D004D8A6C /* LibSession+UserGroups.swift */, + FD2272F62C352D8D004D8A6C /* LibSession+UserProfile.swift */, ); path = "Config Handling"; sourceTree = ""; @@ -4212,38 +4353,56 @@ FD96F3A229DBC3BA00401309 /* Jobs */ = { isa = PBXGroup; children = ( - FD96F3A329DBC3D000401309 /* Types */, + FD3FAB662AF0C47000DC5421 /* DisplayPictureDownloadJobSpec.swift */, + FD96F3A429DBC3DC00401309 /* MessageSendJobSpec.swift */, + FD481AA22CB889A400ECC4CF /* RetrieveDefaultOpenGroupRoomsJobSpec.swift */, ); path = Jobs; sourceTree = ""; }; - FD96F3A329DBC3D000401309 /* Types */ = { + FDAA16792AC28E2200DDBF77 /* Models */ = { isa = PBXGroup; children = ( - FD96F3A429DBC3DC00401309 /* MessageSendJobSpec.swift */, + FDE754AB2C9B967D002A2623 /* FileUploadResponseSpec.swift */, + FDAA167A2AC28E2F00DDBF77 /* SnodeRequestSpec.swift */, ); - path = Types; + path = Models; sourceTree = ""; }; - FD9B30F1293EA0AF008DEE3E /* Networking */ = { + FDB5DAD22A9483D4002C8721 /* Group Update Messages */ = { isa = PBXGroup; children = ( - FD7F74732BB276D0006DDFD8 /* HeaderSpec.swift */, - FD7F74752BB276E7006DDFD8 /* PreparedRequestSpec.swift */, - FD7F74712BB276CC006DDFD8 /* RequestSpec.swift */, - FD7F746F2BB276A0006DDFD8 /* BatchRequestSpec.swift */, - FD9B30F2293EA0BF008DEE3E /* BatchResponseSpec.swift */, + FDB5DAD32A9483F3002C8721 /* GroupUpdateInviteMessage.swift */, + FDB5DADD2A95D847002C8721 /* GroupUpdatePromoteMessage.swift */, + FDB5DAD92A95D839002C8721 /* GroupUpdateInfoChangeMessage.swift */, + FDB5DADB2A95D840002C8721 /* GroupUpdateMemberChangeMessage.swift */, + FDB5DADF2A95D84D002C8721 /* GroupUpdateMemberLeftMessage.swift */, + FDEF573D2C40F2A100131302 /* GroupUpdateMemberLeftNotificationMessage.swift */, + FDB5DAE12A95D8A0002C8721 /* GroupUpdateInviteResponseMessage.swift */, + FDB5DAE52A95D8B0002C8721 /* GroupUpdateDeleteMemberContentMessage.swift */, ); - path = Networking; + path = "Group Update Messages"; + sourceTree = ""; + }; + FDB5DAFB2A981C43002C8721 /* SessionSnodeKitTests */ = { + isa = PBXGroup; + children = ( + FD66CB272BF3449B00268FAB /* SessionSnodeKit.xctestplan */, + FD3765DD2AD8F02300DC1489 /* _TestUtilities */, + FDAA16792AC28E2200DDBF77 /* Models */, + FD2272C52C34E9D1004D8A6C /* Types */, + ); + path = SessionSnodeKitTests; sourceTree = ""; }; FDC13D4E2A16EE41007267C7 /* Types */ = { isa = PBXGroup; children = ( FDC13D482A16EC20007267C7 /* Service.swift */, + FD3765F52ADE5BA500DC1489 /* ServiceInfo.swift */, FDD82C3E2A205D0A00425F05 /* ProcessResult.swift */, FDC13D4F2A16EE50007267C7 /* PushNotificationAPIEndpoint.swift */, - FD7F74872BB2929B006DDFD8 /* Request+PushNotificationAPI.swift */, + FD02CC112C367761009AB976 /* Request+PushNotificationAPI.swift */, ); path = Types; sourceTree = ""; @@ -4271,10 +4430,10 @@ children = ( FDF8487D29405993007DCAE5 /* HTTPHeader+OpenGroup.swift */, FDF8487E29405994007DCAE5 /* HTTPQueryParam+OpenGroup.swift */, - FD7F74792BB277E1006DDFD8 /* Request+OpenGroupAPI.swift */, - FDC4381F27B36ADC00C60D73 /* SOGSEndpoint.swift */, FDC4380827B31D4E00C60D73 /* OpenGroupAPIError.swift */, FDC4381627B32EC700C60D73 /* Personalization.swift */, + FD02CC132C3677E6009AB976 /* Request+OpenGroupAPI.swift */, + FDC4381F27B36ADC00C60D73 /* SOGSEndpoint.swift */, 7B81682228A4C1210069F315 /* UpdateTypes.swift */, ); path = Types; @@ -4306,6 +4465,7 @@ FDC4382D27B383A600C60D73 /* Models */ = { isa = PBXGroup; children = ( + FD3765F32ADE5A0800DC1489 /* AuthenticatedRequest.swift */, FDC13D462A16E4CA007267C7 /* SubscribeRequest.swift */, FDC13D4A2A16ECBA007267C7 /* SubscribeResponse.swift */, FDC13D552A171FE4007267C7 /* UnsubscribeRequest.swift */, @@ -4323,12 +4483,14 @@ FDC4388F27B9FFC700C60D73 /* SessionMessagingKitTests */ = { isa = PBXGroup; children = ( + FD66CB2B2BF344C600268FAB /* SessionMessageKit.xctestplan */, FDC4389B27BA01E300C60D73 /* _TestUtilities */, FD3C906527E416A200CD579F /* Contacts */, + FD72BDA22BE368FA00CF6CF6 /* Crypto */, FD96F3A229DBC3BA00401309 /* Jobs */, FDC4389827BA001800C60D73 /* Open Groups */, + FDE754A72C9B964D002A2623 /* Sending & Receiving */, FD7692F52A53A2C7000E4B70 /* Shared Models */, - FD3C906827E417B100CD579F /* Utilities */, FD8ECF802934385900C0D1BB /* LibSession */, ); path = SessionMessagingKitTests; @@ -4337,6 +4499,7 @@ FDC4389827BA001800C60D73 /* Open Groups */ = { isa = PBXGroup; children = ( + FD72BDA52BE369B600CF6CF6 /* Crypto */, FD83B9C127CF33EE005E1583 /* Models */, FDC2909227D710A9005DAE71 /* Types */, FDC4389927BA002500C60D73 /* OpenGroupAPISpec.swift */, @@ -4348,10 +4511,17 @@ FDC4389B27BA01E300C60D73 /* _TestUtilities */ = { isa = PBXGroup; children = ( - FD22724A2C326E75004D8A6C /* CustomArgSummaryDescribable+SMK.swift */, - FDC438BC27BB2AB400C60D73 /* Mockable.swift */, - FD078E4C27E17156000769AF /* MockOGMCache.swift */, - FDC0F0072C00721A002CBFB9 /* MockOGPoller.swift */, + FD336F562CAA28CF00C0B51B /* CommonSMKMockExtensions.swift */, + FD336F572CAA28CF00C0B51B /* CustomArgSummaryDescribable+SMK.swift */, + FD336F6E2CAA37CB00C0B51B /* MockCommunityPoller.swift */, + FD336F582CAA28CF00C0B51B /* MockCommunityPollerCache.swift */, + FD336F592CAA28CF00C0B51B /* MockDisplayPictureCache.swift */, + FD336F5A2CAA28CF00C0B51B /* MockGroupPollerCache.swift */, + FD336F5B2CAA28CF00C0B51B /* MockLibSessionCache.swift */, + FD336F5C2CAA28CF00C0B51B /* MockNotificationsManager.swift */, + FD336F5D2CAA28CF00C0B51B /* MockOGMCache.swift */, + FD336F5E2CAA28CF00C0B51B /* MockPoller.swift */, + FD336F5F2CAA28CF00C0B51B /* MockSwarmPoller.swift */, ); path = _TestUtilities; sourceTree = ""; @@ -4369,7 +4539,7 @@ FDDC08F029A300D500BF9681 /* Utilities */ = { isa = PBXGroup; children = ( - FDDC08F129A300E800BF9681 /* TypeConversionUtilitiesSpec.swift */, + FD01504D2CA243E7005B08A1 /* TypeConversionUtilitiesSpec.swift */, ); path = Utilities; sourceTree = ""; @@ -4396,16 +4566,45 @@ path = Scripts; sourceTree = ""; }; + FDE754A72C9B964D002A2623 /* Sending & Receiving */ = { + isa = PBXGroup; + children = ( + FD336F6A2CAA29BC00C0B51B /* Pollers */, + FDE754A42C9B964D002A2623 /* MessageReceiverGroupsSpec.swift */, + FDE754A52C9B964D002A2623 /* MessageSenderGroupsSpec.swift */, + FDE754A62C9B964D002A2623 /* MessageSenderSpec.swift */, + ); + path = "Sending & Receiving"; + sourceTree = ""; + }; + FDE754E22C9BAFF4002A2623 /* Crypto */ = { + isa = PBXGroup; + children = ( + FDE754E12C9BAFF4002A2623 /* Crypto+SessionSnodeKit.swift */, + ); + path = Crypto; + sourceTree = ""; + }; + FDE754EF2C9BB08B002A2623 /* Crypto */ = { + isa = PBXGroup; + children = ( + FDE754EC2C9BB08B002A2623 /* Crypto+Attachments.swift */, + FDE754ED2C9BB08B002A2623 /* Crypto+LibSession.swift */, + FDE754EE2C9BB08B002A2623 /* Crypto+SessionMessagingKit.swift */, + ); + path = Crypto; + sourceTree = ""; + }; FDEF57202C3CF02000131302 /* WebRTC */ = { isa = PBXGroup; children = ( - 7BFD1A8B2747150E00FB91B9 /* TurnServerInfo.swift */, - B8DE1FB326C22F2F0079C9CE /* WebRTCSession.swift */, - B806ECA026C4A7E4008BDA44 /* WebRTCSession+UI.swift */, - B8B558FE26C4E05E00693325 /* WebRTCSession+MessageHandling.swift */, - B8BF43B926CC95FB007828D1 /* WebRTC+Utilities.swift */, - 7BCD116B27016062006330F1 /* WebRTCSession+DataChannel.swift */, + FDE754AE2C9B96B4002A2623 /* TurnServerInfo.swift */, + FDE754AF2C9B96B4002A2623 /* WebRTC+Utilities.swift */, + FDE754AD2C9B96B3002A2623 /* WebRTCSession.swift */, 9409433D2C7EB81800D9D2E0 /* WebRTCSession+Constants.swift */, + FDE754B52C9B96BB002A2623 /* WebRTCSession+DataChannel.swift */, + FDE754B42C9B96BA002A2623 /* WebRTCSession+MessageHandling.swift */, + FDE754B32C9B96BA002A2623 /* WebRTCSession+UI.swift */, ); path = WebRTC; sourceTree = ""; @@ -4427,29 +4626,16 @@ path = Countries/SourceData; sourceTree = ""; }; - FDF0B7452804F0A8004C14C5 /* Types */ = { - isa = PBXGroup; - children = ( - FDF0B7462804F0CE004C14C5 /* DisappearingMessagesJob.swift */, - FDA8EAFD280E8B78002B68E5 /* FailedMessageSendsJob.swift */, - FDA8EAFF280E8D58002B68E5 /* FailedAttachmentDownloadsJob.swift */, - FD6A7A6A2818C17C00035AC1 /* UpdateProfilePictureJob.swift */, - FD6A7A682818BE7300035AC1 /* RetrieveDefaultOpenGroupRoomsJob.swift */, - FDD2506F2837199200198BDA /* GarbageCollectionJob.swift */, - C352A2FE25574B6300338F3E /* MessageSendJob.swift */, - C352A31225574F5200338F3E /* MessageReceiveJob.swift */, - FD3003652A25D5B300B5A5FB /* ConfigMessageReceiveJob.swift */, - C352A32E2557549C00338F3E /* NotifyPushServerJob.swift */, - FDF0B74E28079E5E004C14C5 /* SendReadReceiptsJob.swift */, - C352A348255781F400338F3E /* AttachmentDownloadJob.swift */, - C352A35A2557824E00338F3E /* AttachmentUploadJob.swift */, - 7B521E0929BFF84400C3C36A /* GroupLeavingJob.swift */, - FD2B4AFE2946C93200AB4848 /* ConfigurationSyncJob.swift */, - 7B7E5B512A4D024C00A8208E /* ExpirationUpdateJob.swift */, - 7B7AD41E2A5512CA00469FB1 /* GetExpirationJob.swift */, - FDDD554B2C1FC812006CBF03 /* CheckForAppUpdatesJob.swift */, + FDF01FAE2A9ED0C800CAF969 /* Dependency Injection */ = { + isa = PBXGroup; + children = ( + FDE7551F2C9BC1A6002A2623 /* CacheConfig.swift */, + FDC6D75F2862B3F600B04575 /* Dependencies.swift */, + FD2272ED2C3521D6004D8A6C /* FeatureConfig.swift */, + FDF01FAC2A9ECC4200CAF969 /* SingletonConfig.swift */, + FDBEE52D2B6A18B900C143A0 /* UserDefaultsConfig.swift */, ); - path = Types; + path = "Dependency Injection"; sourceTree = ""; }; FDF0B7562807F35E004C14C5 /* Errors */ = { @@ -4465,27 +4651,28 @@ FDF8488F29405C13007DCAE5 /* Types */ = { isa = PBXGroup; children = ( - FDF848DF29405D6E007DCAE5 /* SnodeAPIEndpoint.swift */, - FDF848E029405D6E007DCAE5 /* SnodeAPIError.swift */, - FDF8489029405C13007DCAE5 /* SnodeAPINamespace.swift */, - FD7F74832BB283DF006DDFD8 /* SwarmDrainBehaviour.swift */, - FD7F74812BB283CE006DDFD8 /* UpdatableTimestamp.swift */, - FD43242F2999F0BC008A0213 /* ValidatableResponse.swift */, - ); - path = Types; - sourceTree = ""; - }; - FDF8489229405C1B007DCAE5 /* Networking */ = { - isa = PBXGroup; - children = ( + FD22729F2C33E336004D8A6C /* BatchRequest.swift */, + FD2272A12C33E336004D8A6C /* BatchResponse.swift */, + FDD383702AFDD0E1001367F2 /* BencodeResponse.swift */, + FD2272952C33E335004D8A6C /* ContentProxy.swift */, FDF848E129405D6E007DCAE5 /* Destination.swift */, - FDF8489329405C1B007DCAE5 /* SnodeAPI.swift */, - FD7F747F2BB283A9006DDFD8 /* Request+SnodeAPI.swift */, - FD7F74852BB2868E006DDFD8 /* ResponseInfo+SnodeAPI.swift */, - FDF848E829405E4E007DCAE5 /* Network+OnionRequest.swift */, - FD7F747B2BB28182006DDFD8 /* PreparedRequest+OnionRequest.swift */, + FD2272A62C33E337004D8A6C /* HTTPHeader.swift */, + FD2272A72C33E337004D8A6C /* HTTPMethod.swift */, + FD22729A2C33E336004D8A6C /* HTTPQueryParam.swift */, + FD2272982C33E336004D8A6C /* IPv4.swift */, + FD22729B2C33E336004D8A6C /* JSON.swift */, + FD2272992C33E336004D8A6C /* Network.swift */, + FD22729C2C33E336004D8A6C /* NetworkError.swift */, + FD22729E2C33E336004D8A6C /* PreparedRequest.swift */, + FDB5DAF22A96DD4F002C8721 /* PreparedRequest+Sending.swift */, + FD22729D2C33E336004D8A6C /* ProxiedContentDownloader.swift */, + FD2272A32C33E337004D8A6C /* Request.swift */, + FD2272A52C33E337004D8A6C /* ResponseInfo.swift */, + FD2272A02C33E336004D8A6C /* SwarmDrainBehaviour.swift */, + FD2272962C33E335004D8A6C /* UpdatableTimestamp.swift */, + FD2272972C33E335004D8A6C /* ValidatableResponse.swift */, ); - path = Networking; + path = Types; sourceTree = ""; }; FDF8489929405C5A007DCAE5 /* Models */ = { @@ -4515,8 +4702,10 @@ FDF848B129405C5A007DCAE5 /* UpdateExpiryAllResponse.swift */, FDD20C152A09E64A003898FB /* GetExpiriesRequest.swift */, FDD20C172A09E7D3003898FB /* GetExpiriesResponse.swift */, - FDF848BA29405C5A007DCAE5 /* RevokeSubkeyRequest.swift */, - FDF848A229405C5A007DCAE5 /* RevokeSubkeyResponse.swift */, + FD3765E62ADE1AA300DC1489 /* UnrevokeSubaccountRequest.swift */, + FD3765E82ADE1AAE00DC1489 /* UnrevokeSubaccountResponse.swift */, + FDF848BA29405C5A007DCAE5 /* RevokeSubaccountRequest.swift */, + FD02CC152C3681EF009AB976 /* RevokeSubaccountResponse.swift */, FDF848AA29405C5A007DCAE5 /* SnodeReceivedMessage.swift */, FDF848B429405C5A007DCAE5 /* SnodeMessage.swift */, FDF848AB29405C5A007DCAE5 /* GetNetworkTimestampResponse.swift */, @@ -4532,10 +4721,9 @@ FDFBB7522A2023DE00CA7350 /* Utilities */ = { isa = PBXGroup; children = ( - FDFBB7532A2023EB00CA7350 /* BencodeDecoderSpec.swift */, - FD6A391C2C2A99DF00762359 /* BencodeEncoderSpec.swift */, - FD6A391E2C2A99EF00762359 /* BencodeResponseSpec.swift */, - FD29598F2A43BE5F00888A17 /* VersionSpec.swift */, + FD01503D2CA2433D005B08A1 /* BencodeDecoderSpec.swift */, + FD01503E2CA2433D005B08A1 /* BencodeEncoderSpec.swift */, + FD01503F2CA2433D005B08A1 /* VersionSpec.swift */, ); path = Utilities; sourceTree = ""; @@ -4566,7 +4754,6 @@ isa = PBXHeadersBuildPhase; buildActionMask = 2147483647; files = ( - C33FD9AF255A548A00E217F9 /* SignalUtilitiesKit.h in Headers */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -4582,9 +4769,7 @@ isa = PBXHeadersBuildPhase; buildActionMask = 2147483647; files = ( - C352A3772557864000338F3E /* NSTimer+Proxying.h in Headers */, - C3C2A67D255388CC00C340D1 /* SessionUtilitiesKit.h in Headers */, - C32C6018256E07F9003C73A2 /* NSUserDefaults+OWS.h in Headers */, + FDB348892BE8705D00B716C2 /* SessionUtilitiesKit.h in Headers */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -4665,7 +4850,6 @@ buildRules = ( ); dependencies = ( - FD37E9F228A5ED70003AE748 /* PBXTargetDependency */, ); name = SessionUIKit; packageProductDependencies = ( @@ -4711,6 +4895,7 @@ buildRules = ( ); dependencies = ( + FDB348822BE86A4400B716C2 /* PBXTargetDependency */, FD7F74622BAAA4C7006DDFD8 /* PBXTargetDependency */, ); name = SessionSnodeKit; @@ -4730,10 +4915,12 @@ buildRules = ( ); dependencies = ( + FDB348852BE86A4800B716C2 /* PBXTargetDependency */, FD7F74652BAAA4CA006DDFD8 /* PBXTargetDependency */, ); name = SessionUtilitiesKit; packageProductDependencies = ( + FD6A38E52C2A4D8E00762359 /* GRDB */, FD6A38E82C2A630E00762359 /* CocoaLumberjackSwift */, FD6A38EB2C2A63B500762359 /* KeychainSwift */, FD6A38EE2C2A641200762359 /* DifferenceKit */, @@ -4797,6 +4984,7 @@ ); name = Session; packageProductDependencies = ( + FD6A395B2C2D10C700762359 /* YYImage */, FD6A394E2C2D060C00762359 /* YYImage */, FD6A395B2C2D10C700762359 /* YYImage */, FD6A39682C2D283A00762359 /* YYImage */, @@ -4830,28 +5018,6 @@ productReference = FD71160928D00BAE00B47552 /* SessionTests.xctest */; productType = "com.apple.product-type.bundle.unit-test"; }; - FD7C37AD2BBB8B1D009DEEA7 /* SessionSnodeKitTests */ = { - isa = PBXNativeTarget; - buildConfigurationList = FD7C37B52BBB8B1E009DEEA7 /* Build configuration list for PBXNativeTarget "SessionSnodeKitTests" */; - buildPhases = ( - FD7C37AA2BBB8B1D009DEEA7 /* Sources */, - FD7C37AB2BBB8B1D009DEEA7 /* Frameworks */, - FD7C37AC2BBB8B1D009DEEA7 /* Resources */, - ); - buildRules = ( - ); - dependencies = ( - FD7C37B42BBB8B1E009DEEA7 /* PBXTargetDependency */, - ); - name = SessionSnodeKitTests; - packageProductDependencies = ( - FD6A39352C2AD36400762359 /* Quick */, - FD6A393E2C2AD3B100762359 /* Nimble */, - ); - productName = SessionSnodeKitTests; - productReference = FD7C37AE2BBB8B1D009DEEA7 /* SessionSnodeKitTests.xctest */; - productType = "com.apple.product-type.bundle.unit-test"; - }; FD83B9AE27CF200A005E1583 /* SessionUtilitiesKitTests */ = { isa = PBXNativeTarget; buildConfigurationList = FD83B9B627CF200A005E1583 /* Build configuration list for PBXNativeTarget "SessionUtilitiesKitTests" */; @@ -4890,6 +5056,24 @@ productReference = FD9BDDF82A5D2294005F1EBC /* libSessionUtil.a */; productType = "com.apple.product-type.library.static"; }; + FDB5DAF92A981C42002C8721 /* SessionSnodeKitTests */ = { + isa = PBXNativeTarget; + buildConfigurationList = FD2272522C32910F004D8A6C /* Build configuration list for PBXNativeTarget "SessionSnodeKitTests" */; + buildPhases = ( + FDB5DAF62A981C42002C8721 /* Sources */, + FD01504F2CA2445E005B08A1 /* Frameworks */, + FDB5DAF82A981C42002C8721 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + FDB5DB002A981C43002C8721 /* PBXTargetDependency */, + ); + name = SessionSnodeKitTests; + productName = SessionSnodeKitTests; + productReference = FDB5DAFA2A981C42002C8721 /* SessionSnodeKitTests.xctest */; + productType = "com.apple.product-type.bundle.unit-test"; + }; FDC4388D27B9FFC700C60D73 /* SessionMessagingKitTests */ = { isa = PBXNativeTarget; buildConfigurationList = FDC4389527B9FFC700C60D73 /* Build configuration list for PBXNativeTarget "SessionMessagingKitTests" */; @@ -4922,7 +5106,7 @@ DefaultBuildSystemTypeForWorkspace = Original; LastSwiftUpdateCheck = 1520; LastTestingUpgradeCheck = 0600; - LastUpgradeCheck = 1520; + LastUpgradeCheck = 1600; ORGANIZATIONNAME = "Rangeproof Pty Ltd"; TargetAttributes = { 453518671FC635DD00210559 = { @@ -5014,15 +5198,15 @@ CreatedOnToolsVersion = 13.4.1; TestTargetID = D221A088169C9E5E00537ABF; }; - FD7C37AD2BBB8B1D009DEEA7 = { - CreatedOnToolsVersion = 15.2; - }; FD83B9AE27CF200A005E1583 = { CreatedOnToolsVersion = 13.2.1; }; FD9BDDF72A5D2294005F1EBC = { CreatedOnToolsVersion = 14.3; }; + FDB5DAF92A981C42002C8721 = { + CreatedOnToolsVersion = 14.3; + }; FDC4388D27B9FFC700C60D73 = { CreatedOnToolsVersion = 13.2.1; }; @@ -5038,6 +5222,7 @@ ); mainGroup = D221A07E169C9E5E00537ABF; packageReferences = ( + FD6A38E42C2A4D8E00762359 /* XCRemoteSwiftPackageReference "session-grdb-swift" */, FD6A38E72C2A630E00762359 /* XCRemoteSwiftPackageReference "CocoaLumberjack" */, FD6A38EA2C2A63B500762359 /* XCRemoteSwiftPackageReference "keychain-swift" */, FD6A38ED2C2A641200762359 /* XCRemoteSwiftPackageReference "DifferenceKit" */, @@ -5064,7 +5249,7 @@ C3C2A678255388CC00C340D1 /* SessionUtilitiesKit */, FD71160828D00BAE00B47552 /* SessionTests */, FDC4388D27B9FFC700C60D73 /* SessionMessagingKitTests */, - FD7C37AD2BBB8B1D009DEEA7 /* SessionSnodeKitTests */, + FDB5DAF92A981C42002C8721 /* SessionSnodeKitTests */, FD83B9AE27CF200A005E1583 /* SessionUtilitiesKitTests */, FD9BDDF72A5D2294005F1EBC /* SessionUtil */, ); @@ -5194,17 +5379,19 @@ ); runOnlyForDeploymentPostprocessing = 0; }; - FD7C37AC2BBB8B1D009DEEA7 /* Resources */ = { + FD83B9AD27CF200A005E1583 /* Resources */ = { isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; files = ( + FD0150262CA23D5B005B08A1 /* SessionUtilitiesKit.xctestplan in Resources */, ); runOnlyForDeploymentPostprocessing = 0; }; - FD83B9AD27CF200A005E1583 /* Resources */ = { + FDB5DAF82A981C42002C8721 /* Resources */ = { isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; files = ( + FD66CB2A2BF3449B00268FAB /* SessionSnodeKit.xctestplan in Resources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -5516,6 +5703,7 @@ C3ADC66126426688005F1414 /* ShareNavController.swift in Sources */, 7BAF54D427ACCF01003D12F8 /* SAEScreenLockViewController.swift in Sources */, B817AD9C26436F73009DF825 /* ThreadPickerVC.swift in Sources */, + FD2272DE2C34F11F004D8A6C /* _001_ThemePreferences.swift in Sources */, 7BAF54D327ACCF01003D12F8 /* ShareAppExtensionContext.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; @@ -5545,35 +5733,38 @@ FD71165928E436E800B47552 /* ConfirmationModal.swift in Sources */, 7BBBDC44286EAD2D00747E59 /* TappableLabel.swift in Sources */, FD09B7E328865FDA00ED0B66 /* HighlightMentionBackgroundView.swift in Sources */, - C331FFB82558FA8D00070591 /* DeviceUtilities.swift in Sources */, + FD3FAB632AEB9A1500DC5421 /* ToastController.swift in Sources */, + FD2272E42C35134B004D8A6C /* Data+Utilities.swift in Sources */, C331FFE72558FB0000070591 /* TextField.swift in Sources */, 942256962C23F8DD00C0FDBF /* CompatibleScrollingVStack.swift in Sources */, FD71165B28E6DDBC00B47552 /* StyledNavigationController.swift in Sources */, C331FFE32558FB0000070591 /* TabBar.swift in Sources */, FD37E9D528A1FCE8003AE748 /* Theme+OceanLight.swift in Sources */, FDF848F129406A30007DCAE5 /* Format.swift in Sources */, + FDB348632BE3774000B716C2 /* BezierPathView.swift in Sources */, 7BA1E0E82A8087DB00123D0D /* SwiftUI+Utilities.swift in Sources */, FD37E9C828A1D73F003AE748 /* Theme+Colors.swift in Sources */, 942256982C23F8DD00C0FDBF /* SessionTextField.swift in Sources */, 9422EE2B2B8C3A97004C740D /* String+Utilities.swift in Sources */, FD37EA0128A60473003AE748 /* UIKit+Theme.swift in Sources */, FD37E9CF28A1EB1B003AE748 /* Theme.swift in Sources */, + FDE754BA2C9B97B8002A2623 /* UIDevice+Utilities.swift in Sources */, C331FFB92558FA8D00070591 /* UIView+Constraints.swift in Sources */, FD0B77B029B69A65009169BA /* TopBannerController.swift in Sources */, 94E9BC0D2C7BFBDA006984EA /* Localization+Style.swift in Sources */, FD37E9F628A5F106003AE748 /* Configuration.swift in Sources */, + FD2272E62C351378004D8A6C /* SUIKImageFormat.swift in Sources */, 7BF8D1FB2A70AF57005F1D6E /* SwiftUI+Theme.swift in Sources */, + FDE754BE2C9BA16C002A2623 /* UIButtonConfiguration+Utilities.swift in Sources */, FDBB25E72988BBBE00F1508E /* UIContextualAction+Theming.swift in Sources */, C331FFE02558FB0000070591 /* SearchBar.swift in Sources */, FD16AB5B2A1DD7CA0083D849 /* PlaceholderIcon.swift in Sources */, - FD71162C28E1451400B47552 /* Position.swift in Sources */, 942256942C23F8DD00C0FDBF /* ActivityView.swift in Sources */, FD52090328B4680F006098F6 /* RadioButton.swift in Sources */, C331FFE82558FB0000070591 /* TextView.swift in Sources */, - FD71162028D97ABC00B47552 /* UIImage+Tinting.swift in Sources */, + FD71162028D97ABC00B47552 /* UIImage+Utilities.swift in Sources */, FD71162A28DA83DF00B47552 /* GradientView.swift in Sources */, FD37E9D728A20B5D003AE748 /* UIColor+Utilities.swift in Sources */, - FD37E9F928A5F14A003AE748 /* _001_ThemePreferences.swift in Sources */, FD37E9C328A1C6F3003AE748 /* ThemeManager.swift in Sources */, C331FF9A2558FA6B00070591 /* Values.swift in Sources */, FD37E9C628A1D4EC003AE748 /* Theme+ClassicDark.swift in Sources */, @@ -5593,31 +5784,26 @@ files = ( C38EF3C6255B6DE7007E1867 /* ImageEditorModel.swift in Sources */, C38EF3C3255B6DE7007E1867 /* ImageEditorTextItem.swift in Sources */, - C33FDD49255A582000E217F9 /* ParamParser.swift in Sources */, C38EF3C5255B6DE7007E1867 /* OWSViewController+ImageEditor.swift in Sources */, C38EF385255B6DD2007E1867 /* AttachmentTextToolbar.swift in Sources */, - C33FDD23255A582000E217F9 /* FeatureFlags.swift in Sources */, FD71161E28D9772700B47552 /* UIViewController+OWS.swift in Sources */, C38EF389255B6DD2007E1867 /* AttachmentTextView.swift in Sources */, C38EF3FF255B6DF7007E1867 /* TappableView.swift in Sources */, C38EF3C2255B6DE7007E1867 /* ImageEditorPaletteView.swift in Sources */, - FD6A39262C2AB0B500762359 /* OWSViewController.swift in Sources */, C38EF30C255B6DBF007E1867 /* ScreenLock.swift in Sources */, C38EF363255B6DCC007E1867 /* ModalActivityIndicatorViewController.swift in Sources */, C38EF3C1255B6DE7007E1867 /* ImageEditorBrushViewController.swift in Sources */, - C3F0A530255C80BC007BE2A3 /* NoopNotificationsManager.swift in Sources */, C33FDD8D255A582000E217F9 /* OWSSignalAddress.swift in Sources */, C38EF388255B6DD2007E1867 /* AttachmentApprovalViewController.swift in Sources */, - C38EF407255B6DF7007E1867 /* Toast.swift in Sources */, C38EF38C255B6DD2007E1867 /* ApprovalRailCellView.swift in Sources */, C38EF3C7255B6DE7007E1867 /* ImageEditorCanvasView.swift in Sources */, C38EF400255B6DF7007E1867 /* GalleryRailView.swift in Sources */, C38EF32E255B6DBF007E1867 /* ImageCache.swift in Sources */, - FD87DD0428B8727D00AF0F98 /* Configuration.swift in Sources */, C38EF3BA255B6DE7007E1867 /* ImageEditorItem.swift in Sources */, - C33FDD3A255A582000E217F9 /* Notification+Loki.swift in Sources */, C38EF24E255B6D67007E1867 /* Collection+OWS.swift in Sources */, + FD2272DD2C34EFFA004D8A6C /* AppSetup.swift in Sources */, C38EF24D255B6D67007E1867 /* UIView+OWS.swift in Sources */, + FDE754E72C9BB051002A2623 /* OWSViewController.swift in Sources */, C38EF38B255B6DD2007E1867 /* AttachmentPrepViewController.swift in Sources */, C38EF405255B6DF7007E1867 /* OWSButton.swift in Sources */, C38EF3C4255B6DE7007E1867 /* ImageEditorContents.swift in Sources */, @@ -5632,19 +5818,12 @@ FD52090B28B59BB4006098F6 /* ScreenLockViewController.swift in Sources */, C38EF3BD255B6DE7007E1867 /* ImageEditorTransform.swift in Sources */, C33FDC58255A582000E217F9 /* ReverseDispatchQueue.swift in Sources */, - C38EF324255B6DBF007E1867 /* Bench.swift in Sources */, - FD6A392C2C2AC51900762359 /* AppVersion.swift in Sources */, - FDCDB8DE2810F73B00352A0C /* Differentiable+Utilities.swift in Sources */, C38EF3F9255B6DF7007E1867 /* OWSLayerView.swift in Sources */, - C33FDD03255A582000E217F9 /* WeakTimer.swift in Sources */, C38EF3B9255B6DE7007E1867 /* ImageEditorPinchGestureRecognizer.swift in Sources */, - C33FDC98255A582000E217F9 /* SwiftSingletons.swift in Sources */, + FDB3487E2BE856C800B716C2 /* UIBezierPath+Utilities.swift in Sources */, C38EF3B8255B6DE7007E1867 /* ImageEditorTextViewController.swift in Sources */, C38EF386255B6DD2007E1867 /* AttachmentApprovalInputAccessoryView.swift in Sources */, B8C2B2C82563685C00551B4D /* CircleView.swift in Sources */, - C38EF331255B6DBF007E1867 /* UIGestureRecognizer+OWS.swift in Sources */, - FD848B9C284435D7000E298B /* AppSetup.swift in Sources */, - C38EF31C255B6DBF007E1867 /* Searcher.swift in Sources */, C38EF2B3255B6D9C007E1867 /* UIViewController+Utilities.swift in Sources */, C38EF3BE255B6DE7007E1867 /* OrderedDictionary.swift in Sources */, ); @@ -5654,65 +5833,84 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + FD2272B12C33E337004D8A6C /* ProxiedContentDownloader.swift in Sources */, FDF848C329405C5A007DCAE5 /* DeleteMessagesRequest.swift in Sources */, FDF8489129405C13007DCAE5 /* SnodeAPINamespace.swift in Sources */, - C3C2A5E02553860B00C340D1 /* Threading.swift in Sources */, - FD7F746A2BAB8A6D006DDFD8 /* LibSession+Networking.swift in Sources */, + FD2272AB2C33E337004D8A6C /* ValidatableResponse.swift in Sources */, + FD2272B72C33E337004D8A6C /* Request.swift in Sources */, + FD2272B92C33E337004D8A6C /* ResponseInfo.swift in Sources */, + C3C2A5E02553860B00C340D1 /* Threading+SSK.swift in Sources */, FDF848D729405C5B007DCAE5 /* SnodeBatchRequest.swift in Sources */, FDF848CE29405C5B007DCAE5 /* UpdateExpiryAllRequest.swift in Sources */, - FD7F747C2BB28182006DDFD8 /* PreparedRequest+OnionRequest.swift in Sources */, FDF848C229405C5A007DCAE5 /* OxenDaemonRPCRequest.swift in Sources */, - FDF848DC29405C5B007DCAE5 /* RevokeSubkeyRequest.swift in Sources */, - FD4324302999F0BC008A0213 /* ValidatableResponse.swift in Sources */, + FDF848DC29405C5B007DCAE5 /* RevokeSubaccountRequest.swift in Sources */, FD17D7A027F40CC800122BE0 /* _001_InitialSetupMigration.swift in Sources */, FDF848D029405C5B007DCAE5 /* UpdateExpiryResponse.swift in Sources */, FDF848D329405C5B007DCAE5 /* UpdateExpiryAllResponse.swift in Sources */, FDD20C162A09E64A003898FB /* GetExpiriesRequest.swift in Sources */, FDF848BC29405C5A007DCAE5 /* SnodeRecursiveResponse.swift in Sources */, FDF848C029405C5A007DCAE5 /* ONSResolveResponse.swift in Sources */, - FD6A392F2C2ACAA600762359 /* Crypto+SessionSnodeKit.swift in Sources */, - FD7F74822BB283CE006DDFD8 /* UpdatableTimestamp.swift in Sources */, + FD2272AC2C33E337004D8A6C /* IPv4.swift in Sources */, FD17D7A427F40F8100122BE0 /* _003_YDBToGRDBMigration.swift in Sources */, - FD7F74802BB283A9006DDFD8 /* Request+SnodeAPI.swift in Sources */, + FD2272D82C34EDE7004D8A6C /* SnodeAPIEndpoint.swift in Sources */, FDFC4D9A29F0C51500992FB6 /* String+Trimming.swift in Sources */, + FDB5DAF32A96DD4F002C8721 /* PreparedRequest+Sending.swift in Sources */, FDF848C629405C5B007DCAE5 /* DeleteAllMessagesRequest.swift in Sources */, - FD7F74862BB2868E006DDFD8 /* ResponseInfo+SnodeAPI.swift in Sources */, FDF848D429405C5B007DCAE5 /* DeleteAllBeforeResponse.swift in Sources */, + FD47E0B52AA6D7AA00A55E41 /* Request+SnodeAPI.swift in Sources */, FD5E93D12C100FD70038C25A /* FileUploadResponse.swift in Sources */, FDF848D629405C5B007DCAE5 /* SnodeMessage.swift in Sources */, + FD02CC162C3681EF009AB976 /* RevokeSubaccountResponse.swift in Sources */, + FDE754E32C9BAFF4002A2623 /* Crypto+SessionSnodeKit.swift in Sources */, + FD2272AD2C33E337004D8A6C /* Network.swift in Sources */, + FD2272B32C33E337004D8A6C /* BatchRequest.swift in Sources */, FDF848D129405C5B007DCAE5 /* SnodeSwarmItem.swift in Sources */, FDF848DD29405C5B007DCAE5 /* LegacySendMessageRequest.swift in Sources */, FDF848BD29405C5A007DCAE5 /* GetMessagesRequest.swift in Sources */, + FD2272B02C33E337004D8A6C /* NetworkError.swift in Sources */, + FD19363C2ACA3134004BCF0F /* ResponseInfo+SnodeAPI.swift in Sources */, FDF848DB29405C5B007DCAE5 /* DeleteMessagesResponse.swift in Sources */, FDF848E629405D6E007DCAE5 /* Destination.swift in Sources */, FDF848CC29405C5B007DCAE5 /* SnodeReceivedMessage.swift in Sources */, FDF848C129405C5A007DCAE5 /* UpdateExpiryRequest.swift in Sources */, FDF848C729405C5B007DCAE5 /* SendMessageResponse.swift in Sources */, FDF848CA29405C5B007DCAE5 /* DeleteAllBeforeRequest.swift in Sources */, + FD2272AF2C33E337004D8A6C /* JSON.swift in Sources */, + FD2272D62C34ED6A004D8A6C /* RetryWithDependencies.swift in Sources */, FDF848D229405C5B007DCAE5 /* LegacyGetMessagesRequest.swift in Sources */, - FDF848C429405C5A007DCAE5 /* RevokeSubkeyResponse.swift in Sources */, FDF848E529405D6E007DCAE5 /* SnodeAPIError.swift in Sources */, FDF848D529405C5B007DCAE5 /* DeleteAllMessagesResponse.swift in Sources */, + FD2272B22C33E337004D8A6C /* PreparedRequest.swift in Sources */, FDF848BF29405C5A007DCAE5 /* SnodeResponse.swift in Sources */, FDD20C182A09E7D3003898FB /* GetExpiriesResponse.swift in Sources */, + FD2272BB2C33E337004D8A6C /* HTTPMethod.swift in Sources */, C3C2A5E42553860B00C340D1 /* Data+Utilities.swift in Sources */, FDF848D929405C5B007DCAE5 /* SnodeAuthenticatedRequestBody.swift in Sources */, + FD2272BA2C33E337004D8A6C /* HTTPHeader.swift in Sources */, FDF848CD29405C5B007DCAE5 /* GetNetworkTimestampResponse.swift in Sources */, FDF848DA29405C5B007DCAE5 /* GetMessagesResponse.swift in Sources */, FD39353628F7C3390084DADA /* _004_FlagMessageHashAsDeletedOrInvalid.swift in Sources */, FD7F74572BAA9D31006DDFD8 /* _006_DropSnodeCache.swift in Sources */, FDF8489429405C1B007DCAE5 /* SnodeAPI.swift in Sources */, + FD2286682C37DA3B00BC06F7 /* LibSession+Networking.swift in Sources */, + FD2272A92C33E337004D8A6C /* ContentProxy.swift in Sources */, FDF848C829405C5B007DCAE5 /* ONSResolveRequest.swift in Sources */, FD6DF00B2ACFE40D0084BA4C /* _005_AddSnodeReveivedMessageInfoPrimaryKey.swift in Sources */, - FD7F74842BB283DF006DDFD8 /* SwarmDrainBehaviour.swift in Sources */, C3C2A5C2255385EE00C340D1 /* Configuration.swift in Sources */, FDF848C929405C5B007DCAE5 /* SnodeRequest.swift in Sources */, FDF848CF29405C5B007DCAE5 /* SendMessageRequest.swift in Sources */, + FD2272BE2C34B710004D8A6C /* Publisher+Utilities.swift in Sources */, FD6A7A6D2818C61500035AC1 /* _002_SetupStandardJobs.swift in Sources */, - FDF848E429405D6E007DCAE5 /* SnodeAPIEndpoint.swift in Sources */, + FD3765E72ADE1AA300DC1489 /* UnrevokeSubaccountRequest.swift in Sources */, + FD2272BC2C33E337004D8A6C /* URLResponse+Utilities.swift in Sources */, + FD2272B52C33E337004D8A6C /* BatchResponse.swift in Sources */, + FD2272AA2C33E337004D8A6C /* UpdatableTimestamp.swift in Sources */, + FD3765E92ADE1AAE00DC1489 /* UnrevokeSubaccountResponse.swift in Sources */, FD5E93D22C12B0580038C25A /* AppVersionResponse.swift in Sources */, - FDF848EB29405E4F007DCAE5 /* Network+OnionRequest.swift in Sources */, + FD2272B42C33E337004D8A6C /* SwarmDrainBehaviour.swift in Sources */, + FD2272AE2C33E337004D8A6C /* HTTPQueryParam.swift in Sources */, FD17D7AE27F41C4300122BE0 /* SnodeReceivedMessageInfo.swift in Sources */, + FD2272C42C34E9AA004D8A6C /* BencodeResponse.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -5720,145 +5918,127 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( - FDFD645927F26C6800808CA1 /* Array+Utilities.swift in Sources */, + FD2272C82C34EB0A004D8A6C /* Job.swift in Sources */, + FD2272C72C34EAF5004D8A6C /* ColumnExpressible.swift in Sources */, + FDB3486E2BE8457F00B716C2 /* BackgroundTaskManager.swift in Sources */, 943C6D842B86B5F1004ACE64 /* Localization.swift in Sources */, FD0606BF2BC8C10200C3816E /* _005_AddJobUniqueHash.swift in Sources */, 7B1D74B027C365960030B423 /* Timer+MainThread.swift in Sources */, - FDE049032C76A09700B6F9BB /* UIAlertAction+Utilities.swift in Sources */, FD428B192B4B576F006D0888 /* AppContext.swift in Sources */, - FD6A39002C2A8B9100762359 /* UTType+Utilities.swift in Sources */, - FD6A38F92C2A8AF700762359 /* DataSource.swift in Sources */, + FD2272D42C34ECE1004D8A6C /* BencodeEncoder.swift in Sources */, + FDE755052C9BB4EE002A2623 /* BencodeDecoder.swift in Sources */, FD559DF52A7368CB00C7C62A /* DispatchQueue+Utilities.swift in Sources */, - FDF8488329405A12007DCAE5 /* BatchResponse.swift in Sources */, + FDE754DC2C9BAF8A002A2623 /* CryptoError.swift in Sources */, FDFF9FDF2A787F57005E0628 /* JSONEncoder+Utilities.swift in Sources */, - FD6A392A2C2AB3BD00762359 /* UIBezierPath+Utilities.swift in Sources */, - FD7F746E2BB2766D006DDFD8 /* PreparedRequest.swift in Sources */, - C3BBE0A82554D4DE0050F1E3 /* JSON.swift in Sources */, + FDE754CD2C9BAF37002A2623 /* UTType+Utilities.swift in Sources */, FD17D7C127F5200100122BE0 /* TypedTableDefinition.swift in Sources */, - FD23CE1B2A651E6D0000B97C /* Network.swift in Sources */, - FDA8EB10280F8238002B68E5 /* Codable+Utilities.swift in Sources */, + FDE754D22C9BAF53002A2623 /* JobDependencies.swift in Sources */, + FDE755242C9BC1D1002A2623 /* Publisher+Utilities.swift in Sources */, FDBB25E32988B13800F1508E /* _004_AddJobPriority.swift in Sources */, - C352A36D2557858E00338F3E /* NSTimer+Proxying.m in Sources */, 7B7CB192271508AD0079FF93 /* CallRingTonePlayer.swift in Sources */, FD848B8B283DC509000E298B /* PagedDatabaseObserver.swift in Sources */, B8856E09256F1676001CE70E /* UIDevice+featureSupport.swift in Sources */, - FD6A38FE2C2A8B7E00762359 /* MediaUtils.swift in Sources */, - FD09C5E2282212B3000CE219 /* JobDependencies.swift in Sources */, - FDED2E3C282E1B5D00B2CD2A /* UICollectionView+ReusableView.swift in Sources */, - FDF8487B29405906007DCAE5 /* HTTPHeader.swift in Sources */, - FD6A38F52C2A6BD200762359 /* Crypto.swift in Sources */, - FD9004122818ABDC00ABAAF6 /* Job.swift in Sources */, - FD6A38F12C2A66B100762359 /* KeychainStorageType.swift in Sources */, + FD2272F02C352200004D8A6C /* General.swift in Sources */, + FD2272EC2C352155004D8A6C /* Feature.swift in Sources */, + FD2272EE2C3521D6004D8A6C /* FeatureConfig.swift in Sources */, + FDE754DF2C9BAF8A002A2623 /* KeyPair.swift in Sources */, + FD2272D12C34EBD6004D8A6C /* JSONDecoder+Utilities.swift in Sources */, + FD6A38F12C2A66B100762359 /* KeychainStorage.swift in Sources */, 947AD6902C8968FF000B2730 /* Constants.swift in Sources */, - FD6A39022C2A8BDE00762359 /* UIImage+Utilities.swift in Sources */, - FD09797927FAB7E800936362 /* ImageFormat.swift in Sources */, FDC0F0042BFECE12002CBFB9 /* TimeUnit.swift in Sources */, + FD2272EA2C351CA7004D8A6C /* Threading.swift in Sources */, FD7115FE28C8202D00B47552 /* ReplaySubject.swift in Sources */, - C32C5DC9256DD935003C73A2 /* ProxiedContentDownloader.swift in Sources */, + FDE7550D2C9BC135002A2623 /* CGPoint+Utilities.swift in Sources */, + FD0E353C2AB9880B006A81F7 /* AppVersion.swift in Sources */, FD7F745F2BAAA3B4006DDFD8 /* TypeConversion+Utilities.swift in Sources */, + FDE755062C9BB4EE002A2623 /* Bencode.swift in Sources */, C32C5DD2256DD9E5003C73A2 /* LRUCache.swift in Sources */, FD71160228C8255900B47552 /* UIControl+Combine.swift in Sources */, - FDFBB74B2A1EFF4900CA7350 /* Bencode.swift in Sources */, + FD2272E02C3502BE004D8A6C /* Setting+Theme.swift in Sources */, + FDE519F72AB7CDC700450C53 /* Result+Utilities.swift in Sources */, FD5931A72A8DA5DA0040147D /* SQLInterpolation+Utilities.swift in Sources */, - FD6A39062C2A8C1600762359 /* CGPoint+Utilities.swift in Sources */, FD9004152818B46300ABAAF6 /* JobRunner.swift in Sources */, - FDF8487929405906007DCAE5 /* HTTPQueryParam.swift in Sources */, + FD3765EA2ADE37B400DC1489 /* Authentication.swift in Sources */, FD17D7CA27F546D900122BE0 /* _001_InitialSetupMigration.swift in Sources */, + FDE755202C9BC1A6002A2623 /* CacheConfig.swift in Sources */, + FDE755192C9BC169002A2623 /* UIImage+Utilities.swift in Sources */, C3BBE0AA2554D4DE0050F1E3 /* Dictionary+Utilities.swift in Sources */, - FD6A391B2C2A99B600762359 /* BencodeResponse.swift in Sources */, FD97B2402A3FEB050027DD57 /* ARC4RandomNumberGenerator.swift in Sources */, FD37EA1128AB34B3003AE748 /* TypedTableAlteration.swift in Sources */, - FD1936412ACA7BD8004BCF0F /* Result+Utilities.swift in Sources */, - FD6A390A2C2A8F2D00762359 /* FileManagerType.swift in Sources */, - FD12A84B2AD6458800EEBA0D /* DifferenceKit+Utilities.swift in Sources */, - C32C600F256E07F5003C73A2 /* NSUserDefaults+OWS.m in Sources */, - FD6A38F72C2A6C0100762359 /* CryptoError.swift in Sources */, - FDE658A329418E2F00A33BC1 /* KeyPair.swift in Sources */, FD5931AB2A8DCB0A0040147D /* ScopeAdapter+Utilities.swift in Sources */, FD37E9FF28A5F2CD003AE748 /* Configuration.swift in Sources */, + FDE7550F2C9BC135002A2623 /* CGRect+Utilities.swift in Sources */, FDB7400B28EB99A70094D718 /* TimeInterval+Utilities.swift in Sources */, FD09797D27FBDB2000936362 /* Notification+Utilities.swift in Sources */, FD1F9C9F2A862BE60050F671 /* MigrationRequirement.swift in Sources */, FDC6D7602862B3F600B04575 /* Dependencies.swift in Sources */, - C3C2AC2E2553CBEB00C340D1 /* String+Trimming.swift in Sources */, FD17D7C727F5207C00122BE0 /* DatabaseMigrator+Utilities.swift in Sources */, FD848B9328420164000E298B /* UnicodeScalar+Utilities.swift in Sources */, + FDE754CE2C9BAF37002A2623 /* ImageFormat.swift in Sources */, + FDE7551A2C9BC169002A2623 /* UIApplicationState+Utilities.swift in Sources */, FD09796B27F6C67500936362 /* Failable.swift in Sources */, FD7115FA28C8153400B47552 /* UIBarButtonItem+Combine.swift in Sources */, FD705A92278D051200F16121 /* ReusableView.swift in Sources */, FD17D7BA27F51F2100122BE0 /* TargetMigrations.swift in Sources */, FD17D7C327F5204C00122BE0 /* Database+Utilities.swift in Sources */, + FDE754DD2C9BAF8A002A2623 /* Mnemonic.swift in Sources */, + FDBEE52E2B6A18B900C143A0 /* UserDefaultsConfig.swift in Sources */, FD17D7C527F5206300122BE0 /* ColumnDefinition+Utilities.swift in Sources */, - FD8ECF94293856AF00C0D1BB /* Randomness.swift in Sources */, FDC438CD27BC641200C60D73 /* Set+Utilities.swift in Sources */, B8856DE6256F15F2001CE70E /* String+SSK.swift in Sources */, + FDE754E02C9BAF8A002A2623 /* Hex.swift in Sources */, + FDE754CC2C9BAF37002A2623 /* MediaUtils.swift in Sources */, + FDE754DE2C9BAF8A002A2623 /* Crypto+SessionUtilitiesKit.swift in Sources */, FDF2220F281B55E6000A4995 /* QueryInterfaceRequest+Utilities.swift in Sources */, - FDF848EF294067E4007DCAE5 /* URLResponse+Utilities.swift in Sources */, FD848B9A28442CE6000E298B /* StorageError.swift in Sources */, + FDE755222C9BC1BA002A2623 /* LibSessionError.swift in Sources */, FD17D7B827F51ECA00122BE0 /* Migration.swift in Sources */, FD7728982849E8110018502F /* UITableView+ReusableView.swift in Sources */, FD428B1B2B4B6098006D0888 /* Notifications+Lifecycle.swift in Sources */, - FD6A39452C2B783D00762359 /* UINavigationController+Utilities.swift in Sources */, 7B0EFDEE274F598600FFAAE7 /* TimestampUtils.swift in Sources */, - C32C5DDB256DD9FF003C73A2 /* ContentProxy.swift in Sources */, - C3A71F892558BA9F0043A11F /* Mnemonic.swift in Sources */, + FD2272D02C34EBD0004D8A6C /* FileManager.swift in Sources */, + FDE754CF2C9BAF37002A2623 /* DataSource.swift in Sources */, + FD10AF122AF85D11007709E5 /* Feature+ServiceNetwork.swift in Sources */, B8F5F58325EC94A6003BF8D4 /* Collection+Utilities.swift in Sources */, - 7BD477A827EC39F5004E2822 /* Atomic.swift in Sources */, - FD7F748A2BB298A8006DDFD8 /* JSONDecoder+Utilities.swift in Sources */, - B8BC00C0257D90E30032E807 /* General.swift in Sources */, - FD7F74782BB27742006DDFD8 /* BatchRequest.swift in Sources */, - FDF8488629405A61007DCAE5 /* Request.swift in Sources */, - FD23CE302A67B8820000B97C /* Caches.swift in Sources */, + FDE754D02C9BAF37002A2623 /* Data+Image.swift in Sources */, + B8BC00C0257D90E30032E807 /* (null) in Sources */, FD17D7A127F40D2500122BE0 /* Storage.swift in Sources */, - FD428B1D2B4B6FDC006D0888 /* UIApplicationState+Utilities.swift in Sources */, + FDE754DB2C9BAF8A002A2623 /* Crypto.swift in Sources */, FD1A94FB2900D1C2000D73D3 /* PersistableRecord+Utilities.swift in Sources */, FD5D201E27B0D87C00FEA984 /* SessionId.swift in Sources */, - FD428B212B4B75EA006D0888 /* Singleton.swift in Sources */, - FD6A39082C2A8DDA00762359 /* FileSystem.swift in Sources */, - C32C5A24256DB7DB003C73A2 /* SNUserDefaults.swift in Sources */, - FD8ECF922938552800C0D1BB /* Threading.swift in Sources */, + FDE754C22C9BAF0C002A2623 /* Atomic.swift in Sources */, FDF22211281B5E0B000A4995 /* TableRecord+Utilities.swift in Sources */, - B88FA7FB26114EA70049422F /* Hex.swift in Sources */, + FD8FD7642C37C24A001E38C7 /* EquatableIgnoring.swift in Sources */, + FDE7550E2C9BC135002A2623 /* CGFloat+Utilities.swift in Sources */, FD7728962849E7E90018502F /* String+Utilities.swift in Sources */, - FD83DCDD2A739D350065FFAE /* RetryWithDependencies.swift in Sources */, C35D0DB525AE5F1200B6BF49 /* UIEdgeInsets.swift in Sources */, FDF222092818D2B0000A4995 /* NSAttributedString+Utilities.swift in Sources */, C32C5E0C256DDAFA003C73A2 /* NSRegularExpression+SSK.swift in Sources */, FD29598D2A43BC0B00888A17 /* Version.swift in Sources */, - FDF8487C29405906007DCAE5 /* HTTPMethod.swift in Sources */, - FD6A39432C2AD81600762359 /* BackgroundTaskManager.swift in Sources */, - FDF8488429405A2B007DCAE5 /* RequestInfo.swift in Sources */, FD71160028C8253500B47552 /* UIView+Combine.swift in Sources */, B8856D23256F116B001CE70E /* Weak.swift in Sources */, FD17D7CD27F546FF00122BE0 /* Setting.swift in Sources */, - FD7F746C2BB2764F006DDFD8 /* RequestTarget.swift in Sources */, - FD6A38F32C2A6BBB00762359 /* Crypto+SessionUtilitiesKit.swift in Sources */, - FD7F745D2BAAA38B006DDFD8 /* LibSessionError.swift in Sources */, - FD7115FC28C8155800B47552 /* Publisher+Utilities.swift in Sources */, - FD6A39042C2A8C0300762359 /* CGFloat+Utilities.swift in Sources */, - C32C5A48256DB8F0003C73A2 /* BuildConfiguration.swift in Sources */, - FDF84881294059F5007DCAE5 /* ResponseInfo.swift in Sources */, - FD17D7BF27F51F8200122BE0 /* ColumnExpressible.swift in Sources */, - FD6A39282C2AB2AA00762359 /* CGSize+Utilities.swift in Sources */, + FDE754D42C9BAF6B002A2623 /* UICollectionView+ReusableView.swift in Sources */, + FDE754C02C9BAEF6002A2623 /* Array+Utilities.swift in Sources */, FD17D7E527F6A09900122BE0 /* Identity.swift in Sources */, FD9004142818AD0B00ABAAF6 /* _002_SetupStandardJobs.swift in Sources */, - FDF8487A29405906007DCAE5 /* NetworkError.swift in Sources */, + FDE7550C2C9BC135002A2623 /* Codable+Utilities.swift in Sources */, + FDAA16762AC28A3B00DDBF77 /* UserDefaultsType.swift in Sources */, FDDF074429C3E3D000E5E8B5 /* FetchRequest+Utilities.swift in Sources */, - B87EF18126377A1D00124B3C /* Features.swift in Sources */, - FD09797727FAB7A600936362 /* Data+Image.swift in Sources */, FD7F745B2BAAA35E006DDFD8 /* LibSession.swift in Sources */, FDC289472C881A3800020BC2 /* MutableIdentifiable.swift in Sources */, C300A60D2554B31900555489 /* Logging.swift in Sources */, - FD6A39242C2AAE4500762359 /* CGRect+Utilities.swift in Sources */, - B8FF8EA625C11FEF004D1F22 /* IPv4.swift in Sources */, - FD6A39172C2A99A000762359 /* BencodeDecoder.swift in Sources */, FD9004162818B46700ABAAF6 /* JobRunnerError.swift in Sources */, + FDE7551C2C9BC169002A2623 /* UIBezierPath+Utilities.swift in Sources */, FDF8488929405B27007DCAE5 /* Data+Utilities.swift in Sources */, FD09797227FAA2F500936362 /* Optional+Utilities.swift in Sources */, - FD6A39192C2A99AB00762359 /* BencodeEncoder.swift in Sources */, FD7162DB281B6C440060647B /* TypedTableAlias.swift in Sources */, + FDF01FAD2A9ECC4200CAF969 /* SingletonConfig.swift in Sources */, + FDE755102C9BC135002A2623 /* CGSize+Utilities.swift in Sources */, + FDE755182C9BC169002A2623 /* UIAlertAction+Utilities.swift in Sources */, FD7115F828C8151C00B47552 /* DisposableBarButtonItem.swift in Sources */, FD17D7E727F6A16700122BE0 /* _003_YDBToGRDBMigration.swift in Sources */, + FDE7551B2C9BC169002A2623 /* UINavigationController+Utilities.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -5869,64 +6049,68 @@ FD8ECF7B29340FFD00C0D1BB /* LibSession+SessionMessagingKit.swift in Sources */, 7B81682828B310D50069F315 /* _007_HomeQueryOptimisationIndexes.swift in Sources */, FD245C52285065D500B966DD /* SignalAttachment.swift in Sources */, + FD2273002C352D8E004D8A6C /* LibSession+GroupKeys.swift in Sources */, + FD2272FB2C352D8E004D8A6C /* LibSession+UserGroups.swift in Sources */, B8856D08256F10F1001CE70E /* DeviceSleepManager.swift in Sources */, - FD245C672850665E00B966DD /* AttachmentDownloadJob.swift in Sources */, + FD22726F2C32911C004D8A6C /* ProcessPendingGroupMemberRemovalsJob.swift in Sources */, C300A5D32554B05A00555489 /* TypingIndicator.swift in Sources */, - 7B521E0A29BFF84400C3C36A /* GroupLeavingJob.swift in Sources */, + FDF71EA52B07363500A8D6B5 /* MessageReceiver+LibSession.swift in Sources */, FDC13D582A17207D007267C7 /* UnsubscribeResponse.swift in Sources */, FD09799927FFC1A300936362 /* Attachment.swift in Sources */, FD245C5F2850662200B966DD /* OWSWindowManager.m in Sources */, FDF40CDE2897A1BC006A0CC4 /* _004_RemoveLegacyYDB.swift in Sources */, FDF0B74928060D13004C14C5 /* QuotedReplyModel.swift in Sources */, - FD3003662A25D5B300B5A5FB /* ConfigMessageReceiveJob.swift in Sources */, + FDE754A32C9A8FD1002A2623 /* SwarmPoller.swift in Sources */, 7B81682C28B72F480069F315 /* PendingChange.swift in Sources */, FD5C7309285007920029977D /* BlindedIdLookup.swift in Sources */, FD71161C28D194FB00B47552 /* MentionInfo.swift in Sources */, 7B4C75CB26B37E0F0000AC89 /* UnsendRequest.swift in Sources */, C300A5F22554B09800555489 /* MessageSender.swift in Sources */, + FD47E0B12AA6A05800A55E41 /* Authentication+SessionMessagingKit.swift in Sources */, + FD2272832C337830004D8A6C /* GroupPoller.swift in Sources */, + FD22726C2C32911C004D8A6C /* GroupLeavingJob.swift in Sources */, FDF848F729414477007DCAE5 /* CurrentUserPoller.swift in Sources */, C3C2A74D2553A39700C340D1 /* VisibleMessage.swift in Sources */, + FD2272FD2C352D8E004D8A6C /* LibSession+ConvoInfoVolatile.swift in Sources */, + FD22727E2C32911C004D8A6C /* GarbageCollectionJob.swift in Sources */, FD09B7E7288670FD00ED0B66 /* Reaction.swift in Sources */, FD245C5A2850660100B966DD /* LinkPreviewDraft.swift in Sources */, - FDD250702837199200198BDA /* GarbageCollectionJob.swift in Sources */, - FD6A7A6B2818C17C00035AC1 /* UpdateProfilePictureJob.swift in Sources */, FDD82C3F2A205D0A00425F05 /* ProcessResult.swift in Sources */, FD716E6A2850327900C96BF4 /* EndCallMode.swift in Sources */, FDF0B75C2807F41D004C14C5 /* MessageSender+Convenience.swift in Sources */, + FD22726D2C32911C004D8A6C /* CheckForAppUpdatesJob.swift in Sources */, 7B81682A28B6F1420069F315 /* ReactionResponse.swift in Sources */, - 7B7E5B522A4D024C00A8208E /* ExpirationUpdateJob.swift in Sources */, + FD2273082C353109004D8A6C /* DisplayPictureManager.swift in Sources */, + FD2273022C352D8E004D8A6C /* LibSession+GroupInfo.swift in Sources */, FDDD554E2C1FCB77006CBF03 /* _019_ScheduleAppUpdateCheckJob.swift in Sources */, - FDA8EB00280E8D58002B68E5 /* FailedAttachmentDownloadsJob.swift in Sources */, FD09798927FD1C5A00936362 /* OpenGroup.swift in Sources */, + FD2272FC2C352D8E004D8A6C /* LibSession+Contacts.swift in Sources */, FD848B9628422A2A000E298B /* MessageViewModel.swift in Sources */, - FDF0B7472804F0CE004C14C5 /* DisappearingMessagesJob.swift in Sources */, + FD2272782C32911C004D8A6C /* AttachmentDownloadJob.swift in Sources */, FDF0B73C27FFD3D6004C14C5 /* LinkPreview.swift in Sources */, FD428B232B4B9969006D0888 /* _017_RebuildFTSIfNeeded_2_4_5.swift in Sources */, - FD09797527FAB64300936362 /* ProfileManager.swift in Sources */, - FD245C57285065F100B966DD /* Poller.swift in Sources */, - 7B7AD41F2A5512CA00469FB1 /* GetExpirationJob.swift in Sources */, - FD7F747A2BB277E1006DDFD8 /* Request+OpenGroupAPI.swift in Sources */, - FDA8EAFE280E8B78002B68E5 /* FailedMessageSendsJob.swift in Sources */, + FD2272742C32911C004D8A6C /* ConfigMessageReceiveJob.swift in Sources */, FDFBB74D2A1F3C4E00CA7350 /* NotificationMetadata.swift in Sources */, - FDC4386927B4E6B800C60D73 /* String+Utlities.swift in Sources */, FD716E6628502EE200C96BF4 /* CurrentCallProtocol.swift in Sources */, - FD8ECF9029381FC200C0D1BB /* LibSession+UserProfile.swift in Sources */, + FD22727A2C32911C004D8A6C /* GroupInviteMemberJob.swift in Sources */, + FDB5DAC72A9447E7002C8721 /* _022_GroupsRebuildChanges.swift in Sources */, FD09B7E5288670BB00ED0B66 /* _008_EmojiReacts.swift in Sources */, - FDFF61D729F2600300F95FB0 /* Identity+Utilities.swift in Sources */, FDC4385F27B4C4A200C60D73 /* PinnedMessage.swift in Sources */, FDD20C1A2A0A03AC003898FB /* DeleteInboxResponse.swift in Sources */, 7B8D5FC428332600008324D9 /* VisibleMessage+Reaction.swift in Sources */, FDC4386527B4DE7600C60D73 /* RoomPollInfo.swift in Sources */, FD245C6B2850667400B966DD /* VisibleMessage+Profile.swift in Sources */, + FD2272FA2C352D8E004D8A6C /* LibSession+SharedGroup.swift in Sources */, FD37EA0F28AB3330003AE748 /* _006_FixHiddenModAdminSupport.swift in Sources */, - FD2B4AFD294688D000AB4848 /* LibSession+Contacts.swift in Sources */, + FD2272772C32911C004D8A6C /* AttachmentUploadJob.swift in Sources */, 7B81682328A4C1210069F315 /* UpdateTypes.swift in Sources */, FDC13D472A16E4CA007267C7 /* SubscribeRequest.swift in Sources */, - FD2B4AFF2946C93200AB4848 /* ConfigurationSyncJob.swift in Sources */, FDC438A627BB113A00C60D73 /* UserUnbanRequest.swift in Sources */, + FD22727C2C32911C004D8A6C /* GroupPromoteMemberJob.swift in Sources */, FD5C72FB284F0EA10029977D /* MessageReceiver+DataExtractionNotification.swift in Sources */, FDC4386727B4E10E00C60D73 /* Capabilities.swift in Sources */, FDC438A427BB107F00C60D73 /* UserBanRequest.swift in Sources */, + FDE754F22C9BB08B002A2623 /* Crypto+SessionMessagingKit.swift in Sources */, FD4C53AF2CC1D62E003B10F4 /* _021_ReworkRecipientState.swift in Sources */, C379DCF4256735770002D4EB /* VisibleMessage+Attachment.swift in Sources */, FDB4BBC72838B91E00B7C95D /* LinkPreviewError.swift in Sources */, @@ -5934,77 +6118,97 @@ FDC13D542A16FF29007267C7 /* LegacyGroupRequest.swift in Sources */, B8B320B7258C30D70020074B /* HTMLMetadata.swift in Sources */, FD09798727FD1B7800936362 /* GroupMember.swift in Sources */, - FDB4BBC92839BEF000B7C95D /* ProfileManagerError.swift in Sources */, FDCD2E032A41294E00964D6A /* LegacyGroupOnlyRequest.swift in Sources */, FD3E0C84283B5835002A425C /* SessionThreadViewModel.swift in Sources */, FD09C5EC282B8F18000CE219 /* AttachmentError.swift in Sources */, + FDE754F02C9BB08B002A2623 /* Crypto+Attachments.swift in Sources */, FD17D79927F40AB800122BE0 /* _003_YDBToGRDBMigration.swift in Sources */, + FDE754A12C9A60A6002A2623 /* Crypto+OpenGroupAPI.swift in Sources */, FDF0B7512807BA56004C14C5 /* NotificationsProtocol.swift in Sources */, - FDC6D6F32860607300B04575 /* SessionEnvironment.swift in Sources */, + FD2272722C32911C004D8A6C /* FailedAttachmentDownloadsJob.swift in Sources */, FDC13D5A2A1721C5007267C7 /* LegacyNotifyRequest.swift in Sources */, FD245C59285065FC00B966DD /* ControlMessage.swift in Sources */, FD6E4C8A2A1AEE4700C7C243 /* LegacyUnsubscribeRequest.swift in Sources */, B8DE1FB626C22FCB0079C9CE /* CallMessage.swift in Sources */, FD245C50285065C700B966DD /* VisibleMessage+Quote.swift in Sources */, + FD72BDA12BE368C800CF6CF6 /* UIWindowLevel+Utilities.swift in Sources */, + FD4C4E9C2B02E2A300C72199 /* DisplayPictureError.swift in Sources */, FD5C7307284F103B0029977D /* MessageReceiver+MessageRequests.swift in Sources */, C3A71D0B2558989C0043A11F /* MessageWrapper.swift in Sources */, + FD3FAB592ADF906300DC5421 /* Profile+CurrentUser.swift in Sources */, FDE77F6B280FEB28002CFC5D /* ControlMessageProcessRecord.swift in Sources */, + FDEF573E2C40F2A100131302 /* GroupUpdateMemberLeftNotificationMessage.swift in Sources */, + FDE755002C9BB0FA002A2623 /* SessionEnvironment.swift in Sources */, + FDB5DADC2A95D840002C8721 /* GroupUpdateMemberChangeMessage.swift in Sources */, + FD2272812C32911C004D8A6C /* UpdateProfilePictureJob.swift in Sources */, + FDB5DAE02A95D84D002C8721 /* GroupUpdateMemberLeftMessage.swift in Sources */, + FD8FD7622C37B7BD001E38C7 /* Position.swift in Sources */, 7B93D07127CF194000811CB6 /* MessageRequestResponse.swift in Sources */, - FD7F74882BB2929B006DDFD8 /* Request+PushNotificationAPI.swift in Sources */, + FDB5DADE2A95D847002C8721 /* GroupUpdatePromoteMessage.swift in Sources */, FD245C662850665900B966DD /* OpenGroupAPI.swift in Sources */, FD245C5B2850660500B966DD /* ReadReceipt.swift in Sources */, FD428B1F2B4B758B006D0888 /* AppReadiness.swift in Sources */, + FD22726B2C32911C004D8A6C /* SendReadReceiptsJob.swift in Sources */, B8F5F60325EDE16F003BF8D4 /* DataExtractionNotification.swift in Sources */, C3A71D1E25589AC30043A11F /* WebSocketProto.swift in Sources */, C3C2A7852553AAF300C340D1 /* SessionProtos.pb.swift in Sources */, - FDF0B74F28079E5E004C14C5 /* SendReadReceiptsJob.swift in Sources */, FDF0B7422804EA4F004C14C5 /* _002_SetupStandardJobs.swift in Sources */, - FD43EE9F297E2EE0009C87C5 /* LibSession+ConvoInfoVolatile.swift in Sources */, + FD66CB2E2BF5EB0C00268FAB /* Config.swift in Sources */, B8EB20EE2640F28000773E52 /* VisibleMessage+OpenGroupInvitation.swift in Sources */, FDF8487F29405994007DCAE5 /* HTTPHeader+OpenGroup.swift in Sources */, FD8ECF7D2934293A00C0D1BB /* _013_SessionUtilChanges.swift in Sources */, FD17D7A227F40F0500122BE0 /* _001_InitialSetupMigration.swift in Sources */, FD245C5D2850660F00B966DD /* OWSAudioPlayer.m in Sources */, + FD2272FE2C352D8E004D8A6C /* LibSession+GroupMembers.swift in Sources */, FDF0B7582807F368004C14C5 /* MessageReceiverError.swift in Sources */, - FD6A7A692818BE7300035AC1 /* RetrieveDefaultOpenGroupRoomsJob.swift in Sources */, FD09798D27FD1D8900936362 /* DisappearingMessageConfiguration.swift in Sources */, + FD2272732C32911C004D8A6C /* ConfigurationSyncJob.swift in Sources */, FDF0B75A2807F3A3004C14C5 /* MessageSenderError.swift in Sources */, FDC4382F27B383AF00C60D73 /* LegacyPushServerResponse.swift in Sources */, FDC4386327B4D94E00C60D73 /* SOGSMessage.swift in Sources */, FD245C692850666800B966DD /* ExpirationTimerUpdate.swift in Sources */, FD42F9A8285064B800A0C77D /* PushNotificationAPI.swift in Sources */, - FD245C6C2850669200B966DD /* MessageReceiveJob.swift in Sources */, - FD43EE9D297A5190009C87C5 /* LibSession+UserGroups.swift in Sources */, + FD2272752C32911C004D8A6C /* RetrieveDefaultOpenGroupRoomsJob.swift in Sources */, + FD2272712C32911C004D8A6C /* MessageReceiveJob.swift in Sources */, + FDB5DAC12A9443A5002C8721 /* MessageSender+Groups.swift in Sources */, FDC13D4B2A16ECBA007267C7 /* SubscribeResponse.swift in Sources */, + FD2272702C32911C004D8A6C /* DisappearingMessagesJob.swift in Sources */, FD7115F228C6CB3900B47552 /* _010_AddThreadIdToFTS.swift in Sources */, FD716E6428502DDD00C96BF4 /* CallManagerProtocol.swift in Sources */, 943C6D822B75E061004ACE64 /* Message+DisappearingMessages.swift in Sources */, FDC438C727BB6DF000C60D73 /* DirectMessage.swift in Sources */, + FD3765F42ADE5A0800DC1489 /* AuthenticatedRequest.swift in Sources */, FDC13D502A16EE50007267C7 /* PushNotificationAPIEndpoint.swift in Sources */, FD432434299C6985008A0213 /* PendingReadReceipt.swift in Sources */, FDC4381727B32EC700C60D73 /* Personalization.swift in Sources */, FD245C51285065CC00B966DD /* MessageReceiver.swift in Sources */, FD245C652850665400B966DD /* ClosedGroupControlMessage.swift in Sources */, FDC4387827B5C35400C60D73 /* SendMessageRequest.swift in Sources */, + FDC383392A93411100FFD6A2 /* Setting+Utilities.swift in Sources */, + FDB5DAE62A95D8B0002C8721 /* GroupUpdateDeleteMemberContentMessage.swift in Sources */, 7B5233C6290636D700F8F375 /* _018_DisappearingMessagesConfiguration.swift in Sources */, FD5C72FD284F0EC90029977D /* MessageReceiver+ExpirationTimers.swift in Sources */, - C32C5A88256DBCF9003C73A2 /* MessageReceiver+ClosedGroups.swift in Sources */, + C32C5A88256DBCF9003C73A2 /* MessageReceiver+LegacyClosedGroups.swift in Sources */, B8D0A25925E367AC00C1835E /* Notification+MessageReceiver.swift in Sources */, FD245C53285065DB00B966DD /* ProximityMonitoringManager.swift in Sources */, FD245C55285065E500B966DD /* OpenGroupManager.swift in Sources */, C32C599E256DB02B003C73A2 /* TypingIndicators.swift in Sources */, FD716E682850318E00C96BF4 /* CallMode.swift in Sources */, + FDE7549B2C940108002A2623 /* MessageViewModel+DeletionActions.swift in Sources */, FD09799527FE7B8E00936362 /* Interaction.swift in Sources */, - FDDD554C2C1FC812006CBF03 /* CheckForAppUpdatesJob.swift in Sources */, FD37EA0D28AB2A45003AE748 /* _005_FixDeletedMessageReadState.swift in Sources */, 7BAA7B6628D2DE4700AE1489 /* _009_OpenGroupPermission.swift in Sources */, FDC4380927B31D4E00C60D73 /* OpenGroupAPIError.swift in Sources */, + FD2286692C37DA5500BC06F7 /* PollerType.swift in Sources */, + FD22727B2C32911C004D8A6C /* MessageSendJob.swift in Sources */, FDC4382027B36ADC00C60D73 /* SOGSEndpoint.swift in Sources */, + FD72BD9C2BE2F2BC00CF6CF6 /* NoopSessionCallManager.swift in Sources */, FDC438C927BB706500C60D73 /* SendDirectMessageRequest.swift in Sources */, C3A71D1F25589AC30043A11F /* WebSocketResources.pb.swift in Sources */, FDF0B74B28061F7A004C14C5 /* InteractionAttachment.swift in Sources */, FD09796E27FA6D0000936362 /* Contact.swift in Sources */, - C38D5E8D2575011E00B6A65C /* MessageSender+ClosedGroups.swift in Sources */, + FD02CC142C3677E6009AB976 /* Request+OpenGroupAPI.swift in Sources */, + C38D5E8D2575011E00B6A65C /* MessageSender+LegacyClosedGroups.swift in Sources */, FD5C72F7284F0E560029977D /* MessageReceiver+ReadReceipts.swift in Sources */, FDC13D492A16EC20007267C7 /* Service.swift in Sources */, FD778B6429B189FF001BAC6B /* _014_GenerateInitialUserConfigDumps.swift in Sources */, @@ -6012,50 +6216,59 @@ C32C598A256D0664003C73A2 /* SNProtoEnvelope+Conversion.swift in Sources */, FDC438CB27BB7DB100C60D73 /* UpdateMessageRequest.swift in Sources */, FD8ECF7F2934298100C0D1BB /* ConfigDump.swift in Sources */, - FD6A39152C2A954000762359 /* Crypto+OpenGroupAPI.swift in Sources */, - FDA1E83B29A5F2D500C5C3BD /* LibSession+Shared.swift in Sources */, - C352A2FF25574B6300338F3E /* MessageSendJob.swift in Sources */, + FDE519F92AB802BB00450C53 /* Message+Origin.swift in Sources */, + FD22727F2C32911C004D8A6C /* GetExpirationJob.swift in Sources */, + FD2272792C32911C004D8A6C /* DisplayPictureDownloadJob.swift in Sources */, + C352A2FF25574B6300338F3E /* (null) in Sources */, FD16AB612A1DD9B60083D849 /* ProfilePictureView+Convenience.swift in Sources */, - FD23CE242A675C440000B97C /* Crypto+SessionMessagingKit.swift in Sources */, B8856D11256F112A001CE70E /* OWSAudioSession.swift in Sources */, - C3DB66C3260ACCE6001EFC55 /* OpenGroupPoller.swift in Sources */, + FD2272762C32911C004D8A6C /* ExpirationUpdateJob.swift in Sources */, FD716E722850647600C96BF4 /* Data+Utilities.swift in Sources */, FD368A6829DE8F9C000DBF1E /* _012_AddFTSIfNeeded.swift in Sources */, FD5C7301284F0F7A0029977D /* MessageReceiver+UnsendRequests.swift in Sources */, C3C2A75F2553A3C500C340D1 /* VisibleMessage+LinkPreview.swift in Sources */, - FD245C642850664F00B966DD /* Threading.swift in Sources */, FD848B8D283E0B26000E298B /* MessageInputTypes.swift in Sources */, FDFE75B12ABD2D2400655640 /* _016_MakeBrokenProfileTimestampsNullable.swift in Sources */, FD09799B27FFC82D00936362 /* Quote.swift in Sources */, - FD245C6D285066A400B966DD /* NotifyPushServerJob.swift in Sources */, + FD2273012C352D8E004D8A6C /* LibSession+Shared.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 */, - C352A35B2557824E00338F3E /* AttachmentUploadJob.swift in Sources */, + FD2272FF2C352D8E004D8A6C /* LibSession+UserProfile.swift in Sources */, FD5C7305284F0FF30029977D /* MessageReceiver+VisibleMessages.swift in Sources */, + FDB5DAE82A95D96C002C8721 /* MessageReceiver+Groups.swift in Sources */, FD1D732E2A86114600E3F410 /* _015_BlockCommunityMessageRequests.swift in Sources */, FD2B4B042949887A00AB4848 /* QueryInterfaceRequest+Utilities.swift in Sources */, + FDAA167F2AC5290000DDBF77 /* Preferences+NotificationPreviewType.swift in Sources */, + FD22727D2C32911C004D8A6C /* NotifyPushServerJob.swift in Sources */, FD09797027FA6FF300936362 /* Profile.swift in Sources */, FD245C56285065EA00B966DD /* SNProto.swift in Sources */, + FDE7549D2C9961A4002A2623 /* CommunityPoller.swift in Sources */, + FDE754F12C9BB08B002A2623 /* Crypto+LibSession.swift in Sources */, FD09798B27FD1CFE00936362 /* Capability.swift in Sources */, C3BBE0C72554F1570050F1E3 /* FixedWidthInteger+BigEndian.swift in Sources */, - FD1D732A2A85AA2000E3F410 /* Setting+Utilities.swift in Sources */, FD09798127FCFEE800936362 /* SessionThread.swift in Sources */, + FD3765F62ADE5BA500DC1489 /* ServiceInfo.swift in Sources */, FD09C5EA282A1BB2000CE219 /* ThreadTypingIndicator.swift in Sources */, + FDB5DADA2A95D839002C8721 /* GroupUpdateInfoChangeMessage.swift in Sources */, + FDF71EA32B072C2800A8D6B5 /* LibSessionMessage.swift in Sources */, + FDAA167D2AC528A200DDBF77 /* Preferences+Sound.swift in Sources */, + FDE754FE2C9BB0D0002A2623 /* Threading+SMK.swift in Sources */, FDF0B75E280AAF35004C14C5 /* Preferences.swift in Sources */, - FD72BD9A2BDF5EEA00CF6CF6 /* Message+Origin.swift in Sources */, - FD6A39492C2BB85A00762359 /* Crypto+Attachments.swift in Sources */, + FD02CC122C367762009AB976 /* Request+PushNotificationAPI.swift in Sources */, + FD22726E2C32911C004D8A6C /* FailedMessageSendsJob.swift in Sources */, + FDB5DAD42A9483F3002C8721 /* GroupUpdateInviteMessage.swift in Sources */, + FDB5DAE22A95D8A0002C8721 /* GroupUpdateInviteResponseMessage.swift in Sources */, FD3559462CC1FF200088F2A9 /* _020_AddMissingWhisperFlag.swift in Sources */, - FD432432299C6933008A0213 /* _011_AddPendingReadReceipts.swift in Sources */, FD5C72F9284F0E880029977D /* MessageReceiver+TypingIndicators.swift in Sources */, FD5C7303284F0FA50029977D /* MessageReceiver+Calls.swift in Sources */, FD83B9C927D0487A005E1583 /* SendDirectMessageResponse.swift in Sources */, FDC438AA27BB12BB00C60D73 /* UserModeratorRequest.swift in Sources */, FD245C54285065E000B966DD /* ThumbnailService.swift in Sources */, FDC4385D27B4C18900C60D73 /* Room.swift in Sources */, + FDE755022C9BB122002A2623 /* _011_AddPendingReadReceipts.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -6071,12 +6284,12 @@ FD7115EB28C5D78E00B47552 /* ThreadSettingsViewModel.swift in Sources */, B8041AA725C90927003C2166 /* TypingIndicatorCell.swift in Sources */, 7B4EF25A2934743000CB351D /* SessionTableViewTitleView.swift in Sources */, + FD2272D92C34EED6004D8A6C /* _001_ThemePreferences.swift in Sources */, 942256A12C23F90700C0FDBF /* CustomTopTabBar.swift in Sources */, FDFDE12A282D056B0098B17F /* MediaZoomAnimationController.swift in Sources */, 4C1885D2218F8E1C00B67051 /* PhotoGridViewCell.swift in Sources */, 34D1F0501F7D45A60066283D /* GifPickerCell.swift in Sources */, 7BFA8AE32831D0D4001876F3 /* ContextMenuVC+EmojiReactsView.swift in Sources */, - C3E5C2FA251DBABB0040DFFC /* EditClosedGroupVC.swift in Sources */, FD12A8432AD63BF600EEBA0D /* ObservableTableSource.swift in Sources */, FD52090528B4915F006098F6 /* PrivacySettingsViewModel.swift in Sources */, 7BAF54D027ACCEEC003D12F8 /* EmptySearchResultCell.swift in Sources */, @@ -6087,24 +6300,22 @@ B879D449247E1BE300DB3608 /* PathVC.swift in Sources */, B877E24626CA13BA0007970A /* CallVC+Camera.swift in Sources */, 454A84042059C787008B8C75 /* MediaTileViewController.swift in Sources */, - 451A13B11E13DED2000A50FD /* AppNotifications.swift in Sources */, - FDEF57222C3CF03D00131302 /* WebRTCSession+UI.swift in Sources */, + FD3FAB5F2AE9BC2200DC5421 /* EditGroupViewModel.swift in Sources */, + FDEF57222C3CF03D00131302 /* (null) in Sources */, 7BA37AFB2AEB64CA002438F8 /* DisappearingMessageTimerView.swift in Sources */, FD12A8492AD63C4700EEBA0D /* SessionNavItem.swift in Sources */, FD12A83D2AD63BCC00EEBA0D /* EditableState.swift in Sources */, - 34D99CE4217509C2000AFB39 /* AppEnvironment.swift in Sources */, FD37EA0528AA00C1003AE748 /* NotificationSettingsViewModel.swift in Sources */, C328255225CA64470062D0A7 /* ContextMenuVC+ActionView.swift in Sources */, - 450DF2091E0DD2C6003D14BE /* UserNotificationsAdaptee.swift in Sources */, + FD10AF0A2AF319EE007709E5 /* DeveloperSettingsViewModel.swift in Sources */, C3548F0824456AB6009433A8 /* UIView+Wrapping.swift in Sources */, - 45CD81EF1DC030E7004C9430 /* SyncPushTokensJob.swift in Sources */, - 94C5DCB02BE88170003AA8C5 /* BezierPathView.swift in Sources */, B83524A525C3BA4B0089A44F /* InfoMessageCell.swift in Sources */, 7B9F71D82853100A006DFE7B /* EmojiWithSkinTones.swift in Sources */, FD71164E28E3F8CC00B47552 /* SessionCell+Info.swift in Sources */, B84A89BC25DE328A0040017D /* ProfilePictureVC.swift in Sources */, FD12A8452AD63C2200EEBA0D /* TableDataState.swift in Sources */, FDCDB8E02811007F00352A0C /* HomeViewModel.swift in Sources */, + FDE754F82C9BB0B0002A2623 /* UserNotificationConfig.swift in Sources */, 7B0EFDF62755CC5400FFAAE7 /* CallMissedTipsModal.swift in Sources */, C374EEF425DB31D40073A857 /* VoiceMessageRecordingView.swift in Sources */, 7B1581E6271FD2A100848B49 /* VideoPreviewVC.swift in Sources */, @@ -6112,13 +6323,13 @@ B83F2B88240CB75A000A54AB /* UIImage+Scaling.swift in Sources */, 3430FE181F7751D4000EC51B /* GiphyAPI.swift in Sources */, 4C090A1B210FD9C7001FD7F9 /* HapticFeedback.swift in Sources */, + FDE754FA2C9BB0B0002A2623 /* NotificationPresenter.swift in Sources */, FDE125232A837E4E002DA685 /* MainAppContext.swift in Sources */, 7B9F71D12852EEE2006DFE7B /* EmojiWithSkinTones+String.swift in Sources */, - 34F308A21ECB469700BB7697 /* OWSBezierPathView.m in Sources */, B886B4A92398BA1500211ABE /* QRCode.swift in Sources */, 34A8B3512190A40E00218A25 /* MediaAlbumView.swift in Sources */, FD09C5E828264937000CE219 /* MediaDetailViewController.swift in Sources */, - FDEF57262C3CF05F00131302 /* TurnServerInfo.swift in Sources */, + FDEF57262C3CF05F00131302 /* (null) in Sources */, 3496955E219B605E00DCFE74 /* PhotoLibrary.swift in Sources */, 7BA37AFD2AEF7C3D002438F8 /* VoiceMessageView_SwiftUI.swift in Sources */, 7B1B52E028580D51006069F2 /* EmojiSkinTonePicker.swift in Sources */, @@ -6137,15 +6348,16 @@ 7BA37AF92AEB365C002438F8 /* DocumentView_SwiftUI.swift in Sources */, FDB7400D28EBEC240094D718 /* DateHeaderCell.swift in Sources */, B8D0A26925E4A2C200C1835E /* Onboarding.swift in Sources */, - 34D1F0521F7E8EA30066283D /* GiphyDownloader.swift in Sources */, 4CC613362227A00400E21A3A /* ConversationSearch.swift in Sources */, FD87DCFE28B7582C00AF0F98 /* BlockedContactsViewModel.swift in Sources */, FD37E9DD28A384EB003AE748 /* PrimaryColorSelectionView.swift in Sources */, 942256812C23F8BB00C0FDBF /* NewMessageScreen.swift in Sources */, FDC498B72AC15F7D00EDD897 /* AppNotificationCategory.swift in Sources */, + FDE754B82C9B96BB002A2623 /* WebRTCSession+DataChannel.swift in Sources */, FDFDE126282D05380098B17F /* MediaInteractiveDismiss.swift in Sources */, 34BECE301F7ABCF800D7438D /* GifPickerLayout.swift in Sources */, C331FFFE2558FF3B00070591 /* FullConversationCell.swift in Sources */, + FDE754F92C9BB0B0002A2623 /* NotificationActionHandler.swift in Sources */, FDFDE128282D05530098B17F /* MediaPresentationContext.swift in Sources */, FD37EA0328A9FDCC003AE748 /* HelpViewModel.swift in Sources */, FDFDE124282D04F20098B17F /* MediaDismissAnimationController.swift in Sources */, @@ -6154,28 +6366,35 @@ FD71162228D983ED00B47552 /* QRCodeScanningViewController.swift in Sources */, B8D84EA325DF745A005A043E /* LinkPreviewState.swift in Sources */, 45C0DC1E1E69011F00E04C47 /* UIStoryboard+OWS.swift in Sources */, + FDE754B02C9B96B4002A2623 /* WebRTCSession.swift in Sources */, B835246E25C38ABF0089A44F /* ConversationVC.swift in Sources */, 7B7037432834B81F000DCF35 /* ReactionContainerView.swift in Sources */, 7BBBDC462875600700747E59 /* DocumentTitleViewController.swift in Sources */, FD71163F28E2C82C00B47552 /* SessionHeaderView.swift in Sources */, 7B8914772A7CAAE200A4C627 /* SessionHostingViewController.swift in Sources */, B877E24226CA12910007970A /* CallVC.swift in Sources */, - FDEF57232C3CF04300131302 /* WebRTCSession+MessageHandling.swift in Sources */, + FDEF57232C3CF04300131302 /* (null) in Sources */, FDC498B92AC15FE300EDD897 /* AppNotificationAction.swift in Sources */, 7BA6890D27325CCC00EFC32F /* SessionCallManager+CXCallController.swift in Sources */, 7B71A98F2925E2A600E54854 /* SessionFooterView.swift in Sources */, C374EEEB25DA3CA70073A857 /* ConversationTitleView.swift in Sources */, + FDE754E52C9BB012002A2623 /* BezierPathView.swift in Sources */, FD716E7128505E5200C96BF4 /* MessageRequestsCell.swift in Sources */, FDF848F329413DB0007DCAE5 /* ImagePickerHandler.swift in Sources */, + FDE754B22C9B96B4002A2623 /* WebRTC+Utilities.swift in Sources */, B88FA7F2260C3EB10049422F /* OpenGroupSuggestionGrid.swift in Sources */, + FDE754FB2C9BB0B0002A2623 /* PushRegistrationManager.swift in Sources */, 9422569F2C23F8FE00C0FDBF /* ScanQRCodeScreen.swift in Sources */, FD71160428C95B5600B47552 /* PhotoCollectionPickerViewModel.swift in Sources */, + FDE754FC2C9BB0B0002A2623 /* SyncPushTokensJob.swift in Sources */, + FDE754B72C9B96BB002A2623 /* WebRTCSession+MessageHandling.swift in Sources */, FD37EA1928AC5CCA003AE748 /* NotificationSoundViewModel.swift in Sources */, 7BAFA75A2AAEA281001DA43E /* LinkPreviewView_SwiftUI.swift in Sources */, FD71163E28E2C82900B47552 /* SessionCell.swift in Sources */, 9422569C2C23F8F000C0FDBF /* QRCodeScreen.swift in Sources */, 4CA485BB2232339F004B9E7D /* PhotoCaptureViewController.swift in Sources */, C328254925CA60E60062D0A7 /* ContextMenuVC+Action.swift in Sources */, + FD6D9CF92CA152B300F706A8 /* Session+SNUIKit.swift in Sources */, FD71164628E2CC1300B47552 /* SessionHighlightingBackgroundLabel.swift in Sources */, 4542DF54208D40AC007B4E76 /* LoadingViewController.swift in Sources */, B8B558F126C4BB0600693325 /* CameraManager.swift in Sources */, @@ -6185,6 +6404,8 @@ 34B6A903218B3F63007C4606 /* TypingIndicatorView.swift in Sources */, 7BAF54CF27ACCEEC003D12F8 /* GlobalSearchViewController.swift in Sources */, FD37EA1728AC5605003AE748 /* NotificationContentViewModel.swift in Sources */, + FD3FAB612AEA194E00DC5421 /* UserListViewModel.swift in Sources */, + B886B4A72398B23E00211ABE /* (null) in Sources */, 94B3DC172AF8592200C88531 /* QuoteView_SwiftUI.swift in Sources */, 4C4AE6A1224AF35700D4AF6F /* SendMediaNavigationController.swift in Sources */, B82149C125D605C6009C0F2A /* InfoBanner.swift in Sources */, @@ -6215,7 +6436,6 @@ 7BAADFCE27B215FE007BCF92 /* UIView+Draggable.swift in Sources */, 45C0DC1B1E68FE9000E04C47 /* UIApplication+OWS.swift in Sources */, FDF222072818CECF000A4995 /* ConversationViewModel.swift in Sources */, - 4539B5861F79348F007141FF /* PushRegistrationManager.swift in Sources */, 7BD976972A776C76001B466F /* SessionCarouselView+SwiftUI.swift in Sources */, B8041A9525C8FA1D003C2166 /* MediaLoaderView.swift in Sources */, 45F32C232057297A00A300D5 /* MediaPageViewController.swift in Sources */, @@ -6223,20 +6443,19 @@ 7B9F71D42852EEE2006DFE7B /* Emoji+Name.swift in Sources */, 4CA46F4C219CCC630038ABDE /* CaptionView.swift in Sources */, C328253025CA55370062D0A7 /* ContextMenuWindow.swift in Sources */, - FDEF57242C3CF04700131302 /* WebRTC+Utilities.swift in Sources */, + FDEF57242C3CF04700131302 /* (null) in Sources */, 34BECE2E1F7ABCE000D7438D /* GifPickerViewController.swift in Sources */, 9422568C2C23F8C800C0FDBF /* DisplayNameScreen.swift in Sources */, B84664F5235022F30083A1CD /* MentionUtilities.swift in Sources */, 7B9F71D72853100A006DFE7B /* Emoji+Available.swift in Sources */, FD09C5E628260FF9000CE219 /* MediaGalleryViewModel.swift in Sources */, - FDEF57212C3CF03A00131302 /* WebRTCSession.swift in Sources */, + FDEF57212C3CF03A00131302 /* (null) in Sources */, 7B9F71D32852EEE2006DFE7B /* Emoji.swift in Sources */, FDC498BB2AC1606C00EDD897 /* AppNotificationUserInfoKey.swift in Sources */, C328250F25CA06020062D0A7 /* VoiceMessageView.swift in Sources */, 3488F9362191CC4000E524CC /* MediaView.swift in Sources */, B8569AC325CB5D2900DBA3DB /* ConversationVC+Interaction.swift in Sources */, 3496955C219B605E00DCFE74 /* ImagePickerController.swift in Sources */, - C31D1DE32521718E005D4DA8 /* UserSelectionVC.swift in Sources */, 34A6C28021E503E700B5B12E /* OWSImagePickerController.swift in Sources */, C31A6C5C247F2CF3001123EF /* CGRect+Utilities.swift in Sources */, FD4B200E283492210034334B /* InsetLockableTableView.swift in Sources */, @@ -6245,6 +6464,7 @@ B8269D3D25C7B34D00488AB4 /* InputTextView.swift in Sources */, 7B0EFDF0275084AA00FFAAE7 /* CallMessageCell.swift in Sources */, C3AAFFF225AE99710089E6DD /* AppDelegate.swift in Sources */, + FD10AF0C2AF32B9A007709E5 /* SessionListViewModel.swift in Sources */, 942256822C23F8BB00C0FDBF /* InviteAFriendScreen.swift in Sources */, B8BB82A5238F627000BA5194 /* HomeVC.swift in Sources */, 4521C3C01F59F3BA00B4C582 /* TextFieldHelper.swift in Sources */, @@ -6267,14 +6487,14 @@ 7B1B52D828580C6D006069F2 /* EmojiPickerSheet.swift in Sources */, 940943402C7ED62300D9D2E0 /* StartupError.swift in Sources */, FD368A6A29DE9E30000DBF1E /* UIContextualAction+Utilities.swift in Sources */, - FDEF57252C3CF04C00131302 /* WebRTCSession+DataChannel.swift in Sources */, - 7BAFA1192A39669400B76CB9 /* BezierPathView.swift in Sources */, + FDEF57252C3CF04C00131302 /* (null) in Sources */, 7B4C75CD26BB92060000AC89 /* DeletedMessageView.swift in Sources */, FDD250722837234B00198BDA /* MediaGalleryNavigationController.swift in Sources */, FDD2506E283711D600198BDA /* DifferenceKit+Utilities.swift in Sources */, B897621C25D201F7004F83B2 /* RoundIconButton.swift in Sources */, 346B66311F4E29B200E5122F /* CropScaleImageViewController.swift in Sources */, FD1C98E4282E3C5B00B76F9E /* UINavigationBar+Utilities.swift in Sources */, + FDE754B62C9B96BB002A2623 /* WebRTCSession+UI.swift in Sources */, C302093E25DCBF08001F572D /* MentionSelectionView.swift in Sources */, FD71163828E2C50700B47552 /* SessionTableViewModel.swift in Sources */, FD71164A28E3EA5B00B47552 /* DismissType.swift in Sources */, @@ -6284,6 +6504,7 @@ B8EB20F02640F7F000773E52 /* OpenGroupInvitationView.swift in Sources */, C328254025CA55880062D0A7 /* ContextMenuVC.swift in Sources */, 9409433E2C7EB81800D9D2E0 /* WebRTCSession+Constants.swift in Sources */, + FDE754B12C9B96B4002A2623 /* TurnServerInfo.swift in Sources */, 7BD687D12A5D0D1200D8E455 /* MessageInfoScreen.swift in Sources */, B8269D2925C7A4B400488AB4 /* InputView.swift in Sources */, FD71162E28E168C700B47552 /* SettingsViewModel.swift in Sources */, @@ -6294,49 +6515,31 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + FD481A9C2CB4D58300ECC4CF /* MockSnodeAPICache.swift in Sources */, FD71161728D00DA400B47552 /* ThreadSettingsViewModelSpec.swift in Sources */, + FD481A972CAE0AE000ECC4CF /* MockAppContext.swift in Sources */, FD2AAAF028ED57B500A49611 /* SynchronousStorage.swift in Sources */, FD23CE292A6775650000B97C /* MockCrypto.swift in Sources */, FD19363F2ACA66DE004BCF0F /* DatabaseSpec.swift in Sources */, FD23CE332A67C4D90000B97C /* MockNetwork.swift in Sources */, FD71161528D00D6700B47552 /* ThreadDisappearingMessagesViewModelSpec.swift in Sources */, - FD23CE2D2A678E1E0000B97C /* MockCaches.swift in Sources */, + FD01503A2CA24328005B08A1 /* MockJobRunner.swift in Sources */, FD23EA5E28ED00FD0058676E /* NimbleExtensions.swift in Sources */, - FD23EA5F28ED00FF0058676E /* CommonMockedExtensions.swift in Sources */, + FD481A9B2CB4CAF100ECC4CF /* CustomArgSummaryDescribable+SMK.swift in Sources */, FD23EA5D28ED00FA0058676E /* TestConstants.swift in Sources */, FD71161A28D00E1100B47552 /* NotificationContentViewModelSpec.swift in Sources */, FDFE75B52ABD46B700655640 /* MockUserDefaults.swift in Sources */, + FD01504B2CA243CB005B08A1 /* Mock.swift in Sources */, FD0969FA2A6A00B000C5C365 /* Mocked.swift in Sources */, - FD23EA5C28ED00F80058676E /* Mock.swift in Sources */, + FD481A9A2CB4CAE500ECC4CF /* CommonSMKMockExtensions.swift in Sources */, + FD3FAB6C2AF1B28B00DC5421 /* MockFileManager.swift in Sources */, FD23EA6128ED0B260058676E /* CombineExtensions.swift in Sources */, - FD6A39712C2E3F5800762359 /* GRDBExtensions.swift in Sources */, - FDFE75B32ABD469500655640 /* MockJobRunner.swift in Sources */, FD9DD2712A72516D00ECB68E /* TestExtensions.swift in Sources */, + FD65318A2AA025C500DFEEAA /* TestDependencies.swift in Sources */, FD2AAAED28ED3E1000A49611 /* MockGeneralCache.swift in Sources */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; - FD7C37AA2BBB8B1D009DEEA7 /* Sources */ = { - isa = PBXSourcesBuildPhase; - buildActionMask = 2147483647; - files = ( - FD7C37C42BBB8BFC009DEEA7 /* MockNetwork.swift in Sources */, - FD7C37C32BBB8BF0009DEEA7 /* MockGeneralCache.swift in Sources */, - FD7C37C92BBB8C1A009DEEA7 /* CombineExtensions.swift in Sources */, - FD7C37BE2BBB8BBD009DEEA7 /* SnodeRequestSpec.swift in Sources */, - FD7C37C02BBB8BE1009DEEA7 /* Mocked.swift in Sources */, - FD7C37BF2BBB8BDF009DEEA7 /* Mock.swift in Sources */, - FD2DD58E2C6DBEBF0073D9BE /* SSKMockedExtensions.swift in Sources */, - FD7C37C22BBB8BED009DEEA7 /* MockCaches.swift in Sources */, - FD7C37C62BBB8C08009DEEA7 /* TestConstants.swift in Sources */, - FD7C37C82BBB8C11009DEEA7 /* NimbleExtensions.swift in Sources */, - FD7C37C52BBB8C01009DEEA7 /* MockJobRunner.swift in Sources */, - FD7C37CB2BBB8D36009DEEA7 /* MockUserDefaults.swift in Sources */, - FD7C37C12BBB8BEA009DEEA7 /* MockCrypto.swift in Sources */, - FD7C37C72BBB8C0E009DEEA7 /* TestExtensions.swift in Sources */, - FD5E93D82C12E3B50038C25A /* FileUploadResponseSpec.swift in Sources */, - FD6A39732C2E3F5800762359 /* GRDBExtensions.swift in Sources */, - FD7C37CA2BBB8C1D009DEEA7 /* SynchronousStorage.swift in Sources */, + FD481A992CB4CAAA00ECC4CF /* MockLibSessionCache.swift in Sources */, + FD49E2462B05C1D500FFBBB5 /* MockKeychain.swift in Sources */, + FD01502C2CA23DB7005B08A1 /* GRDBExtensions.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -6345,35 +6548,65 @@ buildActionMask = 2147483647; files = ( FD2AAAF228ED57B500A49611 /* SynchronousStorage.swift in Sources */, - FD078E4927E02576000769AF /* CommonMockedExtensions.swift in Sources */, + FD01504E2CA243E7005B08A1 /* TypeConversionUtilitiesSpec.swift in Sources */, + FD65318D2AA025C500DFEEAA /* TestDependencies.swift in Sources */, + FD49E2492B05C1D500FFBBB5 /* MockKeychain.swift in Sources */, FD83B9BF27CF2294005E1583 /* TestConstants.swift in Sources */, - FDFBB7542A2023EB00CA7350 /* BencodeDecoderSpec.swift in Sources */, FD9DD2732A72516D00ECB68E /* TestExtensions.swift in Sources */, - FD7F74762BB276E7006DDFD8 /* PreparedRequestSpec.swift in Sources */, - FDFE75B22ABD469500655640 /* MockJobRunner.swift in Sources */, + FD3FAB6E2AF1B28C00DC5421 /* MockFileManager.swift in Sources */, FD83B9BB27CF20AF005E1583 /* SessionIdSpec.swift in Sources */, FDC290A927D9B46D005DAE71 /* NimbleExtensions.swift in Sources */, + FD0150402CA2433D005B08A1 /* BencodeDecoderSpec.swift in Sources */, + FD0150412CA2433D005B08A1 /* BencodeEncoderSpec.swift in Sources */, + FD0150422CA2433D005B08A1 /* VersionSpec.swift in Sources */, FD23EA6328ED0B260058676E /* CombineExtensions.swift in Sources */, - FD23CE352A67C4DA0000B97C /* MockNetwork.swift in Sources */, + FD0150482CA243CB005B08A1 /* Mock.swift in Sources */, FDFE75B42ABD46B600655640 /* MockUserDefaults.swift in Sources */, + FD0150392CA24328005B08A1 /* MockJobRunner.swift in Sources */, FD23CE282A67755C0000B97C /* MockCrypto.swift in Sources */, FD2AAAEE28ED3E1100A49611 /* MockGeneralCache.swift in Sources */, - FD9B30F3293EA0BF008DEE3E /* BatchResponseSpec.swift in Sources */, FD37EA1528AB42CB003AE748 /* IdentitySpec.swift in Sources */, - FD7F74702BB276A0006DDFD8 /* BatchRequestSpec.swift in Sources */, - FD23CE2C2A678DF80000B97C /* MockCaches.swift in Sources */, FD0B77B229B82B7A009169BA /* ArrayUtilitiesSpec.swift in Sources */, - FD6A391F2C2A99EF00762359 /* BencodeResponseSpec.swift in Sources */, FD1A94FE2900D2EA000D73D3 /* PersistableRecordUtilitiesSpec.swift in Sources */, FD23CE262A676B5B0000B97C /* DependenciesSpec.swift in Sources */, FDDF074A29DAB36900E5E8B5 /* JobRunnerSpec.swift in Sources */, - FD7F74742BB276D0006DDFD8 /* HeaderSpec.swift in Sources */, FD0969FB2A6A00B100C5C365 /* Mocked.swift in Sources */, - FD7F74722BB276CC006DDFD8 /* RequestSpec.swift in Sources */, - FDC290AA27D9B6FD005DAE71 /* Mock.swift in Sources */, - FD6A391D2C2A99DF00762359 /* BencodeEncoderSpec.swift in Sources */, - FD6A39742C2E3F5800762359 /* GRDBExtensions.swift in Sources */, - FD2959902A43BE5F00888A17 /* VersionSpec.swift in Sources */, + FD0150292CA23DB7005B08A1 /* GRDBExtensions.swift in Sources */, + FD481A942CAE0AE000ECC4CF /* MockAppContext.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + FDB5DAF62A981C42002C8721 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + FDB5DB062A981C67002C8721 /* PreparedRequestSendingSpec.swift in Sources */, + FD49E2482B05C1D500FFBBB5 /* MockKeychain.swift in Sources */, + FDB5DB082A981F8B002C8721 /* Mocked.swift in Sources */, + FD336F702CABB96C00C0B51B /* BatchRequestSpec.swift in Sources */, + FD3765E22AD8F53B00DC1489 /* CommonSSKMockExtensions.swift in Sources */, + FDB5DB142A981FAE002C8721 /* CombineExtensions.swift in Sources */, + FD0150382CA24328005B08A1 /* MockJobRunner.swift in Sources */, + FD481A952CAE0AE000ECC4CF /* MockAppContext.swift in Sources */, + FD3765DF2AD8F03100DC1489 /* MockSnodeAPICache.swift in Sources */, + FDB5DB112A981FA6002C8721 /* TestExtensions.swift in Sources */, + FDB5DB092A981F8D002C8721 /* MockCrypto.swift in Sources */, + FDAA167B2AC28E2F00DDBF77 /* SnodeRequestSpec.swift in Sources */, + FD65318C2AA025C500DFEEAA /* TestDependencies.swift in Sources */, + FDB5DB102A981FA3002C8721 /* TestConstants.swift in Sources */, + FDB5DB0C2A981F96002C8721 /* MockNetwork.swift in Sources */, + FD336F712CABB97800C0B51B /* DestinationSpec.swift in Sources */, + FD336F722CABB97800C0B51B /* BatchResponseSpec.swift in Sources */, + FD336F732CABB97800C0B51B /* HeaderSpec.swift in Sources */, + FD336F742CABB97800C0B51B /* PreparedRequestSpec.swift in Sources */, + FD336F752CABB97800C0B51B /* RequestSpec.swift in Sources */, + FD336F762CABB97800C0B51B /* BencodeResponseSpec.swift in Sources */, + FDB5DB0B2A981F92002C8721 /* MockGeneralCache.swift in Sources */, + FD0150492CA243CB005B08A1 /* Mock.swift in Sources */, + FDB5DB122A981FA8002C8721 /* NimbleExtensions.swift in Sources */, + FD0150282CA23DB7005B08A1 /* GRDBExtensions.swift in Sources */, + FDB5DB152A981FB0002C8721 /* SynchronousStorage.swift in Sources */, + FDE754AC2C9B967E002A2623 /* FileUploadResponseSpec.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -6381,46 +6614,64 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( - FDDC08F229A300E800BF9681 /* TypeConversionUtilitiesSpec.swift in Sources */, + FD72BDA72BE369DC00CF6CF6 /* CryptoOpenGroupAPISpec.swift in Sources */, FDC2909427D710B4005DAE71 /* SOGSEndpointSpec.swift in Sources */, FD96F3A529DBC3DC00401309 /* MessageSendJobSpec.swift in Sources */, FDC2909127D709CA005DAE71 /* SOGSMessageSpec.swift in Sources */, - FD3C906A27E417CE00CD579F /* CryptoSMKSpec.swift in Sources */, - FD96F3A729DBD43D00401309 /* MockJobRunner.swift in Sources */, FDC2909627D71252005DAE71 /* SOGSErrorSpec.swift in Sources */, FDC2908727D7047F005DAE71 /* RoomSpec.swift in Sources */, - FD22724B2C326E75004D8A6C /* CustomArgSummaryDescribable+SMK.swift in Sources */, + FDE754A82C9B964D002A2623 /* MessageReceiverGroupsSpec.swift in Sources */, + FD01504C2CA243CB005B08A1 /* Mock.swift in Sources */, FD83B9C727CF3F10005E1583 /* CapabilitiesSpec.swift in Sources */, FD2AAAF128ED57B500A49611 /* SynchronousStorage.swift in Sources */, - FD078E4827E02561000769AF /* CommonMockedExtensions.swift in Sources */, FD23CE2A2A6775660000B97C /* MockCrypto.swift in Sources */, + FD336F6F2CAA37CB00C0B51B /* MockCommunityPoller.swift in Sources */, FDC2909827D7129B005DAE71 /* PersonalizationSpec.swift in Sources */, - FD078E4D27E17156000769AF /* MockOGMCache.swift in Sources */, + FD481A962CAE0AE000ECC4CF /* MockAppContext.swift in Sources */, + FD0150452CA243BB005B08A1 /* LibSessionUtilSpec.swift in Sources */, + FD336F602CAA28CF00C0B51B /* CommonSMKMockExtensions.swift in Sources */, + FD336F612CAA28CF00C0B51B /* MockNotificationsManager.swift in Sources */, + FD336F622CAA28CF00C0B51B /* CustomArgSummaryDescribable+SMK.swift in Sources */, + FD336F632CAA28CF00C0B51B /* MockOGMCache.swift in Sources */, + FD481A922CAD17DE00ECC4CF /* LibSessionGroupMembersSpec.swift in Sources */, + FD336F642CAA28CF00C0B51B /* MockCommunityPollerCache.swift in Sources */, + FD336F652CAA28CF00C0B51B /* MockDisplayPictureCache.swift in Sources */, + FD336F662CAA28CF00C0B51B /* MockSwarmPoller.swift in Sources */, + FD336F672CAA28CF00C0B51B /* MockGroupPollerCache.swift in Sources */, + FD336F682CAA28CF00C0B51B /* MockPoller.swift in Sources */, + FD336F692CAA28CF00C0B51B /* MockLibSessionCache.swift in Sources */, + FD0150462CA243BB005B08A1 /* LibSessionSpec.swift in Sources */, + FD3765E02AD8F05100DC1489 /* MockSnodeAPICache.swift in Sources */, FD9DD2722A72516D00ECB68E /* TestExtensions.swift in Sources */, - FDC0F0082C00721A002CBFB9 /* MockOGPoller.swift in Sources */, - FDC290A627D860CE005DAE71 /* Mock.swift in Sources */, - FDA1E83D29AC71A800C5C3BD /* LibSessionSpec.swift in Sources */, + FDE754AA2C9B964D002A2623 /* MessageSenderSpec.swift in Sources */, FD83B9C027CF2294005E1583 /* TestConstants.swift in Sources */, + FD65318B2AA025C500DFEEAA /* TestDependencies.swift in Sources */, + FD49E2472B05C1D500FFBBB5 /* MockKeychain.swift in Sources */, + FD3765E32AD8F56200DC1489 /* CommonSSKMockExtensions.swift in Sources */, FDC4389A27BA002500C60D73 /* OpenGroupAPISpec.swift in Sources */, - FD23CE2E2A678E1E0000B97C /* MockCaches.swift in Sources */, FD23EA6228ED0B260058676E /* CombineExtensions.swift in Sources */, FD83B9C527CF3E2A005E1583 /* OpenGroupSpec.swift in Sources */, - FD22724F2C327BCA004D8A6C /* SSKMockedExtensions.swift in Sources */, FD23CE342A67C4D90000B97C /* MockNetwork.swift in Sources */, FDC2908B27D707F3005DAE71 /* SendMessageRequestSpec.swift in Sources */, FDC290A827D9B46D005DAE71 /* NimbleExtensions.swift in Sources */, FD7692F72A53A2ED000E4B70 /* SessionThreadViewModelSpec.swift in Sources */, FD0969F92A69FFE700C5C365 /* Mocked.swift in Sources */, + FD481A902CAD16F100ECC4CF /* LibSessionGroupInfoSpec.swift in Sources */, + FDE754A92C9B964D002A2623 /* MessageSenderGroupsSpec.swift in Sources */, FDC2908F27D70938005DAE71 /* SendDirectMessageRequestSpec.swift in Sources */, - FDC438BD27BB2AB400C60D73 /* Mockable.swift in Sources */, - FDA1E83929A5771A00C5C3BD /* LibSessionUtilSpec.swift in Sources */, + FD3FAB672AF0C47000DC5421 /* DisplayPictureDownloadJobSpec.swift in Sources */, + FD72BDA42BE3690B00CF6CF6 /* CryptoSMKSpec.swift in Sources */, FDC2908927D70656005DAE71 /* RoomPollInfoSpec.swift in Sources */, + FD336F6C2CAA29C600C0B51B /* CommunityPollerSpec.swift in Sources */, + FD481AA32CB889AE00ECC4CF /* RetrieveDefaultOpenGroupRoomsJobSpec.swift in Sources */, FDFD645D27F273F300808CA1 /* MockGeneralCache.swift in Sources */, + FD01502A2CA23DB7005B08A1 /* GRDBExtensions.swift in Sources */, FDC2908D27D70905005DAE71 /* UpdateMessageRequestSpec.swift in Sources */, - FD6A39722C2E3F5800762359 /* GRDBExtensions.swift in Sources */, + FD01503B2CA24328005B08A1 /* MockJobRunner.swift in Sources */, FD078E5427E197CA000769AF /* OpenGroupManagerSpec.swift in Sources */, FD3C906727E416AF00CD579F /* BlindedIdLookupSpec.swift in Sources */, FD83B9D227D59495005E1583 /* MockUserDefaults.swift in Sources */, + FD3FAB6D2AF1B28B00DC5421 /* MockFileManager.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -6507,23 +6758,11 @@ target = C33FD9AA255A548A00E217F9 /* SignalUtilitiesKit */; targetProxy = C3D90A7025773A44002C9DF5 /* PBXContainerItemProxy */; }; - FD37E9F228A5ED70003AE748 /* PBXTargetDependency */ = { - isa = PBXTargetDependency; - platformFilter = ios; - target = C3C2A678255388CC00C340D1 /* SessionUtilitiesKit */; - targetProxy = FD37E9F128A5ED70003AE748 /* PBXContainerItemProxy */; - }; FD71160E28D00BAE00B47552 /* PBXTargetDependency */ = { isa = PBXTargetDependency; target = D221A088169C9E5E00537ABF /* Session */; targetProxy = FD71160D28D00BAE00B47552 /* PBXContainerItemProxy */; }; - FD7C37B42BBB8B1E009DEEA7 /* PBXTargetDependency */ = { - isa = PBXTargetDependency; - platformFilter = ios; - target = C3C2A59E255385C100C340D1 /* SessionSnodeKit */; - targetProxy = FD7C37B32BBB8B1E009DEEA7 /* PBXContainerItemProxy */; - }; FD7F74622BAAA4C7006DDFD8 /* PBXTargetDependency */ = { isa = PBXTargetDependency; target = FD9BDDF72A5D2294005F1EBC /* SessionUtil */; @@ -6539,6 +6778,22 @@ target = C3C2A678255388CC00C340D1 /* SessionUtilitiesKit */; targetProxy = FD83B9B427CF200A005E1583 /* PBXContainerItemProxy */; }; + FDB348822BE86A4400B716C2 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = FD9BDDF72A5D2294005F1EBC /* SessionUtil */; + targetProxy = FDB348812BE86A4400B716C2 /* PBXContainerItemProxy */; + }; + FDB348852BE86A4800B716C2 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = FD9BDDF72A5D2294005F1EBC /* SessionUtil */; + targetProxy = FDB348842BE86A4800B716C2 /* PBXContainerItemProxy */; + }; + FDB5DB002A981C43002C8721 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + platformFilter = ios; + target = C3C2A59E255385C100C340D1 /* SessionSnodeKit */; + targetProxy = FDB5DAFF2A981C43002C8721 /* PBXContainerItemProxy */; + }; FDC4389427B9FFC700C60D73 /* PBXTargetDependency */ = { isa = PBXTargetDependency; platformFilter = ios; @@ -6606,7 +6861,6 @@ PRODUCT_BUNDLE_IDENTIFIER = "com.loki-project.loki-messenger.ShareExtension"; PRODUCT_NAME = "$(TARGET_NAME)"; SKIP_INSTALL = YES; - STRIP_INSTALLED_PRODUCT = NO; SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; SWIFT_OBJC_BRIDGING_HEADER = ""; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; @@ -6738,7 +6992,6 @@ PRODUCT_BUNDLE_IDENTIFIER = "com.loki-project.loki-messenger.NotificationServiceExtension"; PRODUCT_NAME = "$(TARGET_NAME)"; SKIP_INSTALL = YES; - STRIP_INSTALLED_PRODUCT = NO; SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; SWIFT_VERSION = 5.0; @@ -6850,7 +7103,6 @@ DYLIB_COMPATIBILITY_VERSION = 1; DYLIB_CURRENT_VERSION = 1; DYLIB_INSTALL_NAME_BASE = "@rpath"; - ENABLE_MODULE_VERIFIER = YES; GCC_C_LANGUAGE_STANDARD = gnu11; GCC_DYNAMIC_NO_PIC = NO; GCC_OPTIMIZATION_LEVEL = 0; @@ -6871,7 +7123,6 @@ PRODUCT_BUNDLE_IDENTIFIER = "com.loki-project.SessionUIKit"; PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; SKIP_INSTALL = YES; - STRIP_INSTALLED_PRODUCT = NO; SUPPORTS_MACCATALYST = NO; SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; @@ -6925,7 +7176,6 @@ DYLIB_COMPATIBILITY_VERSION = 1; DYLIB_CURRENT_VERSION = 1; DYLIB_INSTALL_NAME_BASE = "@rpath"; - ENABLE_MODULE_VERIFIER = YES; ENABLE_NS_ASSERTIONS = NO; ENABLE_STRICT_OBJC_MSGSEND = YES; GCC_C_LANGUAGE_STANDARD = gnu11; @@ -6990,7 +7240,6 @@ DYLIB_COMPATIBILITY_VERSION = 1; DYLIB_CURRENT_VERSION = 1; DYLIB_INSTALL_NAME_BASE = "@rpath"; - ENABLE_MODULE_VERIFIER = NO; GCC_C_LANGUAGE_STANDARD = gnu11; GCC_DYNAMIC_NO_PIC = NO; GCC_OPTIMIZATION_LEVEL = 0; @@ -7018,7 +7267,6 @@ PRODUCT_BUNDLE_IDENTIFIER = "com.loki-project.SignalUtilitiesKit"; PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; SKIP_INSTALL = YES; - STRIP_INSTALLED_PRODUCT = NO; SUPPORTS_MACCATALYST = NO; SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; @@ -7072,7 +7320,6 @@ DYLIB_COMPATIBILITY_VERSION = 1; DYLIB_CURRENT_VERSION = 1; DYLIB_INSTALL_NAME_BASE = "@rpath"; - ENABLE_MODULE_VERIFIER = NO; ENABLE_NS_ASSERTIONS = NO; ENABLE_STRICT_OBJC_MSGSEND = YES; GCC_C_LANGUAGE_STANDARD = gnu11; @@ -7144,7 +7391,6 @@ DYLIB_COMPATIBILITY_VERSION = 1; DYLIB_CURRENT_VERSION = 1; DYLIB_INSTALL_NAME_BASE = "@rpath"; - ENABLE_MODULE_VERIFIER = YES; GCC_C_LANGUAGE_STANDARD = gnu11; GCC_DYNAMIC_NO_PIC = NO; GCC_OPTIMIZATION_LEVEL = 0; @@ -7175,7 +7421,6 @@ PRODUCT_BUNDLE_IDENTIFIER = "com.loki-project.SessionSnodeKit"; PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; SKIP_INSTALL = YES; - STRIP_INSTALLED_PRODUCT = NO; SUPPORTS_MACCATALYST = NO; SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; SWIFT_INCLUDE_PATHS = "$(inherited) \"$(TARGET_BUILD_DIR)/libSessionUtil\""; @@ -7230,7 +7475,6 @@ DYLIB_COMPATIBILITY_VERSION = 1; DYLIB_CURRENT_VERSION = 1; DYLIB_INSTALL_NAME_BASE = "@rpath"; - ENABLE_MODULE_VERIFIER = YES; ENABLE_NS_ASSERTIONS = NO; ENABLE_STRICT_OBJC_MSGSEND = YES; GCC_C_LANGUAGE_STANDARD = gnu11; @@ -7300,13 +7544,13 @@ CODE_SIGN_IDENTITY = ""; CODE_SIGN_STYLE = Automatic; COPY_PHASE_STRIP = NO; + CURRENT_PROJECT_VERSION = 1; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEFINES_MODULE = YES; DEVELOPMENT_TEAM = SUQ8J2PCT7; DYLIB_COMPATIBILITY_VERSION = 1; DYLIB_CURRENT_VERSION = 1; DYLIB_INSTALL_NAME_BASE = "@rpath"; - ENABLE_MODULE_VERIFIER = NO; GCC_C_LANGUAGE_STANDARD = gnu11; GCC_DYNAMIC_NO_PIC = NO; GCC_OPTIMIZATION_LEVEL = 0; @@ -7330,7 +7574,6 @@ /usr/lib/swift, "\"$(TARGET_BUILD_DIR)/libSessionUtil\"", ); - MODULE_VERIFIER_SUPPORTED_LANGUAGES = "objective-c objective-c++"; MODULE_VERIFIER_SUPPORTED_LANGUAGE_STANDARDS = "gnu11 gnu++14"; MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; MTL_FAST_MATH = YES; @@ -7338,7 +7581,6 @@ PRODUCT_BUNDLE_IDENTIFIER = "com.loki-project.SessionUtilitiesKit"; PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; SKIP_INSTALL = YES; - STRIP_INSTALLED_PRODUCT = NO; SUPPORTS_MACCATALYST = NO; SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; SWIFT_INCLUDE_PATHS = "$(inherited) \"$(TARGET_BUILD_DIR)/libSessionUtil\""; @@ -7393,7 +7635,6 @@ DYLIB_COMPATIBILITY_VERSION = 1; DYLIB_CURRENT_VERSION = 1; DYLIB_INSTALL_NAME_BASE = "@rpath"; - ENABLE_MODULE_VERIFIER = NO; ENABLE_NS_ASSERTIONS = NO; ENABLE_STRICT_OBJC_MSGSEND = YES; GCC_C_LANGUAGE_STANDARD = gnu11; @@ -7470,7 +7711,6 @@ DYLIB_COMPATIBILITY_VERSION = 1; DYLIB_CURRENT_VERSION = 1; DYLIB_INSTALL_NAME_BASE = "@rpath"; - ENABLE_MODULE_VERIFIER = NO; GCC_C_LANGUAGE_STANDARD = gnu11; GCC_DYNAMIC_NO_PIC = NO; GCC_OPTIMIZATION_LEVEL = 0; @@ -7501,7 +7741,6 @@ PRODUCT_BUNDLE_IDENTIFIER = "com.loki-project.SessionMessagingKit"; PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; SKIP_INSTALL = YES; - STRIP_INSTALLED_PRODUCT = NO; SUPPORTS_MACCATALYST = NO; SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; SWIFT_INCLUDE_PATHS = "$(inherited) \"$(TARGET_BUILD_DIR)/libSessionUtil\""; @@ -7557,7 +7796,6 @@ DYLIB_COMPATIBILITY_VERSION = 1; DYLIB_CURRENT_VERSION = 1; DYLIB_INSTALL_NAME_BASE = "@rpath"; - ENABLE_MODULE_VERIFIER = NO; ENABLE_NS_ASSERTIONS = NO; ENABLE_STRICT_OBJC_MSGSEND = YES; GCC_C_LANGUAGE_STANDARD = gnu11; @@ -7594,7 +7832,6 @@ PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; SDKROOT = iphoneos; SKIP_INSTALL = YES; - STRIP_INSTALLED_PRODUCT = YES; SUPPORTS_MACCATALYST = NO; SWIFT_COMPILATION_MODE = wholemodule; SWIFT_INCLUDE_PATHS = "$(inherited) \"$(TARGET_BUILD_DIR)/libSessionUtil\""; @@ -7636,10 +7873,11 @@ CLANG_WARN__ARC_BRIDGE_CAST_NONARC = YES; CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; CODE_SIGN_IDENTITY = "iPhone Developer"; - CURRENT_PROJECT_VERSION = 495; + CURRENT_PROJECT_VERSION = 498; ENABLE_BITCODE = NO; ENABLE_STRICT_OBJC_MSGSEND = YES; ENABLE_TESTABILITY = YES; + ENABLE_USER_SCRIPT_SANDBOXING = NO; GCC_NO_COMMON_BLOCKS = YES; GCC_PREPROCESSOR_DEFINITIONS = ( "DEBUG=1", @@ -7673,16 +7911,12 @@ GCC_WARN_UNUSED_VARIABLE = YES; HEADER_SEARCH_PATHS = ""; IPHONEOS_DEPLOYMENT_TARGET = 13.0; - MARKETING_VERSION = 2.8.1; + MARKETING_VERSION = 2.9.0; ONLY_ACTIVE_ARCH = YES; - OTHER_CFLAGS = ( - "-fobjc-arc-exceptions", - "-Werror=protocol", - ); + OTHER_CFLAGS = "-Werror=protocol"; "OTHER_SWIFT_FLAGS[arch=*]" = "-D DEBUG"; SDKROOT = iphoneos; - SWIFT_EMIT_LOC_STRINGS = YES; - SWIFT_VERSION = 4.0; + SWIFT_VERSION = 5.0; VALIDATE_PRODUCT = YES; }; name = Debug; @@ -7715,9 +7949,11 @@ CLANG_WARN__ARC_BRIDGE_CAST_NONARC = YES; CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; CODE_SIGN_IDENTITY = "iPhone Distribution"; - CURRENT_PROJECT_VERSION = 495; + CURRENT_PROJECT_VERSION = 498; ENABLE_BITCODE = NO; + ENABLE_MODULE_VERIFIER = YES; ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_USER_SCRIPT_SANDBOXING = NO; GCC_NO_COMMON_BLOCKS = YES; GCC_TREAT_IMPLICIT_FUNCTION_DECLARATIONS_AS_ERRORS = YES; GCC_TREAT_INCOMPATIBLE_POINTER_TYPE_WARNINGS_AS_ERRORS = YES; @@ -7747,18 +7983,17 @@ GCC_WARN_UNUSED_VARIABLE = YES; HEADER_SEARCH_PATHS = ""; IPHONEOS_DEPLOYMENT_TARGET = 13.0; - MARKETING_VERSION = 2.8.1; + MARKETING_VERSION = 2.9.0; ONLY_ACTIVE_ARCH = NO; OTHER_CFLAGS = ( "-DNS_BLOCK_ASSERTIONS=1", - "-fobjc-arc-exceptions", "-Werror=protocol", ); SDKROOT = iphoneos; SWIFT_COMPILATION_MODE = wholemodule; SWIFT_EMIT_LOC_STRINGS = YES; SWIFT_OPTIMIZATION_LEVEL = "-O"; - SWIFT_VERSION = 4.0; + SWIFT_VERSION = 5.0; VALIDATE_PRODUCT = YES; }; name = App_Store_Release; @@ -7766,7 +8001,6 @@ D221A0BD169C9E5F00537ABF /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { - ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = "$(inherited)"; ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_ADDRESS_SANITIZER_CONTAINER_OVERFLOW = YES; CLANG_ANALYZER_SECURITY_FLOATLOOPCOUNTER = YES; @@ -7823,8 +8057,6 @@ PROVISIONING_PROFILE_SPECIFIER = ""; RUN_CLANG_STATIC_ANALYZER = YES; SDKROOT = iphoneos; - STRIP_INSTALLED_PRODUCT = NO; - SWIFT_OBJC_BRIDGING_HEADER = "Session/Meta/Signal-Bridging-Header.h"; SWIFT_OBJC_INTERFACE_HEADER_NAME = "Session-Swift.h"; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; SWIFT_VERSION = 5.0; @@ -7837,7 +8069,6 @@ D221A0BE169C9E5F00537ABF /* App_Store_Release */ = { isa = XCBuildConfiguration; buildSettings = { - ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = "$(inherited)"; ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_ADDRESS_SANITIZER_CONTAINER_OVERFLOW = NO; CLANG_ANALYZER_SECURITY_FLOATLOOPCOUNTER = YES; @@ -7890,7 +8121,6 @@ PROVISIONING_PROFILE = ""; RUN_CLANG_STATIC_ANALYZER = YES; SDKROOT = iphoneos; - SWIFT_OBJC_BRIDGING_HEADER = "Session/Meta/Signal-Bridging-Header.h"; SWIFT_OBJC_INTERFACE_HEADER_NAME = "Session-Swift.h"; SWIFT_VERSION = 5.0; TARGETED_DEVICE_FAMILY = "1,2"; @@ -7899,116 +8129,38 @@ }; name = App_Store_Release; }; - FD71161028D00BAE00B47552 /* Debug */ = { + FD2272502C32910F004D8A6C /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { - BUNDLE_LOADER = "$(TEST_HOST)"; - CLANG_ANALYZER_NONNULL = YES; - CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; - CLANG_CXX_LANGUAGE_STANDARD = "gnu++17"; - CLANG_ENABLE_OBJC_WEAK = YES; - CLANG_WARN_BOOL_CONVERSION = YES; - CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; - CLANG_WARN_DOCUMENTATION_COMMENTS = YES; - CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; - CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; - CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; - CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = "$(inherited)"; - CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; - CODE_SIGN_STYLE = Automatic; COPY_PHASE_STRIP = NO; - DEBUG_INFORMATION_FORMAT = dwarf; - GCC_C_LANGUAGE_STANDARD = gnu11; + ENABLE_MODULE_VERIFIER = NO; GCC_DYNAMIC_NO_PIC = NO; GCC_OPTIMIZATION_LEVEL = 0; - GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; - GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GENERATE_INFOPLIST_FILE = YES; IPHONEOS_DEPLOYMENT_TARGET = 15.0; - MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; - MTL_FAST_MATH = YES; - OTHER_SWIFT_FLAGS = "$(inherited) -D COCOAPODS $(inherited) -D SQLITE_HAS_CODEC -D GRDBCIPHER -D SQLITE_ENABLE_FTS5 -Xfrontend -warn-long-expression-type-checking=100"; - PRODUCT_BUNDLE_IDENTIFIER = io.oxen.SessionTests; - PRODUCT_NAME = "$(TARGET_NAME)"; - SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; - SWIFT_EMIT_LOC_STRINGS = NO; - SWIFT_OPTIMIZATION_LEVEL = "-Onone"; - SWIFT_VERSION = 5.0; - TARGETED_DEVICE_FAMILY = "1,2"; - TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Session.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Session"; + PRODUCT_BUNDLE_IDENTIFIER = io.oxen.SessionSnodeKitTests; + PRODUCT_NAME = SessionSnodeKitTests; }; name = Debug; }; - FD71161128D00BAE00B47552 /* App_Store_Release */ = { + FD2272512C32910F004D8A6C /* App_Store_Release */ = { isa = XCBuildConfiguration; buildSettings = { - ALWAYS_SEARCH_USER_PATHS = NO; - BUNDLE_LOADER = "$(TEST_HOST)"; - CLANG_ANALYZER_NONNULL = YES; - CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; - CLANG_CXX_LANGUAGE_STANDARD = "gnu++17"; - CLANG_ENABLE_MODULES = YES; - CLANG_ENABLE_OBJC_ARC = YES; - CLANG_ENABLE_OBJC_WEAK = YES; - CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; - CLANG_WARN_BOOL_CONVERSION = YES; - CLANG_WARN_COMMA = YES; - CLANG_WARN_CONSTANT_CONVERSION = YES; - CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; - CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; - CLANG_WARN_DOCUMENTATION_COMMENTS = YES; - CLANG_WARN_EMPTY_BODY = YES; - CLANG_WARN_ENUM_CONVERSION = YES; - CLANG_WARN_INFINITE_RECURSION = YES; - CLANG_WARN_INT_CONVERSION = YES; - CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; - CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; - CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; - CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; - CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = "$(inherited)"; - CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; - CLANG_WARN_STRICT_PROTOTYPES = YES; - CLANG_WARN_SUSPICIOUS_MOVE = YES; - CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; - CLANG_WARN_UNREACHABLE_CODE = YES; - CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; - CODE_SIGN_STYLE = Automatic; - COPY_PHASE_STRIP = NO; - DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; - ENABLE_NS_ASSERTIONS = NO; - ENABLE_STRICT_OBJC_MSGSEND = YES; - GCC_C_LANGUAGE_STANDARD = gnu11; - GCC_NO_COMMON_BLOCKS = YES; - GCC_WARN_64_TO_32_BIT_CONVERSION = YES; - GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; - GCC_WARN_UNDECLARED_SELECTOR = YES; - GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; - GCC_WARN_UNUSED_FUNCTION = YES; - GCC_WARN_UNUSED_VARIABLE = YES; + ENABLE_MODULE_VERIFIER = NO; GENERATE_INFOPLIST_FILE = YES; IPHONEOS_DEPLOYMENT_TARGET = 15.0; - MTL_ENABLE_DEBUG_INFO = NO; - MTL_FAST_MATH = YES; - PRODUCT_BUNDLE_IDENTIFIER = io.oxen.SessionTests; - PRODUCT_NAME = "$(TARGET_NAME)"; - SDKROOT = iphoneos; - SWIFT_COMPILATION_MODE = wholemodule; - SWIFT_EMIT_LOC_STRINGS = NO; - SWIFT_OPTIMIZATION_LEVEL = "-O"; - SWIFT_VERSION = 5.0; - TARGETED_DEVICE_FAMILY = "1,2"; - TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Session.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Session"; - VALIDATE_PRODUCT = YES; + PRODUCT_BUNDLE_IDENTIFIER = io.oxen.SessionSnodeKitTests; + PRODUCT_NAME = SessionSnodeKitTests; }; name = App_Store_Release; }; - FD7C37B62BBB8B1E009DEEA7 /* Debug */ = { + FD71161028D00BAE00B47552 /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { - ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + BUNDLE_LOADER = "$(TEST_HOST)"; CLANG_ANALYZER_NONNULL = YES; CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; - CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++17"; CLANG_ENABLE_OBJC_WEAK = YES; CLANG_WARN_BOOL_CONVERSION = YES; CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; @@ -8021,35 +8173,36 @@ CODE_SIGN_STYLE = Automatic; COPY_PHASE_STRIP = NO; DEBUG_INFORMATION_FORMAT = dwarf; - ENABLE_USER_SCRIPT_SANDBOXING = NO; - GCC_C_LANGUAGE_STANDARD = gnu17; + ENABLE_MODULE_VERIFIER = NO; + GCC_C_LANGUAGE_STANDARD = gnu11; GCC_DYNAMIC_NO_PIC = NO; GCC_OPTIMIZATION_LEVEL = 0; GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GENERATE_INFOPLIST_FILE = YES; IPHONEOS_DEPLOYMENT_TARGET = 15.0; - LOCALIZATION_PREFERS_STRING_CATALOGS = YES; MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; MTL_FAST_MATH = YES; - PRODUCT_BUNDLE_IDENTIFIER = io.oxen.SessionSnodeKitTests; + OTHER_SWIFT_FLAGS = "$(inherited) -D COCOAPODS $(inherited) -D SQLITE_HAS_CODEC -D GRDBCIPHER -D SQLITE_ENABLE_FTS5 -Xfrontend -warn-long-expression-type-checking=100"; + PRODUCT_BUNDLE_IDENTIFIER = io.oxen.SessionTests; PRODUCT_NAME = "$(TARGET_NAME)"; - SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited)"; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; SWIFT_EMIT_LOC_STRINGS = NO; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; SWIFT_VERSION = 5.0; TARGETED_DEVICE_FAMILY = "1,2"; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Session.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Session"; }; name = Debug; }; - FD7C37B72BBB8B1E009DEEA7 /* App_Store_Release */ = { + FD71161128D00BAE00B47552 /* App_Store_Release */ = { isa = XCBuildConfiguration; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; - ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + BUNDLE_LOADER = "$(TEST_HOST)"; CLANG_ANALYZER_NONNULL = YES; CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; - CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++17"; CLANG_ENABLE_MODULES = YES; CLANG_ENABLE_OBJC_ARC = YES; CLANG_ENABLE_OBJC_WEAK = YES; @@ -8078,10 +8231,10 @@ CODE_SIGN_STYLE = Automatic; COPY_PHASE_STRIP = NO; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_MODULE_VERIFIER = NO; ENABLE_NS_ASSERTIONS = NO; ENABLE_STRICT_OBJC_MSGSEND = YES; - ENABLE_USER_SCRIPT_SANDBOXING = NO; - GCC_C_LANGUAGE_STANDARD = gnu17; + GCC_C_LANGUAGE_STANDARD = gnu11; GCC_NO_COMMON_BLOCKS = YES; GCC_WARN_64_TO_32_BIT_CONVERSION = YES; GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; @@ -8091,16 +8244,17 @@ GCC_WARN_UNUSED_VARIABLE = YES; GENERATE_INFOPLIST_FILE = YES; IPHONEOS_DEPLOYMENT_TARGET = 15.0; - LOCALIZATION_PREFERS_STRING_CATALOGS = YES; MTL_ENABLE_DEBUG_INFO = NO; MTL_FAST_MATH = YES; - PRODUCT_BUNDLE_IDENTIFIER = io.oxen.SessionSnodeKitTests; + PRODUCT_BUNDLE_IDENTIFIER = io.oxen.SessionTests; PRODUCT_NAME = "$(TARGET_NAME)"; SDKROOT = iphoneos; SWIFT_COMPILATION_MODE = wholemodule; SWIFT_EMIT_LOC_STRINGS = NO; + SWIFT_OPTIMIZATION_LEVEL = "-O"; SWIFT_VERSION = 5.0; TARGETED_DEVICE_FAMILY = "1,2"; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Session.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Session"; VALIDATE_PRODUCT = YES; }; name = App_Store_Release; @@ -8124,6 +8278,7 @@ CODE_SIGN_STYLE = Automatic; COPY_PHASE_STRIP = NO; DEBUG_INFORMATION_FORMAT = dwarf; + ENABLE_MODULE_VERIFIER = NO; GCC_C_LANGUAGE_STANDARD = gnu11; GCC_DYNAMIC_NO_PIC = NO; GCC_OPTIMIZATION_LEVEL = 0; @@ -8180,6 +8335,7 @@ CODE_SIGN_STYLE = Automatic; COPY_PHASE_STRIP = NO; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_MODULE_VERIFIER = NO; ENABLE_NS_ASSERTIONS = NO; ENABLE_STRICT_OBJC_MSGSEND = YES; GCC_C_LANGUAGE_STANDARD = gnu11; @@ -8225,7 +8381,6 @@ COPY_PHASE_STRIP = NO; DEBUG_INFORMATION_FORMAT = dwarf; DEFINES_MODULE = YES; - ENABLE_MODULE_VERIFIER = YES; GCC_C_LANGUAGE_STANDARD = gnu11; GCC_DYNAMIC_NO_PIC = NO; GCC_OPTIMIZATION_LEVEL = 0; @@ -8289,7 +8444,6 @@ COPY_PHASE_STRIP = NO; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEFINES_MODULE = YES; - ENABLE_MODULE_VERIFIER = YES; ENABLE_NS_ASSERTIONS = NO; ENABLE_STRICT_OBJC_MSGSEND = YES; GCC_C_LANGUAGE_STANDARD = gnu11; @@ -8338,6 +8492,7 @@ CODE_SIGN_STYLE = Automatic; COPY_PHASE_STRIP = NO; DEBUG_INFORMATION_FORMAT = dwarf; + ENABLE_MODULE_VERIFIER = NO; GCC_C_LANGUAGE_STANDARD = gnu11; GCC_DYNAMIC_NO_PIC = NO; GCC_OPTIMIZATION_LEVEL = 0; @@ -8394,6 +8549,7 @@ CODE_SIGN_STYLE = Automatic; COPY_PHASE_STRIP = NO; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_MODULE_VERIFIER = NO; ENABLE_NS_ASSERTIONS = NO; ENABLE_STRICT_OBJC_MSGSEND = YES; GCC_C_LANGUAGE_STANDARD = gnu11; @@ -8504,20 +8660,20 @@ defaultConfigurationIsVisible = 0; defaultConfigurationName = App_Store_Release; }; - FD71160F28D00BAE00B47552 /* Build configuration list for PBXNativeTarget "SessionTests" */ = { + FD2272522C32910F004D8A6C /* Build configuration list for PBXNativeTarget "SessionSnodeKitTests" */ = { isa = XCConfigurationList; buildConfigurations = ( - FD71161028D00BAE00B47552 /* Debug */, - FD71161128D00BAE00B47552 /* App_Store_Release */, + FD2272502C32910F004D8A6C /* Debug */, + FD2272512C32910F004D8A6C /* App_Store_Release */, ); defaultConfigurationIsVisible = 0; defaultConfigurationName = App_Store_Release; }; - FD7C37B52BBB8B1E009DEEA7 /* Build configuration list for PBXNativeTarget "SessionSnodeKitTests" */ = { + FD71160F28D00BAE00B47552 /* Build configuration list for PBXNativeTarget "SessionTests" */ = { isa = XCConfigurationList; buildConfigurations = ( - FD7C37B62BBB8B1E009DEEA7 /* Debug */, - FD7C37B72BBB8B1E009DEEA7 /* App_Store_Release */, + FD71161028D00BAE00B47552 /* Debug */, + FD71161128D00BAE00B47552 /* App_Store_Release */, ); defaultConfigurationIsVisible = 0; defaultConfigurationName = App_Store_Release; @@ -8552,6 +8708,14 @@ /* End XCConfigurationList section */ /* Begin XCRemoteSwiftPackageReference section */ + FD6A38E42C2A4D8E00762359 /* XCRemoteSwiftPackageReference "session-grdb-swift" */ = { + isa = XCRemoteSwiftPackageReference; + repositoryURL = "https://github.com/oxen-io/session-grdb-swift"; + requirement = { + kind = upToNextMajorVersion; + minimumVersion = 106.27.0; + }; + }; FD6A38E72C2A630E00762359 /* XCRemoteSwiftPackageReference "CocoaLumberjack" */ = { isa = XCRemoteSwiftPackageReference; repositoryURL = "https://github.com/CocoaLumberjack/CocoaLumberjack.git"; @@ -8643,6 +8807,16 @@ /* End XCRemoteSwiftPackageReference section */ /* Begin XCSwiftPackageProductDependency section */ + FD0150512CA2446D005B08A1 /* Quick */ = { + isa = XCSwiftPackageProductDependency; + package = FD6A39302C2AD33E00762359 /* XCRemoteSwiftPackageReference "Quick" */; + productName = Quick; + }; + FD0150532CA24471005B08A1 /* Nimble */ = { + isa = XCSwiftPackageProductDependency; + package = FD6A39392C2AD3A300762359 /* XCRemoteSwiftPackageReference "Nimble" */; + productName = Nimble; + }; FD22866E2C38D42300BC06F7 /* DifferenceKit */ = { isa = XCSwiftPackageProductDependency; package = FD6A38ED2C2A641200762359 /* XCRemoteSwiftPackageReference "DifferenceKit" */; @@ -8673,6 +8847,11 @@ package = FD6A38ED2C2A641200762359 /* XCRemoteSwiftPackageReference "DifferenceKit" */; productName = DifferenceKit; }; + FD6A38E52C2A4D8E00762359 /* GRDB */ = { + isa = XCSwiftPackageProductDependency; + package = FD6A38E42C2A4D8E00762359 /* XCRemoteSwiftPackageReference "session-grdb-swift" */; + productName = GRDB; + }; FD6A38E82C2A630E00762359 /* CocoaLumberjackSwift */ = { isa = XCSwiftPackageProductDependency; package = FD6A38E72C2A630E00762359 /* XCRemoteSwiftPackageReference "CocoaLumberjack" */; @@ -8708,11 +8887,6 @@ package = FD6A39302C2AD33E00762359 /* XCRemoteSwiftPackageReference "Quick" */; productName = Quick; }; - FD6A39352C2AD36400762359 /* Quick */ = { - isa = XCSwiftPackageProductDependency; - package = FD6A39302C2AD33E00762359 /* XCRemoteSwiftPackageReference "Quick" */; - productName = Quick; - }; FD6A39372C2AD36900762359 /* Quick */ = { isa = XCSwiftPackageProductDependency; package = FD6A39302C2AD33E00762359 /* XCRemoteSwiftPackageReference "Quick" */; @@ -8728,11 +8902,6 @@ package = FD6A39392C2AD3A300762359 /* XCRemoteSwiftPackageReference "Nimble" */; productName = Nimble; }; - FD6A393E2C2AD3B100762359 /* Nimble */ = { - isa = XCSwiftPackageProductDependency; - package = FD6A39392C2AD3A300762359 /* XCRemoteSwiftPackageReference "Nimble" */; - productName = Nimble; - }; FD6A39402C2AD3B600762359 /* Nimble */ = { isa = XCSwiftPackageProductDependency; package = FD6A39392C2AD3A300762359 /* XCRemoteSwiftPackageReference "Nimble" */; diff --git a/Session.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/Session.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index 97979c57d34..e193d2705c9 100644 --- a/Session.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/Session.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -1,5 +1,5 @@ { - "originHash" : "53c1182a7d64df9a13a5c0ec4420b2c8da9c4d81d97b2b79b6e8f2946978471b", + "originHash" : "46c1bce8a1122a70b9fab0446bdb5930cb4e518c02756562c7cb4862fd885b11", "pins" : [ { "identity" : "cocoalumberjack", @@ -85,7 +85,7 @@ { "identity" : "session-grdb-swift", "kind" : "remoteSourceControl", - "location" : "https://github.com/oxen-io/session-grdb-swift.git", + "location" : "https://github.com/oxen-io/session-grdb-swift", "state" : { "revision" : "04f480b95263b7c517085100ceb249f879c021d8", "version" : "106.29.3" diff --git a/Session.xcodeproj/xcshareddata/xcschemes/Session.xcscheme b/Session.xcodeproj/xcshareddata/xcschemes/Session.xcscheme index 70c030cf80f..9f0430f0929 100644 --- a/Session.xcodeproj/xcshareddata/xcschemes/Session.xcscheme +++ b/Session.xcodeproj/xcshareddata/xcschemes/Session.xcscheme @@ -1,6 +1,6 @@ @@ -131,6 +131,12 @@ ReferencedContainer = "container:Session.xcodeproj"> + + + + + + + + @@ -219,6 +237,11 @@ value = "1" isEnabled = "YES"> + + diff --git a/Session.xcodeproj/xcshareddata/xcschemes/SessionNotificationServiceExtension.xcscheme b/Session.xcodeproj/xcshareddata/xcschemes/SessionNotificationServiceExtension.xcscheme index 4948a6509dc..3b78c5c14bf 100644 --- a/Session.xcodeproj/xcshareddata/xcschemes/SessionNotificationServiceExtension.xcscheme +++ b/Session.xcodeproj/xcshareddata/xcschemes/SessionNotificationServiceExtension.xcscheme @@ -1,6 +1,6 @@ diff --git a/Session.xcodeproj/xcshareddata/xcschemes/SessionShareExtension.xcscheme b/Session.xcodeproj/xcshareddata/xcschemes/SessionShareExtension.xcscheme index 90848db0a1f..5f40788da66 100644 --- a/Session.xcodeproj/xcshareddata/xcschemes/SessionShareExtension.xcscheme +++ b/Session.xcodeproj/xcshareddata/xcschemes/SessionShareExtension.xcscheme @@ -1,6 +1,6 @@ diff --git a/Session.xcodeproj/xcshareddata/xcschemes/SessionSnodeKit.xcscheme b/Session.xcodeproj/xcshareddata/xcschemes/SessionSnodeKit.xcscheme index abffbecab37..ee504e302d6 100644 --- a/Session.xcodeproj/xcshareddata/xcschemes/SessionSnodeKit.xcscheme +++ b/Session.xcodeproj/xcshareddata/xcschemes/SessionSnodeKit.xcscheme @@ -1,6 +1,6 @@ + shouldUseLaunchSchemeArgsEnv = "YES"> + + + + diff --git a/Session.xcodeproj/xcshareddata/xcschemes/SessionUIKit.xcscheme b/Session.xcodeproj/xcshareddata/xcschemes/SessionUIKit.xcscheme new file mode 100644 index 00000000000..1d94c50ce78 --- /dev/null +++ b/Session.xcodeproj/xcshareddata/xcschemes/SessionUIKit.xcscheme @@ -0,0 +1,66 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Session.xcodeproj/xcshareddata/xcschemes/SessionUtil.xcscheme b/Session.xcodeproj/xcshareddata/xcschemes/SessionUtil.xcscheme new file mode 100644 index 00000000000..53ff9761a8e --- /dev/null +++ b/Session.xcodeproj/xcshareddata/xcschemes/SessionUtil.xcscheme @@ -0,0 +1,66 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Session.xcodeproj/xcshareddata/xcschemes/SessionUtilitiesKit.xcscheme b/Session.xcodeproj/xcshareddata/xcschemes/SessionUtilitiesKit.xcscheme index 086ad4722a6..356b476af9a 100644 --- a/Session.xcodeproj/xcshareddata/xcschemes/SessionUtilitiesKit.xcscheme +++ b/Session.xcodeproj/xcshareddata/xcschemes/SessionUtilitiesKit.xcscheme @@ -1,6 +1,6 @@ diff --git a/Session.xcodeproj/xcshareddata/xcschemes/SignalUtilitiesKit.xcscheme b/Session.xcodeproj/xcshareddata/xcschemes/SignalUtilitiesKit.xcscheme index 1109058f11d..96f5175302a 100644 --- a/Session.xcodeproj/xcshareddata/xcschemes/SignalUtilitiesKit.xcscheme +++ b/Session.xcodeproj/xcshareddata/xcschemes/SignalUtilitiesKit.xcscheme @@ -1,6 +1,6 @@ diff --git a/Session/Calls/Call Management/SessionCall.swift b/Session/Calls/Call Management/SessionCall.swift index 010f9c78a45..e3b8befa528 100644 --- a/Session/Calls/Call Management/SessionCall.swift +++ b/Session/Calls/Call Management/SessionCall.swift @@ -15,6 +15,8 @@ import SessionSnodeKit public final class SessionCall: CurrentCallProtocol, WebRTCSessionDelegate { @objc static let isEnabled = true + private let dependencies: Dependencies + // MARK: - Metadata Properties public let uuid: String public let callId: UUID // This is for CallKit @@ -147,22 +149,23 @@ public final class SessionCall: CurrentCallProtocol, WebRTCSessionDelegate { // MARK: - Initialization - init(_ db: Database, for sessionId: String, uuid: String, mode: CallMode, outgoing: Bool = false) { + init(_ db: Database, for sessionId: String, uuid: String, mode: CallMode, outgoing: Bool = false, using dependencies: Dependencies) { + self.dependencies = dependencies self.sessionId = sessionId self.uuid = uuid self.callId = UUID() self.mode = mode self.audioMode = .earpiece - self.webRTCSession = WebRTCSession.current ?? WebRTCSession(for: sessionId, with: uuid) + self.webRTCSession = WebRTCSession.current ?? WebRTCSession(for: sessionId, with: uuid, using: dependencies) self.isOutgoing = outgoing - let avatarData: Data? = ProfileManager.profileAvatar(db, id: sessionId) - self.contactName = Profile.displayName(db, id: sessionId, threadVariant: .contact) + let avatarData: Data? = DisplayPictureManager.displayPicture(db, id: .user(sessionId), using: dependencies) + self.contactName = Profile.displayName(db, id: sessionId, threadVariant: .contact, using: dependencies) self.profilePicture = avatarData .map { UIImage(data: $0) } .defaulting(to: PlaceholderIcon.generate(seed: sessionId, text: self.contactName, size: 300)) self.animatedProfilePicture = avatarData - .map { data in + .map { data -> YYImage? in switch data.guessedImageFormat { case .gif, .webp: return YYImage(data: data) default: return nil @@ -172,23 +175,23 @@ public final class SessionCall: CurrentCallProtocol, WebRTCSessionDelegate { WebRTCSession.current = self.webRTCSession self.webRTCSession.delegate = self - if AppEnvironment.shared.callManager.currentCall == nil { - AppEnvironment.shared.callManager.currentCall = self + if dependencies[singleton: .callManager].currentCall == nil { + dependencies[singleton: .callManager].setCurrentCall(self) } else { - SNLog("[Calls] A call is ongoing.") + Log.info(.calls, "A call is ongoing.") } } // stringlint:ignore_contents func reportIncomingCallIfNeeded(completion: @escaping (Error?) -> Void) { guard case .answer = mode else { - SessionCallManager.reportFakeCall(info: "Call not in answer mode") + SessionCallManager.reportFakeCall(info: "Call not in answer mode", using: dependencies) return } setupTimeoutTimer() - AppEnvironment.shared.callManager.reportIncomingCall(self, callerName: contactName) { error in + dependencies[singleton: .callManager].reportIncomingCall(self, callerName: contactName) { error in completion(error) } } @@ -201,7 +204,7 @@ public final class SessionCall: CurrentCallProtocol, WebRTCSessionDelegate { return } - SNLog("[Calls] Did receive remote sdp.") + Log.info(.calls, "Did receive remote sdp.") remoteSDP = sdp if hasStartedConnecting { webRTCSession.handleRemoteSDP(sdp, from: sessionId) // This sends an answer message internally @@ -221,7 +224,7 @@ public final class SessionCall: CurrentCallProtocol, WebRTCSessionDelegate { else { return } let webRTCSession: WebRTCSession = self.webRTCSession - let timestampMs: Int64 = SnodeAPI.currentOffsetTimestampMs() + let timestampMs: Int64 = dependencies[cache: .snodeAPI].currentOffsetTimestampMs() let disappearingMessagesConfiguration = try? thread.disappearingMessagesConfiguration.fetchOne(db)?.forcedWithDisappearAfterReadIfNeeded() let message: CallMessage = CallMessage( uuid: self.uuid, @@ -235,12 +238,13 @@ public final class SessionCall: CurrentCallProtocol, WebRTCSessionDelegate { messageUuid: self.uuid, threadId: sessionId, threadVariant: thread.variant, - authorId: getUserHexEncodedPublicKey(db), + authorId: dependencies[cache: .general].sessionId.hexString, variant: .infoCall, body: String(data: messageInfoData, encoding: .utf8), timestampMs: timestampMs, expiresInSeconds: message.expiresInSeconds, - expiresStartedAtMs: message.expiresStartedAtMs + expiresStartedAtMs: message.expiresStartedAtMs, + using: dependencies ) .inserted(db) @@ -281,7 +285,7 @@ public final class SessionCall: CurrentCallProtocol, WebRTCSessionDelegate { webRTCSession.hangUp() - Storage.shared.writeAsync { [weak self] db in + dependencies[singleton: .storage].writeAsync { [weak self] db in try self?.webRTCSession.endCall(db, with: sessionId) } @@ -290,13 +294,13 @@ public final class SessionCall: CurrentCallProtocol, WebRTCSessionDelegate { // MARK: - Call Message Handling - public func updateCallMessage(mode: EndCallMode) { + public func updateCallMessage(mode: EndCallMode, using dependencies: Dependencies) { guard let callInteractionId: Int64 = callInteractionId else { return } let duration: TimeInterval = self.duration let hasStartedConnecting: Bool = self.hasStartedConnecting - Storage.shared.writeAsync( + dependencies[singleton: .storage].writeAsync( updates: { db in guard let interaction: Interaction = try? Interaction.fetchOne(db, id: callInteractionId) else { return @@ -307,17 +311,18 @@ public final class SessionCall: CurrentCallProtocol, WebRTCSessionDelegate { guard let infoMessageData: Data = (interaction.body ?? "").data(using: .utf8), - let messageInfo: CallMessage.MessageInfo = try? JSONDecoder().decode( + let messageInfo: CallMessage.MessageInfo = try? JSONDecoder(using: dependencies).decode( CallMessage.MessageInfo.self, from: infoMessageData ), messageInfo.state == .incoming, - let missedCallInfoData: Data = try? JSONEncoder().encode(missedCallInfo) + let missedCallInfoData: Data = try? JSONEncoder(using: dependencies) + .encode(missedCallInfo) else { return } - _ = try interaction + try interaction .with(body: String(data: missedCallInfoData, encoding: .utf8)) - .saved(db) + .upserted(db) } let shouldMarkAsRead: Bool = try { if duration > 0 { return true } @@ -351,11 +356,12 @@ public final class SessionCall: CurrentCallProtocol, WebRTCSessionDelegate { threadId: interaction.threadId, threadVariant: threadVariant, includingOlder: false, - trySendReadReceipt: false + trySendReadReceipt: false, + using: dependencies ) }, - completion: { _, _ in - SessionCallManager.suspendDatabaseIfCallEndedInBackground() + completion: { [dependencies] _, _ in + dependencies[singleton: .callManager].suspendDatabaseIfCallEndedInBackground() } ) } @@ -399,12 +405,12 @@ public final class SessionCall: CurrentCallProtocol, WebRTCSessionDelegate { public func didReceiveHangUpSignal() { self.hasEnded = true - DispatchQueue.main.async { + DispatchQueue.main.async { [dependencies] in if let currentBanner = IncomingCallBanner.current { currentBanner.dismiss() } - guard Singleton.hasAppContext else { return } - if let callVC = Singleton.appContext.frontmostViewController as? CallVC { callVC.handleEndCallMessage() } + guard dependencies[singleton: .appContext].isValid else { return } + if let callVC = dependencies[singleton: .appContext].frontMostViewController as? CallVC { callVC.handleEndCallMessage() } if let miniCallView = MiniCallView.current { miniCallView.dismiss() } - AppEnvironment.shared.callManager.reportCurrentCallEnded(reason: .remoteEnded) + dependencies[singleton: .callManager].reportCurrentCallEnded(reason: .remoteEnded) } } @@ -429,19 +435,21 @@ public final class SessionCall: CurrentCallProtocol, WebRTCSessionDelegate { // Register a callback to get the current network status then remove it immediately as we only // care about the current status - let networkStatusCallbackId: UUID = LibSession.onNetworkStatusChanged { [weak self] status in - guard status != .connected else { return } - - self?.reconnectTimer = Timer.scheduledTimerOnMainThread(withTimeInterval: 5, repeats: false) { _ in - self?.tryToReconnect() - } - } - LibSession.removeNetworkChangedCallback(callbackId: networkStatusCallbackId) + dependencies[cache: .libSessionNetwork].networkStatus + .sinkUntilComplete( + receiveValue: { [weak self, dependencies] status in + guard status != .connected else { return } + + self?.reconnectTimer = Timer.scheduledTimerOnMainThread(withTimeInterval: 5, repeats: false, using: dependencies) { _ in + self?.tryToReconnect() + } + } + ) let sessionId: String = self.sessionId let webRTCSession: WebRTCSession = self.webRTCSession - guard let thread: SessionThread = Storage.shared.read({ db in try SessionThread.fetchOne(db, id: sessionId) }) else { + guard let thread: SessionThread = dependencies[singleton: .storage].read({ db in try SessionThread.fetchOne(db, id: sessionId) }) else { return } @@ -458,11 +466,11 @@ public final class SessionCall: CurrentCallProtocol, WebRTCSessionDelegate { let timeInterval: TimeInterval = 60 - timeOutTimer = Timer.scheduledTimerOnMainThread(withTimeInterval: timeInterval, repeats: false) { _ in - self.didTimeout = true + timeOutTimer = Timer.scheduledTimerOnMainThread(withTimeInterval: timeInterval, repeats: false, using: dependencies) { [weak self, dependencies] _ in + self?.didTimeout = true - AppEnvironment.shared.callManager.endCall(self) { error in - self.timeOutTimer = nil + dependencies[singleton: .callManager].endCall(self) { error in + self?.timeOutTimer = nil } } } diff --git a/Session/Calls/Call Management/SessionCallManager+Action.swift b/Session/Calls/Call Management/SessionCallManager+Action.swift index 8de3699d92b..217d6d4e7d6 100644 --- a/Session/Calls/Call Management/SessionCallManager+Action.swift +++ b/Session/Calls/Call Management/SessionCallManager+Action.swift @@ -2,6 +2,7 @@ import UIKit import GRDB +import SessionMessagingKit import SessionUtilitiesKit extension SessionCallManager { @@ -9,7 +10,7 @@ extension SessionCallManager { public func startCallAction() -> Bool { guard let call: CurrentCallProtocol = self.currentCall else { return false } - Storage.shared.writeAsync { db in + dependencies[singleton: .storage].writeAsync { db in call.startSessionCall(db) } @@ -18,17 +19,17 @@ extension SessionCallManager { @discardableResult public func answerCallAction() -> Bool { - guard let call: SessionCall = (self.currentCall as? SessionCall) else { return false } + guard + let call: SessionCall = (self.currentCall as? SessionCall), + dependencies[singleton: .appContext].isValid + else { return false } - if Singleton.hasAppContext, Singleton.appContext.frontmostViewController is CallVC { + if dependencies[singleton: .appContext].frontMostViewController is CallVC { call.answerSessionCall() } else { - guard - Singleton.hasAppContext, - let presentingVC = Singleton.appContext.frontmostViewController - else { return false } // FIXME: Handle more gracefully - let callVC = CallVC(for: call) + guard let presentingVC = dependencies[singleton: .appContext].frontMostViewController else { return false } // FIXME: Handle more gracefully + let callVC = CallVC(for: call, using: dependencies) if let conversationVC = presentingVC as? ConversationVC { callVC.conversationVC = conversationVC @@ -40,6 +41,7 @@ extension SessionCallManager { call.answerSessionCall() } } + return true } diff --git a/Session/Calls/Call Management/SessionCallManager+CXCallController.swift b/Session/Calls/Call Management/SessionCallManager+CXCallController.swift index 1824a9d5c16..ad078c5251b 100644 --- a/Session/Calls/Call Management/SessionCallManager+CXCallController.swift +++ b/Session/Calls/Call Management/SessionCallManager+CXCallController.swift @@ -2,12 +2,16 @@ import Foundation import CallKit +import SessionMessagingKit import SessionUtilitiesKit extension SessionCallManager { - public func startCall(_ call: SessionCall, completion: ((Error?) -> Void)?) { - guard case .offer = call.mode else { return } - guard !call.hasConnected else { return } + public func startCall(_ call: CurrentCallProtocol?, completion: ((Error?) -> Void)?) { + guard + let call: SessionCall = call as? SessionCall, + case .offer = call.mode, + !call.hasConnected + else { return } reportOutgoingCall(call) @@ -28,9 +32,9 @@ extension SessionCallManager { } } - public func answerCall(_ call: SessionCall, completion: ((Error?) -> Void)?) { - if callController != nil { - let answerCallAction = CXAnswerCallAction(call: call.callId) + public func answerCall(_ call: CurrentCallProtocol?, completion: ((Error?) -> Void)?) { + if callController != nil, let callId: UUID = call?.callId { + let answerCallAction = CXAnswerCallAction(call: callId) let transaction = CXTransaction() transaction.addAction(answerCallAction) @@ -42,9 +46,9 @@ extension SessionCallManager { } } - public func endCall(_ call: SessionCall, completion: ((Error?) -> Void)?) { - if callController != nil { - let endCallAction = CXEndCallAction(call: call.callId) + public func endCall(_ call: CurrentCallProtocol?, completion: ((Error?) -> Void)?) { + if callController != nil, let callId: UUID = call?.callId { + let endCallAction = CXEndCallAction(call: callId) let transaction = CXTransaction() transaction.addAction(endCallAction) diff --git a/Session/Calls/Call Management/SessionCallManager+CXProvider.swift b/Session/Calls/Call Management/SessionCallManager+CXProvider.swift index af853661980..18bd6659ccc 100644 --- a/Session/Calls/Call Management/SessionCallManager+CXProvider.swift +++ b/Session/Calls/Call Management/SessionCallManager+CXProvider.swift @@ -23,11 +23,11 @@ extension SessionCallManager: CXProviderDelegate { public func provider(_ provider: CXProvider, perform action: CXAnswerCallAction) { Log.assertOnMainThread() - Log.debug("[CallKit] Perform CXAnswerCallAction") + Log.debug(.calls, "Perform CXAnswerCallAction") guard let call: SessionCall = (self.currentCall as? SessionCall) else { return action.fail() } - if Singleton.hasAppContext && Singleton.appContext.isMainAppAndActive { + if dependencies[singleton: .appContext].isMainAppAndActive { if answerCallAction() { action.fulfill() } @@ -41,7 +41,7 @@ extension SessionCallManager: CXProviderDelegate { } public func provider(_ provider: CXProvider, perform action: CXEndCallAction) { - Log.debug("[CallKit] Perform CXEndCallAction") + Log.debug(.calls, "Perform CXEndCallAction") Log.assertOnMainThread() if endCallAction() { @@ -53,7 +53,7 @@ extension SessionCallManager: CXProviderDelegate { } public func provider(_ provider: CXProvider, perform action: CXSetMutedCallAction) { - Log.debug("[CallKit] Perform CXSetMutedCallAction, isMuted: \(action.isMuted)") + Log.debug(.calls, "Perform CXSetMutedCallAction, isMuted: \(action.isMuted)") Log.assertOnMainThread() if setMutedCallAction(isMuted: action.isMuted) { @@ -65,15 +65,15 @@ extension SessionCallManager: CXProviderDelegate { } public func provider(_ provider: CXProvider, perform action: CXSetHeldCallAction) { - // TODO: set on hold + // TODO: [CALLS] set on hold } public func provider(_ provider: CXProvider, timedOutPerforming action: CXAction) { - // TODO: handle timeout + // TODO: [CALLS] handle timeout } public func provider(_ provider: CXProvider, didActivate audioSession: AVAudioSession) { - Log.debug("[CallKit] Audio session did activate.") + Log.debug(.calls, "Audio session did activate.") Log.assertOnMainThread() guard let call: SessionCall = (self.currentCall as? SessionCall) else { return } @@ -82,7 +82,7 @@ extension SessionCallManager: CXProviderDelegate { } public func provider(_ provider: CXProvider, didDeactivate audioSession: AVAudioSession) { - Log.debug("[CallKit] Audio session did deactivate.") + Log.debug(.calls, "Audio session did deactivate.") Log.assertOnMainThread() guard let call: SessionCall = (self.currentCall as? SessionCall) else { return } diff --git a/Session/Calls/Call Management/SessionCallManager.swift b/Session/Calls/Call Management/SessionCallManager.swift index 7235ec02aed..5a93165f049 100644 --- a/Session/Calls/Call Management/SessionCallManager.swift +++ b/Session/Calls/Call Management/SessionCallManager.swift @@ -8,8 +8,23 @@ import SessionUIKit import SessionMessagingKit import SignalUtilitiesKit import SessionUtilitiesKit + +// MARK: - Cache + +public extension Cache { + static let callManager: CacheConfig = Dependencies.create( + identifier: "callManager", + createInstance: { _ in SessionCallManager.Cache() }, + mutableInstance: { $0 }, + immutableInstance: { $0 } + ) +} + +// MARK: - SessionCallManager public final class SessionCallManager: NSObject, CallManagerProtocol { + let dependencies: Dependencies + let provider: CXProvider? let callController: CXCallController? @@ -27,40 +42,15 @@ public final class SessionCallManager: NSObject, CallManagerProtocol { } } - private static var _sharedProvider: CXProvider? - static func sharedProvider(useSystemCallLog: Bool) -> CXProvider { - let configuration = buildProviderConfiguration(useSystemCallLog: useSystemCallLog) - - if let sharedProvider = self._sharedProvider { - sharedProvider.configuration = configuration - return sharedProvider - } - else { - SwiftSingletons.register(self) - let provider = CXProvider(configuration: configuration) - _sharedProvider = provider - return provider - } - } - - static func buildProviderConfiguration(useSystemCallLog: Bool) -> CXProviderConfiguration { - let providerConfiguration = CXProviderConfiguration() - providerConfiguration.supportsVideo = true - providerConfiguration.maximumCallGroups = 1 - providerConfiguration.maximumCallsPerCallGroup = 1 - providerConfiguration.supportedHandleTypes = [.generic] - let iconMaskImage = #imageLiteral(resourceName: "SessionGreen32") - providerConfiguration.iconTemplateImageData = iconMaskImage.pngData() - providerConfiguration.includesCallsInRecents = useSystemCallLog - - return providerConfiguration - } - // MARK: - Initialization - init(useSystemCallLog: Bool = false) { + init(useSystemCallLog: Bool = false, using dependencies: Dependencies) { + self.dependencies = dependencies + if Preferences.isCallKitSupported { - self.provider = SessionCallManager.sharedProvider(useSystemCallLog: useSystemCallLog) + self.provider = dependencies.mutate(cache: .callManager) { + $0.getOrCreateProvider(useSystemCallLog: useSystemCallLog) + } self.callController = CXCallController() } else { @@ -76,14 +66,16 @@ public final class SessionCallManager: NSObject, CallManagerProtocol { // MARK: - Report calls - public static func reportFakeCall(info: String) { + public static func reportFakeCall(info: String, using dependencies: Dependencies) { let callId = UUID() - let provider = SessionCallManager.sharedProvider(useSystemCallLog: false) + let provider: CXProvider = dependencies.mutate(cache: .callManager) { + $0.getOrCreateProvider(useSystemCallLog: false) + } provider.reportNewIncomingCall( with: callId, update: CXCallUpdate() ) { _ in - SNLog("[Calls] Reported fake incoming call to CallKit due to: \(info)") + Log.error(.calls, "Reported fake incoming call to CallKit due to: \(info)") } provider.reportCall( with: callId, @@ -92,10 +84,14 @@ public final class SessionCallManager: NSObject, CallManagerProtocol { ) } + public func setCurrentCall(_ call: CurrentCallProtocol?) { + self.currentCall = call + } + public func reportOutgoingCall(_ call: SessionCall) { Log.assertOnMainThread() - UserDefaults.sharedLokiProject?[.isCallOngoing] = true - UserDefaults.sharedLokiProject?[.lastCallPreOffer] = Date() + dependencies[defaults: .appGroup, key: .isCallOngoing] = true + dependencies[defaults: .appGroup, key: .lastCallPreOffer] = Date() call.stateDidChange = { if call.hasStartedConnecting { @@ -108,8 +104,14 @@ public final class SessionCallManager: NSObject, CallManagerProtocol { } } - public func reportIncomingCall(_ call: SessionCall, callerName: String, completion: @escaping (Error?) -> Void) { - let provider = provider ?? Self.sharedProvider(useSystemCallLog: false) + public func reportIncomingCall( + _ call: CurrentCallProtocol, + callerName: String, + completion: @escaping (Error?) -> Void + ) { + let provider: CXProvider = dependencies.mutate(cache: .callManager) { + $0.getOrCreateProvider(useSystemCallLog: false) + } // Construct a CXCallUpdate describing the incoming call, including the caller. let update = CXCallUpdate() update.localizedCallerName = callerName @@ -119,14 +121,14 @@ public final class SessionCallManager: NSObject, CallManagerProtocol { disableUnsupportedFeatures(callUpdate: update) // Report the incoming call to the system - provider.reportNewIncomingCall(with: call.callId, update: update) { error in + provider.reportNewIncomingCall(with: call.callId, update: update) { [dependencies] error in guard error == nil else { self.reportCurrentCallEnded(reason: .failed) completion(error) return } - UserDefaults.sharedLokiProject?[.isCallOngoing] = true - UserDefaults.sharedLokiProject?[.lastCallPreOffer] = Date() + dependencies[defaults: .appGroup, key: .isCallOngoing] = true + dependencies[defaults: .appGroup, key: .lastCallPreOffer] = Date() completion(nil) } } @@ -141,10 +143,10 @@ public final class SessionCallManager: NSObject, CallManagerProtocol { func handleCallEnded() { WebRTCSession.current = nil - UserDefaults.sharedLokiProject?[.isCallOngoing] = false - UserDefaults.sharedLokiProject?[.lastCallPreOffer] = nil + dependencies[defaults: .appGroup, key: .isCallOngoing] = false + dependencies[defaults: .appGroup, key: .lastCallPreOffer] = nil - if Singleton.hasAppContext && Singleton.appContext.isInBackground { + if dependencies[singleton: .appContext].isInBackground { (UIApplication.shared.delegate as? AppDelegate)?.stopPollers() Log.flush() } @@ -152,7 +154,7 @@ public final class SessionCallManager: NSObject, CallManagerProtocol { guard let call = currentCall else { handleCallEnded() - Self.suspendDatabaseIfCallEndedInBackground() + suspendDatabaseIfCallEndedInBackground() return } @@ -160,14 +162,14 @@ public final class SessionCallManager: NSObject, CallManagerProtocol { self.provider?.reportCall(with: call.callId, endedAt: nil, reason: reason) switch (reason) { - case .answeredElsewhere: call.updateCallMessage(mode: .answeredElsewhere) - case .unanswered: call.updateCallMessage(mode: .unanswered) - case .declinedElsewhere: call.updateCallMessage(mode: .local) - default: call.updateCallMessage(mode: .remote) + case .answeredElsewhere: call.updateCallMessage(mode: .answeredElsewhere, using: dependencies) + case .unanswered: call.updateCallMessage(mode: .unanswered, using: dependencies) + case .declinedElsewhere: call.updateCallMessage(mode: .local, using: dependencies) + default: call.updateCallMessage(mode: .remote, using: dependencies) } } else { - call.updateCallMessage(mode: .local) + call.updateCallMessage(mode: .local, using: dependencies) } (call as? SessionCall)?.webRTCSession.dropConnection() @@ -197,15 +199,12 @@ public final class SessionCallManager: NSObject, CallManagerProtocol { callUpdate.supportsDTMF = false } - public static func suspendDatabaseIfCallEndedInBackground() { - if Singleton.hasAppContext && Singleton.appContext.isInBackground { - // FIXME: Initialise the `SessionCallManager` with a dependencies instance - let dependencies: Dependencies = Dependencies() - + public func suspendDatabaseIfCallEndedInBackground() { + if dependencies[singleton: .appContext].isInBackground { // Stop all jobs except for message sending and when completed suspend the database - JobRunner.stopAndClearPendingJobs(exceptForVariant: .messageSend, using: dependencies) { _ in - LibSession.suspendNetworkAccess() - dependencies.storage.suspendDatabaseAccess() + dependencies[singleton: .jobRunner].stopAndClearPendingJobs(exceptForVariant: .messageSend) { [dependencies] _ in + dependencies.mutate(cache: .libSessionNetwork) { $0.suspendNetworkAccess() } + dependencies[singleton: .storage].suspendDatabaseAccess() Log.flush() } } @@ -214,36 +213,37 @@ public final class SessionCallManager: NSObject, CallManagerProtocol { // MARK: - UI public func showCallUIForCall(caller: String, uuid: String, mode: CallMode, interactionId: Int64?) { - guard let call: SessionCall = Storage.shared.read({ db in SessionCall(db, for: caller, uuid: uuid, mode: mode) }) else { - return - } + guard + let call: SessionCall = dependencies[singleton: .storage].read({ [dependencies] db in + SessionCall(db, for: caller, uuid: uuid, mode: mode, using: dependencies) + }) + else { return } call.callInteractionId = interactionId - call.reportIncomingCallIfNeeded { error in + call.reportIncomingCallIfNeeded { [dependencies] error in if let error = error { - SNLog("[Calls] Failed to report incoming call to CallKit due to error: \(error)") + Log.error(.calls, "Failed to report incoming call to CallKit due to error: \(error)") return } DispatchQueue.main.async { - guard Singleton.hasAppContext && Singleton.appContext.isMainAppAndActive else { return } - - guard let presentingVC = Singleton.appContext.frontmostViewController else { - preconditionFailure() // FIXME: Handle more gracefully - } + guard + dependencies[singleton: .appContext].isMainAppAndActive, + let presentingVC: UIViewController = dependencies[singleton: .appContext].frontMostViewController + else { return } if let conversationVC: ConversationVC = (presentingVC as? TopBannerController)?.wrappedViewController() as? ConversationVC, conversationVC.viewModel.threadData.threadId == call.sessionId { - let callVC = CallVC(for: call) + let callVC = CallVC(for: call, using: dependencies) callVC.conversationVC = conversationVC conversationVC.inputAccessoryView?.isHidden = true conversationVC.inputAccessoryView?.alpha = 0 presentingVC.present(callVC, animated: true, completion: nil) } else if !Preferences.isCallKitSupported { - let incomingCallBanner = IncomingCallBanner(for: call) + let incomingCallBanner = IncomingCallBanner(for: call, using: dependencies) incomingCallBanner.show() } } @@ -269,7 +269,7 @@ public final class SessionCallManager: NSObject, CallManagerProtocol { } public func handleAnswerMessage(_ message: CallMessage) { - guard Singleton.hasAppContext else { return } + guard dependencies[singleton: .appContext].isValid else { return } guard Thread.isMainThread else { DispatchQueue.main.async { self.handleAnswerMessage(message) @@ -277,11 +277,11 @@ public final class SessionCallManager: NSObject, CallManagerProtocol { return } - (Singleton.appContext.frontmostViewController as? CallVC)?.handleAnswerMessage(message) + (dependencies[singleton: .appContext].frontMostViewController as? CallVC)?.handleAnswerMessage(message) } public func dismissAllCallUI() { - guard Singleton.hasAppContext else { return } + guard dependencies[singleton: .appContext].isValid else { return } guard Thread.isMainThread else { DispatchQueue.main.async { self.dismissAllCallUI() @@ -290,8 +290,43 @@ public final class SessionCallManager: NSObject, CallManagerProtocol { } IncomingCallBanner.current?.dismiss() - (Singleton.appContext.frontmostViewController as? CallVC)?.handleEndCallMessage() + (dependencies[singleton: .appContext].frontMostViewController as? CallVC)?.handleEndCallMessage() MiniCallView.current?.dismiss() } } +// MARK: - SessionCallManager Cache + +public extension SessionCallManager { + class Cache: CallManagerCacheType { + public var provider: CXProvider? + + public func getOrCreateProvider(useSystemCallLog: Bool) -> CXProvider { + if let provider: CXProvider = self.provider { + return provider + } + + let iconMaskImage: UIImage = #imageLiteral(resourceName: "SessionGreen32") + let configuration = CXProviderConfiguration() + configuration.supportsVideo = true + configuration.maximumCallGroups = 1 + configuration.maximumCallsPerCallGroup = 1 + configuration.supportedHandleTypes = [.generic] + configuration.iconTemplateImageData = iconMaskImage.pngData() + configuration.includesCallsInRecents = useSystemCallLog + + let provider: CXProvider = CXProvider(configuration: configuration) + self.provider = provider + return provider + } + } +} + +// MARK: - OGMCacheType + +/// This is a read-only version of the Cache designed to avoid unintentionally mutating the instance in a non-thread-safe way +public protocol CallManagerImmutableCacheType: ImmutableCacheType {} + +public protocol CallManagerCacheType: CallManagerImmutableCacheType, MutableCacheType { + func getOrCreateProvider(useSystemCallLog: Bool) -> CXProvider +} diff --git a/Session/Calls/CallVC.swift b/Session/Calls/CallVC.swift index b6b35276757..511f855c4d9 100644 --- a/Session/Calls/CallVC.swift +++ b/Session/Calls/CallVC.swift @@ -12,6 +12,7 @@ final class CallVC: UIViewController, VideoPreviewDelegate { private static let floatingVideoViewWidth: CGFloat = (UIDevice.current.isIPad ? 160 : 80) private static let floatingVideoViewHeight: CGFloat = (UIDevice.current.isIPad ? 346: 173) + private let dependencies: Dependencies let call: SessionCall var latestKnownAudioOutputDeviceName: String? var durationTimer: Timer? @@ -334,9 +335,12 @@ final class CallVC: UIViewController, VideoPreviewDelegate { // MARK: - Lifecycle - init(for call: SessionCall) { + init(for call: SessionCall, using dependencies: Dependencies) { + self.dependencies = dependencies self.call = call + super.init(nibName: nil, bundle: nil) + setupStateChangeCallbacks() self.modalPresentationStyle = .overFullScreen self.modalTransitionStyle = .crossDissolve @@ -429,7 +433,7 @@ final class CallVC: UIViewController, VideoPreviewDelegate { _ = call.videoCapturer // Force the lazy var to instantiate titleLabel.text = self.call.contactName - AppEnvironment.shared.callManager.startCall(call) { [weak self] error in + dependencies[singleton: .callManager].startCall(call) { [weak self] error in DispatchQueue.main.async { if let _ = error { self?.callInfoLabel.text = "callsErrorStart".localized() @@ -519,10 +523,7 @@ final class CallVC: UIViewController, VideoPreviewDelegate { } private func addFloatingVideoView() { - guard - Singleton.hasAppContext, - let window: UIWindow = Singleton.appContext.mainWindow - else { return } + guard let window: UIWindow = dependencies[singleton: .appContext].mainWindow else { return } window.addSubview(floatingViewContainer) floatingViewContainer.pin(.top, to: .top, of: window, withInset: (window.safeAreaInsets.top + Values.veryLargeSpacing)) @@ -586,7 +587,7 @@ final class CallVC: UIViewController, VideoPreviewDelegate { } func handleEndCallMessage() { - SNLog("[Calls] Ending call.") + Log.info(.calls, "Ending call.") self.callInfoLabel.isHidden = false self.callDurationLabel.isHidden = true self.callInfoLabel.text = "callsEnded".localized() @@ -606,7 +607,7 @@ final class CallVC: UIViewController, VideoPreviewDelegate { } @objc private func answerCall() { - AppEnvironment.shared.callManager.answerCall(call) { [weak self] error in + dependencies[singleton: .callManager].answerCall(call) { [weak self] error in DispatchQueue.main.async { if let _ = error { self?.callInfoLabel.text = "callsErrorAnswer".localized() @@ -617,15 +618,17 @@ final class CallVC: UIViewController, VideoPreviewDelegate { } @objc private func endCall() { - AppEnvironment.shared.callManager.endCall(call) { [weak self] error in + dependencies[singleton: .callManager].endCall(call) { [weak self, dependencies] error in if let _ = error { self?.call.endSessionCall() - AppEnvironment.shared.callManager.reportCurrentCallEnded(reason: nil) + dependencies[singleton: .callManager].reportCurrentCallEnded(reason: nil) } DispatchQueue.main.async { self?.conversationVC?.showInputAccessoryView() - self?.presentingViewController?.dismiss(animated: true, completion: nil) + self?.presentingViewController?.dismiss(animated: true) { + self?.conversationVC?.becomeFirstResponder() + } } } } @@ -642,7 +645,7 @@ final class CallVC: UIViewController, VideoPreviewDelegate { self.shouldRestartCamera = false self.conversationVC?.showInputAccessoryView() - let miniCallView = MiniCallView(from: self) + let miniCallView = MiniCallView(from: self, using: dependencies) miniCallView.show() presentingViewController?.dismiss(animated: true, completion: nil) @@ -660,7 +663,7 @@ final class CallVC: UIViewController, VideoPreviewDelegate { call.isVideoEnabled = false } else { - guard Permissions.requestCameraPermissionIfNeeded() else { return } + guard Permissions.requestCameraPermissionIfNeeded(using: dependencies) else { return } let previewVC = VideoPreviewVC() previewVC.delegate = self present(previewVC, animated: true, completion: nil) diff --git a/Session/Calls/Views & Modals/CallMissedTipsModal.swift b/Session/Calls/Views & Modals/CallMissedTipsModal.swift index 3a2df54bf9d..ee15d9b5435 100644 --- a/Session/Calls/Views & Modals/CallMissedTipsModal.swift +++ b/Session/Calls/Views & Modals/CallMissedTipsModal.swift @@ -2,6 +2,7 @@ import UIKit import SessionUIKit +import SessionUtilitiesKit final class CallMissedTipsModal: Modal { private let caller: String diff --git a/Session/Calls/Views & Modals/IncomingCallBanner.swift b/Session/Calls/Views & Modals/IncomingCallBanner.swift index 5e0f7850e79..e56408b8d1e 100644 --- a/Session/Calls/Views & Modals/IncomingCallBanner.swift +++ b/Session/Calls/Views & Modals/IncomingCallBanner.swift @@ -9,6 +9,8 @@ import SessionUtilitiesKit final class IncomingCallBanner: UIView, UIGestureRecognizerDelegate { private static let swipeToOperateThreshold: CGFloat = 60 private var previousY: CGFloat = 0 + + private let dependencies: Dependencies let call: SessionCall // MARK: - UI Components @@ -32,41 +34,53 @@ final class IncomingCallBanner: UIView, UIGestureRecognizerDelegate { return result }() - private lazy var answerButton: UIButton = { - let result = UIButton(type: .custom) - result.setImage( - UIImage(named: "AnswerCall")? - .resized(to: CGSize(width: 24.8, height: 24.8))? - .withRenderingMode(.alwaysTemplate), - for: .normal + private lazy var answerButton: UIButton = UIButton(primaryAction: UIAction { [weak self] _ in self?.answerCall() }) + .withConfiguration( + UIButton.Configuration + .plain() + .withImage(UIImage(named: "AnswerCall")?.withRenderingMode(.alwaysTemplate)) + .withContentInsets(NSDirectionalEdgeInsets(top: 6, leading: 6, bottom: 6, trailing: 6)) ) - result.themeTintColor = .white - result.themeBackgroundColor = .callAccept_background - result.layer.cornerRadius = 24 - result.addTarget(self, action: #selector(answerCall), for: .touchUpInside) - result.set(.width, to: 48) - result.set(.height, to: 48) - - return result - }() + .withConfigurationUpdateHandler { button in + switch button.state { + case .highlighted: button.imageView?.tintAdjustmentMode = .dimmed + default: button.imageView?.tintAdjustmentMode = .normal + } + } + .withImageViewContentMode(.scaleAspectFit) + .withThemeTintColor(.white) + .withThemeBackgroundColor(.callAccept_background) + .withAccessibility( + identifier: "Close button", + label: "Close button" + ) + .withCornerRadius(24) + .with(.width, of: 48) + .with(.height, of: 48) - private lazy var hangUpButton: UIButton = { - let result = UIButton(type: .custom) - result.setImage( - UIImage(named: "EndCall")? - .resized(to: CGSize(width: 29.6, height: 11.2))? - .withRenderingMode(.alwaysTemplate), - for: .normal + private lazy var hangUpButton: UIButton = UIButton(primaryAction: UIAction { [weak self] _ in self?.endCall() }) + .withConfiguration( + UIButton.Configuration + .plain() + .withImage(UIImage(named: "EndCall")?.withRenderingMode(.alwaysTemplate)) + .withContentInsets(NSDirectionalEdgeInsets(top: 13, leading: 9, bottom: 13, trailing: 9)) ) - result.themeTintColor = .white - result.themeBackgroundColor = .callDecline_background - result.layer.cornerRadius = 24 - result.addTarget(self, action: #selector(endCall), for: .touchUpInside) - result.set(.width, to: 48) - result.set(.height, to: 48) - - return result - }() + .withConfigurationUpdateHandler { button in + switch button.state { + case .highlighted: button.imageView?.tintAdjustmentMode = .dimmed + default: button.imageView?.tintAdjustmentMode = .normal + } + } + .withImageViewContentMode(.scaleAspectFit) + .withThemeTintColor(.white) + .withThemeBackgroundColor(.callDecline_background) + .withAccessibility( + identifier: "Close button", + label: "Close button" + ) + .withCornerRadius(24) + .with(.width, of: 48) + .with(.height, of: 48) private lazy var panGestureRecognizer: UIPanGestureRecognizer = { let result = UIPanGestureRecognizer(target: self, action: #selector(handlePan)) @@ -79,7 +93,8 @@ final class IncomingCallBanner: UIView, UIGestureRecognizerDelegate { public static var current: IncomingCallBanner? - init(for call: SessionCall) { + init(for call: SessionCall, using dependencies: Dependencies) { + self.dependencies = dependencies self.call = call super.init(frame: CGRect.zero) @@ -113,9 +128,12 @@ final class IncomingCallBanner: UIView, UIGestureRecognizerDelegate { profilePictureView.update( publicKey: call.sessionId, threadVariant: .contact, - customImageData: nil, - profile: Storage.shared.read { [sessionId = call.sessionId] db in Profile.fetchOrCreate(db, id: sessionId) }, - additionalProfile: nil + displayPictureFilename: nil, + profile: dependencies[singleton: .storage].read { [sessionId = call.sessionId] db in + Profile.fetchOrCreate(db, id: sessionId) + }, + additionalProfile: nil, + using: dependencies ) displayNameLabel.text = call.contactName @@ -126,7 +144,8 @@ final class IncomingCallBanner: UIView, UIGestureRecognizerDelegate { self.addSubview(stackView) stackView.center(.vertical, in: self) - stackView.set(.width, to: .width, of: self, withOffset: Values.mediumSpacing) + stackView.pin(.leading, to: .leading, of: self, withInset: Values.mediumSpacing) + stackView.pin(.trailing, to: .trailing, of: self, withInset: -Values.mediumSpacing) } private func setUpGestureRecognizers() { @@ -168,7 +187,7 @@ final class IncomingCallBanner: UIView, UIGestureRecognizerDelegate { showCallVC(answer: false) } else { - endCall() // TODO: Or just put the call on hold? + endCall() // TODO: [CALLS] Or just put the call on hold? } } else { @@ -179,29 +198,29 @@ final class IncomingCallBanner: UIView, UIGestureRecognizerDelegate { } } - @objc private func answerCall() { + private func answerCall() { showCallVC(answer: true) } - @objc private func endCall() { - AppEnvironment.shared.callManager.endCall(call) { error in + private func endCall() { + dependencies[singleton: .callManager].endCall(call) { [weak self, dependencies] error in if let _ = error { - self.call.endSessionCall() - AppEnvironment.shared.callManager.reportCurrentCallEnded(reason: nil) + self?.call.endSessionCall() + dependencies[singleton: .callManager].reportCurrentCallEnded(reason: nil) } - self.dismiss() + self?.dismiss() } } public func showCallVC(answer: Bool) { dismiss() - guard - Singleton.hasAppContext, - let presentingVC = Singleton.appContext.frontmostViewController - else { preconditionFailure() } // FIXME: Handle more gracefully + guard let presentingVC: UIViewController = dependencies[singleton: .appContext].frontMostViewController else { + Log.critical(.calls, "Failed to retrieve front view controller when showing the call UI") + return endCall() + } - let callVC = CallVC(for: self.call) + let callVC = CallVC(for: self.call, using: dependencies) if let conversationVC = (presentingVC as? TopBannerController)?.wrappedViewController() as? ConversationVC { callVC.conversationVC = conversationVC conversationVC.inputAccessoryView?.isHidden = true @@ -216,13 +235,14 @@ final class IncomingCallBanner: UIView, UIGestureRecognizerDelegate { } public func show() { - guard Singleton.hasAppContext, let window: UIWindow = Singleton.appContext.mainWindow else { return } - self.alpha = 0.0 + + guard let window: UIWindow = dependencies[singleton: .appContext].mainWindow else { return } + window.addSubview(self) let topMargin = window.safeAreaInsets.top - Values.smallSpacing - self.set(.width, to: .width, of: window, withOffset: Values.smallSpacing) + self.set(.width, to: .width, of: window, withOffset: -Values.smallSpacing) self.pin(.top, to: .top, of: window, withInset: topMargin) UIView.animate(withDuration: 0.5, delay: 0, options: [], animations: { @@ -244,5 +264,4 @@ final class IncomingCallBanner: UIView, UIGestureRecognizerDelegate { self.removeFromSuperview() }) } - } diff --git a/Session/Calls/Views & Modals/MiniCallView.swift b/Session/Calls/Views & Modals/MiniCallView.swift index e51aa8a49e2..cd6126998b2 100644 --- a/Session/Calls/Views & Modals/MiniCallView.swift +++ b/Session/Calls/Views & Modals/MiniCallView.swift @@ -6,6 +6,7 @@ import SessionUIKit import SessionUtilitiesKit final class MiniCallView: UIView, RTCVideoViewDelegate { + private let dependencies: Dependencies var callVC: CallVC // MARK: UI @@ -60,7 +61,8 @@ final class MiniCallView: UIView, RTCVideoViewDelegate { public static var current: MiniCallView? - init(from callVC: CallVC) { + init(from callVC: CallVC, using dependencies: Dependencies) { + self.dependencies = dependencies self.callVC = callVC super.init(frame: CGRect.zero) @@ -153,18 +155,25 @@ final class MiniCallView: UIView, RTCVideoViewDelegate { @objc private func handleTap(_ gestureRecognizer: UITapGestureRecognizer) { dismiss() - guard - Singleton.hasAppContext, - let presentingVC = Singleton.appContext.frontmostViewController else { preconditionFailure() } // FIXME: Handle more gracefully + + guard let presentingVC: UIViewController = dependencies[singleton: .appContext].frontMostViewController else { + Log.critical(.calls, "Failed to retrieve front view controller when returning from the MiniCallView") + dependencies[singleton: .callManager].endCall(callVC.call) { [callVC, dependencies] error in + if let _ = error { + callVC.call.endSessionCall() + dependencies[singleton: .callManager].reportCurrentCallEnded(reason: nil) + } + } + return + } + presentingVC.present(callVC, animated: true, completion: nil) } public func show() { self.alpha = 0.0 - guard - Singleton.hasAppContext, - let window: UIWindow = Singleton.appContext.mainWindow - else { return } + + guard let window: UIWindow = dependencies[singleton: .appContext].mainWindow else { return } window.addSubview(self) left = self.pin(.left, to: .left, of: window) diff --git a/Session/Calls/WebRTC/TurnServerInfo.swift b/Session/Calls/WebRTC/TurnServerInfo.swift index 5baf52ccc43..466787ac458 100644 --- a/Session/Calls/WebRTC/TurnServerInfo.swift +++ b/Session/Calls/WebRTC/TurnServerInfo.swift @@ -1,5 +1,5 @@ // Copyright © 2021 Rangeproof Pty Ltd. All rights reserved. - +// // stringlint:disable import Foundation @@ -11,7 +11,7 @@ struct TurnServerInfo { let username: String let urls: [String] - init?(attributes: JSON, random: Int? = nil) { + init?(attributes: [String: Any], random: Int? = nil) { guard let passwordAttribute = attributes["password"] as? String, let usernameAttribute = attributes["username"] as? String, diff --git a/Session/Calls/WebRTC/WebRTC+Utilities.swift b/Session/Calls/WebRTC/WebRTC+Utilities.swift index 169a90edbdb..191864c4562 100644 --- a/Session/Calls/WebRTC/WebRTC+Utilities.swift +++ b/Session/Calls/WebRTC/WebRTC+Utilities.swift @@ -2,7 +2,7 @@ import WebRTC -extension RTCSignalingState : CustomStringConvertible { +extension RTCSignalingState : @retroactive CustomStringConvertible { public var description: String { switch self { @@ -17,7 +17,7 @@ extension RTCSignalingState : CustomStringConvertible { } } -extension RTCIceConnectionState : CustomStringConvertible { +extension RTCIceConnectionState : @retroactive CustomStringConvertible { public var description: String { switch self { @@ -34,7 +34,7 @@ extension RTCIceConnectionState : CustomStringConvertible { } } -extension RTCIceGatheringState : CustomStringConvertible { +extension RTCIceGatheringState : @retroactive CustomStringConvertible { public var description: String { switch self { diff --git a/Session/Calls/WebRTC/WebRTCSession+DataChannel.swift b/Session/Calls/WebRTC/WebRTCSession+DataChannel.swift index 9e3b632b586..dc755623494 100644 --- a/Session/Calls/WebRTC/WebRTCSession+DataChannel.swift +++ b/Session/Calls/WebRTC/WebRTCSession+DataChannel.swift @@ -3,6 +3,7 @@ import Foundation import WebRTC import Foundation +import SessionMessagingKit import SessionUtilitiesKit extension WebRTCSession: RTCDataChannelDelegate { @@ -13,15 +14,15 @@ extension WebRTCSession: RTCDataChannelDelegate { dataChannelConfiguration.isNegotiated = true dataChannelConfiguration.channelId = 548 guard let dataChannel = peerConnection?.dataChannel(forLabel: "CONTROL", configuration: dataChannelConfiguration) else { - SNLog("[Calls] Couldn't create data channel.") + Log.error(.calls, "Couldn't create data channel.") return nil } return dataChannel } - public func sendJSON(_ json: JSON) { + public func sendJSON(_ json: [String: Any]) { if let dataChannel = self.dataChannel, let jsonAsData = try? JSONSerialization.data(withJSONObject: json, options: [ .fragmentsAllowed ]) { - SNLog("[Calls] Send json to data channel") + Log.info(.calls, "Send json to data channel") let dataBuffer = RTCDataBuffer(data: jsonAsData, isBinary: false) dataChannel.sendData(dataBuffer) } @@ -29,7 +30,7 @@ extension WebRTCSession: RTCDataChannelDelegate { // MARK: Data channel delegate public func dataChannelDidChangeState(_ dataChannel: RTCDataChannel) { - SNLog("[Calls] Data channel did change to \(dataChannel.readyState.rawValue)") + Log.info(.calls, "Data channel did change to \(dataChannel.readyState.rawValue)") if dataChannel.readyState == .open { delegate?.dataChannelDidOpen() } @@ -37,8 +38,8 @@ extension WebRTCSession: RTCDataChannelDelegate { // stringlint:ignore_contents public func dataChannel(_ dataChannel: RTCDataChannel, didReceiveMessageWith buffer: RTCDataBuffer) { - if let json = try? JSONSerialization.jsonObject(with: buffer.data, options: [ .fragmentsAllowed ]) as? JSON { - SNLog("[Calls] Data channel did receive data: \(json)") + if let json: [String: Any] = try? JSONSerialization.jsonObject(with: buffer.data, options: [ .fragmentsAllowed ]) as? [String: Any] { + Log.info(.calls, "Data channel did receive data: \(json)") if let isRemoteVideoEnabled = json["video"] as? Bool { delegate?.isRemoteVideoDidChange(isEnabled: isRemoteVideoEnabled) } diff --git a/Session/Calls/WebRTC/WebRTCSession+MessageHandling.swift b/Session/Calls/WebRTC/WebRTCSession+MessageHandling.swift index ddbe690b19a..da0c97ad470 100644 --- a/Session/Calls/WebRTC/WebRTCSession+MessageHandling.swift +++ b/Session/Calls/WebRTC/WebRTCSession+MessageHandling.swift @@ -3,21 +3,22 @@ import Foundation import Combine import WebRTC +import SessionMessagingKit import SessionUtilitiesKit extension WebRTCSession { public func handleICECandidates(_ candidate: [RTCIceCandidate]) { - SNLog("[Calls] Received ICE candidate message.") + Log.info(.calls, "Received ICE candidate message.") candidate.forEach { peerConnection?.add($0, completionHandler: { _ in }) } } public func handleRemoteSDP(_ sdp: RTCSessionDescription, from sessionId: String) { - SNLog("[Calls] Received remote SDP: \(sdp.sdp).") + Log.info(.calls, "Received remote SDP: \(sdp.sdp).") peerConnection?.setRemoteDescription(sdp, completionHandler: { [weak self] error in if let error = error { - SNLog("[Calls] Couldn't set SDP due to error: \(error).") + Log.error(.calls, "Couldn't set SDP due to error: \(error).") } else { guard sdp.type == .offer else { return } diff --git a/Session/Calls/WebRTC/WebRTCSession.swift b/Session/Calls/WebRTC/WebRTCSession.swift index 125bc9b32d5..041d8393990 100644 --- a/Session/Calls/WebRTC/WebRTCSession.swift +++ b/Session/Calls/WebRTC/WebRTCSession.swift @@ -4,8 +4,9 @@ import Foundation import Combine import GRDB import WebRTC -import SessionUtilitiesKit import SessionSnodeKit +import SessionMessagingKit +import SessionUtilitiesKit public protocol WebRTCSessionDelegate: AnyObject { var videoCapturer: RTCVideoCapturer { get } @@ -19,6 +20,7 @@ public protocol WebRTCSessionDelegate: AnyObject { /// See https://webrtc.org/getting-started/overview for more information. public final class WebRTCSession : NSObject, RTCPeerConnectionDelegate { + private let dependencies: Dependencies public weak var delegate: WebRTCSessionDelegate? public let uuid: String private let contactSessionId: String @@ -26,7 +28,7 @@ public final class WebRTCSession : NSObject, RTCPeerConnectionDelegate { private var iceCandidateSendTimer: Timer? private lazy var defaultICEServer: TurnServerInfo? = { - let url = Bundle.main.url(forResource: "Session-Turn-Server", withExtension: nil)! + let url = Bundle.main.url(forResource: "Session-Turn-Server", withExtension: nil)! // stringlint:ignroe let data = try! Data(contentsOf: url) let json = try! JSONSerialization.jsonObject(with: data, options: [ .fragmentsAllowed ]) as! JSON return TurnServerInfo(attributes: json, random: 2) @@ -95,10 +97,11 @@ public final class WebRTCSession : NSObject, RTCPeerConnectionDelegate { // MARK: Initialization public static var current: WebRTCSession? - public init(for contactSessionId: String, with uuid: String) { + public init(for contactSessionId: String, with uuid: String, using dependencies: Dependencies) { RTCAudioSession.sharedInstance().useManualAudio = true RTCAudioSession.sharedInstance().isAudioEnabled = false + self.dependencies = dependencies self.contactSessionId = contactSessionId self.uuid = uuid @@ -125,66 +128,63 @@ public final class WebRTCSession : NSObject, RTCPeerConnectionDelegate { _ db: Database, message: CallMessage, interactionId: Int64?, - in thread: SessionThread, - using dependencies: Dependencies = Dependencies() + in thread: SessionThread ) throws -> AnyPublisher { - SNLog("[Calls] Sending pre-offer message.") + Log.info(.calls, "Sending pre-offer message.") - return MessageSender - .sendImmediate( - data: try MessageSender - .preparedSendData( - db, - message: message, - to: try Message.Destination.from(db, threadId: thread.id, threadVariant: thread.variant), - namespace: try Message.Destination - .from(db, threadId: thread.id, threadVariant: thread.variant) - .defaultNamespace, - interactionId: interactionId, - using: dependencies - ), + return try MessageSender + .preparedSend( + db, + message: message, + to: try Message.Destination.from(db, threadId: thread.id, threadVariant: thread.variant), + namespace: try Message.Destination + .from(db, threadId: thread.id, threadVariant: thread.variant) + .defaultNamespace, + interactionId: interactionId, + fileIds: [], using: dependencies ) - .handleEvents(receiveOutput: { _ in SNLog("[Calls] Pre-offer message has been sent.") }) + .send(using: dependencies) + .map { _ in () } + .handleEvents(receiveOutput: { _ in Log.info(.calls, "Pre-offer message has been sent.") }) .eraseToAnyPublisher() } public func sendOffer( to thread: SessionThread, - isRestartingICEConnection: Bool = false, - using dependencies: Dependencies = Dependencies() + isRestartingICEConnection: Bool = false ) -> AnyPublisher { - SNLog("[Calls] Sending offer message.") + Log.info(.calls, "Sending offer message.") let uuid: String = self.uuid let mediaConstraints: RTCMediaConstraints = mediaConstraints(isRestartingICEConnection) - return Deferred { - Future { [weak self] resolver in + return Deferred { [weak self, dependencies] in + Future { resolver in self?.peerConnection?.offer(for: mediaConstraints) { sdp, error in guard error == nil else { return } - + guard let sdp: RTCSessionDescription = self?.correctSessionDescription(sdp: sdp) else { preconditionFailure() } self?.peerConnection?.setLocalDescription(sdp) { error in if let error = error { - print("Couldn't initiate call due to error: \(error).") + Log.error(.calls, "Couldn't initiate call due to error: \(error).") resolver(Result.failure(error)) return } } - dependencies.storage - .writePublisher { db in + dependencies[singleton: .storage] + .writePublisher { db -> Network.PreparedRequest in try MessageSender - .preparedSendData( + .preparedSend( db, message: CallMessage( uuid: uuid, kind: .offer, sdps: [ sdp.sdp ], - sentTimestampMs: UInt64(SnodeAPI.currentOffsetTimestampMs()) + sentTimestampMs: dependencies[cache: .snodeAPI].currentOffsetTimestampMs() ) .with(try? thread.disappearingMessagesConfiguration .fetchOne(db)? @@ -196,10 +196,11 @@ public final class WebRTCSession : NSObject, RTCPeerConnectionDelegate { .from(db, threadId: thread.id, threadVariant: thread.variant) .defaultNamespace, interactionId: nil, + fileIds: [], using: dependencies ) } - .flatMap { MessageSender.sendImmediate(data: $0, using: dependencies) } + .flatMap { $0.send(using: dependencies) } .subscribe(on: DispatchQueue.global(qos: .userInitiated)) .sinkUntilComplete( receiveCompletion: { result in @@ -215,15 +216,12 @@ public final class WebRTCSession : NSObject, RTCPeerConnectionDelegate { .eraseToAnyPublisher() } - public func sendAnswer( - to sessionId: String, - using dependencies: Dependencies = Dependencies() - ) -> AnyPublisher { - SNLog("[Calls] Sending answer message.") + public func sendAnswer(to sessionId: String) -> AnyPublisher { + Log.info(.calls, "Sending answer message.") let uuid: String = self.uuid let mediaConstraints: RTCMediaConstraints = mediaConstraints(false) - return dependencies.storage + return dependencies[singleton: .storage] .readPublisher { db -> SessionThread in guard let thread: SessionThread = try? SessionThread.fetchOne(db, id: sessionId) else { throw WebRTCSessionError.noThread @@ -231,7 +229,7 @@ public final class WebRTCSession : NSObject, RTCPeerConnectionDelegate { return thread } - .flatMap { [weak self] thread in + .flatMap { [weak self, dependencies] thread in Future { resolver in self?.peerConnection?.answer(for: mediaConstraints) { [weak self] sdp, error in if let error = error { @@ -245,15 +243,15 @@ public final class WebRTCSession : NSObject, RTCPeerConnectionDelegate { self?.peerConnection?.setLocalDescription(sdp) { error in if let error = error { - print("Couldn't accept call due to error: \(error).") + Log.error(.calls, "Couldn't accept call due to error: \(error).") return resolver(Result.failure(error)) } } - dependencies.storage - .writePublisher { db in + dependencies[singleton: .storage] + .writePublisher { db -> Network.PreparedRequest in try MessageSender - .preparedSendData( + .preparedSend( db, message: CallMessage( uuid: uuid, @@ -270,10 +268,11 @@ public final class WebRTCSession : NSObject, RTCPeerConnectionDelegate { .from(db, threadId: thread.id, threadVariant: thread.variant) .defaultNamespace, interactionId: nil, + fileIds: [], using: dependencies ) } - .flatMap { MessageSender.sendImmediate(data: $0, using: dependencies) } + .flatMap { $0.send(using: dependencies) } .subscribe(on: DispatchQueue.global(qos: .userInitiated)) .sinkUntilComplete( receiveCompletion: { result in @@ -299,7 +298,7 @@ public final class WebRTCSession : NSObject, RTCPeerConnectionDelegate { } } - private func sendICECandidates(using dependencies: Dependencies = Dependencies()) { + private func sendICECandidates() { let candidates: [RTCIceCandidate] = self.queuedICECandidates let uuid: String = self.uuid let contactSessionId: String = self.contactSessionId @@ -307,16 +306,16 @@ public final class WebRTCSession : NSObject, RTCPeerConnectionDelegate { // Empty the queue self.queuedICECandidates.removeAll() - dependencies.storage - .writePublisher { db in + dependencies[singleton: .storage] + .writePublisher { [dependencies] db -> Network.PreparedRequest in guard let thread: SessionThread = try SessionThread.fetchOne(db, id: contactSessionId) else { throw WebRTCSessionError.noThread } - SNLog("[Calls] Batch sending \(candidates.count) ICE candidates.") + Log.info(.calls, "Batch sending \(candidates.count) ICE candidates.") return try MessageSender - .preparedSendData( + .preparedSend( db, message: CallMessage( uuid: uuid, @@ -336,25 +335,22 @@ public final class WebRTCSession : NSObject, RTCPeerConnectionDelegate { .from(db, threadId: thread.id, threadVariant: thread.variant) .defaultNamespace, interactionId: nil, + fileIds: [], using: dependencies ) } .subscribe(on: DispatchQueue.global(qos: .userInitiated)) - .flatMap { MessageSender.sendImmediate(data: $0, using: dependencies) } + .flatMap { [dependencies] preparedRequest in preparedRequest.send(using: dependencies) } .sinkUntilComplete() } - public func endCall( - _ db: Database, - with sessionId: String, - using dependencies: Dependencies = Dependencies() - ) throws { + public func endCall(_ db: Database, with sessionId: String) throws { guard let thread: SessionThread = try SessionThread.fetchOne(db, id: sessionId) else { return } - SNLog("[Calls] Sending end call message.") + Log.info(.calls, "Sending end call message.") - let preparedSendData: MessageSender.PreparedSendData = try MessageSender - .preparedSendData( + try MessageSender + .preparedSend( db, message: CallMessage( uuid: self.uuid, @@ -370,12 +366,11 @@ public final class WebRTCSession : NSObject, RTCPeerConnectionDelegate { .from(db, threadId: thread.id, threadVariant: thread.variant) .defaultNamespace, interactionId: nil, + fileIds: [], using: dependencies ) - - MessageSender - .sendImmediate(data: preparedSendData, using: dependencies) - .subscribe(on: DispatchQueue.global(qos: .userInitiated)) + .send(using: dependencies) + .subscribe(on: DispatchQueue.global(qos: .userInitiated), using: dependencies) .sinkUntilComplete() } @@ -403,23 +398,23 @@ public final class WebRTCSession : NSObject, RTCPeerConnectionDelegate { // MARK: Peer connection delegate public func peerConnection(_ peerConnection: RTCPeerConnection, didChange state: RTCSignalingState) { - SNLog("[Calls] Signaling state changed to: \(state).") + Log.info(.calls, "Signaling state changed to: \(state).") } public func peerConnection(_ peerConnection: RTCPeerConnection, didAdd stream: RTCMediaStream) { - SNLog("[Calls] Peer connection did add stream.") + Log.info(.calls, "Peer connection did add stream.") } public func peerConnection(_ peerConnection: RTCPeerConnection, didRemove stream: RTCMediaStream) { - SNLog("[Calls] Peer connection did remove stream.") + Log.info(.calls, "Peer connection did remove stream.") } public func peerConnectionShouldNegotiate(_ peerConnection: RTCPeerConnection) { - SNLog("[Calls] Peer connection should negotiate.") + Log.info(.calls, "Peer connection should negotiate.") } public func peerConnection(_ peerConnection: RTCPeerConnection, didChange state: RTCIceConnectionState) { - SNLog("[Calls] ICE connection state changed to: \(state).") + Log.info(.calls, "ICE connection state changed to: \(state).") if state == .connected { delegate?.webRTCIsConnected() } else if state == .disconnected { @@ -430,7 +425,7 @@ public final class WebRTCSession : NSObject, RTCPeerConnectionDelegate { } public func peerConnection(_ peerConnection: RTCPeerConnection, didChange state: RTCIceGatheringState) { - SNLog("[Calls] ICE gathering state changed to: \(state).") + Log.info(.calls, "ICE gathering state changed to: \(state).") } public func peerConnection(_ peerConnection: RTCPeerConnection, didGenerate candidate: RTCIceCandidate) { @@ -438,11 +433,11 @@ public final class WebRTCSession : NSObject, RTCPeerConnectionDelegate { } public func peerConnection(_ peerConnection: RTCPeerConnection, didRemove candidates: [RTCIceCandidate]) { - SNLog("[Calls] \(candidates.count) ICE candidate(s) removed.") + Log.info(.calls, "\(candidates.count) ICE candidate(s) removed.") } public func peerConnection(_ peerConnection: RTCPeerConnection, didOpen dataChannel: RTCDataChannel) { - SNLog("[Calls] Data channel opened.") + Log.info(.calls, "Data channel opened.") } } @@ -456,7 +451,7 @@ extension WebRTCSession { try audioSession.overrideOutputAudioPort(outputAudioPort) try audioSession.setActive(true) } catch let error { - SNLog("Couldn't set up WebRTC audio session due to error: \(error)") + Log.error(.calls, "Couldn't set up WebRTC audio session due to error: \(error)") } audioSession.unlockForConfiguration() } diff --git a/Session/Closed Groups/EditClosedGroupVC.swift b/Session/Closed Groups/EditClosedGroupVC.swift deleted file mode 100644 index b1fb6e78231..00000000000 --- a/Session/Closed Groups/EditClosedGroupVC.swift +++ /dev/null @@ -1,521 +0,0 @@ -// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved. - -import UIKit -import Combine -import GRDB -import DifferenceKit -import SessionUIKit -import SessionMessagingKit -import SignalUtilitiesKit -import SessionUtilitiesKit - -final class EditClosedGroupVC: BaseVC, UITableViewDataSource, UITableViewDelegate { - private struct GroupMemberDisplayInfo: FetchableRecord, Equatable, Hashable, Decodable, Differentiable { - let profileId: String - let role: GroupMember.Role - let profile: Profile? - let accessibilityLabel: String? - let accessibilityId: String? - } - - private let threadId: String - private let threadVariant: SessionThread.Variant - private var originalName: String = "" - private var originalMembersAndZombieIds: Set = [] - private var name: String = "" - private var hasContactsToAdd: Bool = false - private var userPublicKey: String = "" - private var membersAndZombies: [GroupMemberDisplayInfo] = [] - private var adminIds: Set = [] - private var isEditingGroupName = false { didSet { handleIsEditingGroupNameChanged() } } - private var tableViewHeightConstraint: NSLayoutConstraint! - - // MARK: - Components - - private lazy var groupNameLabel: UILabel = { - let result: UILabel = UILabel() - result.accessibilityLabel = "Group name" - result.isAccessibilityElement = true - result.font = .boldSystemFont(ofSize: Values.veryLargeFontSize) - result.themeTextColor = .textPrimary - result.lineBreakMode = .byTruncatingTail - result.textAlignment = .center - - return result - }() - - private lazy var groupNameTextField: TextField = { - let result: TextField = TextField( - placeholder: "groupNameEnter".localized(), - usesDefaultHeight: false - ) - result.textAlignment = .center - result.isAccessibilityElement = true - result.accessibilityIdentifier = "Group name text field" - - return result - }() - - private lazy var addMembersButton: SessionButton = { - let result: SessionButton = SessionButton(style: .bordered, size: .medium) - result.accessibilityLabel = "Add members" - result.isAccessibilityElement = true - result.setTitle("membersInvite".localized(), for: .normal) - result.addTarget(self, action: #selector(addMembers), for: UIControl.Event.touchUpInside) - result.contentEdgeInsets = UIEdgeInsets(top: 0, leading: Values.mediumSpacing, bottom: 0, trailing: Values.mediumSpacing) - - return result - }() - - @objc private lazy var tableView: UITableView = { - let result: UITableView = UITableView() - result.accessibilityLabel = "Contact" - result.accessibilityIdentifier = "Contact" - result.isAccessibilityElement = true - result.dataSource = self - result.delegate = self - result.separatorStyle = .none - result.themeBackgroundColor = .clear - result.isScrollEnabled = false - result.register(view: SessionCell.self) - - return result - }() - - // MARK: - Lifecycle - - init(threadId: String, threadVariant: SessionThread.Variant) { - self.threadId = threadId - self.threadVariant = threadVariant - - super.init(nibName: nil, bundle: nil) - } - - required init?(coder: NSCoder) { - preconditionFailure("Use init(with:) instead.") - } - - override func viewDidLoad() { - super.viewDidLoad() - - setNavBarTitle("groupEdit".localized()) - - let threadId: String = self.threadId - - Storage.shared.read { [weak self] db in - let userPublicKey: String = getUserHexEncodedPublicKey(db) - self?.userPublicKey = userPublicKey - self?.name = try ClosedGroup - .select(.name) - .filter(id: threadId) - .asRequest(of: String.self) - .fetchOne(db) - .defaulting(to: "groupUnknown".localized()) - self?.originalName = (self?.name ?? "") - - let profileAlias: TypedTableAlias = TypedTableAlias() - let allGroupMembers: [GroupMemberDisplayInfo] = try GroupMember - .filter(GroupMember.Columns.groupId == threadId) - .including(optional: GroupMember.profile.aliased(profileAlias)) - .order( - (GroupMember.Columns.role == GroupMember.Role.zombie), // Non-zombies at the top - profileAlias[.nickname], - profileAlias[.name], - GroupMember.Columns.profileId - ) - .asRequest(of: GroupMemberDisplayInfo.self) - .fetchAll(db) - self?.membersAndZombies = allGroupMembers - .filter { $0.role == .standard || $0.role == .zombie } - self?.adminIds = allGroupMembers - .filter { $0.role == .admin } - .map { $0.profileId } - .asSet() - - let uniqueGroupMemberIds: Set = allGroupMembers - .map { $0.profileId } - .asSet() - self?.originalMembersAndZombieIds = uniqueGroupMemberIds - self?.hasContactsToAdd = ((try? Profile - .allContactProfiles( - excluding: uniqueGroupMemberIds.inserting(userPublicKey) - ) - .fetchCount(db)) - .defaulting(to: 0) > 0) - } - - setUpViewHierarchy() - updateNavigationBarButtons() - handleMembersChanged() - } - - private func setUpViewHierarchy() { - // Group name container - groupNameLabel.text = name - - let groupNameContainer = UIView() - groupNameContainer.addSubview(groupNameLabel) - groupNameLabel.pin(to: groupNameContainer) - groupNameContainer.addSubview(groupNameTextField) - groupNameTextField.pin(to: groupNameContainer) - groupNameContainer.set(.height, to: 40) - groupNameTextField.alpha = 0 - - // Top container - let topContainer = UIView() - topContainer.addSubview(groupNameContainer) - groupNameContainer.center(in: topContainer) - topContainer.set(.height, to: 40) - let topContainerTapGestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(showEditGroupNameUI)) - topContainer.addGestureRecognizer(topContainerTapGestureRecognizer) - - // Members label - let membersLabel = UILabel() - membersLabel.font = .systemFont(ofSize: Values.mediumFontSize) - membersLabel.themeTextColor = .textPrimary - membersLabel.text = "groupMembers".localized() - - addMembersButton.isEnabled = self.hasContactsToAdd - - // Middle stack view - let middleStackView = UIStackView(arrangedSubviews: [ membersLabel, addMembersButton ]) - middleStackView.axis = .horizontal - middleStackView.alignment = .center - middleStackView.layoutMargins = UIEdgeInsets(top: Values.smallSpacing, leading: Values.mediumSpacing, bottom: Values.smallSpacing, trailing: Values.mediumSpacing) - middleStackView.isLayoutMarginsRelativeArrangement = true - middleStackView.set(.height, to: Values.largeButtonHeight + Values.smallSpacing * 2) - - // Table view - tableViewHeightConstraint = tableView.set(.height, to: 0) - - // Main stack view - let mainStackView = UIStackView(arrangedSubviews: [ - UIView.vSpacer(Values.veryLargeSpacing), - topContainer, - UIView.vSpacer(Values.veryLargeSpacing), - UIView.separator(), - middleStackView, - UIView.separator(), - tableView - ]) - mainStackView.axis = .vertical - mainStackView.alignment = .fill - mainStackView.set(.width, to: UIScreen.main.bounds.width) - - // Scroll view - let scrollView = UIScrollView() - scrollView.showsVerticalScrollIndicator = false - scrollView.addSubview(mainStackView) - mainStackView.pin(to: scrollView) - view.addSubview(scrollView) - scrollView.pin(to: view) - } - - // MARK: - Table View Data Source / Delegate - - func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { - return membersAndZombies.count - } - - func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { - let cell: SessionCell = tableView.dequeue(type: SessionCell.self, for: indexPath) - let displayInfo: GroupMemberDisplayInfo = membersAndZombies[indexPath.row] - cell.update( - with: SessionCell.Info( - id: displayInfo, - 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) - ), - rightAccessory: (adminIds.contains(userPublicKey) ? nil : - .icon( - UIImage(named: "ic_lock_outline")? - .withRenderingMode(.alwaysTemplate), - customTint: .textSecondary - ) - ), - styling: SessionCell.StyleInfo(backgroundStyle: .edgeToEdge) - ) - ) - - return cell - } - - func tableView(_ tableView: UITableView, canEditRowAt indexPath: IndexPath) -> Bool { - return adminIds.contains(userPublicKey) - } - - func tableView(_ tableView: UITableView, willBeginEditingRowAt indexPath: IndexPath) { - UIContextualAction.willBeginEditing(indexPath: indexPath, tableView: tableView) - } - - func tableView(_ tableView: UITableView, didEndEditingRowAt indexPath: IndexPath?) { - UIContextualAction.didEndEditing(indexPath: indexPath, tableView: tableView) - } - - func tableView(_ tableView: UITableView, trailingSwipeActionsConfigurationForRowAt indexPath: IndexPath) -> UISwipeActionsConfiguration? { - let profileId: String = self.membersAndZombies[indexPath.row].profileId - - let delete: UIContextualAction = UIContextualAction( - title: "remove".localized(), - icon: UIImage(named: "icon_bin"), - themeTintColor: .white, - themeBackgroundColor: .conversationButton_swipeDestructive, - side: .trailing, - actionIndex: 0, - indexPath: indexPath, - tableView: tableView - ) { [weak self] _, _, completionHandler in - self?.adminIds.remove(profileId) - self?.membersAndZombies.remove(at: indexPath.row) - self?.handleMembersChanged() - - completionHandler(true) - } - - return UISwipeActionsConfiguration(actions: [ delete ]) - } - - // MARK: - Updating - - private func updateNavigationBarButtons() { - if isEditingGroupName { - let cancelButton = UIBarButtonItem(barButtonSystemItem: .cancel, target: self, action: #selector(handleCancelGroupNameEditingButtonTapped)) - cancelButton.themeTintColor = .textPrimary - navigationItem.leftBarButtonItem = cancelButton - } - else { - navigationItem.leftBarButtonItem = nil - } - - let doneButton = UIBarButtonItem(barButtonSystemItem: .done, target: self, action: #selector(handleDoneButtonTapped)) - if isEditingGroupName { - doneButton.accessibilityLabel = "Accept name change" - } - else { - doneButton.accessibilityLabel = "Apply changes" - } - doneButton.themeTintColor = .textPrimary - navigationItem.rightBarButtonItem = doneButton - } - - private func handleMembersChanged() { - tableViewHeightConstraint.constant = CGFloat(membersAndZombies.count) * 78 - tableView.reloadData() - } - - private func handleIsEditingGroupNameChanged() { - updateNavigationBarButtons() - - UIView.animate(withDuration: 0.25) { - self.groupNameLabel.alpha = self.isEditingGroupName ? 0 : 1 - self.groupNameTextField.alpha = self.isEditingGroupName ? 1 : 0 - } - - if isEditingGroupName { - groupNameTextField.becomeFirstResponder() - } - else { - groupNameTextField.resignFirstResponder() - } - } - - // MARK: - Interaction - - @objc private func showEditGroupNameUI() { - isEditingGroupName = true - } - - @objc private func handleCancelGroupNameEditingButtonTapped() { - isEditingGroupName = false - } - - @objc private func handleDoneButtonTapped() { - if isEditingGroupName { - updateGroupName() - } - else { - commitChanges() - } - } - - private func updateGroupName() { - let updatedName: String = groupNameTextField.text - .defaulting(to: "") - .trimmingCharacters(in: CharacterSet.whitespacesAndNewlines) - - guard !updatedName.isEmpty else { - return showError(title: "groupNameEnterPlease".localized()) - } - guard updatedName.utf8CString.count < LibSession.libSessionMaxGroupNameByteLength else { - return showError(title: "groupNameEnterShorter".localized()) - } - - self.isEditingGroupName = false - self.groupNameLabel.text = updatedName - self.name = updatedName - } - - @objc private func addMembers() { - let title: String = "membersInvite".localized() - - let userPublicKey: String = self.userPublicKey - let userSelectionVC: UserSelectionVC = UserSelectionVC( - with: title, - excluding: membersAndZombies - .map { $0.profileId } - .asSet() - ) { [weak self] selectedUserIds in - Storage.shared.read { [weak self] db in - let selectedGroupMembers: [GroupMemberDisplayInfo] = try Profile - .filter(selectedUserIds.contains(Profile.Columns.id)) - .fetchAll(db) - .map { profile in - GroupMemberDisplayInfo( - profileId: profile.id, - role: .standard, - profile: profile, - accessibilityLabel: "Contact", - accessibilityId: "Contact" - ) - } - self?.membersAndZombies = (self?.membersAndZombies ?? []) - .appending(contentsOf: selectedGroupMembers) - .sorted(by: { lhs, rhs in - if lhs.role == .zombie && rhs.role != .zombie { - return false - } - else if lhs.role != .zombie && rhs.role == .zombie { - return true - } - - let lhsDisplayName: String = Profile.displayName( - for: .contact, - id: lhs.profileId, - name: lhs.profile?.name, - nickname: lhs.profile?.nickname, - suppressId: false - ) - let rhsDisplayName: String = Profile.displayName( - for: .contact, - id: rhs.profileId, - name: rhs.profile?.name, - nickname: rhs.profile?.nickname, - suppressId: false - ) - - return (lhsDisplayName < rhsDisplayName) - }) - .filter { $0.role == .standard || $0.role == .zombie } - - let uniqueGroupMemberIds: Set = (self?.membersAndZombies ?? []) - .map { $0.profileId } - .asSet() - .inserting(contentsOf: self?.adminIds) - self?.hasContactsToAdd = ((try? Profile - .allContactProfiles( - excluding: uniqueGroupMemberIds.inserting(userPublicKey) - ) - .fetchCount(db)) - .defaulting(to: 0) > 0) - } - - self?.addMembersButton.isEnabled = (self?.hasContactsToAdd == true) - self?.handleMembersChanged() - } - - navigationController?.pushViewController(userSelectionVC, animated: true, completion: nil) - } - - private func commitChanges() { - let popToConversationVC: ((EditClosedGroupVC?) -> ()) = { editVC in - guard - let viewControllers: [UIViewController] = editVC?.navigationController?.viewControllers, - let conversationVC: ConversationVC = viewControllers.first(where: { $0 is ConversationVC }) as? ConversationVC - else { - editVC?.navigationController?.popViewController(animated: true) - return - } - - editVC?.navigationController?.popToViewController(conversationVC, animated: true) - } - - let threadId: String = self.threadId - let updatedName: String = self.name - let userPublicKey: String = self.userPublicKey - let updatedMemberIds: Set = self.membersAndZombies - .map { $0.profileId } - .asSet() - - guard updatedMemberIds != self.originalMembersAndZombieIds || updatedName != self.originalName else { - return popToConversationVC(self) - } - - if !updatedMemberIds.contains(userPublicKey) { - guard self.originalMembersAndZombieIds.removing(userPublicKey) == updatedMemberIds else { - return showError( - title: "deleteAfterLegacyGroupsGroupUpdateErrorTitle".localized(), - message: "deleteAfterGroupPR3GroupErrorLeave".localized() - ) - } - } - guard updatedMemberIds.count <= 100 else { - return showError(title: "groupAddMemberMaximum".localized()) - } - - ModalActivityIndicatorViewController.present(fromViewController: navigationController) { _ in - Storage.shared - .writePublisher { db in - // If the user is no longer a member then leave the group - guard !updatedMemberIds.contains(userPublicKey) else { return } - - try MessageSender.leave( - db, - groupPublicKey: threadId, - deleteThread: true - ) - - } - .flatMap { - MessageSender.update( - groupPublicKey: threadId, - with: updatedMemberIds, - name: updatedName - ) - } - .subscribe(on: DispatchQueue.global(qos: .userInitiated)) - .receive(on: DispatchQueue.main) - .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: "deleteAfterLegacyGroupsGroupUpdateErrorTitle".localized(), - message: "\(error)" - ) - } - } - ) - } - } - - // MARK: - Convenience - - private func showError(title: String, message: String = "") { - let modal: ConfirmationModal = ConfirmationModal( - targetView: self.view, - info: ConfirmationModal.Info( - title: title, - body: .text(message), - cancelTitle: "okay".localized(), - cancelStyle: .alert_text - ) - ) - self.present(modal, animated: true) - } -} diff --git a/Session/Closed Groups/EditGroupViewModel.swift b/Session/Closed Groups/EditGroupViewModel.swift new file mode 100644 index 00000000000..bff3642ba1e --- /dev/null +++ b/Session/Closed Groups/EditGroupViewModel.swift @@ -0,0 +1,800 @@ +// Copyright © 2023 Rangeproof Pty Ltd. All rights reserved. + +import Foundation +import Combine +import GRDB +import YYImage +import DifferenceKit +import SessionUIKit +import SessionSnodeKit +import SessionMessagingKit +import SessionUtilitiesKit +import SignalUtilitiesKit + +class EditGroupViewModel: SessionTableViewModel, NavigatableStateHolder, EditableStateHolder, ObservableTableSource { + private static let minVersionBannerInfo: InfoBanner.Info = InfoBanner.Info( + font: .systemFont(ofSize: Values.verySmallFontSize), + message: "groupInviteVersion".localized(), + icon: .none, + tintColor: .black, + backgroundColor: .warning, + accessibility: Accessibility(identifier: "Version warning banner") + ) + + public let dependencies: Dependencies + public let navigatableState: NavigatableState = NavigatableState() + public let editableState: EditableState = EditableState() + public let state: TableDataState = TableDataState() + public let observableState: ObservableTableSourceState = ObservableTableSourceState() + private let selectedIdsSubject: CurrentValueSubject<(name: String, ids: Set), Never> = CurrentValueSubject(("", [])) + + private let threadId: String + private let userSessionId: SessionId + private var inviteByIdValue: String? + + // MARK: - Initialization + + init(threadId: String, using dependencies: Dependencies) { + self.dependencies = dependencies + self.threadId = threadId + self.userSessionId = dependencies[cache: .general].sessionId + } + + // MARK: - Config + + public enum Section: SessionTableSection { + case groupInfo + case invite + case members + + public var title: String? { + switch self { + case .members: return "groupMembers".localized() + default: return nil + } + } + + var style: SessionTableSectionStyle { + switch self { + case .members: return .titleEdgeToEdgeContent + default: return .none + } + } + } + + public enum TableItem: Equatable, Hashable, Differentiable { + case avatar + case groupName + case groupDescription + + case invite + case inviteById + + case member(String) + } + + // MARK: - Content + + private struct State: Equatable { + let group: ClosedGroup + let profileFront: Profile? + let profileBack: Profile? + let members: [WithProfile] + let isValid: Bool + + static let invalidState: State = State( + group: ClosedGroup(threadId: "", name: "", formationTimestamp: 0, shouldPoll: false, invited: false), + profileFront: nil, + profileBack: nil, + members: [], + isValid: false + ) + } + + let title: String = "groupEdit".localized() + + var bannerInfo: AnyPublisher { + guard (try? SessionId.Prefix(from: threadId)) == .group else { + return Just(nil).eraseToAnyPublisher() + } + + return Just(EditGroupViewModel.minVersionBannerInfo).eraseToAnyPublisher() + } + + lazy var observation: TargetObservation = ObservationBuilder + .databaseObservation(self) { [dependencies, threadId, userSessionId] db -> State in + guard let group: ClosedGroup = try ClosedGroup.fetchOne(db, id: threadId) else { + return State.invalidState + } + + var profileFront: Profile? + var profileBack: Profile? + + if group.displayPictureFilename == nil { + let frontProfileId: String? = try GroupMember + .filter(GroupMember.Columns.groupId == threadId) + .filter(GroupMember.Columns.role == GroupMember.Role.standard) + .filter(GroupMember.Columns.profileId != userSessionId.hexString) + .select(min(GroupMember.Columns.profileId)) + .asRequest(of: String.self) + .fetchOne(db) + let backProfileId: String? = try GroupMember + .filter(GroupMember.Columns.groupId == threadId) + .filter(GroupMember.Columns.role == GroupMember.Role.standard) + .filter(GroupMember.Columns.profileId != userSessionId.hexString) + .filter(GroupMember.Columns.profileId != frontProfileId) + .select(max(GroupMember.Columns.profileId)) + .asRequest(of: String.self) + .fetchOne(db) + + profileFront = try frontProfileId.map { try Profile.fetchOne(db, id: $0) } + profileBack = try Profile.fetchOne(db, id: backProfileId ?? userSessionId.hexString) + } + + return State( + group: group, + profileFront: profileFront, + profileBack: profileBack, + members: try GroupMember + .filter(GroupMember.Columns.groupId == threadId) + .fetchAllWithProfiles(db, using: dependencies), + isValid: true + ) + } + .compactMap { [weak self] state -> [SectionModel]? in self?.content(state) } + + private func content(_ state: State) -> [SectionModel] { + guard state.isValid else { + return [ + SectionModel( + model: .groupInfo, + elements: [ + SessionCell.Info( + id: .groupName, + title: SessionCell.TextInfo( + "errorUnknown".localized(), + font: .subtitle, + alignment: .center + ), + styling: SessionCell.StyleInfo( + tintColor: .textSecondary, + alignment: .centerHugging, + customPadding: SessionCell.Padding(top: Values.smallSpacing), + backgroundStyle: .noBackground + ) + ) + ] + ) + ] + } + + let userSessionId: SessionId = dependencies[cache: .general].sessionId + let isUpdatedGroup: Bool = (((try? SessionId.Prefix(from: threadId)) ?? .group) == .group) + let sortedMembers: [WithProfile] = { + guard !isUpdatedGroup else { return state.members } + + // FIXME: Remove this once legacy groups are deprecated + /// In legacy groups there would be both `standard` and `admin` `GroupMember` entries for admins so + /// pre-process the members in order to remove the duplicates + return Array(state.members + .sorted(by: { lhs, rhs in lhs.value.role.rawValue < rhs.value.role.rawValue }) + .reduce(into: [:]) { result, next in result[next.profileId] = next } + .values) + }() + .sorted() + + return [ + SectionModel( + model: .groupInfo, + elements: [ + SessionCell.Info( + id: .avatar, + accessory: .profile( + id: threadId, + size: .hero, + threadVariant: (isUpdatedGroup ? .group : .legacyGroup), + displayPictureFilename: state.group.displayPictureFilename, + profile: state.profileFront, + profileIcon: .none, + additionalProfile: state.profileBack, + additionalProfileIcon: .none, + accessibility: nil + ), + styling: SessionCell.StyleInfo( + alignment: .centerHugging, + customPadding: SessionCell.Padding(bottom: Values.smallSpacing), + backgroundStyle: .noBackground + ), + accessibility: Accessibility( + label: "Profile picture" + ) + ), + SessionCell.Info( + id: .groupName, + title: SessionCell.TextInfo( + state.group.name, + font: .titleLarge, + alignment: .center, + editingPlaceholder: "groupNameEnter".localized() + ), + styling: SessionCell.StyleInfo( + alignment: .centerHugging, + customPadding: SessionCell.Padding( + top: Values.smallSpacing, + bottom: Values.smallSpacing + ), + backgroundStyle: .noBackground + ), + accessibility: Accessibility( + identifier: "Group name text field", + label: state.group.name + ) + ), + ((state.group.groupDescription ?? "").isEmpty ? nil : + SessionCell.Info( + id: .groupDescription, + title: SessionCell.TextInfo( + (state.group.groupDescription ?? ""), + font: .subtitle, + alignment: .center, + editingPlaceholder: "groupDescriptionEnter".localized() + ), + styling: SessionCell.StyleInfo( + tintColor: .textSecondary, + alignment: .centerHugging, + customPadding: SessionCell.Padding( + top: 0, + bottom: Values.smallSpacing + ), + backgroundStyle: .noBackground + ), + accessibility: Accessibility( + identifier: "Group description text field", + label: (state.group.groupDescription ?? "") + ) + ) + ) + ].compactMap { $0 } + ), + SectionModel( + model: .invite, + elements: [ + SessionCell.Info( + id: .invite, + leadingAccessory: .icon(UIImage(named: "icon_invite")?.withRenderingMode(.alwaysTemplate)), + title: "membersInvite".localized(), + accessibility: Accessibility( + identifier: "Invite button", + label: "Invite button" + ), + onTap: { [weak self] in self?.inviteContacts(currentGroupName: state.group.name) } + ), + (!isUpdatedGroup || !dependencies[feature: .updatedGroupsAllowInviteById] ? nil : + SessionCell.Info( + id: .inviteById, + leadingAccessory: .icon(UIImage(named: "ic_plus_24")?.withRenderingMode(.alwaysTemplate)), + title: "accountIdOrOnsInvite".localized(), + accessibility: Accessibility( + identifier: "Invite by id", + label: "Invite by id" + ), + onTap: { [weak self] in self?.inviteById(currentGroupName: state.group.name) } + ) + ) + ].compactMap { $0 } + ), + SectionModel( + model: .members, + elements: sortedMembers + .map { memberInfo -> SessionCell.Info in + SessionCell.Info( + id: .member(memberInfo.profileId), + leadingAccessory: .profile( + id: memberInfo.profileId, + profile: memberInfo.profile, + profileIcon: memberInfo.value.profileIcon + ), + title: SessionCell.TextInfo( + { + guard memberInfo.profileId != userSessionId.hexString else { return "you".localized() } + + return ( + memberInfo.profile?.displayName() ?? + Profile.truncated(id: memberInfo.profileId, truncating: .middle) + ) + }(), + font: .title, + accessibility: Accessibility( + identifier: "Contact" + ) + ), + subtitle: (!isUpdatedGroup ? nil : SessionCell.TextInfo( + memberInfo.value.statusDescription, + font: .subtitle, + accessibility: Accessibility( + identifier: "Contact status" + ) + )), + trailingAccessory: { + switch (memberInfo.value.role, memberInfo.value.roleStatus) { + case (.admin, _), (.moderator, _): return nil + case (.standard, .failed), (.standard, .notSentYet), (.standard, .pending): + return .highlightingBackgroundLabelAndRadio( + title: "resend".localized(), + isSelected: selectedIdsSubject.value.ids.contains(memberInfo.profileId), + labelAccessibility: Accessibility( + identifier: "Resend invite button", + label: "Resend invite button" + ), + radioAccessibility: Accessibility( + identifier: "Select contact", + label: "Select contact" + ) + ) + + case (.standard, .accepted), (.zombie, _): + return .radio( + isSelected: selectedIdsSubject.value.ids.contains(memberInfo.profileId) + ) + } + }(), + styling: SessionCell.StyleInfo( + subtitleTintColor: (isUpdatedGroup ? memberInfo.value.statusDescriptionColor : nil), + allowedSeparators: [], + customPadding: SessionCell.Padding( + top: Values.smallSpacing, + bottom: Values.smallSpacing + ), + backgroundStyle: .noBackgroundEdgeToEdge + ), + onTapView: { [weak self, selectedIdsSubject] targetView in + let didTapResend: Bool = (targetView is SessionHighlightingBackgroundLabel) + + switch (memberInfo.value.role, memberInfo.value.roleStatus, didTapResend) { + case (.moderator, _, _): return + case (.admin, _, _): + self?.showToast( + text: "adminCannotBeRemoved".localized(), + backgroundColor: .backgroundSecondary + ) + + case (.standard, .failed, true), (.standard, .notSentYet, true), (.standard, .pending, true): + self?.resendInvitation(memberId: memberInfo.profileId) + + case (.standard, .failed, _), (.standard, .notSentYet, _), (.standard, .pending, _), + (.standard, .accepted, _), (.zombie, _, _): + if !selectedIdsSubject.value.ids.contains(memberInfo.profileId) { + selectedIdsSubject.send(( + state.group.name, + selectedIdsSubject.value.ids.inserting(memberInfo.profileId) + )) + } + else { + selectedIdsSubject.send(( + state.group.name, + selectedIdsSubject.value.ids.removing(memberInfo.profileId) + )) + } + + // Force the table data to be refreshed (the database wouldn't + // have been changed) + self?.forceRefresh(type: .postDatabaseQuery) + } + } + ) + } + ) + ] + } + + lazy var footerButtonInfo: AnyPublisher = selectedIdsSubject + .prepend([]) + .map { currentGroupName, selectedIds in + SessionButton.Info( + style: .destructive, + title: "remove".localized(), + isEnabled: !selectedIds.isEmpty, + accessibility: Accessibility( + identifier: "Remove contact button" + ), + onTap: { [weak self] in self?.removeMembers(currentGroupName: currentGroupName, memberIds: selectedIds) } + ) + } + .eraseToAnyPublisher() + + // MARK: - Functions + + private func inviteContacts( + currentGroupName: String + ) { + let contact: TypedTableAlias = TypedTableAlias() + let groupMember: TypedTableAlias = TypedTableAlias() + let currentMemberIds: Set = (tableData + .first(where: { $0.model == .members })? + .elements + .compactMap { item -> String? in + switch item.id { + case .member(let profileId): return profileId + default: return nil + } + }) + .defaulting(to: []) + .asSet() + + self.transitionToScreen( + SessionTableViewController( + viewModel: UserListViewModel( + title: "membersInvite".localized(), + infoBanner: ((try? SessionId.Prefix(from: threadId)) != .group ? nil : + EditGroupViewModel.minVersionBannerInfo + ), + emptyState: "contactNone".localized(), + showProfileIcons: true, + request: SQLRequest(""" + SELECT \(contact.allColumns) + FROM \(contact) + LEFT JOIN \(groupMember) ON ( + \(groupMember[.groupId]) = \(threadId) AND + \(groupMember[.profileId]) = \(contact[.id]) + ) + WHERE ( + \(groupMember[.profileId]) IS NULL AND + \(contact[.isApproved]) = TRUE AND + \(contact[.didApproveMe]) = TRUE + ) + """), + footerTitle: "membersInviteTitle".localized(), + footerAccessibility: Accessibility( + identifier: "Confirm invite button" + ), + onSubmit: { [weak self, threadId, dependencies] in + switch try? SessionId.Prefix(from: threadId) { + case .group: + return .callback { viewModel, selectedMemberInfo in + let updatedMemberIds: Set = currentMemberIds + .inserting(contentsOf: selectedMemberInfo.map { $0.profileId }.asSet()) + + guard updatedMemberIds.count <= LibSession.sizeMaxGroupMemberCount else { + throw UserListError.error("groupAddMemberMaximum".localized()) + } + + self?.addMembers( + currentGroupName: currentGroupName, + memberInfo: selectedMemberInfo.map { ($0.profileId, $0.profile) } + ) + } + + case .standard: // Assume it's a legacy group + return .publisher { [dependencies, threadId] _, selectedMemberInfo in + let updatedMemberIds: Set = currentMemberIds + .inserting(contentsOf: selectedMemberInfo.map { $0.profileId }.asSet()) + + guard updatedMemberIds.count <= LibSession.sizeMaxGroupMemberCount else { + return Fail(error: .error("groupAddMemberMaximum".localized())) + .eraseToAnyPublisher() + } + + return MessageSender.update( + legacyGroupSessionId: threadId, + with: updatedMemberIds, + name: currentGroupName, + using: dependencies + ) + .mapError { _ in UserListError.error("deleteAfterLegacyGroupsGroupUpdateErrorTitle".localized()) } + .eraseToAnyPublisher() + } + + default: return .none + } + }(), + using: dependencies + ) + ), + transitionType: .push + ) + } + + private func inviteById(currentGroupName: String) { + // Convenience functions to avoid duplicate code + func showError(_ errorString: String) { + let modal: ConfirmationModal = ConfirmationModal( + info: ConfirmationModal.Info( + title: "theError".localized(), + body: .text(errorString), + cancelTitle: "okay".localized(), + cancelStyle: .alert_text, + dismissType: .single + ) + ) + self.transitionToScreen(modal, transitionType: .present) + } + + let currentMemberIds: Set = (tableData + .first(where: { $0.model == .members })? + .elements + .compactMap { item -> String? in + switch item.id { + case .member(let profileId): return profileId + default: return nil + } + }) + .defaulting(to: []) + .asSet() + + // Make sure inviting another member wouldn't hit the member limit + guard (currentMemberIds.count + 1) <= LibSession.sizeMaxGroupMemberCount else { + return showError("groupAddMemberMaximum".localized()) + } + + self.transitionToScreen( + ConfirmationModal( + info: ConfirmationModal.Info( + title: "accountIdOrOnsInvite".localized(), + body: .input( + explanation: nil, + info: ConfirmationModal.Info.Body.InputInfo( + placeholder: "accountIdOrOnsEnter".localized() + ), + onChange: { [weak self] updatedString in self?.inviteByIdValue = updatedString } + ), + confirmTitle: "membersInviteTitle".localized(), + confirmStyle: .danger, + cancelStyle: .alert_text, + dismissOnConfirm: false, + onConfirm: { [weak self, dependencies] modal in + switch (self?.inviteByIdValue, try? SessionId(from: self?.inviteByIdValue)) { + case (_, .some(let sessionId)) where sessionId.prefix == .standard: + guard !currentMemberIds.contains(sessionId.hexString) else { + // FIXME: Localise this + return showError("This Account ID or ONS belongs to an existing member") + } + + modal.dismiss(animated: true) { + self?.addMembers( + currentGroupName: currentGroupName, + memberInfo: [(sessionId.hexString, nil)] + ) + } + + case (.none, _), (_, .some): return showError("accountIdErrorInvalid".localized()) + + case (.some(let inviteByIdValue), _): + // This could be an ONS name + let viewController = ModalActivityIndicatorViewController() { modalActivityIndicator in + SnodeAPI + .getSessionID(for: inviteByIdValue, using: dependencies) + .subscribe(on: DispatchQueue.global(qos: .userInitiated)) + .receive(on: DispatchQueue.main) + .sinkUntilComplete( + receiveCompletion: { result in + switch result { + case .finished: break + case .failure(let error): + modalActivityIndicator.dismiss { + switch error { + case SnodeAPIError.onsNotFound: + return showError("onsErrorNotRecognized".localized()) + default: + return showError("onsErrorUnableToSearch".localized()) + } + } + } + }, + receiveValue: { sessionIdHexString in + guard !currentMemberIds.contains(sessionIdHexString) else { + // FIXME: Localise this + return showError("This Account ID or ONS belongs to an existing member") + } + + modalActivityIndicator.dismiss { + modal.dismiss(animated: true) { + self?.addMembers( + currentGroupName: currentGroupName, + memberInfo: [(sessionIdHexString, nil)] + ) + } + } + } + ) + } + self?.transitionToScreen(viewController, transitionType: .present) + } + }, + afterClosed: { [weak self] in self?.inviteByIdValue = nil } + ) + ), + transitionType: .present + ) + } + + private func addMembers( + currentGroupName: String, + memberInfo: [(id: String, profile: Profile?)] + ) { + /// Show a toast that we have sent the invitations + self.showToast( + text: "groupInviteSending" + .putNumber(memberInfo.count) + .localized(), + backgroundColor: .backgroundSecondary + ) + + /// Actually trigger the sending + MessageSender + .addGroupMembers( + groupSessionId: threadId, + members: memberInfo, + allowAccessToHistoricMessages: dependencies[feature: .updatedGroupsAllowHistoricAccessOnInvite], + using: dependencies + ) + .sinkUntilComplete( + receiveCompletion: { [weak self] result in + switch result { + case .finished: break + case .failure: + self?.showToast( + text: GroupInviteMemberJob.failureMessage( + groupName: currentGroupName, + memberIds: memberInfo.map { $0.id }, + profileInfo: memberInfo + .reduce(into: [:]) { result, next in + result[next.id] = next.profile + } + ), + backgroundColor: .backgroundSecondary + ) + } + } + ) + } + + private func resendInvitation(memberId: String) { + MessageSender.resendInvitation( + groupSessionId: threadId, + memberId: memberId, + using: dependencies + ) + self.showToast(text: "groupInviteSending".putNumber(1).localized()) + } + + private func removeMembers(currentGroupName: String, memberIds: Set) { + guard !memberIds.isEmpty else { return } + + let memberNames: [String] = memberIds + .compactMap { memberId in + guard + let section: SectionModel = self.tableData + .first(where: { section in section.model == .members }), + let info: SessionCell.Info = section.elements + .first(where: { info in + switch info.id { + case .member(let infoMemberId): return infoMemberId == memberId + default: return false + } + }) + else { + return Profile.truncated(id: memberId, truncating: .middle) + } + + return info.title?.text + } + let confirmationBody: NSAttributedString = { + switch memberNames.count { + case 1: + return "groupRemoveDescription" + .put(key: "name", value: memberNames[0]) + .put(key: "group_name", value: currentGroupName) + .localizedFormatted(baseFont: .systemFont(ofSize: Values.smallFontSize)) + + case 2: + return "groupRemoveDescriptionTwo" + .put(key: "name", value: memberNames[0]) + .put(key: "other_name", value: memberNames[1]) + .put(key: "group_name", value: currentGroupName) + .localizedFormatted(baseFont: .systemFont(ofSize: Values.smallFontSize)) + + default: + return "groupRemoveDescriptionMultiple" + .put(key: "name", value: memberNames[0]) + .put(key: "count", value: memberNames.count - 1) + .put(key: "group_name", value: currentGroupName) + .localizedFormatted(baseFont: .systemFont(ofSize: Values.smallFontSize)) + } + }() + let confirmationModal: ConfirmationModal = ConfirmationModal( + info: ConfirmationModal.Info( + title: "remove".localized(), + body: .attributedText(confirmationBody), + confirmTitle: "remove".localized(), + confirmStyle: .danger, + cancelStyle: .alert_text, + dismissOnConfirm: false, + onConfirm: { [weak self, threadId, dependencies] modal in + switch try? SessionId.Prefix(from: threadId) { + case .group: + MessageSender + .removeGroupMembers( + groupSessionId: threadId, + memberIds: memberIds, + removeTheirMessages: dependencies[feature: .updatedGroupsRemoveMessagesOnKick], + sendMemberChangedMessage: true, + using: dependencies + ) + .subscribe(on: DispatchQueue.global(qos: .userInitiated), using: dependencies) + .sinkUntilComplete() + self?.selectedIdsSubject.send((currentGroupName, [])) + modal.dismiss(animated: true) + + case .standard: // Assume it's a legacy group + let updatedMemberIds: Set = (self?.tableData + .first(where: { $0.model == .members })? + .elements + .compactMap { item -> String? in + switch item.id { + case .member(let profileId): return profileId + default: return nil + } + }) + .defaulting(to: []) + .asSet() + .removing(contentsOf: memberIds) + + let viewController = ModalActivityIndicatorViewController(canCancel: false) { [weak self, dependencies, threadId] modalActivityIndicator in + MessageSender + .update( + legacyGroupSessionId: threadId, + with: updatedMemberIds, + name: currentGroupName, + using: dependencies + ) + .eraseToAnyPublisher() + .subscribe(on: DispatchQueue.global(qos: .userInitiated)) + .receive(on: DispatchQueue.main) + .sinkUntilComplete( + receiveCompletion: { [weak self] result in + modalActivityIndicator.dismiss(completion: { + switch result { + case .finished: + self?.selectedIdsSubject.send((currentGroupName, [])) + modalActivityIndicator.dismiss { + modal.dismiss(animated: true) + } + + case .failure: + self?.transitionToScreen( + ConfirmationModal( + info: ConfirmationModal.Info( + title: "theError".localized(), + body: .text("deleteAfterLegacyGroupsGroupUpdateErrorTitle".localized()), + cancelTitle: "okay".localized(), + cancelStyle: .alert_text + ) + ), + transitionType: .present + ) + } + }) + } + ) + } + self?.transitionToScreen(viewController, transitionType: .present) + + default: + self?.transitionToScreen( + ConfirmationModal( + info: ConfirmationModal.Info( + title: "theError".localized(), + body: .text("deleteAfterLegacyGroupsGroupUpdateErrorTitle".localized()), + cancelTitle: "okay".localized(), + cancelStyle: .alert_text + ) + ), + transitionType: .present + ) + } + } + ) + ) + self.transitionToScreen(confirmationModal, transitionType: .present) + } +} diff --git a/Session/Closed Groups/NewClosedGroupVC.swift b/Session/Closed Groups/NewClosedGroupVC.swift index 7620dc6a547..31d2086e58f 100644 --- a/Session/Closed Groups/NewClosedGroupVC.swift +++ b/Session/Closed Groups/NewClosedGroupVC.swift @@ -1,6 +1,7 @@ // Copyright © 2022 Rangeproof Pty Ltd. All rights reserved. import UIKit +import Combine import GRDB import DifferenceKit import SessionUIKit @@ -28,18 +29,56 @@ final class NewClosedGroupVC: BaseVC, UITableViewDataSource, UITableViewDelegate case contacts } - private let contactProfiles: [Profile] = Profile.fetchAllContactProfiles(excludeCurrentUser: true) + private let dependencies: Dependencies + private let contactProfiles: [Profile] private lazy var data: [ArraySection] = [ ArraySection(model: .contacts, elements: contactProfiles) ] - private var selectedContacts: Set = [] + private var selectedProfiles: [String: Profile] = [:] private var searchText: String = "" - + + // MARK: - Initialization + + init(using dependencies: Dependencies) { + self.dependencies = dependencies + self.contactProfiles = Profile.fetchAllContactProfiles(excludeCurrentUser: true, using: dependencies) + + super.init(nibName: nil, bundle: nil) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + // MARK: - Components private static let textFieldHeight: CGFloat = 50 private static let searchBarHeight: CGFloat = (36 + (Values.mediumSpacing * 2)) + private let contentStackView: UIStackView = { + let result: UIStackView = UIStackView() + result.axis = .vertical + result.distribution = .fill + + return result + }() + + private lazy var minVersionBanner: InfoBanner = { + let result: InfoBanner = InfoBanner( + info: InfoBanner.Info( + font: .systemFont(ofSize: Values.verySmallFontSize), + message: "groupInviteVersion".localized(), + icon: .none, + tintColor: .black, + backgroundColor: .warning, + accessibility: Accessibility(label: "Version warning banner") + ) + ) + result.isHidden = !dependencies[feature: .updatedGroups] + + return result + }() + private lazy var nameTextField: TextField = { let result = TextField( placeholder: "groupNameEnter".localized(), @@ -61,6 +100,7 @@ final class NewClosedGroupVC: BaseVC, UITableViewDataSource, UITableViewDelegate result.themeTintColor = .textPrimary result.themeBackgroundColor = .clear result.delegate = self + result.searchTextField.accessibilityIdentifier = "Search contacts field" result.set(.height, to: NewClosedGroupVC.searchBarHeight) return result @@ -177,11 +217,14 @@ final class NewClosedGroupVC: BaseVC, UITableViewDataSource, UITableViewDelegate return } - view.addSubview(tableView) - tableView.pin(.top, to: .top, of: view) - tableView.pin(.leading, to: .leading, of: view) - tableView.pin(.trailing, to: .trailing, of: view) - tableView.pin(.bottom, to: .bottom, of: view) + view.addSubview(contentStackView) + contentStackView.pin(.top, to: .top, of: view) + contentStackView.pin(.leading, to: .leading, of: view) + contentStackView.pin(.trailing, to: .trailing, of: view) + contentStackView.pin(.bottom, to: .bottom, of: view) + + contentStackView.addArrangedSubview(minVersionBanner) + contentStackView.addArrangedSubview(tableView) view.addSubview(fadeView) fadeView.pin(.leading, to: .leading, of: view) @@ -206,16 +249,15 @@ final class NewClosedGroupVC: BaseVC, UITableViewDataSource, UITableViewDelegate with: SessionCell.Info( id: profile, position: Position.with(indexPath.row, count: data[indexPath.section].elements.count), - leftAccessory: .profile(id: profile.id, profile: profile), + leadingAccessory: .profile(id: profile.id, profile: profile), title: profile.displayName(), - rightAccessory: .radio(isSelected: { [weak self] in - self?.selectedContacts.contains(profile.id) == true - }), + trailingAccessory: .radio(isSelected: (selectedProfiles[profile.id] != nil)), styling: SessionCell.StyleInfo(backgroundStyle: .edgeToEdge), accessibility: Accessibility( identifier: "Contact" ) - ) + ), + using: dependencies ) return cell @@ -232,13 +274,13 @@ final class NewClosedGroupVC: BaseVC, UITableViewDataSource, UITableViewDelegate } func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { - let profileId: String = data[indexPath.section].elements[indexPath.row].id + let profile: Profile = data[indexPath.section].elements[indexPath.row] - if !selectedContacts.contains(profileId) { - selectedContacts.insert(profileId) + if selectedProfiles[profile.id] == nil { + selectedProfiles[profile.id] = profile } else { - selectedContacts.remove(profileId) + selectedProfiles.removeValue(forKey: profile.id) } tableView.deselectRow(at: indexPath, animated: true) @@ -318,20 +360,43 @@ final class NewClosedGroupVC: BaseVC, UITableViewDataSource, UITableViewDelegate else { return showError(title: "groupNameEnterPlease".localized()) } - guard name.utf8CString.count < LibSession.libSessionMaxGroupNameByteLength else { + guard !LibSession.isTooLong(groupName: name) else { return showError(title: "groupNameEnterShorter".localized()) } - guard selectedContacts.count >= 1 else { + guard selectedProfiles.count >= 1 else { return showError(title: "groupCreateErrorNoMembers".localized()) } - guard selectedContacts.count < 100 else { // Minus one because we're going to include self later + /// Minus one because we're going to include self later + guard selectedProfiles.count < (LibSession.sizeMaxGroupMemberCount - 1) else { return showError(title: "groupAddMemberMaximum".localized()) } - let selectedContacts = self.selectedContacts - let message: String? = (selectedContacts.count > 20 ? "deleteAfterLegacyGroupsGroupCreation".localized() : nil) - ModalActivityIndicatorViewController.present(fromViewController: navigationController!, message: message) { [weak self] _ in - MessageSender - .createClosedGroup(name: name, members: selectedContacts) + let selectedProfiles: [(String, Profile?)] = self.selectedProfiles + .reduce(into: []) { result, next in result.append((next.key, next.value)) } + let message: String? = (dependencies[feature: .updatedGroups] || selectedProfiles.count <= 20 ? nil : "deleteAfterLegacyGroupsGroupCreation".localized() + ) + + ModalActivityIndicatorViewController.present(fromViewController: navigationController!, message: message) { [weak self, dependencies] _ in + let createPublisher: AnyPublisher = { + switch dependencies[feature: .updatedGroups] { + case true: + return MessageSender.createGroup( + name: name, + description: nil, + displayPictureData: nil, + members: selectedProfiles, + using: dependencies + ) + + case false: + return MessageSender.createLegacyClosedGroup( + name: name, + members: selectedProfiles.map { $0.0 }.asSet(), + using: dependencies + ) + } + }() + + createPublisher .subscribe(on: DispatchQueue.global(qos: .userInitiated)) .receive(on: DispatchQueue.main) .sinkUntilComplete( @@ -354,9 +419,10 @@ final class NewClosedGroupVC: BaseVC, UITableViewDataSource, UITableViewDelegate } }, receiveValue: { thread in - SessionApp.presentConversationCreatingIfNeeded( + dependencies[singleton: .app].presentConversationCreatingIfNeeded( for: thread.id, variant: thread.variant, + action: .none, dismissing: self?.presentingViewController, animated: false ) diff --git a/Session/Conversations/Context Menu/ContextMenuVC+Action.swift b/Session/Conversations/Context Menu/ContextMenuVC+Action.swift index bdf0147114d..3878194c319 100644 --- a/Session/Conversations/Context Menu/ContextMenuVC+Action.swift +++ b/Session/Conversations/Context Menu/ContextMenuVC+Action.swift @@ -49,15 +49,15 @@ extension ContextMenuVC { // MARK: - Actions - static func info(_ cellViewModel: MessageViewModel, _ delegate: ContextMenuActionDelegate?, using dependencies: Dependencies) -> Action { + static func info(_ cellViewModel: MessageViewModel, _ delegate: ContextMenuActionDelegate?) -> Action { return Action( icon: UIImage(named: "ic_info"), title: "info".localized(), accessibilityLabel: "Message info" - ) { delegate?.info(cellViewModel, using: dependencies) } + ) { delegate?.info(cellViewModel) } } - static func retry(_ cellViewModel: MessageViewModel, _ delegate: ContextMenuActionDelegate?, using dependencies: Dependencies) -> Action { + static func retry(_ cellViewModel: MessageViewModel, _ delegate: ContextMenuActionDelegate?) -> Action { return Action( icon: UIImage(systemName: "arrow.triangle.2.circlepath"), title: (cellViewModel.state == .failedToSync ? @@ -65,23 +65,23 @@ extension ContextMenuVC { "resend".localized() ), accessibilityLabel: (cellViewModel.state == .failedToSync ? "Resync message" : "Resend message") - ) { delegate?.retry(cellViewModel, using: dependencies) } + ) { delegate?.retry(cellViewModel) } } - static func reply(_ cellViewModel: MessageViewModel, _ delegate: ContextMenuActionDelegate?, using dependencies: Dependencies) -> Action { + static func reply(_ cellViewModel: MessageViewModel, _ delegate: ContextMenuActionDelegate?) -> Action { return Action( icon: UIImage(named: "ic_reply"), title: "reply".localized(), accessibilityLabel: "Reply to message" - ) { delegate?.reply(cellViewModel, using: dependencies) } + ) { delegate?.reply(cellViewModel) } } - static func copy(_ cellViewModel: MessageViewModel, _ delegate: ContextMenuActionDelegate?, using dependencies: Dependencies) -> Action { + static func copy(_ cellViewModel: MessageViewModel, _ delegate: ContextMenuActionDelegate?) -> Action { return Action( icon: UIImage(named: "ic_copy"), title: "copy".localized(), accessibilityLabel: "Copy text" - ) { delegate?.copy(cellViewModel, using: dependencies) } + ) { delegate?.copy(cellViewModel) } } static func copySessionID(_ cellViewModel: MessageViewModel, _ delegate: ContextMenuActionDelegate?) -> Action { @@ -89,11 +89,10 @@ extension ContextMenuVC { icon: UIImage(named: "ic_copy"), title: "accountIDCopy".localized(), accessibilityLabel: "Copy Session ID" - ) { delegate?.copySessionID(cellViewModel) } } - static func delete(_ cellViewModel: MessageViewModel, _ delegate: ContextMenuActionDelegate?, using dependencies: Dependencies) -> Action { + static func delete(_ cellViewModel: MessageViewModel, _ delegate: ContextMenuActionDelegate?) -> Action { return Action( icon: UIImage(named: "ic_trash"), title: "delete".localized(), @@ -103,47 +102,47 @@ extension ContextMenuVC { ), themeColor: .danger, accessibilityLabel: "Delete message" - ) { delegate?.delete(cellViewModel, using: dependencies) } + ) { delegate?.delete(cellViewModel) } } - static func save(_ cellViewModel: MessageViewModel, _ delegate: ContextMenuActionDelegate?, using dependencies: Dependencies) -> Action { + static func save(_ cellViewModel: MessageViewModel, _ delegate: ContextMenuActionDelegate?) -> Action { return Action( icon: UIImage(named: "ic_download"), title: "save".localized(), accessibilityLabel: "Save attachment" - ) { delegate?.save(cellViewModel, using: dependencies) } + ) { delegate?.save(cellViewModel) } } - static func ban(_ cellViewModel: MessageViewModel, _ delegate: ContextMenuActionDelegate?, using dependencies: Dependencies) -> Action { + static func ban(_ cellViewModel: MessageViewModel, _ delegate: ContextMenuActionDelegate?) -> Action { return Action( icon: UIImage(named: "ic_block"), title: "banUser".localized(), themeColor: .danger, accessibilityLabel: "Ban user" - ) { delegate?.ban(cellViewModel, using: dependencies) } + ) { delegate?.ban(cellViewModel) } } - static func banAndDeleteAllMessages(_ cellViewModel: MessageViewModel, _ delegate: ContextMenuActionDelegate?, using dependencies: Dependencies) -> Action { + static func banAndDeleteAllMessages(_ cellViewModel: MessageViewModel, _ delegate: ContextMenuActionDelegate?) -> Action { return Action( icon: UIImage(named: "ic_block"), title: "banDeleteAll".localized(), themeColor: .danger, accessibilityLabel: "Ban user and delete" - ) { delegate?.banAndDeleteAllMessages(cellViewModel, using: dependencies) } + ) { delegate?.banAndDeleteAllMessages(cellViewModel) } } - static func react(_ cellViewModel: MessageViewModel, _ emoji: EmojiWithSkinTones, _ delegate: ContextMenuActionDelegate?, using dependencies: Dependencies) -> Action { + static func react(_ cellViewModel: MessageViewModel, _ emoji: EmojiWithSkinTones, _ delegate: ContextMenuActionDelegate?) -> Action { return Action( title: emoji.rawValue, actionType: .emoji - ) { delegate?.react(cellViewModel, with: emoji, using: dependencies) } + ) { delegate?.react(cellViewModel, with: emoji) } } - static func emojiPlusButton(_ cellViewModel: MessageViewModel, _ delegate: ContextMenuActionDelegate?, using dependencies: Dependencies) -> Action { + static func emojiPlusButton(_ cellViewModel: MessageViewModel, _ delegate: ContextMenuActionDelegate?) -> Action { return Action( actionType: .emojiPlus, accessibilityLabel: "Add emoji" - ) { delegate?.showFullEmojiKeyboard(cellViewModel, using: dependencies) } + ) { delegate?.showFullEmojiKeyboard(cellViewModel) } } static func dismiss(_ delegate: ContextMenuActionDelegate?) -> Action { @@ -166,23 +165,24 @@ extension ContextMenuVC { static func actions( for cellViewModel: MessageViewModel, recentEmojis: [EmojiWithSkinTones], - currentUserPublicKey: String, - currentUserBlinded15PublicKey: String?, - currentUserBlinded25PublicKey: String?, + currentUserSessionId: String, + currentUserBlinded15SessionId: String?, + currentUserBlinded25SessionId: String?, currentUserIsOpenGroupModerator: Bool, currentThreadIsMessageRequest: Bool, forMessageInfoScreen: Bool, delegate: ContextMenuActionDelegate?, - using dependencies: Dependencies = Dependencies() + using dependencies: Dependencies ) -> [Action]? { switch cellViewModel.variant { - case .standardIncomingDeleted, .infoCall, - .infoScreenshotNotification, .infoMediaSavedNotification, - .infoClosedGroupCreated, .infoClosedGroupUpdated, - .infoClosedGroupCurrentUserLeft, .infoClosedGroupCurrentUserLeaving, .infoClosedGroupCurrentUserErrorLeaving, - .infoMessageRequestAccepted, .infoDisappearingMessagesUpdate: + case .standardIncomingDeleted, .standardIncomingDeletedLocally, .standardOutgoingDeleted, + .standardOutgoingDeletedLocally, .infoCall, .infoScreenshotNotification, .infoMediaSavedNotification, + .infoLegacyGroupCreated, .infoLegacyGroupUpdated, .infoLegacyGroupCurrentUserLeft, + .infoGroupCurrentUserLeaving, .infoGroupCurrentUserErrorLeaving, + .infoMessageRequestAccepted, .infoDisappearingMessagesUpdate, .infoGroupInfoInvited, + .infoGroupInfoUpdated, .infoGroupMembersUpdated: // Let the user delete info messages and unsent messages - return [ Action.delete(cellViewModel, delegate, using: dependencies) ] + return [ Action.delete(cellViewModel, delegate) ] case .standardOutgoing, .standardIncoming: break } @@ -227,9 +227,9 @@ extension ContextMenuVC { let canDelete: Bool = ( cellViewModel.threadVariant != .community || currentUserIsOpenGroupModerator || - cellViewModel.authorId == currentUserPublicKey || - cellViewModel.authorId == currentUserBlinded15PublicKey || - cellViewModel.authorId == currentUserBlinded25PublicKey || + cellViewModel.authorId == currentUserSessionId || + cellViewModel.authorId == currentUserBlinded15SessionId || + cellViewModel.authorId == currentUserBlinded25SessionId || cellViewModel.state == .failed ) let canBan: Bool = ( @@ -239,7 +239,7 @@ extension ContextMenuVC { let shouldShowEmojiActions: Bool = { if cellViewModel.threadVariant == .community { - return OpenGroupManager.doesOpenGroupSupport( + return dependencies[singleton: .openGroupManager].doesOpenGroupSupport( capability: .reactions, on: cellViewModel.threadOpenGroupServer ) @@ -248,21 +248,21 @@ extension ContextMenuVC { }() let generatedActions: [Action] = [ - (canRetry ? Action.retry(cellViewModel, delegate, using: dependencies) : nil), - (viewModelCanReply(cellViewModel) ? Action.reply(cellViewModel, delegate, using: dependencies) : nil), - (canCopy ? Action.copy(cellViewModel, delegate, using: dependencies) : nil), - (canSave ? Action.save(cellViewModel, delegate, using: dependencies) : nil), + (canRetry ? Action.retry(cellViewModel, delegate) : nil), + (viewModelCanReply(cellViewModel) ? Action.reply(cellViewModel, delegate) : nil), + (canCopy ? Action.copy(cellViewModel, delegate) : nil), + (canSave ? Action.save(cellViewModel, delegate) : nil), (canCopySessionId ? Action.copySessionID(cellViewModel, delegate) : nil), - (canDelete ? Action.delete(cellViewModel, delegate, using: dependencies) : nil), - (canBan ? Action.ban(cellViewModel, delegate, using: dependencies) : nil), - (canBan ? Action.banAndDeleteAllMessages(cellViewModel, delegate, using: dependencies) : nil), - (forMessageInfoScreen ? nil : Action.info(cellViewModel, delegate, using: dependencies)), + (canDelete ? Action.delete(cellViewModel, delegate) : nil), + (canBan ? Action.ban(cellViewModel, delegate) : nil), + (canBan ? Action.banAndDeleteAllMessages(cellViewModel, delegate) : nil), + (forMessageInfoScreen ? nil : Action.info(cellViewModel, delegate)), ] .appending( contentsOf: (shouldShowEmojiActions ? recentEmojis : []) - .map { Action.react(cellViewModel, $0, delegate, using: dependencies) } + .map { Action.react(cellViewModel, $0, delegate) } ) - .appending(forMessageInfoScreen ? nil : Action.emojiPlusButton(cellViewModel, delegate, using: dependencies)) + .appending(forMessageInfoScreen ? nil : Action.emojiPlusButton(cellViewModel, delegate)) .compactMap { $0 } guard !generatedActions.isEmpty else { return [] } @@ -274,16 +274,16 @@ extension ContextMenuVC { // MARK: - Delegate protocol ContextMenuActionDelegate { - func info(_ cellViewModel: MessageViewModel, using dependencies: Dependencies) - func retry(_ cellViewModel: MessageViewModel, using dependencies: Dependencies) - func reply(_ cellViewModel: MessageViewModel, using dependencies: Dependencies) - func copy(_ cellViewModel: MessageViewModel, using dependencies: Dependencies) + func info(_ cellViewModel: MessageViewModel) + func retry(_ cellViewModel: MessageViewModel) + func reply(_ cellViewModel: MessageViewModel) + func copy(_ cellViewModel: MessageViewModel) func copySessionID(_ cellViewModel: MessageViewModel) - func delete(_ cellViewModel: MessageViewModel, using dependencies: Dependencies) - func save(_ cellViewModel: MessageViewModel, using dependencies: Dependencies) - func ban(_ cellViewModel: MessageViewModel, using dependencies: Dependencies) - func banAndDeleteAllMessages(_ cellViewModel: MessageViewModel, using dependencies: Dependencies) - func react(_ cellViewModel: MessageViewModel, with emoji: EmojiWithSkinTones, using dependencies: Dependencies) - func showFullEmojiKeyboard(_ cellViewModel: MessageViewModel, using dependencies: Dependencies) + func delete(_ cellViewModel: MessageViewModel) + func save(_ cellViewModel: MessageViewModel) + func ban(_ cellViewModel: MessageViewModel) + func banAndDeleteAllMessages(_ cellViewModel: MessageViewModel) + func react(_ cellViewModel: MessageViewModel, with emoji: EmojiWithSkinTones) + func showFullEmojiKeyboard(_ cellViewModel: MessageViewModel) func contextMenuDismissed() } diff --git a/Session/Conversations/Context Menu/ContextMenuVC+ActionView.swift b/Session/Conversations/Context Menu/ContextMenuVC+ActionView.swift index 0f132daeb6c..2838a83eb2b 100644 --- a/Session/Conversations/Context Menu/ContextMenuVC+ActionView.swift +++ b/Session/Conversations/Context Menu/ContextMenuVC+ActionView.swift @@ -10,6 +10,7 @@ extension ContextMenuVC { private static let iconSize: CGFloat = 16 private static let iconImageViewSize: CGFloat = 24 + private let dependencies: Dependencies private let action: Action private let dismiss: () -> Void private var didTouchDownInside: Bool = false @@ -17,9 +18,8 @@ extension ContextMenuVC { // MARK: - UI - private lazy var iconImageView: UIImageView = { + private lazy var iconContainerView: UIImageView = { let result: UIImageView = UIImageView() - result.contentMode = .center result.themeTintColor = action.themeColor result.set(.width, to: ActionView.iconImageViewSize) result.set(.height, to: ActionView.iconImageViewSize) @@ -27,6 +27,16 @@ extension ContextMenuVC { return result }() + private lazy var iconImageView: UIImageView = { + let result: UIImageView = UIImageView() + result.contentMode = .scaleAspectFit + result.themeTintColor = action.themeColor + result.set(.width, to: ActionView.iconSize) + result.set(.height, to: ActionView.iconSize) + + return result + }() + private lazy var titleLabel: UILabel = { let result: UILabel = UILabel() result.font = .systemFont(ofSize: Values.mediumFontSize) @@ -58,7 +68,8 @@ extension ContextMenuVC { // MARK: - Lifecycle - init(for action: Action, dismiss: @escaping () -> Void) { + init(for action: Action, using dependencies: Dependencies, dismiss: @escaping () -> Void) { + self.dependencies = dependencies self.action = action self.dismiss = dismiss @@ -78,14 +89,15 @@ extension ContextMenuVC { private func setUpViewHierarchy() { themeBackgroundColor = .clear - iconImageView.image = action.icon? - .resized(to: CGSize(width: ActionView.iconSize, height: ActionView.iconSize))? - .withRenderingMode(.alwaysTemplate) + iconImageView.image = action.icon?.withRenderingMode(.alwaysTemplate) + iconContainerView.addSubview(iconImageView) + iconImageView.center(in: iconContainerView) + titleLabel.text = action.title setUpSubtitle() // Stack view - let stackView: UIStackView = UIStackView(arrangedSubviews: [ iconImageView, labelContainer ]) + let stackView: UIStackView = UIStackView(arrangedSubviews: [ iconContainerView, labelContainer ]) stackView.axis = .horizontal stackView.spacing = Values.smallSpacing stackView.alignment = .center @@ -119,13 +131,13 @@ extension ContextMenuVC { subtitleLabel.isHidden = false subtitleWidthConstraint.isActive = true // To prevent a negative timer - let timeToExpireInSeconds: TimeInterval = max(0, (expiresStartedAtMs + expiresInSeconds * 1000 - Double(SnodeAPI.currentOffsetTimestampMs())) / 1000) + let timeToExpireInSeconds: TimeInterval = max(0, (expiresStartedAtMs + expiresInSeconds * 1000 - dependencies[cache: .snodeAPI].currentOffsetTimestampMs()) / 1000) subtitleLabel.text = "disappearingMessagesCountdownBigMobile" .put(key: "time_large", value: timeToExpireInSeconds.formatted(format: .twoUnits)) .localized() - timer = Timer.scheduledTimerOnMainThread(withTimeInterval: 1, repeats: true, block: { [weak self] _ in - let timeToExpireInSeconds: TimeInterval = (expiresStartedAtMs + expiresInSeconds * 1000 - Double(SnodeAPI.currentOffsetTimestampMs())) / 1000 + timer = Timer.scheduledTimerOnMainThread(withTimeInterval: 1, repeats: true, using: dependencies, block: { [weak self, dependencies] _ in + let timeToExpireInSeconds: TimeInterval = (expiresStartedAtMs + expiresInSeconds * 1000 - dependencies[cache: .snodeAPI].currentOffsetTimestampMs()) / 1000 if timeToExpireInSeconds <= 0 { self?.dismissWithTimerInvalidationIfNeeded() } else { diff --git a/Session/Conversations/Context Menu/ContextMenuVC.swift b/Session/Conversations/Context Menu/ContextMenuVC.swift index af142145e3a..6560785fb52 100644 --- a/Session/Conversations/Context Menu/ContextMenuVC.swift +++ b/Session/Conversations/Context Menu/ContextMenuVC.swift @@ -3,11 +3,17 @@ import UIKit import SessionUIKit import SessionMessagingKit +import SessionUtilitiesKit final class ContextMenuVC: UIViewController { + public static let dismissDurationPartOne: TimeInterval = 0.15 + public static let dismissDurationPartTwo: TimeInterval = 0.25 + public static let dismissDuration: TimeInterval = (dismissDurationPartOne + dismissDurationPartTwo) + private static let actionViewHeight: CGFloat = 40 private static let menuCornerRadius: CGFloat = 8 + private let dependencies: Dependencies private let snapshot: UIView private let frame: CGRect private var targetFrame: CGRect = .zero @@ -88,8 +94,10 @@ final class ContextMenuVC: UIViewController { frame: CGRect, cellViewModel: MessageViewModel, actions: [Action], + using dependencies: Dependencies, dismiss: @escaping () -> Void ) { + self.dependencies = dependencies self.snapshot = snapshot self.frame = frame self.cellViewModel = cellViewModel @@ -167,7 +175,7 @@ final class ContextMenuVC: UIViewController { arrangedSubviews: actions .filter { $0.actionType == .generic } .map { action -> ActionView in - ActionView(for: action, dismiss: snDismiss) + ActionView(for: action, using: dependencies, dismiss: snDismiss) } ) menuStackView.axis = .vertical @@ -225,11 +233,11 @@ final class ContextMenuVC: UIViewController { menuView.pin(.top, to: .top, of: view, withInset: targetFrame.maxY + spacing) switch cellViewModel.variant { - case .standardOutgoing: + case .standardOutgoing, .standardOutgoingDeleted, .standardOutgoingDeletedLocally: menuView.pin(.right, to: .right, of: view, withInset: -(UIScreen.main.bounds.width - targetFrame.maxX)) emojiBar.pin(.right, to: .right, of: view, withInset: -(UIScreen.main.bounds.width - targetFrame.maxX)) - case .standardIncoming, .standardIncomingDeleted: + case .standardIncoming, .standardIncomingDeleted, .standardIncomingDeletedLocally: menuView.pin(.left, to: .left, of: view, withInset: targetFrame.minX) emojiBar.pin(.left, to: .left, of: view, withInset: targetFrame.minX) @@ -386,7 +394,7 @@ final class ContextMenuVC: UIViewController { self.timestampLabel.frame = currentLabelFrame UIView.animate( - withDuration: 0.15, + withDuration: ContextMenuVC.dismissDurationPartOne, delay: 0, options: .curveEaseOut, animations: { [weak self] in @@ -397,7 +405,7 @@ final class ContextMenuVC: UIViewController { ) UIView.animate( - withDuration: 0.25, + withDuration: ContextMenuVC.dismissDurationPartTwo, animations: { [weak self] in self?.blurView.effect = nil self?.menuView.alpha = 0 diff --git a/Session/Conversations/ConversationSearch.swift b/Session/Conversations/ConversationSearch.swift index b21f4ddd1ed..bb8361db4ed 100644 --- a/Session/Conversations/ConversationSearch.swift +++ b/Session/Conversations/ConversationSearch.swift @@ -4,6 +4,7 @@ import UIKit import GRDB import SignalUtilitiesKit import SessionUIKit +import SessionMessagingKit import SessionUtilitiesKit public class StyledSearchController: UISearchController { @@ -74,6 +75,7 @@ extension ConversationSearchController: UISearchResultsUpdating { Log.verbose("searchBar.text: \( searchController.searchBar.text ?? "")") guard + let dependencies: Dependencies = self.delegate?.conversationSearchControllerDependencies(), let searchText: String = searchController.searchBar.text?.stripped, searchText.count >= ConversationSearchController.minimumSearchTextLength else { @@ -85,7 +87,7 @@ extension ConversationSearchController: UISearchResultsUpdating { let threadId: String = self.threadId DispatchQueue.global(qos: .default).async { [weak self] in - let results: [Interaction.TimestampInfo]? = Storage.shared.read { db -> [Interaction.TimestampInfo] in + let results: [Interaction.TimestampInfo]? = dependencies[singleton: .storage].read { db -> [Interaction.TimestampInfo] in self?.resultsBar.willStartSearching(readConnection: db) return try Interaction.idsForTermWithin( @@ -360,6 +362,7 @@ public final class SearchResultsBar: UIView { // MARK: - ConversationSearchControllerDelegate public protocol ConversationSearchControllerDelegate: UISearchControllerDelegate { + func conversationSearchControllerDependencies() -> Dependencies func currentVisibleIds() -> [Int64] func conversationSearchController(_ conversationSearchController: ConversationSearchController, didUpdateSearchResults results: [Interaction.TimestampInfo]?, searchText: String?) func conversationSearchController(_ conversationSearchController: ConversationSearchController, didSelectInteractionInfo: Interaction.TimestampInfo) diff --git a/Session/Conversations/ConversationVC+Interaction.swift b/Session/Conversations/ConversationVC+Interaction.swift index ba1669fcb97..a796ca985b5 100644 --- a/Session/Conversations/ConversationVC+Interaction.swift +++ b/Session/Conversations/ConversationVC+Interaction.swift @@ -7,6 +7,7 @@ import Combine import CoreServices import Photos import PhotosUI +import UniformTypeIdentifiers import GRDB import SessionUIKit import SessionMessagingKit @@ -33,35 +34,69 @@ extension ConversationVC: openSettingsFromTitleView() } - @objc func openSettingsFromTitleView() { - switch self.titleView.currentLabelType { - case .userCount: - if self.viewModel.threadData.threadVariant == .group || self.viewModel.threadData.threadVariant == .legacyGroup { - let viewController = EditClosedGroupVC( + func openSettingsFromTitleView() { + switch (titleView.currentLabelType, viewModel.threadData.threadVariant, viewModel.threadData.currentUserIsClosedGroupMember, viewModel.threadData.currentUserIsClosedGroupAdmin) { + case (.userCount, .group, _, true), (.userCount, .legacyGroup, _, true): + let viewController = SessionTableViewController( + viewModel: EditGroupViewModel( threadId: self.viewModel.threadData.threadId, - threadVariant: self.viewModel.threadData.threadVariant + using: self.viewModel.dependencies ) - navigationController?.pushViewController(viewController, animated: true) - } else { - openSettings() + ) + navigationController?.pushViewController(viewController, animated: true) + + case (.userCount, .group, true, _), (.userCount, .legacyGroup, true, _): + let viewController: SessionTableViewController = SessionTableViewController( + viewModel: UserListViewModel( + title: "groupMembers".localized(), + showProfileIcons: true, + request: GroupMember + .filter(GroupMember.Columns.groupId == self.viewModel.threadData.threadId), + onTap: .callback { [weak self, dependencies = viewModel.dependencies] _, memberInfo in + dependencies[singleton: .storage].write { db in + try SessionThread.fetchOrCreate( + db, + id: memberInfo.profileId, + variant: .contact, + creationDateTimestamp: (dependencies[cache: .snodeAPI].currentOffsetTimestampMs() / 1000), + shouldBeVisible: nil, + calledFromConfig: nil, + using: dependencies + ) + } + + self?.navigationController?.pushViewController( + ConversationVC( + threadId: memberInfo.profileId, + threadVariant: .contact, + using: dependencies + ), + animated: true + ) + }, + using: self.viewModel.dependencies + ) + ) + navigationController?.pushViewController(viewController, animated: true) + + case (.disappearingMessageSetting, _, _, _): + guard let config: DisappearingMessagesConfiguration = self.viewModel.threadData.disappearingMessagesConfiguration else { + return openSettings() } - break - case .none, .notificationSettings: - openSettings() - break - - case .disappearingMessageSetting: + let viewController = SessionTableViewController( viewModel: ThreadDisappearingMessagesSettingsViewModel( threadId: self.viewModel.threadData.threadId, threadVariant: self.viewModel.threadData.threadVariant, currentUserIsClosedGroupMember: self.viewModel.threadData.currentUserIsClosedGroupMember, currentUserIsClosedGroupAdmin: self.viewModel.threadData.currentUserIsClosedGroupAdmin, - config: self.viewModel.threadData.disappearingMessagesConfiguration! + config: config, + using: self.viewModel.dependencies ) ) navigationController?.pushViewController(viewController, animated: true) - break + + case (.userCount, _, _, _), (.none, _, _, _), (.notificationSettings, _, _, _): openSettings() } } @@ -79,7 +114,8 @@ extension ConversationVC: } } } - } + }, + using: self.viewModel.dependencies ) ) navigationController?.pushViewController(viewController, animated: true) @@ -90,19 +126,20 @@ extension ConversationVC: @objc func startCall(_ sender: Any?) { guard SessionCall.isEnabled else { return } guard viewModel.threadData.threadIsBlocked == false else { return } - guard Storage.shared[.areCallsEnabled] else { + guard viewModel.dependencies[singleton: .storage, key: .areCallsEnabled] else { let confirmationModal: ConfirmationModal = ConfirmationModal( info: ConfirmationModal.Info( title: "callsPermissionsRequired".localized(), body: .text("callsPermissionsRequiredDescription".localized()), confirmTitle: "sessionSettings".localized(), dismissOnConfirm: false // Custom dismissal logic - ) { [weak self] _ in + ) { [weak self, dependencies = viewModel.dependencies] _ in self?.dismiss(animated: true) { let navController: UINavigationController = StyledNavigationController( rootViewController: SessionTableViewController( viewModel: PrivacySettingsViewModel( - shouldShowCloseButton: true + shouldShowCloseButton: true, + using: dependencies ) ) ) @@ -116,18 +153,28 @@ extension ConversationVC: return } - Permissions.requestMicrophonePermissionIfNeeded() + Permissions.requestMicrophonePermissionIfNeeded(using: viewModel.dependencies) let threadId: String = self.viewModel.threadData.threadId - guard AVAudioSession.sharedInstance().recordPermission == .granted else { return } - guard self.viewModel.threadData.threadVariant == .contact else { return } - guard AppEnvironment.shared.callManager.currentCall == nil else { return } - guard let call: SessionCall = Storage.shared.read({ db in SessionCall(db, for: threadId, uuid: UUID().uuidString.lowercased(), mode: .offer, outgoing: true) }) else { - return - } + guard + AVAudioSession.sharedInstance().recordPermission == .granted, + self.viewModel.threadData.threadVariant == .contact, + viewModel.dependencies[singleton: .callManager].currentCall == nil, + let call: SessionCall = viewModel.dependencies[singleton: .storage] + .read({ [dependencies = viewModel.dependencies] db in + SessionCall( + db, + for: threadId, + uuid: UUID().uuidString.lowercased(), + mode: .offer, + outgoing: true, + using: dependencies + ) + }) + else { return } - let callVC = CallVC(for: call) + let callVC = CallVC(for: call, using: viewModel.dependencies) callVC.conversationVC = self hideInputAccessoryView() resignFirstResponder() @@ -245,7 +292,7 @@ extension ConversationVC: // MARK: - ExpandingAttachmentsButtonDelegate func handleGIFButtonTapped() { - guard Storage.shared[.isGiphyEnabled] else { + guard viewModel.dependencies[singleton: .storage, key: .isGiphyEnabled] else { let modal: ConfirmationModal = ConfirmationModal( info: ConfirmationModal.Info( title: "giphyWarning".localized(), @@ -255,8 +302,8 @@ extension ConversationVC: .localized() ), confirmTitle: "theContinue".localized() - ) { [weak self] _ in - Storage.shared.writeAsync( + ) { [weak self, dependencies = viewModel.dependencies] _ in + dependencies[singleton: .storage].writeAsync( updates: { db in db[.isGiphyEnabled] = true }, @@ -273,7 +320,7 @@ extension ConversationVC: return } - let gifVC = GifPickerViewController() + let gifVC = GifPickerViewController(using: viewModel.dependencies) gifVC.delegate = self let navController = StyledNavigationController(rootViewController: gifVC) @@ -295,7 +342,7 @@ extension ConversationVC: let threadId: String = self.viewModel.threadData.threadId let threadVariant: SessionThread.Variant = self.viewModel.threadData.threadVariant - Permissions.requestLibraryPermissionIfNeeded(isSavingMedia: false) { [weak self, dependencies = viewModel.dependencies] in + Permissions.requestLibraryPermissionIfNeeded(isSavingMedia: false, using: viewModel.dependencies) { [weak self, dependencies = viewModel.dependencies] in DispatchQueue.main.async { let sendMediaNavController = SendMediaNavigationController.showingMediaLibraryFirst( threadId: threadId, @@ -310,9 +357,9 @@ extension ConversationVC: } func handleCameraButtonTapped() { - guard Permissions.requestCameraPermissionIfNeeded(presentingViewController: self) else { return } + guard Permissions.requestCameraPermissionIfNeeded(presentingViewController: self, using: viewModel.dependencies) else { return } - Permissions.requestMicrophonePermissionIfNeeded() + Permissions.requestMicrophonePermissionIfNeeded(using: viewModel.dependencies) if AVAudioSession.sharedInstance().recordPermission != .granted { SNLog("Proceeding without microphone access. Any recorded video will be silent.") @@ -368,8 +415,8 @@ extension ConversationVC: return } - let fileName = urlResourceValues.name ?? "attachment".localized() - guard let dataSource = DataSourcePath(fileUrl: url, shouldDeleteOnDeinit: false) else { + let fileName: String = (urlResourceValues.name ?? "attachment".localized()) + guard let dataSource = DataSourcePath(fileUrl: url, shouldDeleteOnDeinit: false, using: viewModel.dependencies) else { DispatchQueue.main.async { [weak self] in self?.viewModel.showToast(text: "attachmentsErrorLoad".localized()) } @@ -384,7 +431,7 @@ extension ConversationVC: } // "Document picker" attachments _SHOULD NOT_ be resized - let attachment = SignalAttachment.attachment(dataSource: dataSource, type: type, imageQuality: .original) + let attachment = SignalAttachment.attachment(dataSource: dataSource, type: type, imageQuality: .original, using: viewModel.dependencies) showAttachmentApprovalDialog(for: [ attachment ]) } @@ -403,7 +450,11 @@ extension ConversationVC: func showAttachmentApprovalDialogAfterProcessingVideo(at url: URL, with fileName: String) { ModalActivityIndicatorViewController.present(fromViewController: self, canCancel: true, message: nil) { [weak self, dependencies = viewModel.dependencies] modalActivityIndicator in - let dataSource = DataSourcePath(fileUrl: url, shouldDeleteOnDeinit: false)! + + guard let dataSource = DataSourcePath(fileUrl: url, shouldDeleteOnDeinit: false, using: dependencies) else { + self?.showErrorAlert(for: SignalAttachment.empty(using: dependencies)) + return + } dataSource.sourceFilename = fileName SignalAttachment @@ -498,16 +549,8 @@ extension ConversationVC: // use it to determine if the user is creating a new thread and update the 'isApproved' // flags appropriately let oldThreadShouldBeVisible: Bool = (self.viewModel.threadData.threadShouldBeVisible == true) - let sentTimestampMs: Int64 = SnodeAPI.currentOffsetTimestampMs() + let sentTimestampMs: Int64 = viewModel.dependencies[cache: .snodeAPI].currentOffsetTimestampMs() - // If this was a message request then approve it - approveMessageRequestIfNeeded( - for: self.viewModel.threadData.threadId, - threadVariant: self.viewModel.threadData.threadVariant, - isNewThread: !oldThreadShouldBeVisible, - timestampMs: (sentTimestampMs - 1) // Set 1ms earlier as this is used for sorting - ) - // Optimistically insert the outgoing message (this will trigger a UI update) self.viewModel.sentMessageBeforeUpdate = true let optimisticData: ConversationViewModel.OptimisticMessageData = self.viewModel.optimisticallyAppendOutgoingMessage( @@ -518,7 +561,17 @@ extension ConversationVC: quoteModel: quoteModel ) - sendMessage(optimisticData: optimisticData) + // If this was a message request then approve it + approveMessageRequestIfNeeded( + for: self.viewModel.threadData.threadId, + threadVariant: self.viewModel.threadData.threadVariant, + isNewThread: !oldThreadShouldBeVisible, + timestampMs: (sentTimestampMs - 1) // Set 1ms earlier as this is used for sorting + ).sinkUntilComplete( + receiveCompletion: { [weak self] _ in + self?.sendMessage(optimisticData: optimisticData) + } + ) } private func sendMessage(optimisticData: ConversationViewModel.OptimisticMessageData) { @@ -528,10 +581,11 @@ extension ConversationVC: DispatchQueue.global(qos:.userInitiated).async(using: viewModel.dependencies) { [dependencies = viewModel.dependencies] in // Generate the quote thumbnail if needed (want this to happen outside of the DBWrite thread as // this can take up to 0.5s - let quoteThumbnailAttachment: Attachment? = optimisticData.quoteModel?.attachment?.cloneAsQuoteThumbnail() + let quoteThumbnailAttachment: Attachment? = optimisticData.quoteModel?.attachment? + .cloneAsQuoteThumbnail(using: dependencies) // Actually send the message - dependencies.storage + dependencies[singleton: .storage] .writePublisher { [weak self] db in // Update the thread to be visible (if it isn't already) if self?.viewModel.threadData.threadShouldBeVisible == false { @@ -540,7 +594,9 @@ extension ConversationVC: .updateAllAndConfig( db, SessionThread.Columns.shouldBeVisible.set(to: true), - SessionThread.Columns.pinnedPriority.set(to: LibSession.visiblePriority) + SessionThread.Columns.pinnedPriority.set(to: LibSession.visiblePriority), + calledFromConfig: nil, + using: dependencies ) } @@ -574,8 +630,9 @@ extension ConversationVC: try LinkPreview( url: linkPreviewDraft.urlString, title: linkPreviewDraft.title, - attachmentId: try optimisticData.linkPreviewAttachment?.inserted(db).id - ).save(db) + attachmentId: try optimisticData.linkPreviewAttachment?.inserted(db).id, + using: dependencies + ).upsert(db) } } @@ -593,7 +650,7 @@ extension ConversationVC: // Process any attachments try Attachment.process( db, - data: optimisticData.attachmentData, + attachments: optimisticData.attachmentData, for: insertedInteraction.id ) @@ -621,15 +678,15 @@ extension ConversationVC: } func handleMessageSent() { - if Storage.shared[.playNotificationSoundInForeground] { + if viewModel.dependencies[singleton: .storage, key: .playNotificationSoundInForeground] { let soundID = Preferences.Sound.systemSoundId(for: .messageSent, quiet: true) AudioServicesPlaySystemSound(soundID) } let threadId: String = self.viewModel.threadData.threadId - Storage.shared.writeAsync { db in - TypingIndicators.didStopTyping(db, threadId: threadId, direction: .outgoing) + viewModel.dependencies[singleton: .storage].writeAsync { [dependencies = viewModel.dependencies] db in + dependencies[singleton: .typingIndicators].didStopTyping(db, threadId: threadId, direction: .outgoing) _ = try SessionThread .filter(id: threadId) @@ -649,8 +706,8 @@ extension ConversationVC: confirmTitle: "enable".localized(), confirmStyle: .danger, cancelStyle: .alert_text - ) { [weak self] _ in - Storage.shared.writeAsync { db in + ) { [weak self, dependencies = viewModel.dependencies] _ in + dependencies[singleton: .storage].writeAsync { db in db[.areLinkPreviewsEnabled] = true } @@ -673,18 +730,18 @@ extension ConversationVC: let threadVariant: SessionThread.Variant = self.viewModel.threadData.threadVariant let threadIsMessageRequest: Bool = (self.viewModel.threadData.threadIsMessageRequest == true) let threadIsBlocked: Bool = (self.viewModel.threadData.threadIsBlocked == true) - let needsToStartTypingIndicator: Bool = TypingIndicators.didStartTypingNeedsToStart( + let needsToStartTypingIndicator: Bool = viewModel.dependencies[singleton: .typingIndicators].didStartTypingNeedsToStart( threadId: threadId, threadVariant: threadVariant, threadIsBlocked: threadIsBlocked, threadIsMessageRequest: threadIsMessageRequest, direction: .outgoing, - timestampMs: SnodeAPI.currentOffsetTimestampMs() + timestampMs: viewModel.dependencies[cache: .snodeAPI].currentOffsetTimestampMs() ) if needsToStartTypingIndicator { - Storage.shared.writeAsync { db in - TypingIndicators.start(db, threadId: threadId, direction: .outgoing) + viewModel.dependencies[singleton: .storage].writeAsync { [dependencies = viewModel.dependencies] db in + dependencies[singleton: .typingIndicators].start(db, threadId: threadId, direction: .outgoing) } } } @@ -697,8 +754,8 @@ extension ConversationVC: func didPasteImageFromPasteboard(_ image: UIImage) { guard let imageData = image.jpegData(compressionQuality: 1.0) else { return } - let dataSource = DataSourceValue(data: imageData, dataType: .jpeg) - let attachment = SignalAttachment.attachment(dataSource: dataSource, type: .jpeg, imageQuality: .medium) + let dataSource = DataSourceValue(data: imageData, dataType: .jpeg, using: viewModel.dependencies) + let attachment = SignalAttachment.attachment(dataSource: dataSource, type: .jpeg, imageQuality: .medium, using: viewModel.dependencies) guard let approvalVC = AttachmentApprovalViewController.wrappedInNavController( threadId: self.viewModel.threadData.threadId, @@ -823,17 +880,18 @@ extension ConversationVC: let actions: [ContextMenuVC.Action] = ContextMenuVC.actions( for: cellViewModel, recentEmojis: (self.viewModel.threadData.recentReactionEmoji ?? []).compactMap { EmojiWithSkinTones(rawValue: $0) }, - currentUserPublicKey: self.viewModel.threadData.currentUserPublicKey, - currentUserBlinded15PublicKey: self.viewModel.threadData.currentUserBlinded15PublicKey, - currentUserBlinded25PublicKey: self.viewModel.threadData.currentUserBlinded25PublicKey, - currentUserIsOpenGroupModerator: OpenGroupManager.isUserModeratorOrAdmin( - self.viewModel.threadData.currentUserPublicKey, + currentUserSessionId: self.viewModel.threadData.currentUserSessionId, + currentUserBlinded15SessionId: self.viewModel.threadData.currentUserBlinded15SessionId, + currentUserBlinded25SessionId: self.viewModel.threadData.currentUserBlinded25SessionId, + currentUserIsOpenGroupModerator: viewModel.dependencies[singleton: .openGroupManager].isUserModeratorOrAdmin( + publicKey: self.viewModel.threadData.currentUserSessionId, for: self.viewModel.threadData.openGroupRoomToken, on: self.viewModel.threadData.openGroupServer ), currentThreadIsMessageRequest: (self.viewModel.threadData.threadIsMessageRequest == true), forMessageInfoScreen: false, - delegate: self + delegate: self, + using: viewModel.dependencies ) else { return } @@ -846,7 +904,8 @@ extension ConversationVC: snapshot: snapshot, frame: contextSnapshotView.convert(contextSnapshotView.bounds, to: keyWindow), cellViewModel: cellViewModel, - actions: actions + actions: actions, + using: viewModel.dependencies ) { [weak self] in self?.contextMenuWindow?.isHidden = true self?.contextMenuVC = nil @@ -876,8 +935,7 @@ extension ConversationVC: func handleItemTapped( _ cellViewModel: MessageViewModel, cell: UITableViewCell, - cellLocation: CGPoint, - using dependencies: Dependencies = Dependencies() + cellLocation: CGPoint ) { // For call info messages show the "call missed" modal guard cellViewModel.variant != .infoCall else { @@ -896,7 +954,7 @@ extension ConversationVC: return } - Permissions.requestMicrophonePermissionIfNeeded(presentingViewController: self) + Permissions.requestMicrophonePermissionIfNeeded(presentingViewController: self, using: viewModel.dependencies) return } @@ -925,24 +983,25 @@ extension ConversationVC: confirmStyle: .danger, cancelStyle: .textPrimary, dismissOnConfirm: false // Custom dismissal logic - ) { [weak self] _ in - dependencies.storage.writeAsync { db in - let userPublicKey: String = getUserHexEncodedPublicKey(db, using: dependencies) - let currentTimestampMs: Int64 = SnodeAPI.currentOffsetTimestampMs() + ) { [weak self, dependencies = viewModel.dependencies] _ in + dependencies[singleton: .storage].writeAsync { db in + let userSessionId: SessionId = dependencies[cache: .general].sessionId + let currentTimestampMs: Int64 = dependencies[cache: .snodeAPI].currentOffsetTimestampMs() let interactionId = try messageDisappearingConfig .saved(db) .insertControlMessage( db, threadVariant: cellViewModel.threadVariant, - authorId: userPublicKey, + authorId: userSessionId.hexString, timestampMs: currentTimestampMs, serverHash: nil, - serverExpirationTimestamp: nil + serverExpirationTimestamp: nil, + using: dependencies ) let expirationTimerUpdateMessage: ExpirationTimerUpdate = ExpirationTimerUpdate() - .with(sentTimestamp: UInt64(currentTimestampMs)) + .with(sentTimestampMs: UInt64(currentTimestampMs)) .with(messageDisappearingConfig) try MessageSender.send( @@ -958,7 +1017,8 @@ extension ConversationVC: .update( db, sessionId: cellViewModel.threadId, - disappearingMessagesConfig: messageDisappearingConfig + disappearingMessagesConfig: messageDisappearingConfig, + using: dependencies ) } self?.dismiss(animated: true, completion: nil) @@ -1029,8 +1089,8 @@ extension ConversationVC: let threadId: String = self.viewModel.threadData.threadId // Retry downloading the failed attachment - dependencies.storage.writeAsync { db in - dependencies.jobRunner.add( + viewModel.dependencies[singleton: .storage].writeAsync { [dependencies = viewModel.dependencies] db in + dependencies[singleton: .jobRunner].add( db, job: Job( variant: .attachmentDownload, @@ -1040,8 +1100,7 @@ extension ConversationVC: attachmentId: mediaView.attachment.id ) ), - canStartJob: true, - using: dependencies + canStartJob: true ) } break @@ -1052,8 +1111,8 @@ extension ConversationVC: guard albumView.numItems > 1 || !mediaView.attachment.isVideo else { guard - let originalFilePath: String = mediaView.attachment.originalFilePath, - FileManager.default.fileExists(atPath: originalFilePath) + let originalFilePath: String = mediaView.attachment.originalFilePath(using: viewModel.dependencies), + viewModel.dependencies[singleton: .fileManager].fileExists(atPath: originalFilePath) else { return SNLog("Missing video file") } /// When playing media we need to change the AVAudioSession to 'playback' mode so the device "silent mode" @@ -1070,7 +1129,8 @@ extension ConversationVC: threadVariant: self.viewModel.threadData.threadVariant, interactionId: cellViewModel.id, selectedAttachmentId: mediaView.attachment.id, - options: [ .sliderEnabled, .showAllMediaButton ] + options: [ .sliderEnabled, .showAllMediaButton ], + using: viewModel.dependencies ) if let viewController: UIViewController = viewController { @@ -1099,7 +1159,7 @@ extension ConversationVC: guard !handleLinkTapIfNeeded(cell: cell, targetView: (cell as? VisibleMessageCell)?.documentView), let attachment: Attachment = cellViewModel.attachments?.first, - let originalFilePath: String = attachment.originalFilePath + let originalFilePath: String = attachment.originalFilePath(using: viewModel.dependencies) else { return } /// When playing media we need to change the AVAudioSession to 'playback' mode so the device "silent mode" @@ -1113,7 +1173,7 @@ extension ConversationVC: guard !handleLinkTapIfNeeded(cell: cell, targetView: (cell as? VisibleMessageCell)?.documentView), let attachment: Attachment = cellViewModel.attachments?.first, - let originalFilePath: String = attachment.originalFilePath + let originalFilePath: String = attachment.originalFilePath(using: viewModel.dependencies) else { return } let fileUrl: URL = URL(fileURLWithPath: originalFilePath) @@ -1166,7 +1226,7 @@ extension ConversationVC: // If the message contains both links and a quote, and the user tapped on the quote; OR the // message only contained a quote, then scroll to the quote case (true, true, _, .some(let quote), _), (false, _, _, .some(let quote), _): - let maybeOriginalInteractionInfo: Interaction.TimestampInfo? = Storage.shared.read { db in + let maybeOriginalInteractionInfo: Interaction.TimestampInfo? = viewModel.dependencies[singleton: .storage].read { db in try quote.originalInteraction .select(.id, .timestampMs) .asRequest(of: Interaction.TimestampInfo.self) @@ -1233,6 +1293,7 @@ extension ConversationVC: confirmStyle: .danger, cancelTitle: "urlCopy".localized(), cancelStyle: .alert_text, + hasCloseButton: true, onConfirm: { [weak self] _ in UIApplication.shared.open(url, options: [:], completionHandler: nil) self?.showInputAccessoryView() @@ -1247,18 +1308,29 @@ extension ConversationVC: self.present(modal, animated: true) } - func handleReplyButtonTapped(for cellViewModel: MessageViewModel, using dependencies: Dependencies) { - reply(cellViewModel, using: dependencies) + func handleReplyButtonTapped(for cellViewModel: MessageViewModel) { + reply(cellViewModel) } - func startThread(with sessionId: String, openGroupServer: String?, openGroupPublicKey: String?) { - guard viewModel.threadData.canWrite else { return } - // FIXME: Add in support for starting a thread with a 'blinded25' id + func startThread( + with sessionId: String, + openGroupServer: String?, + openGroupPublicKey: String? + ) { + guard viewModel.threadData.canWrite(using: viewModel.dependencies) else { return } + // FIXME: Add in support for starting a thread with a 'blinded25' id (disabled until we support this decoding) guard (try? SessionId.Prefix(from: sessionId)) != .blinded25 else { return } guard (try? SessionId.Prefix(from: sessionId)) == .blinded15 else { - Storage.shared.write { db in - try SessionThread - .fetchOrCreate(db, id: sessionId, variant: .contact, shouldBeVisible: nil) + viewModel.dependencies[singleton: .storage].write { [dependencies = viewModel.dependencies] db in + try SessionThread.fetchOrCreate( + db, + id: sessionId, + variant: .contact, + creationDateTimestamp: (dependencies[cache: .snodeAPI].currentOffsetTimestampMs() / 1000), + shouldBeVisible: nil, + calledFromConfig: nil, + using: dependencies + ) } let conversationVC: ConversationVC = ConversationVC( @@ -1277,14 +1349,15 @@ extension ConversationVC: return } - let targetThreadId: String? = Storage.shared.write { db in + let targetThreadId: String? = viewModel.dependencies[singleton: .storage].write { [dependencies = viewModel.dependencies] db in let lookup: BlindedIdLookup = try BlindedIdLookup .fetchOrCreate( db, blindedId: sessionId, openGroupServer: openGroupServer, openGroupPublicKey: openGroupPublicKey, - isCheckingForOutbox: false + isCheckingForOutbox: false, + using: dependencies ) return try SessionThread @@ -1292,7 +1365,10 @@ extension ConversationVC: db, id: (lookup.sessionId ?? lookup.blindedId), variant: .contact, - shouldBeVisible: nil + creationDateTimestamp: (dependencies[cache: .snodeAPI].currentOffsetTimestampMs() / 1000), + shouldBeVisible: nil, + calledFromConfig: nil, + using: dependencies ) .id } @@ -1320,7 +1396,7 @@ extension ConversationVC: .elements else { return } - let reactionListSheet: ReactionListSheet = ReactionListSheet(for: cellViewModel.id) { [weak self] in + let reactionListSheet: ReactionListSheet = ReactionListSheet(for: cellViewModel.id, using: viewModel.dependencies) { [weak self] in self?.currentReactionListSheet = nil } reactionListSheet.delegate = self @@ -1328,8 +1404,8 @@ extension ConversationVC: allMessages, selectedReaction: selectedReaction, initialLoad: true, - shouldShowClearAllButton: OpenGroupManager.isUserModeratorOrAdmin( - self.viewModel.threadData.currentUserPublicKey, + shouldShowClearAllButton: viewModel.dependencies[singleton: .openGroupManager].isUserModeratorOrAdmin( + publicKey: self.viewModel.threadData.currentUserSessionId, for: self.viewModel.threadData.openGroupRoomToken, on: self.viewModel.threadData.openGroupServer ) @@ -1365,19 +1441,19 @@ extension ConversationVC: UIView.setAnimationsEnabled(true) } - func react(_ cellViewModel: MessageViewModel, with emoji: EmojiWithSkinTones, using dependencies: Dependencies) { - react(cellViewModel, with: emoji.rawValue, remove: false, using: dependencies) + func react(_ cellViewModel: MessageViewModel, with emoji: EmojiWithSkinTones) { + react(cellViewModel, with: emoji.rawValue, remove: false) } - func removeReact(_ cellViewModel: MessageViewModel, for emoji: EmojiWithSkinTones, using dependencies: Dependencies) { - react(cellViewModel, with: emoji.rawValue, remove: true, using: dependencies) + func removeReact(_ cellViewModel: MessageViewModel, for emoji: EmojiWithSkinTones) { + react(cellViewModel, with: emoji.rawValue, remove: true) } - func removeAllReactions(_ cellViewModel: MessageViewModel, for emoji: String, using dependencies: Dependencies) { + func removeAllReactions(_ cellViewModel: MessageViewModel, for emoji: String) { guard cellViewModel.threadVariant == .community else { return } - Storage.shared - .readPublisher { db -> (Network.PreparedRequest, OpenGroupAPI.PendingChange) in + viewModel.dependencies[singleton: .storage] + .readPublisher { [dependencies = viewModel.dependencies] db -> (Network.PreparedRequest, OpenGroupAPI.PendingChange) in guard let openGroup: OpenGroup = try? OpenGroup .fetchOne(db, id: cellViewModel.threadId), @@ -1397,7 +1473,7 @@ extension ConversationVC: on: openGroup.server, using: dependencies ) - let pendingChange: OpenGroupAPI.PendingChange = OpenGroupManager + let pendingChange: OpenGroupAPI.PendingChange = dependencies[singleton: .openGroupManager] .addPendingReaction( emoji: emoji, id: openGroupServerMessageId, @@ -1408,23 +1484,22 @@ extension ConversationVC: return (preparedRequest, pendingChange) } - .subscribe(on: DispatchQueue.global(qos: .userInitiated)) - .flatMap { preparedRequest, pendingChange in + .subscribe(on: DispatchQueue.global(qos: .userInitiated), using: viewModel.dependencies) + .flatMap { [dependencies = viewModel.dependencies] preparedRequest, pendingChange in preparedRequest.send(using: dependencies) .handleEvents( receiveOutput: { _, response in - OpenGroupManager - .updatePendingChange( - pendingChange, - seqNo: response.seqNo - ) + dependencies[singleton: .openGroupManager].updatePendingChange( + pendingChange, + seqNo: response.seqNo + ) } ) .eraseToAnyPublisher() } .sinkUntilComplete( - receiveCompletion: { _ in - Storage.shared.writeAsync { db in + receiveCompletion: { [dependencies = viewModel.dependencies] _ in + dependencies[singleton: .storage].writeAsync { db in _ = try Reaction .filter(Reaction.Columns.interactionId == cellViewModel.id) .filter(Reaction.Columns.emoji == emoji) @@ -1434,12 +1509,7 @@ extension ConversationVC: ) } - func react( - _ cellViewModel: MessageViewModel, - with emoji: String, - remove: Bool, - using dependencies: Dependencies = Dependencies() - ) { + func react(_ cellViewModel: MessageViewModel, with emoji: String, remove: Bool) { guard self.viewModel.threadData.threadIsMessageRequest != true && ( cellViewModel.variant == .standardIncoming || @@ -1450,12 +1520,12 @@ extension ConversationVC: // Perform local rate limiting (don't allow more than 20 reactions within 60 seconds) let threadVariant: SessionThread.Variant = self.viewModel.threadData.threadVariant let openGroupRoom: String? = self.viewModel.threadData.openGroupRoomToken - let sentTimestamp: Int64 = SnodeAPI.currentOffsetTimestampMs() - let recentReactionTimestamps: [Int64] = dependencies.caches[.general].recentReactionTimestamps + let sentTimestampMs: Int64 = viewModel.dependencies[cache: .snodeAPI].currentOffsetTimestampMs() + let recentReactionTimestamps: [Int64] = viewModel.dependencies[cache: .general].recentReactionTimestamps guard recentReactionTimestamps.count < 20 || - (sentTimestamp - (recentReactionTimestamps.first ?? sentTimestamp)) > (60 * 1000) + (sentTimestampMs - (recentReactionTimestamps.first ?? sentTimestampMs)) > (60 * 1000) else { let toastController: ToastController = ToastController( text: "emojiReactsCoolDown".localized(), @@ -1469,10 +1539,10 @@ extension ConversationVC: return } - dependencies.caches.mutate(cache: .general) { + viewModel.dependencies.mutate(cache: .general) { $0.recentReactionTimestamps = Array($0.recentReactionTimestamps .suffix(19)) - .appending(sentTimestamp) + .appending(sentTimestampMs) } typealias OpenGroupInfo = ( @@ -1483,7 +1553,7 @@ extension ConversationVC: /// Perform the sending logic, we generate the pending reaction first in a deferred future closure to prevent the OpenGroup /// cache from blocking either the main thread or the database write thread - Deferred { + Deferred { [dependencies = viewModel.dependencies] in Future { resolver in guard threadVariant == .community, @@ -1494,7 +1564,7 @@ extension ConversationVC: // Create the pending change if we have open group info return resolver(Result.success( - OpenGroupManager.addPendingReaction( + dependencies[singleton: .openGroupManager].addPendingReaction( emoji: emoji, id: serverMessageId, in: openGroupServer, @@ -1504,21 +1574,26 @@ extension ConversationVC: )) } } - .subscribe(on: DispatchQueue.global(qos: .userInitiated), using: dependencies) - .flatMap { pendingChange -> AnyPublisher<(MessageSender.PreparedSendData?, OpenGroupInfo?), Error> in - dependencies.storage.writePublisher { [weak self] db -> (MessageSender.PreparedSendData?, OpenGroupInfo?) in + .subscribe(on: DispatchQueue.global(qos: .userInitiated), using: viewModel.dependencies) + .flatMap { [dependencies = viewModel.dependencies] pendingChange -> AnyPublisher, Error> in + dependencies[singleton: .storage].writePublisher { [weak self] db -> Network.PreparedRequest in // Update the thread to be visible (if it isn't already) if self?.viewModel.threadData.threadShouldBeVisible == false { _ = try SessionThread .filter(id: cellViewModel.threadId) - .updateAllAndConfig(db, SessionThread.Columns.shouldBeVisible.set(to: true)) + .updateAllAndConfig( + db, + SessionThread.Columns.shouldBeVisible.set(to: true), + calledFromConfig: nil, + using: dependencies + ) } let pendingReaction: Reaction? = { guard !remove else { return try? Reaction .filter(Reaction.Columns.interactionId == cellViewModel.id) - .filter(Reaction.Columns.authorId == cellViewModel.currentUserPublicKey) + .filter(Reaction.Columns.authorId == cellViewModel.currentUserSessionId) .filter(Reaction.Columns.emoji == emoji) .fetchOne(db) } @@ -1532,8 +1607,8 @@ extension ConversationVC: return Reaction( interactionId: cellViewModel.id, serverHash: nil, - timestampMs: sentTimestamp, - authorId: cellViewModel.currentUserPublicKey, + timestampMs: sentTimestampMs, + authorId: cellViewModel.currentUserSessionId, emoji: emoji, count: 1, sortId: sortId @@ -1544,7 +1619,7 @@ extension ConversationVC: if remove { try Reaction .filter(Reaction.Columns.interactionId == cellViewModel.id) - .filter(Reaction.Columns.authorId == cellViewModel.currentUserPublicKey) + .filter(Reaction.Columns.authorId == cellViewModel.currentUserSessionId) .filter(Reaction.Columns.emoji == emoji) .deleteAll(db) } @@ -1562,7 +1637,7 @@ extension ConversationVC: let openGroupServer: String = cellViewModel.threadOpenGroupServer, let openGroupRoom: String = openGroupRoom, let pendingChange: OpenGroupAPI.PendingChange = pendingChange, - OpenGroupManager.doesOpenGroupSupport(db, capability: .reactions, on: openGroupServer) + dependencies[singleton: .openGroupManager].doesOpenGroupSupport(db, capability: .reactions, on: openGroupServer) else { throw MessageSenderError.invalidMessage } let preparedRequest: Network.PreparedRequest = try { @@ -1591,19 +1666,37 @@ extension ConversationVC: .map { _, response in response.seqNo } }() - return (nil, (pendingReaction, pendingChange, preparedRequest)) + return preparedRequest + .handleEvents( + receiveOutput: { _, seqNo in + dependencies[singleton: .openGroupManager].updatePendingChange( + pendingChange, + seqNo: seqNo + ) + }, + receiveCompletion: { [weak self] result in + switch result { + case .finished: break + case .failure: + dependencies[singleton: .openGroupManager].removePendingChange(pendingChange) + + self?.handleReactionSentFailure(pendingReaction, remove: remove) + } + } + ) + .map { _, _ in () } default: - let sendData: MessageSender.PreparedSendData = try MessageSender.preparedSendData( + return try MessageSender.preparedSend( db, message: VisibleMessage( - sentTimestamp: UInt64(sentTimestamp), + sentTimestampMs: UInt64(sentTimestampMs), text: nil, reaction: VisibleMessage.VMReaction( timestamp: UInt64(cellViewModel.timestampMs), publicKey: { guard cellViewModel.variant == .standardIncoming else { - return cellViewModel.currentUserPublicKey + return cellViewModel.currentUserSessionId } return cellViewModel.authorId @@ -1618,53 +1711,19 @@ extension ConversationVC: .from(db, threadId: cellViewModel.threadId, threadVariant: cellViewModel.threadVariant) .defaultNamespace, interactionId: cellViewModel.id, + fileIds: [], using: dependencies ) - - return (sendData, nil) } } } - .tryFlatMap { messageSendData, openGroupInfo -> AnyPublisher in - switch (messageSendData, openGroupInfo) { - case (.some(let sendData), _): - return MessageSender.sendImmediate(data: sendData, using: dependencies) - - case (_, .some(let info)): - return info.preparedRequest.send(using: dependencies) - .handleEvents( - receiveOutput: { _, seqNo in - OpenGroupManager - .updatePendingChange( - info.pendingChange, - seqNo: seqNo - ) - }, - receiveCompletion: { [weak self] result in - switch result { - case .finished: break - case .failure: - OpenGroupManager.removePendingChange(info.pendingChange) - - self?.handleReactionSentFailure( - info.pendingReaction, - remove: remove - ) - } - } - ) - .map { _ in () } - .eraseToAnyPublisher() - - default: throw MessageSenderError.invalidMessage - } - } + .flatMap { [dependencies = viewModel.dependencies] request in request.send(using: dependencies) } .sinkUntilComplete() } func handleReactionSentFailure(_ pendingReaction: Reaction?, remove: Bool) { guard let pendingReaction = pendingReaction else { return } - Storage.shared.writeAsync { db in + viewModel.dependencies[singleton: .storage].writeAsync { db in // Reverse the database if remove { try pendingReaction.insert(db) @@ -1679,18 +1738,19 @@ extension ConversationVC: } } - func showFullEmojiKeyboard(_ cellViewModel: MessageViewModel, using dependencies: Dependencies) { + func showFullEmojiKeyboard(_ cellViewModel: MessageViewModel) { hideInputAccessoryView() let emojiPicker = EmojiPickerSheet( completionHandler: { [weak self] emoji in guard let emoji: EmojiWithSkinTones = emoji else { return } - self?.react(cellViewModel, with: emoji, using: dependencies) + self?.react(cellViewModel, with: emoji) }, dismissHandler: { [weak self] in self?.showInputAccessoryView() - } + }, + using: self.viewModel.dependencies ) present(emojiPicker, animated: true, completion: nil) @@ -1719,7 +1779,7 @@ extension ConversationVC: ) ), confirmTitle: "join".localized(), - onConfirm: { modal in + onConfirm: { [dependencies = viewModel.dependencies] modal in guard let presentingViewController: UIViewController = modal.presentingViewController else { return } @@ -1738,23 +1798,24 @@ extension ConversationVC: return presentingViewController.present(errorModal, animated: true, completion: nil) } - Storage.shared + dependencies[singleton: .storage] .writePublisher { db in - OpenGroupManager.shared.add( + dependencies[singleton: .openGroupManager].add( db, roomToken: room, server: server, publicKey: publicKey, - calledFromConfigHandling: false + calledFromConfig: nil ) } .flatMap { successfullyAddedGroup in - OpenGroupManager.shared.performInitialRequestsAfterAdd( + dependencies[singleton: .openGroupManager].performInitialRequestsAfterAdd( + queue: DispatchQueue.global(qos: .userInitiated), successfullyAddedGroup: successfullyAddedGroup, roomToken: room, server: server, publicKey: publicKey, - calledFromConfigHandling: false + calledFromConfig: nil ) } .subscribe(on: DispatchQueue.global(qos: .userInitiated)) @@ -1767,11 +1828,11 @@ extension ConversationVC: // If there was a failure then the group will be in invalid state until // the next launch so remove it (the user will be left on the previous // screen so can re-trigger the join) - Storage.shared.writeAsync { db in - OpenGroupManager.shared.delete( + dependencies[singleton: .storage].writeAsync { db in + try dependencies[singleton: .openGroupManager].delete( db, openGroupId: OpenGroup.idFor(roomToken: room, server: server), - calledFromConfigHandling: false + calledFromConfig: nil ) } @@ -1800,34 +1861,35 @@ extension ConversationVC: // MARK: - ContextMenuActionDelegate - func info(_ cellViewModel: MessageViewModel, using dependencies: Dependencies) { + func info(_ cellViewModel: MessageViewModel) { let actions: [ContextMenuVC.Action] = ContextMenuVC.actions( for: cellViewModel, recentEmojis: [], - currentUserPublicKey: self.viewModel.threadData.currentUserPublicKey, - currentUserBlinded15PublicKey: self.viewModel.threadData.currentUserBlinded15PublicKey, - currentUserBlinded25PublicKey: self.viewModel.threadData.currentUserBlinded25PublicKey, - currentUserIsOpenGroupModerator: OpenGroupManager.isUserModeratorOrAdmin( - self.viewModel.threadData.currentUserPublicKey, + currentUserSessionId: self.viewModel.threadData.currentUserSessionId, + currentUserBlinded15SessionId: self.viewModel.threadData.currentUserBlinded15SessionId, + currentUserBlinded25SessionId: self.viewModel.threadData.currentUserBlinded25SessionId, + currentUserIsOpenGroupModerator: viewModel.dependencies[singleton: .openGroupManager].isUserModeratorOrAdmin( + publicKey: self.viewModel.threadData.currentUserSessionId, for: self.viewModel.threadData.openGroupRoomToken, on: self.viewModel.threadData.openGroupServer ), currentThreadIsMessageRequest: (self.viewModel.threadData.threadIsMessageRequest == true), forMessageInfoScreen: true, delegate: self, - using: dependencies + using: viewModel.dependencies ) ?? [] let messageInfoViewController = MessageInfoViewController( actions: actions, - messageViewModel: cellViewModel + messageViewModel: cellViewModel, + using: viewModel.dependencies ) DispatchQueue.main.asyncAfter(deadline: .now() + 0.2) { [weak self] in self?.navigationController?.pushViewController(messageInfoViewController, animated: true) } } - func retry(_ cellViewModel: MessageViewModel, using dependencies: Dependencies) { + func retry(_ cellViewModel: MessageViewModel) { guard cellViewModel.id != MessageViewModel.optimisticUpdateId else { guard let optimisticMessageId: UUID = cellViewModel.optimisticMessageId, @@ -1852,7 +1914,7 @@ extension ConversationVC: return } - dependencies.storage.writeAsync { [weak self] db in + viewModel.dependencies[singleton: .storage].writeAsync { [weak self, dependencies = viewModel.dependencies] db in guard let threadId: String = self?.viewModel.threadData.threadId, let threadVariant: SessionThread.Variant = self?.viewModel.threadData.threadVariant, @@ -1880,7 +1942,7 @@ extension ConversationVC: return nil }() try quote.with( - attachmentId: attachment?.cloneAsQuoteThumbnail()?.inserted(db).id + attachmentId: attachment?.cloneAsQuoteThumbnail(using: dependencies)?.inserted(db).id ).update(db) } @@ -1899,7 +1961,7 @@ extension ConversationVC: } } - func reply(_ cellViewModel: MessageViewModel, using dependencies: Dependencies) { + func reply(_ cellViewModel: MessageViewModel) { let maybeQuoteDraft: QuotedReplyModel? = QuotedReplyModel.quotedReplyForSending( threadId: self.viewModel.threadData.threadId, authorId: cellViewModel.authorId, @@ -1908,9 +1970,9 @@ extension ConversationVC: timestampMs: cellViewModel.timestampMs, attachments: cellViewModel.attachments, linkPreviewAttachment: cellViewModel.linkPreviewAttachment, - currentUserPublicKey: cellViewModel.currentUserPublicKey, - currentUserBlinded15PublicKey: cellViewModel.currentUserBlinded15PublicKey, - currentUserBlinded25PublicKey: cellViewModel.currentUserBlinded25PublicKey + currentUserSessionId: cellViewModel.currentUserSessionId, + currentUserBlinded15SessionId: cellViewModel.currentUserBlinded15SessionId, + currentUserBlinded25SessionId: cellViewModel.currentUserBlinded25SessionId ) guard let quoteDraft: QuotedReplyModel = maybeQuoteDraft else { return } @@ -1922,7 +1984,7 @@ extension ConversationVC: snInputView.becomeFirstResponder() } - func copy(_ cellViewModel: MessageViewModel, using dependencies: Dependencies) { + func copy(_ cellViewModel: MessageViewModel) { switch cellViewModel.cellType { case .typingIndicator, .dateHeader, .unreadMarker: break @@ -1944,7 +2006,7 @@ extension ConversationVC: attachment.state == .uploaded ), let type: UTType = UTType(sessionMimeType: attachment.contentType), - let originalFilePath: String = attachment.originalFilePath, + let originalFilePath: String = attachment.originalFilePath(using: viewModel.dependencies), let data: Data = try? Data(contentsOf: URL(fileURLWithPath: originalFilePath)) else { return } @@ -1953,312 +2015,99 @@ extension ConversationVC: } func copySessionID(_ cellViewModel: MessageViewModel) { - guard cellViewModel.variant == .standardIncoming || cellViewModel.variant == .standardIncomingDeleted else { - return - } + guard cellViewModel.variant == .standardIncoming else { return } UIPasteboard.general.string = cellViewModel.authorId } - func delete(_ cellViewModel: MessageViewModel, using dependencies: Dependencies) { - switch cellViewModel.variant { - case .standardIncomingDeleted, .infoCall, - .infoScreenshotNotification, .infoMediaSavedNotification, - .infoClosedGroupCreated, .infoClosedGroupUpdated, - .infoClosedGroupCurrentUserLeft, .infoClosedGroupCurrentUserLeaving, .infoClosedGroupCurrentUserErrorLeaving, - .infoMessageRequestAccepted, .infoDisappearingMessagesUpdate: - // Info messages and unsent messages should just trigger a local - // deletion (they are created as side effects so we wouldn't be - // able to delete them for all participants anyway) - Storage.shared.writeAsync { db in - _ = try Interaction - .filter(id: cellViewModel.id) - .deleteAll(db) - } - return - - case .standardOutgoing, .standardIncoming: break - } + func delete(_ cellViewModel: MessageViewModel) { + /// Retrieve the deletion actions for the selected message(s) of there are any + let messagesToDelete: [MessageViewModel] = [cellViewModel] - let userPublicKey: String = getUserHexEncodedPublicKey() + guard let deletionBehaviours: MessageViewModel.DeletionBehaviours = self.viewModel.deletionActions(for: messagesToDelete) else { + return + } - // Remote deletion logic - func deleteRemotely(from viewController: UIViewController?, request: AnyPublisher, onComplete: (() -> ())?) { - // Show a loading indicator - Deferred { - Future { resolver in - DispatchQueue.main.async { - ModalActivityIndicatorViewController.present(fromViewController: viewController, canCancel: false) { _ in - resolver(Result.success(())) - } + let modal: ConfirmationModal = ConfirmationModal( + info: ConfirmationModal.Info( + title: deletionBehaviours.title, + body: .radio( + explanation: NSAttributedString(string: deletionBehaviours.body), + warning: deletionBehaviours.warning.map { NSAttributedString(string: $0) }, + options: deletionBehaviours.actions.map { action in + ( + action.title, + action.state != .disabled, + action.state == .enabledAndDefaultSelected, + action.accessibility + ) } - } - } - .flatMap { _ in request } - .subscribe(on: DispatchQueue.global(qos: .userInitiated)) - .receive(on: DispatchQueue.main) - .sinkUntilComplete( - receiveCompletion: { [weak self] result in - switch result { - case .failure: break - case .finished: - // Delete the interaction (and associated data) from the database - Storage.shared.writeAsync { db in - _ = try Interaction - .filter(id: cellViewModel.id) - .deleteAll(db) - } + ), + confirmTitle: "delete".localized(), + confirmStyle: .danger, + cancelTitle: "cancel".localized(), + cancelStyle: .alert_text, + onConfirm: { [weak self, dependencies = viewModel.dependencies] modal in + /// Determine the selected action index + let selectedIndex: Int = { + switch modal.info.body { + case .radio(_, _, let options): + return options + .enumerated() + .first(where: { _, value in value.selected }) + .map { index, _ in index } + .defaulting(to: 0) - // Stop it's audio if needed - self?.viewModel.stopAudioIfNeeded(for: cellViewModel) - } + default: return 0 + } + }() - // Regardless of success we should dismiss and callback - if self?.presentedViewController is ModalActivityIndicatorViewController { - self?.dismiss(animated: true, completion: nil) // Dismiss the loader + /// Stop the messages audio if needed + messagesToDelete.forEach { cellViewModel in + self?.viewModel.stopAudioIfNeeded(for: cellViewModel) } - onComplete?() + /// Trigger the deletion behaviours + deletionBehaviours + .publisherForAction(at: selectedIndex, using: dependencies) + .sinkUntilComplete( + receiveCompletion: { result in + DispatchQueue.main.async { + self?.viewModel.showToast( + text: { + switch result { + case .finished: + return "deleteMessageDeleted" + .putNumber(messagesToDelete.count) + .localized() + + case .failure: + return "deleteMessageFailed" + .putNumber(messagesToDelete.count) + .localized() + } + }(), + backgroundColor: .backgroundSecondary, + inset: (self?.inputAccessoryView?.frame.height ?? Values.mediumSpacing) + Values.smallSpacing + ) + } + } + ) + }, + afterClosed: { [weak self] in + self?.becomeFirstResponder() } ) - } + ) - // How we delete the message differs depending on the type of thread - switch cellViewModel.threadVariant { - // Handle open group messages the old way - case .community: - // If it's an incoming message the user must have moderator status - let result: (openGroupServerMessageId: Int64?, openGroup: OpenGroup?)? = Storage.shared.read { db -> (Int64?, OpenGroup?) in - ( - try Interaction - .select(.openGroupServerMessageId) - .filter(id: cellViewModel.id) - .asRequest(of: Int64.self) - .fetchOne(db), - try OpenGroup.fetchOne(db, id: cellViewModel.threadId) - ) - } - - guard - let openGroup: OpenGroup = result?.openGroup, - let openGroupServerMessageId: Int64 = result?.openGroupServerMessageId, ( - cellViewModel.variant != .standardIncoming || - OpenGroupManager.isUserModeratorOrAdmin( - userPublicKey, - for: openGroup.roomToken, - on: openGroup.server - ) - ) - else { - // If the message hasn't been sent yet then just delete locally - guard cellViewModel.state == .sending || cellViewModel.state == .failed else { return } - - // Retrieve any message send jobs for this interaction - let jobs: [Job] = Storage.shared - .read { db in - try? Job - .filter(Job.Columns.variant == Job.Variant.messageSend) - .filter(Job.Columns.interactionId == cellViewModel.id) - .fetchAll(db) - } - .defaulting(to: []) - - // If the job is currently running then wait until it's done before triggering - // the deletion - let targetJob: Job? = jobs.first(where: { JobRunner.isCurrentlyRunning($0) }) - - guard targetJob == nil else { - JobRunner.afterJob(targetJob, state: .running) { [weak self] result in - switch result { - // If it succeeded then we'll need to delete from the server so re-run - // this function (if we still don't have the server id for some reason - // then this would result in a local-only deletion which should be fine - case .succeeded: self?.delete(cellViewModel) - - // Otherwise we just need to cancel the pending job (in case it retries) - // and delete the interaction - default: - JobRunner.removePendingJob(targetJob) - - Storage.shared.writeAsync { db in - _ = try Interaction - .filter(id: cellViewModel.id) - .deleteAll(db) - } - - // Stop it's audio if needed - self?.viewModel.stopAudioIfNeeded(for: cellViewModel) - } - } - return - } - - // If it's not currently running then remove any pending jobs (just to be safe) and - // delete the interaction locally - jobs.forEach { JobRunner.removePendingJob($0) } - - Storage.shared.writeAsync { db in - _ = try Interaction - .filter(id: cellViewModel.id) - .deleteAll(db) - } - - // Stop it's audio if needed - viewModel.stopAudioIfNeeded(for: cellViewModel) - return - } - - // Delete the message from the open group - deleteRemotely( - from: self, - request: Storage.shared - .readPublisher { db in - try OpenGroupAPI.preparedMessageDelete( - db, - id: openGroupServerMessageId, - in: openGroup.roomToken, - on: openGroup.server, - using: dependencies - ) - } - .flatMap { $0.send(using: dependencies) } - .map { _ in () } - .eraseToAnyPublisher() - ) { [weak self] in - self?.showInputAccessoryView() - } - - case .contact, .legacyGroup, .group: - let targetPublicKey: String = (cellViewModel.threadVariant == .contact ? - userPublicKey : - cellViewModel.threadId - ) - let serverHash: String? = Storage.shared.read { db -> String? in - try Interaction - .select(.serverHash) - .filter(id: cellViewModel.id) - .asRequest(of: String.self) - .fetchOne(db) - } - let unsendRequest: UnsendRequest = UnsendRequest( - timestamp: UInt64(cellViewModel.timestampMs), - author: (cellViewModel.variant == .standardOutgoing ? - userPublicKey : - cellViewModel.authorId - ) - ) - .with( - expiresInSeconds: cellViewModel.expiresInSeconds, - expiresStartedAtMs: cellViewModel.expiresStartedAtMs - ) - - // For incoming interactions or interactions with no serverHash just delete them locally - guard cellViewModel.variant == .standardOutgoing, let serverHash: String = serverHash else { - Storage.shared.writeAsync { db in - _ = try Interaction - .filter(id: cellViewModel.id) - .deleteAll(db) - - // No need to send the unsendRequest if there is no serverHash (ie. the message - // was outgoing but never got to the server) - guard serverHash != nil else { return } - - MessageSender - .send( - db, - message: unsendRequest, - threadId: cellViewModel.threadId, - interactionId: nil, - to: .contact(publicKey: userPublicKey), - using: dependencies - ) - } - - // Stop it's audio if needed - viewModel.stopAudioIfNeeded(for: cellViewModel) - return - } - - let actionSheet: UIAlertController = UIAlertController(title: nil, message: nil, preferredStyle: .actionSheet) - actionSheet.addAction(UIAlertAction( - title: "deleteMessageDeviceOnly".localized(), - accessibilityIdentifier: "Delete for me", - style: .destructive - ) { [weak self] _ in - Storage.shared.writeAsync { db in - _ = try Interaction - .filter(id: cellViewModel.id) - .deleteAll(db) - - MessageSender - .send( - db, - message: unsendRequest, - threadId: cellViewModel.threadId, - interactionId: nil, - to: .contact(publicKey: userPublicKey), - using: dependencies - ) - } - self?.showInputAccessoryView() - - // Stop it's audio if needed - self?.viewModel.stopAudioIfNeeded(for: cellViewModel) - }) - - actionSheet.addAction(UIAlertAction( - title: { - switch (cellViewModel.threadVariant, cellViewModel.threadId) { - case (.legacyGroup, _), (.group, _): return "clearMessagesForEveryone".localized() - case (_, userPublicKey): return "deleteMessageDevicesAll".localized() - default: return "deleteMessageEveryone".localized() - } - }(), - accessibilityIdentifier: "Delete for everyone", - style: .destructive - ) { [weak self] _ in - let completeServerDeletion = { - Storage.shared.writeAsync { db in - try MessageSender - .send( - db, - message: unsendRequest, - interactionId: nil, - threadId: cellViewModel.threadId, - threadVariant: cellViewModel.threadVariant, - using: dependencies - ) - } - } - - // We can only delete messages on the server for `contact` and `group` conversations - guard cellViewModel.threadVariant == .contact || cellViewModel.threadVariant == .group else { - return completeServerDeletion() - } - - deleteRemotely( - from: self, - request: SnodeAPI - .deleteMessages( - swarmPublicKey: targetPublicKey, - serverHashes: [serverHash] - ) - .map { _ in () } - .eraseToAnyPublisher() - ) { completeServerDeletion() } - }) - - actionSheet.addAction(UIAlertAction.init(title: "cancel".localized(), style: .cancel) { [weak self] _ in - self?.showInputAccessoryView() - }) - - self.hideInputAccessoryView() - Modal.setupForIPadIfNeeded(actionSheet, targetView: self.view) - self.present(actionSheet, animated: true) + /// Show the modal after a small delay so it doesn't look as weird with the context menu dismissal + DispatchQueue.main.asyncAfter(deadline: .now() + .milliseconds(Int(ContextMenuVC.dismissDurationPartOne * 1000))) { [weak self] in + self?.present(modal, animated: true) + self?.resignFirstResponder() } } - func save(_ cellViewModel: MessageViewModel, using dependencies: Dependencies) { + func save(_ cellViewModel: MessageViewModel) { guard cellViewModel.cellType == .mediaMessage else { return } let mediaAttachments: [(Attachment, String)] = (cellViewModel.attachments ?? []) @@ -2270,7 +2119,9 @@ extension ConversationVC: ) } .compactMap { attachment in - guard let originalFilePath: String = attachment.originalFilePath else { return nil } + guard let originalFilePath: String = attachment.originalFilePath(using: viewModel.dependencies) else { + return nil + } return (attachment, originalFilePath) } @@ -2279,7 +2130,8 @@ extension ConversationVC: Permissions.requestLibraryPermissionIfNeeded( isSavingMedia: true, - presentingViewController: self + presentingViewController: self, + using: viewModel.dependencies ) { [weak self] in mediaAttachments.forEach { attachment, originalFilePath in PHPhotoLibrary.shared().performChanges( @@ -2308,7 +2160,7 @@ extension ConversationVC: } } - func ban(_ cellViewModel: MessageViewModel, using dependencies: Dependencies) { + func ban(_ cellViewModel: MessageViewModel) { guard cellViewModel.threadVariant == .community else { return } let threadId: String = self.viewModel.threadData.threadId @@ -2320,8 +2172,8 @@ extension ConversationVC: confirmTitle: "theContinue".localized(), confirmStyle: .danger, cancelStyle: .alert_text, - onConfirm: { [weak self] _ in - Storage.shared + onConfirm: { [weak self, dependencies = viewModel.dependencies] _ in + dependencies[singleton: .storage] .readPublisher { db -> Network.PreparedRequest in guard let openGroup: OpenGroup = try OpenGroup.fetchOne(db, id: threadId) else { throw StorageError.objectNotFound @@ -2337,8 +2189,8 @@ extension ConversationVC: ) } .flatMap { $0.send(using: dependencies) } - .subscribe(on: DispatchQueue.global(qos: .userInitiated)) - .receive(on: DispatchQueue.main) + .subscribe(on: DispatchQueue.global(qos: .userInitiated), using: dependencies) + .receive(on: DispatchQueue.main, using: dependencies) .sinkUntilComplete( receiveCompletion: { result in switch result { @@ -2347,7 +2199,7 @@ extension ConversationVC: self?.viewModel.showToast( text: "banUserBanned".localized(), backgroundColor: .backgroundSecondary, - insect: (self?.inputAccessoryView?.frame.height ?? Values.mediumSpacing) + Values.smallSpacing + inset: (self?.inputAccessoryView?.frame.height ?? Values.mediumSpacing) + Values.smallSpacing ) } case .failure: @@ -2355,7 +2207,7 @@ extension ConversationVC: self?.viewModel.showToast( text: "banErrorFailed".localized(), backgroundColor: .backgroundSecondary, - insect: (self?.inputAccessoryView?.frame.height ?? Values.mediumSpacing) + Values.smallSpacing + inset: (self?.inputAccessoryView?.frame.height ?? Values.mediumSpacing) + Values.smallSpacing ) } } @@ -2370,7 +2222,7 @@ extension ConversationVC: self.present(modal, animated: true) } - func banAndDeleteAllMessages(_ cellViewModel: MessageViewModel, using dependencies: Dependencies) { + func banAndDeleteAllMessages(_ cellViewModel: MessageViewModel) { guard cellViewModel.threadVariant == .community else { return } let threadId: String = self.viewModel.threadData.threadId @@ -2382,8 +2234,8 @@ extension ConversationVC: confirmTitle: "theContinue".localized(), confirmStyle: .danger, cancelStyle: .alert_text, - onConfirm: { [weak self] _ in - Storage.shared + onConfirm: { [weak self, dependencies = viewModel.dependencies] _ in + dependencies[singleton: .storage] .readPublisher { db in guard let openGroup: OpenGroup = try OpenGroup.fetchOne(db, id: threadId) else { throw StorageError.objectNotFound @@ -2399,8 +2251,8 @@ extension ConversationVC: ) } .flatMap { $0.send(using: dependencies) } - .subscribe(on: DispatchQueue.global(qos: .userInitiated)) - .receive(on: DispatchQueue.main) + .subscribe(on: DispatchQueue.global(qos: .userInitiated), using: dependencies) + .receive(on: DispatchQueue.main, using: dependencies) .sinkUntilComplete( receiveCompletion: { result in switch result { @@ -2409,7 +2261,7 @@ extension ConversationVC: self?.viewModel.showToast( text: "banUserBanned".localized(), backgroundColor: .backgroundSecondary, - insect: (self?.inputAccessoryView?.frame.height ?? Values.mediumSpacing) + Values.smallSpacing + inset: (self?.inputAccessoryView?.frame.height ?? Values.mediumSpacing) + Values.smallSpacing ) } case .failure: @@ -2417,7 +2269,7 @@ extension ConversationVC: self?.viewModel.showToast( text: "banErrorFailed".localized(), backgroundColor: .backgroundSecondary, - insect: (self?.inputAccessoryView?.frame.height ?? Values.mediumSpacing) + Values.smallSpacing + inset: (self?.inputAccessoryView?.frame.height ?? Values.mediumSpacing) + Values.smallSpacing ) } } @@ -2434,9 +2286,9 @@ extension ConversationVC: // MARK: - VoiceMessageRecordingViewDelegate - func startVoiceMessageRecording(using dependencies: Dependencies) { + func startVoiceMessageRecording() { // Request permission if needed - Permissions.requestMicrophonePermissionIfNeeded() { [weak self] in + Permissions.requestMicrophonePermissionIfNeeded(using: viewModel.dependencies) { [weak self] in DispatchQueue.main.async { self?.cancelVoiceMessageRecording() } @@ -2450,8 +2302,9 @@ extension ConversationVC: self.viewModel.stopAudio() // Create URL - let directory: String = Singleton.appContext.temporaryDirectory - let fileName: String = "\(SnodeAPI.currentOffsetTimestampMs()).m4a" // stringlint:ignore + let currentOffsetTimestamp: Int64 = viewModel.dependencies[cache: .snodeAPI].currentOffsetTimestampMs() + let directory: String = viewModel.dependencies[singleton: .fileManager].temporaryDirectory + let fileName: String = "\(currentOffsetTimestamp).m4a" // stringlint:ignore let url: URL = URL(fileURLWithPath: directory).appendingPathComponent(fileName) // Set up audio session @@ -2483,7 +2336,7 @@ extension ConversationVC: // Limit voice messages to a minute audioTimer = Timer.scheduledTimer(withTimeInterval: 180, repeats: false, block: { [weak self] _ in self?.snInputView.hideVoiceMessageUI() - self?.endVoiceMessageRecording(using: dependencies) + self?.endVoiceMessageRecording() }) // Prepare audio recorder and start recording @@ -2512,7 +2365,7 @@ extension ConversationVC: } } - func endVoiceMessageRecording(using dependencies: Dependencies) { + func endVoiceMessageRecording() { UIApplication.shared.isIdleTimerDisabled = true // Hide the UI @@ -2548,7 +2401,7 @@ extension ConversationVC: } // Get data - let dataSourceOrNil = DataSourcePath(fileUrl: audioRecorder.url, shouldDeleteOnDeinit: true) + let dataSourceOrNil = DataSourcePath(fileUrl: audioRecorder.url, shouldDeleteOnDeinit: true, using: viewModel.dependencies) self.audioRecorder = nil guard let dataSource = dataSourceOrNil else { return SNLog("Couldn't load recorded data.") } @@ -2558,7 +2411,7 @@ extension ConversationVC: .appendingPathExtension("m4a") // stringlint:ignore dataSource.sourceFilename = fileName - let attachment = SignalAttachment.voiceMessageAttachment(dataSource: dataSource, type: .mpeg4Audio) + let attachment = SignalAttachment.voiceMessageAttachment(dataSource: dataSource, type: .mpeg4Audio, using: viewModel.dependencies) guard !attachment.hasError else { return showErrorAlert(for: attachment) @@ -2584,22 +2437,19 @@ extension ConversationVC: @objc func sendScreenshotNotification() { sendDataExtraction(kind: .screenshot) } - func sendDataExtraction( - kind: DataExtractionNotification.Kind, - using dependencies: Dependencies = Dependencies() - ) { + func sendDataExtraction(kind: DataExtractionNotification.Kind) { // Only send screenshot notifications to one-to-one conversations guard self.viewModel.threadData.threadVariant == .contact else { return } let threadId: String = self.viewModel.threadData.threadId let threadVariant: SessionThread.Variant = self.viewModel.threadData.threadVariant - dependencies.storage.writeAsync { db in + viewModel.dependencies[singleton: .storage].writeAsync { [dependencies = viewModel.dependencies] db in try MessageSender.send( db, message: DataExtractionNotification( kind: kind, - sentTimestamp: UInt64(SnodeAPI.currentOffsetTimestampMs()) + sentTimestampMs: dependencies[cache: .snodeAPI].currentOffsetTimestampMs() ) .with(DisappearingMessagesConfiguration .fetchOne(db, id: threadId)? @@ -2646,11 +2496,8 @@ extension ConversationVC { for threadId: String, threadVariant: SessionThread.Variant, isNewThread: Bool, - timestampMs: Int64, - using dependencies: Dependencies = Dependencies() - ) { - guard threadVariant == .contact else { return } - + timestampMs: Int64 + ) -> AnyPublisher { let updateNavigationBackStack: () -> Void = { // Remove the 'SessionTableViewController' from the nav hierarchy if present DispatchQueue.main.async { [weak self] in @@ -2668,73 +2515,148 @@ extension ConversationVC { } } } - - // If the contact doesn't exist then we should create it so we can store the 'isApproved' state - // (it'll be updated with correct profile info if they accept the message request so this - // shouldn't cause weird behaviours) - guard - let contact: Contact = Storage.shared.read({ db in Contact.fetchOrCreate(db, id: threadId) }), - !contact.isApproved - else { return } - Storage.shared - .writePublisher { db in - // If we aren't creating a new thread (ie. sending a message request) then send a - // messageRequestResponse back to the sender (this allows the sender to know that - // they have been approved and can now use this contact in closed groups) - if !isNewThread { - let interaction = try? Interaction( - threadId: threadId, - threadVariant: threadVariant, - authorId: getUserHexEncodedPublicKey(db), - variant: .infoMessageRequestAccepted, - body: "messageRequestYouHaveAccepted" - .put(key: "name", value: self.viewModel.threadData.displayName) - .localized(), - timestampMs: timestampMs - ).inserted(db) - - try MessageSender.send( - db, - message: MessageRequestResponse( - isApproved: true, - sentTimestampMs: UInt64(timestampMs) - ), - interactionId: interaction?.id, - threadId: threadId, - threadVariant: threadVariant, - using: dependencies + switch threadVariant { + case .contact: + // If the contact doesn't exist then we should create it so we can store the 'isApproved' state + // (it'll be updated with correct profile info if they accept the message request so this + // shouldn't cause weird behaviours) + guard + let contact: Contact = viewModel.dependencies[singleton: .storage].read({ [dependencies = viewModel.dependencies] db in + Contact.fetchOrCreate(db, id: threadId, using: dependencies) + }), + !contact.isApproved + else { return Just(()).eraseToAnyPublisher() } + + return viewModel.dependencies[singleton: .storage] + .writePublisher { [displayName = self.viewModel.threadData.displayName, dependencies = viewModel.dependencies] db in + // If we aren't creating a new thread (ie. sending a message request) then send a + // messageRequestResponse back to the sender (this allows the sender to know that + // they have been approved and can now use this contact in closed groups) + if !isNewThread { + _ = try? Interaction( + threadId: threadId, + threadVariant: threadVariant, + authorId: dependencies[cache: .general].sessionId.hexString, + variant: .infoMessageRequestAccepted, + body: "messageRequestYouHaveAccepted" + .put(key: "name", value: displayName) + .localized(), + timestampMs: timestampMs, + using: dependencies + ).inserted(db) + + try MessageSender.send( + db, + message: MessageRequestResponse( + isApproved: true, + sentTimestampMs: UInt64(timestampMs) + ), + interactionId: nil, + threadId: threadId, + threadVariant: threadVariant, + using: dependencies + ) + } + + // Default 'didApproveMe' to true for the person approving the message request + try contact.upsert(db) + try Contact + .filter(id: contact.id) + .updateAllAndConfig( + db, + Contact.Columns.isApproved.set(to: true), + Contact.Columns.didApproveMe + .set(to: contact.didApproveMe || !isNewThread), + calledFromConfig: nil, + using: dependencies + ) + } + .map { _ in () } + .catch { _ in Just(()).eraseToAnyPublisher() } + .handleEvents( + receiveOutput: { _ in + // Update the UI + updateNavigationBackStack() + } ) - } + .eraseToAnyPublisher() - // Default 'didApproveMe' to true for the person approving the message request - try contact.save(db) - try Contact - .filter(id: contact.id) - .updateAllAndConfig( - db, - Contact.Columns.isApproved.set(to: true), - Contact.Columns.didApproveMe - .set(to: contact.didApproveMe || !isNewThread) + case .group: + // If the group is not in the invited state then don't bother doing anything + guard + let group: ClosedGroup = viewModel.dependencies[singleton: .storage].read({ db in + try ClosedGroup.fetchOne(db, id: threadId) + }), + group.invited == true + else { return Just(()).eraseToAnyPublisher() } + + return viewModel.dependencies[singleton: .storage] + .writePublisher { [dependencies = viewModel.dependencies] db in + /// Remove any existing `infoGroupInfoInvited` interactions from the group (don't want to have a duplicate one from + /// inside the group history) + _ = try Interaction + .filter(Interaction.Columns.threadId == group.id) + .filter(Interaction.Columns.variant == Interaction.Variant.infoGroupInfoInvited) + .deleteAll(db) + + /// Optimistically insert a `standard` member for the current user in this group (it'll be update to the correct + /// one once we receive the first `GROUP_MEMBERS` config message but adding it here means the `canWrite` + /// state of the group will continue to be `true` while we wait on the initial poll to get back) + try GroupMember( + groupId: group.id, + profileId: dependencies[cache: .general].sessionId.hexString, + role: .standard, + roleStatus: .accepted, + isHidden: false + ).upsert(db) + + /// If we aren't creating a new thread (ie. sending a message request) and the user is not an admin + /// then schedule sending a `GroupUpdateInviteResponseMessage` to the group (this allows + /// other members to know that the user has joined the group) + if !isNewThread && group.groupIdentityPrivateKey == nil { + try MessageSender.send( + db, + message: GroupUpdateInviteResponseMessage( + isApproved: true, + sentTimestampMs: UInt64(timestampMs) + ), + interactionId: nil, + threadId: threadId, + threadVariant: threadVariant, + using: dependencies + ) + } + + /// Actually trigger the approval + try ClosedGroup.approveGroupIfNeeded( + db, + group: group, + calledFromConfig: nil, + using: dependencies + ) + } + .map { _ in () } + .catch { _ in Just(()).eraseToAnyPublisher() } + .handleEvents( + receiveOutput: { _ in + // Update the UI + updateNavigationBackStack() + } ) - } - .subscribe(on: DispatchQueue.global(qos: .userInitiated)) - .receive(on: DispatchQueue.main) - .sinkUntilComplete( - receiveCompletion: { _ in - // Update the UI - updateNavigationBackStack() - } - ) + .eraseToAnyPublisher() + + default: return Just(()).eraseToAnyPublisher() + } } func acceptMessageRequest() { - self.approveMessageRequestIfNeeded( + approveMessageRequestIfNeeded( for: self.viewModel.threadData.threadId, threadVariant: self.viewModel.threadData.threadVariant, isNewThread: false, - timestampMs: SnodeAPI.currentOffsetTimestampMs() - ) + timestampMs: viewModel.dependencies[cache: .snodeAPI].currentOffsetTimestampMs() + ).sinkUntilComplete() } func declineMessageRequest() { @@ -2745,7 +2667,8 @@ extension ConversationVC { tableView: self.tableView, threadViewModel: self.viewModel.threadData, viewController: self, - navigatableStateHolder: nil + navigatableStateHolder: nil, + using: viewModel.dependencies ) guard let action: UIContextualAction = actions?.first else { return } @@ -2769,7 +2692,8 @@ extension ConversationVC { tableView: self.tableView, threadViewModel: self.viewModel.threadData, viewController: self, - navigatableStateHolder: nil + navigatableStateHolder: nil, + using: viewModel.dependencies ) guard let action: UIContextualAction = actions?.first else { return } @@ -2790,7 +2714,7 @@ extension ConversationVC { extension ConversationVC: MediaPresentationContextProvider { func mediaPresentationContext(mediaItem: Media, in coordinateSpace: UICoordinateSpace) -> MediaPresentationContext? { - guard case let .gallery(galleryItem) = mediaItem else { return nil } + guard case let .gallery(galleryItem, _) = mediaItem else { return nil } // Note: According to Apple's docs the 'indexPathsForVisibleRows' method returns an // unsorted array which means we can't use it to determine the desired 'visibleCell' diff --git a/Session/Conversations/ConversationVC.swift b/Session/Conversations/ConversationVC.swift index b5ced6e7a45..e62cf80b0a9 100644 --- a/Session/Conversations/ConversationVC.swift +++ b/Session/Conversations/ConversationVC.swift @@ -74,8 +74,7 @@ final class ConversationVC: BaseVC, LibSessionRespondingViewController, Conversa } override var inputAccessoryView: UIView? { - return ( - (viewModel.threadData.canWrite && isShowingSearchUI) ? + return (viewModel.threadData.canWrite(using: viewModel.dependencies) && isShowingSearchUI ? searchController.resultsBar : snInputView ) @@ -98,7 +97,7 @@ final class ConversationVC: BaseVC, LibSessionRespondingViewController, Conversa return margin <= ConversationVC.scrollToBottomMargin } - lazy var mnemonic: String = { ((try? Identity.mnemonic()) ?? "") }() + lazy var mnemonic: String = { ((try? Identity.mnemonic(using: viewModel.dependencies)) ?? "") }() // FIXME: Would be good to create a Swift-based cache and replace this lazy var mediaCache: NSCache = { @@ -129,10 +128,9 @@ final class ConversationVC: BaseVC, LibSessionRespondingViewController, Conversa var scrollButtonBottomConstraint: NSLayoutConstraint? var scrollButtonMessageRequestsBottomConstraint: NSLayoutConstraint? var messageRequestsViewBotomConstraint: NSLayoutConstraint? - var emptyStateLabelTopConstraint: NSLayoutConstraint? lazy var titleView: ConversationTitleView = { - let result: ConversationTitleView = ConversationTitleView() + let result: ConversationTitleView = ConversationTitleView(using: viewModel.dependencies) let tapGestureRecognizer = UITapGestureRecognizer( target: self, action: #selector(handleTitleViewTapped) @@ -151,7 +149,7 @@ final class ConversationVC: BaseVC, LibSessionRespondingViewController, Conversa result.contentInset = UIEdgeInsets( top: 0, leading: 0, - bottom: (viewModel.threadData.canWrite ? + bottom: (viewModel.threadData.canWrite(using: viewModel.dependencies) ? Values.mediumSpacing : (Values.mediumSpacing + (UIApplication.shared.keyWindow?.safeAreaInsets.bottom ?? 0)) ), @@ -175,7 +173,8 @@ final class ConversationVC: BaseVC, LibSessionRespondingViewController, Conversa lazy var snInputView: InputView = InputView( threadVariant: self.viewModel.initialThreadVariant, - delegate: self + delegate: self, + using: self.viewModel.dependencies ) lazy var unreadCountView: UIView = { @@ -201,7 +200,13 @@ final class ConversationVC: BaseVC, LibSessionRespondingViewController, Conversa }() lazy var stateStackView: UIStackView = { - let result: UIStackView = UIStackView(arrangedSubviews: [ outdatedClientBanner, emptyStateLabelContainer ]) + let result: UIStackView = UIStackView(arrangedSubviews: [ + outdatedClientBanner, + legacyGroupsBanner, + blockedBanner, + emptyStatePaddingView, + emptyStateLabelContainer + ]) result.axis = .vertical result.spacing = Values.smallSpacing result.alignment = .fill @@ -210,39 +215,67 @@ final class ConversationVC: BaseVC, LibSessionRespondingViewController, Conversa }() lazy var outdatedClientBanner: InfoBanner = { - let info: InfoBanner.Info = InfoBanner.Info( - message: "disappearingMessagesLegacy" - .put(key: "name", value: self.viewModel.threadData.displayName) - .localized(), - backgroundColor: .primary, - messageFont: .systemFont(ofSize: Values.verySmallFontSize), - messageTintColor: .messageBubble_outgoingText, - messageLabelAccessibilityLabel: "Outdated client banner text", - height: 40 + let result: InfoBanner = InfoBanner( + info: InfoBanner.Info( + font: .systemFont(ofSize: Values.verySmallFontSize), + message: "disappearingMessagesLegacy" + .put(key: "name", value: self.viewModel.threadData.displayName) + .localized(), + icon: .close, + tintColor: .messageBubble_outgoingText, + backgroundColor: .primary, + accessibility: Accessibility(label: "Outdated client banner"), + labelAccessibility: Accessibility(label: "Outdated client banner text"), + height: 40, + onTap: { [weak self] in self?.removeOutdatedClientBanner() } + ) ) - let result: InfoBanner = InfoBanner(info: info, dismiss: { [weak self] in - self?.removeOutdatedClientBanner() - }) - result.accessibilityLabel = "Outdated client banner" - result.isAccessibilityElement = true + + return result + }() + + lazy var legacyGroupsBanner: InfoBanner = { + let result: InfoBanner = InfoBanner( + info: InfoBanner.Info( + font: .systemFont(ofSize: Values.miniFontSize), + message: "groupLegacyBanner" + .put(key: "date", value: Features.legacyGroupDepricationDate.formattedForBanner) + .localized(), + icon: .link, + tintColor: .messageBubble_outgoingText, + backgroundColor: .primary, + accessibility: Accessibility(label: "Legacy group banner"), + height: nil, + onTap: { [weak self] in self?.openUrl(Features.legacyGroupDepricationUrl) } + ) + ) + result.isHidden = (self.viewModel.threadData.threadVariant != .legacyGroup) return result }() lazy var blockedBanner: InfoBanner = { - let info: InfoBanner.Info = InfoBanner.Info( - message: self.viewModel.blockedBannerMessage, - backgroundColor: .danger, - messageFont: .boldSystemFont(ofSize: Values.smallFontSize), - messageTintColor: .textPrimary, - messageLabelAccessibilityLabel: "Blocked banner text", - height: 54 + let result: InfoBanner = InfoBanner( + info: InfoBanner.Info( + font: .boldSystemFont(ofSize: Values.smallFontSize), + message: self.viewModel.blockedBannerMessage, + icon: .none, + tintColor: .textPrimary, + backgroundColor: .danger, + accessibility: Accessibility(label: "Blocked banner"), + labelAccessibility: Accessibility(label: "Blocked banner text"), + height: 54, + onTap: { [weak self] in self?.unblock() } + ) ) - let result: InfoBanner = InfoBanner(info: info) - result.accessibilityLabel = "Blocked banner" - result.isAccessibilityElement = true - let tapGestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(unblock)) - result.addGestureRecognizer(tapGestureRecognizer) + result.isHidden = true + + return result + }() + + private lazy var emptyStatePaddingView: UIView = { + let result: UIView = UIView() + result.set(.height, to: Values.largeSpacing) return result }() @@ -257,20 +290,12 @@ final class ConversationVC: BaseVC, LibSessionRespondingViewController, Conversa }() private lazy var emptyStateLabel: UILabel = { - let text: String = emptyStateText(for: viewModel.threadData) let result: UILabel = UILabel() result.isAccessibilityElement = true - result.accessibilityIdentifier = "Empty state label" - result.accessibilityLabel = "Empty state label" + result.accessibilityIdentifier = "Control message" result.translatesAutoresizingMaskIntoConstraints = false result.font = .systemFont(ofSize: Values.verySmallFontSize) - result.attributedText = NSAttributedString(string: text) - .adding( - attributes: [.font: UIFont.boldSystemFont(ofSize: Values.verySmallFontSize)], - range: text.range(of: self.viewModel.threadData.displayName) - .map { NSRange($0, in: text) } - .defaulting(to: NSRange(location: 0, length: 0)) - ) + result.attributedText = viewModel.emptyStateText(for: viewModel.threadData).formatted(in: result) result.themeTextColor = .textSecondary result.textAlignment = .center result.lineBreakMode = .byWordWrapping @@ -311,9 +336,10 @@ final class ConversationVC: BaseVC, LibSessionRespondingViewController, Conversa lazy var messageRequestFooterView: MessageRequestFooterView = MessageRequestFooterView( threadVariant: self.viewModel.threadData.threadVariant, - canWrite: self.viewModel.threadData.canWrite, + canWrite: self.viewModel.threadData.canWrite(using: self.viewModel.dependencies), threadIsMessageRequest: (self.viewModel.threadData.threadIsMessageRequest == true), threadRequiresApproval: (self.viewModel.threadData.threadRequiresApproval == true), + closedGroupAdminProfile: self.viewModel.threadData.closedGroupAdminProfile, onBlock: { [weak self] in self?.blockMessageRequest() }, onAccept: { [weak self] in self?.acceptMessageRequest() }, onDecline: { [weak self] in self?.declineMessageRequest() } @@ -341,9 +367,14 @@ final class ConversationVC: BaseVC, LibSessionRespondingViewController, Conversa focusedInteractionInfo: Interaction.TimestampInfo? = nil, using dependencies: Dependencies ) { - self.viewModel = ConversationViewModel(threadId: threadId, threadVariant: threadVariant, focusedInteractionInfo: focusedInteractionInfo, using: dependencies) + self.viewModel = ConversationViewModel( + threadId: threadId, + threadVariant: threadVariant, + focusedInteractionInfo: focusedInteractionInfo, + using: dependencies + ) - Storage.shared.addObserver(viewModel.pagedDataObserver) + dependencies[singleton: .storage].addObserver(viewModel.pagedDataObserver) super.init(nibName: nil, bundle: nil) } @@ -375,7 +406,8 @@ final class ConversationVC: BaseVC, LibSessionRespondingViewController, Conversa ) titleView.initialSetup( with: self.viewModel.initialThreadVariant, - isNoteToSelf: self.viewModel.threadData.threadIsNoteToSelf + isNoteToSelf: self.viewModel.threadData.threadIsNoteToSelf, + isMessageRequest: (self.viewModel.threadData.threadIsMessageRequest == true) ) // Constraints @@ -390,7 +422,6 @@ final class ConversationVC: BaseVC, LibSessionRespondingViewController, Conversa stateStackView.pin(.top, to: .top, of: view, withInset: 0) stateStackView.pin(.leading, to: .leading, of: view, withInset: 0) stateStackView.pin(.trailing, to: .trailing, of: view, withInset: 0) - self.emptyStateLabelTopConstraint = emptyStateLabel.pin(.top, to: .top, of: emptyStateLabelContainer, withInset: Values.largeSpacing) scrollButton.pin(.trailing, to: .trailing, of: view, withInset: -20) messageRequestFooterView.pin(.leading, to: .leading, of: view, withInset: 16) @@ -470,16 +501,6 @@ final class ConversationVC: BaseVC, LibSessionRespondingViewController, Conversa override func viewDidAppear(_ animated: Bool) { super.viewDidAppear(animated) - /// When the `ConversationVC` is on the screen we want to store it so we can avoid sending notification without accessing the - /// main thread (we don't currently care if it's still in the nav stack though - so if a user is on a conversation settings screen this should - /// get cleared within `viewWillDisappear`) - /// - /// **Note:** We do this on an async queue because `Atomic` can block if something else is mutating it and we want to avoid - /// the risk of blocking the conversation transition - DispatchQueue.global(qos: .userInitiated).async { [weak self] in - SessionApp.currentlyOpenConversationViewController.mutate { $0 = self } - } - if delayFirstResponder || isShowingSearchUI { delayFirstResponder = false @@ -510,16 +531,6 @@ final class ConversationVC: BaseVC, LibSessionRespondingViewController, Conversa override func viewWillDisappear(_ animated: Bool) { super.viewWillDisappear(animated) - /// When the `ConversationVC` is on the screen we want to store it so we can avoid sending notification without accessing the - /// main thread (we don't currently care if it's still in the nav stack though - so if a user leaves a conversation settings screen we clear - /// it, and if a user moves to a different `ConversationVC` this will get updated to that one within `viewDidAppear`) - /// - /// **Note:** We do this on an async queue because `Atomic` can block if something else is mutating it and we want to avoid - /// the risk of blocking the conversation transition - DispatchQueue.global(qos: .userInitiated).async { - SessionApp.currentlyOpenConversationViewController.mutate { $0 = nil } - } - viewIsDisappearing = true lastPresentedViewController = self.presentedViewController @@ -558,7 +569,7 @@ final class ConversationVC: BaseVC, LibSessionRespondingViewController, Conversa using: viewModel.dependencies ) { - Storage.shared.writeAsync { db in + viewModel.dependencies[singleton: .storage].writeAsync { db in _ = try SessionThread // Intentionally use `deleteAll` here instead of `deleteOrLeave` .filter(id: threadId) .deleteAll(db) @@ -593,10 +604,10 @@ final class ConversationVC: BaseVC, LibSessionRespondingViewController, Conversa private func startObservingChanges(didReturnFromBackground: Bool = false) { guard dataChangeObservable == nil else { return } - dataChangeObservable = Storage.shared.start( + dataChangeObservable = viewModel.dependencies[singleton: .storage].start( viewModel.observableThreadData, onError: { _ in }, - onChange: { [weak self] maybeThreadData in + onChange: { [weak self, dependencies = viewModel.dependencies] maybeThreadData in guard let threadData: SessionThreadViewModel = maybeThreadData else { // If the thread data is null and the id was blinded then we just unblinded the thread // and need to swap over to the new one @@ -606,7 +617,7 @@ final class ConversationVC: BaseVC, LibSessionRespondingViewController, Conversa (try? SessionId.Prefix(from: sessionId)) == .blinded15 || (try? SessionId.Prefix(from: sessionId)) == .blinded25 ), - let blindedLookup: BlindedIdLookup = Storage.shared.read({ db in + let blindedLookup: BlindedIdLookup = dependencies[singleton: .storage].read({ db in try BlindedIdLookup .filter(id: sessionId) .fetchOne(db) @@ -630,14 +641,14 @@ final class ConversationVC: BaseVC, LibSessionRespondingViewController, Conversa // Stop observing changes self?.stopObservingChanges() - Storage.shared.removeObserver(self?.viewModel.pagedDataObserver) + dependencies[singleton: .storage].removeObserver(self?.viewModel.pagedDataObserver) // Swap the observing to the updated thread let newestVisibleMessageId: Int64? = self?.fullyVisibleCellViewModels()?.last?.id self?.viewModel.swapToThread(updatedThreadId: unblindedId, focussedMessageId: newestVisibleMessageId) // Start observing changes again - Storage.shared.addObserver(self?.viewModel.pagedDataObserver) + dependencies[singleton: .storage].addObserver(self?.viewModel.pagedDataObserver) self?.startObservingChanges() return } @@ -670,27 +681,6 @@ final class ConversationVC: BaseVC, LibSessionRespondingViewController, Conversa self.viewModel.onInteractionChange = nil } - private func emptyStateText(for threadData: SessionThreadViewModel) -> String { - switch (threadData.threadIsNoteToSelf, threadData.canWrite) { - case (true, _): - return "noteToSelfEmpty".localized() - case (_, false): - if threadData.profile?.blocksCommunityMessageRequests == true { - return "messageRequestsTurnedOff" - .put(key: "name", value: threadData.displayName) - .localized() - } else { - return "conversationsEmpty" - .put(key: "conversation_name", value: threadData.displayName) - .localized() - } - default: - return "groupNoMessages" - .put(key: "group_name", value: threadData.displayName) - .localized() - } - } - private func handleThreadUpdates(_ updatedThreadData: SessionThreadViewModel, initialLoad: Bool = false) { // Ensure the first load or a load when returning from a child screen runs without animations (if // we don't do this the cells will animate in from a frame of CGRect.zero or have a buggy transition) @@ -725,6 +715,7 @@ final class ConversationVC: BaseVC, LibSessionRespondingViewController, Conversa titleView.update( with: updatedThreadData.displayName, isNoteToSelf: updatedThreadData.threadIsNoteToSelf, + isMessageRequest: (updatedThreadData.threadIsMessageRequest == true), threadVariant: updatedThreadData.threadVariant, mutedUntilTimestamp: updatedThreadData.threadMutedUntilTimestamp, onlyNotifyForMentions: (updatedThreadData.threadOnlyNotifyForMentions == true), @@ -733,8 +724,7 @@ final class ConversationVC: BaseVC, LibSessionRespondingViewController, Conversa ) // Update the empty state - let text: String = emptyStateText(for: updatedThreadData) - emptyStateLabel.attributedText = text.formatted(in: emptyStateLabel) + emptyStateLabel.attributedText = viewModel.emptyStateText(for: updatedThreadData).formatted(in: emptyStateLabel) } if @@ -756,12 +746,13 @@ final class ConversationVC: BaseVC, LibSessionRespondingViewController, Conversa if initialLoad || - viewModel.threadData.canWrite != updatedThreadData.canWrite || + viewModel.threadData.canWrite(using: viewModel.dependencies) != updatedThreadData.canWrite(using: viewModel.dependencies) || viewModel.threadData.threadVariant != updatedThreadData.threadVariant || viewModel.threadData.threadIsMessageRequest != updatedThreadData.threadIsMessageRequest || - viewModel.threadData.threadRequiresApproval != updatedThreadData.threadRequiresApproval + viewModel.threadData.threadRequiresApproval != updatedThreadData.threadRequiresApproval || + viewModel.threadData.closedGroupAdminProfile != updatedThreadData.closedGroupAdminProfile { - if updatedThreadData.canWrite { + if updatedThreadData.canWrite(using: viewModel.dependencies) { self.showInputAccessoryView() } else { self.hideInputAccessoryView() @@ -769,12 +760,13 @@ final class ConversationVC: BaseVC, LibSessionRespondingViewController, Conversa let messageRequestsViewWasVisible: Bool = (self.messageRequestFooterView.isHidden == false) - UIView.animate(withDuration: 0.3) { [weak self] in + UIView.animate(withDuration: 0.3) { [weak self, dependencies = viewModel.dependencies] in self?.messageRequestFooterView.update( threadVariant: updatedThreadData.threadVariant, - canWrite: updatedThreadData.canWrite, + canWrite: updatedThreadData.canWrite(using: dependencies), threadIsMessageRequest: (updatedThreadData.threadIsMessageRequest == true), - threadRequiresApproval: (updatedThreadData.threadRequiresApproval == true) + threadRequiresApproval: (updatedThreadData.threadRequiresApproval == true), + closedGroupAdminProfile: updatedThreadData.closedGroupAdminProfile ) self?.scrollButtonMessageRequestsBottomConstraint?.isActive = ( self?.messageRequestFooterView.isHidden == false @@ -809,6 +801,14 @@ final class ConversationVC: BaseVC, LibSessionRespondingViewController, Conversa ) } + if + initialLoad || + viewModel.threadData.threadVariant != updatedThreadData.threadVariant || + viewModel.threadData.currentUserIsClosedGroupAdmin != updatedThreadData.currentUserIsClosedGroupAdmin + { + legacyGroupsBanner.isHidden = (updatedThreadData.threadVariant != .legacyGroup) + } + if initialLoad || viewModel.threadData.threadIsBlocked != updatedThreadData.threadIsBlocked { addOrRemoveBlockedBanner(threadIsBlocked: (updatedThreadData.threadIsBlocked == true)) } @@ -830,10 +830,12 @@ final class ConversationVC: BaseVC, LibSessionRespondingViewController, Conversa } // Now we have done all the needed diffs update the viewModel with the latest data + let oldCanWrite: Bool = viewModel.threadData.canWrite(using: viewModel.dependencies) self.viewModel.updateThreadData(updatedThreadData) - /// **Note:** This needs to happen **after** we have update the viewModel's thread data - if initialLoad || viewModel.threadData.currentUserIsClosedGroupMember != updatedThreadData.currentUserIsClosedGroupMember { + /// **Note:** This needs to happen **after** we have update the viewModel's thread data (otherwise the `inputAccessoryView` + /// won't be generated correctly) + if initialLoad || oldCanWrite != updatedThreadData.canWrite(using: viewModel.dependencies) { if !self.isFirstResponder { self.becomeFirstResponder() } @@ -1324,9 +1326,10 @@ final class ConversationVC: BaseVC, LibSessionRespondingViewController, Conversa profilePictureView.update( publicKey: threadData.threadId, // Contact thread uses the contactId threadVariant: threadData.threadVariant, - customImageData: nil, + displayPictureFilename: nil, profile: threadData.profile, - additionalProfile: nil + additionalProfile: nil, + using: viewModel.dependencies ) profilePictureView.customWidth = (44 - 16) // Width of the standard back button @@ -1462,7 +1465,10 @@ final class ConversationVC: BaseVC, LibSessionRespondingViewController, Conversa // Do not show the banner until the new disappearing messages is enabled guard currentDisappearingMessagesConfiguration?.isEnabled == true else { self.outdatedClientBanner.isHidden = true - self.emptyStateLabelTopConstraint?.constant = Values.largeSpacing + self.emptyStatePaddingView.isHidden = (stateStackView + .arrangedSubviews + .filter { !$0.isHidden } + .count > 1) return } @@ -1475,7 +1481,11 @@ final class ConversationVC: BaseVC, LibSessionRespondingViewController, Conversa completion: { [weak self] _ in self?.outdatedClientBanner.isHidden = true self?.outdatedClientBanner.alpha = 1 - self?.emptyStateLabelTopConstraint?.constant = Values.largeSpacing + self?.emptyStatePaddingView.isHidden = ((self?.stateStackView + .arrangedSubviews + .filter { !$0.isHidden }) + .defaulting(to: []) + .count > 1) } ) return @@ -1483,18 +1493,29 @@ final class ConversationVC: BaseVC, LibSessionRespondingViewController, Conversa self.outdatedClientBanner.update( message: "disappearingMessagesLegacy" - .put(key: "name", value: Profile.displayName(id: outdatedMemberId, threadVariant: self.viewModel.threadData.threadVariant)) + .put( + key: "name", + value: Profile.displayName( + id: outdatedMemberId, + threadVariant: self.viewModel.threadData.threadVariant, + using: viewModel.dependencies + ) + ) .localized(), - dismiss: self.removeOutdatedClientBanner + onTap: { [weak self] in self?.removeOutdatedClientBanner() } ) self.outdatedClientBanner.isHidden = false - self.emptyStateLabelTopConstraint?.constant = 0 + self.emptyStatePaddingView.isHidden = (stateStackView + .arrangedSubviews + .filter { !$0.isHidden } + .count > 1) } private func removeOutdatedClientBanner() { guard let outdatedMemberId: String = self.viewModel.threadData.outdatedMemberId else { return } - Storage.shared.writeAsync { db in + + viewModel.dependencies[singleton: .storage].writeAsync { db in try Contact .filter(id: outdatedMemberId) .updateAll(db, Contact.Columns.lastKnownClientVersion.set(to: nil)) @@ -1510,14 +1531,22 @@ final class ConversationVC: BaseVC, LibSessionRespondingViewController, Conversa }, completion: { [weak self] _ in self?.blockedBanner.alpha = 1 - self?.blockedBanner.removeFromSuperview() + self?.blockedBanner.isHidden = true + self?.emptyStatePaddingView.isHidden = ((self?.stateStackView + .arrangedSubviews + .filter { !$0.isHidden }) + .defaulting(to: []) + .count > 1) } ) return } - self.view.addSubview(self.blockedBanner) - self.blockedBanner.pin([ UIView.HorizontalEdge.left, UIView.VerticalEdge.top, UIView.HorizontalEdge.right ], to: self.view) + self.blockedBanner.isHidden = false + self.emptyStatePaddingView.isHidden = (stateStackView + .arrangedSubviews + .filter { !$0.isHidden } + .count > 1) } func recoverInputView(completion: (() -> ())? = nil) { @@ -1572,7 +1601,8 @@ final class ConversationVC: BaseVC, LibSessionRespondingViewController, Conversa }, showExpandedReactions: viewModel.reactionExpandedInteractionIds .contains(cellViewModel.id), - lastSearchText: viewModel.lastSearchedText + lastSearchText: viewModel.lastSearchedText, + using: viewModel.dependencies ) cell.delegate = self @@ -1863,6 +1893,7 @@ final class ConversationVC: BaseVC, LibSessionRespondingViewController, Conversa hideSearchUI() } + func conversationSearchControllerDependencies() -> Dependencies { return viewModel.dependencies } func currentVisibleIds() -> [Int64] { return (fullyVisibleCellViewModels() ?? []).map { $0.id } } func conversationSearchController(_ conversationSearchController: ConversationSearchController, didUpdateSearchResults results: [Interaction.TimestampInfo]?, searchText: String?) { diff --git a/Session/Conversations/ConversationViewModel.swift b/Session/Conversations/ConversationViewModel.swift index 3d89d4c11f5..5c911068b44 100644 --- a/Session/Conversations/ConversationViewModel.swift +++ b/Session/Conversations/ConversationViewModel.swift @@ -2,10 +2,13 @@ import Foundation import Combine +import UniformTypeIdentifiers import GRDB import DifferenceKit +import SessionSnodeKit import SessionMessagingKit import SessionUtilitiesKit +import SessionUIKit public class ConversationViewModel: OWSAudioPlayerDelegate, NavigatableStateHolder { public typealias SectionModel = ArraySection @@ -67,7 +70,8 @@ public class ConversationViewModel: OWSAudioPlayerDelegate, NavigatableStateHold case .contact: let name: String = Profile.displayName( id: threadData.threadId, - threadVariant: threadData.threadVariant + threadVariant: threadData.threadVariant, + using: dependencies ) return "blockBlockedDescription".localized() @@ -85,20 +89,22 @@ public class ConversationViewModel: OWSAudioPlayerDelegate, NavigatableStateHold using dependencies: Dependencies ) { typealias InitialData = ( - currentUserPublicKey: String, + userSessionId: SessionId, initialUnreadInteractionInfo: Interaction.TimestampInfo?, threadIsBlocked: Bool, + threadIsMessageRequest: Bool, + closedGroupAdminProfile: Profile?, currentUserIsClosedGroupMember: Bool?, currentUserIsClosedGroupAdmin: Bool?, openGroupPermissions: OpenGroup.Permissions?, - blinded15Key: String?, - blinded25Key: String? + blinded15SessionId: SessionId?, + blinded25SessionId: SessionId? ) - let initialData: InitialData? = Storage.shared.read { db -> InitialData in + let initialData: InitialData? = dependencies[singleton: .storage].read { db -> InitialData in let interaction: TypedTableAlias = TypedTableAlias() let groupMember: TypedTableAlias = TypedTableAlias() - let currentUserPublicKey: String = getUserHexEncodedPublicKey(db) + let userSessionId: SessionId = dependencies[cache: .general].sessionId // If we have a specified 'focusedInteractionInfo' then use that, otherwise retrieve the oldest // unread interaction and start focused around that one @@ -117,10 +123,45 @@ public class ConversationViewModel: OWSAudioPlayerDelegate, NavigatableStateHold .fetchOne(db) .defaulting(to: false) ) + let threadIsMessageRequest: Bool = try { + switch threadVariant { + case .contact: + let isApproved: Bool = try Contact + .filter(id: threadId) + .select(.isApproved) + .asRequest(of: Bool.self) + .fetchOne(db) + .defaulting(to: true) + + return !isApproved + + case .group: + let isInvite: Bool = try ClosedGroup + .filter(id: threadId) + .select(.invited) + .asRequest(of: Bool.self) + .fetchOne(db) + .defaulting(to: true) + + return !isInvite + + default: return false + } + }() + + let closedGroupAdminProfile: Profile? = (threadVariant != .group ? nil : + try Profile + .joining( + required: Profile.groupMembers + .filter(GroupMember.Columns.groupId == threadId) + .filter(GroupMember.Columns.role == GroupMember.Role.admin) + ) + .fetchOne(db) + ) let currentUserIsClosedGroupAdmin: Bool? = (![.legacyGroup, .group].contains(threadVariant) ? nil : GroupMember .filter(groupMember[.groupId] == threadId) - .filter(groupMember[.profileId] == currentUserPublicKey) + .filter(groupMember[.profileId] == userSessionId.hexString) .filter(groupMember[.role] == GroupMember.Role.admin) .isNotEmpty(db) ) @@ -130,7 +171,7 @@ public class ConversationViewModel: OWSAudioPlayerDelegate, NavigatableStateHold return GroupMember .filter(groupMember[.groupId] == threadId) - .filter(groupMember[.profileId] == currentUserPublicKey) + .filter(groupMember[.profileId] == userSessionId.hexString) .filter(groupMember[.role] == GroupMember.Role.standard) .isNotEmpty(db) }() @@ -141,28 +182,32 @@ public class ConversationViewModel: OWSAudioPlayerDelegate, NavigatableStateHold .asRequest(of: OpenGroup.Permissions.self) .fetchOne(db) ) - let blinded15Key: String? = SessionThread.getUserHexEncodedBlindedKey( + let blinded15SessionId: SessionId? = SessionThread.getCurrentUserBlindedSessionId( db, threadId: threadId, threadVariant: threadVariant, - blindingPrefix: .blinded15 + blindingPrefix: .blinded15, + using: dependencies ) - let blinded25Key: String? = SessionThread.getUserHexEncodedBlindedKey( + let blinded25SessionId: SessionId? = SessionThread.getCurrentUserBlindedSessionId( db, threadId: threadId, threadVariant: threadVariant, - blindingPrefix: .blinded25 + blindingPrefix: .blinded25, + using: dependencies ) return ( - currentUserPublicKey, + userSessionId, initialUnreadInteractionInfo, threadIsBlocked, + threadIsMessageRequest, + closedGroupAdminProfile, currentUserIsClosedGroupMember, currentUserIsClosedGroupAdmin, openGroupPermissions, - blinded15Key, - blinded25Key + blinded15SessionId, + blinded25SessionId ) } @@ -175,14 +220,26 @@ public class ConversationViewModel: OWSAudioPlayerDelegate, NavigatableStateHold SessionThreadViewModel( threadId: threadId, threadVariant: threadVariant, - threadIsNoteToSelf: (initialData?.currentUserPublicKey == threadId), + threadIsNoteToSelf: (initialData?.userSessionId.hexString == threadId), + threadIsMessageRequest: initialData?.threadIsMessageRequest, threadIsBlocked: initialData?.threadIsBlocked, + closedGroupAdminProfile: initialData?.closedGroupAdminProfile, currentUserIsClosedGroupMember: initialData?.currentUserIsClosedGroupMember, currentUserIsClosedGroupAdmin: initialData?.currentUserIsClosedGroupAdmin, - openGroupPermissions: initialData?.openGroupPermissions - ).populatingCurrentUserBlindedKeys( - currentUserBlinded15PublicKeyForThisThread: initialData?.blinded15Key, - currentUserBlinded25PublicKeyForThisThread: initialData?.blinded25Key + openGroupPermissions: initialData?.openGroupPermissions, + using: dependencies + ).populatingCurrentUserBlindedIds( + currentUserBlinded15SessionIdForThisThread: initialData?.blinded15SessionId?.hexString, + currentUserBlinded25SessionIdForThisThread: initialData?.blinded25SessionId?.hexString, + wasKickedFromGroup: ( + threadVariant == .group && + LibSession.wasKickedFromGroup(groupSessionId: SessionId(.group, hex: threadId), using: dependencies) + ), + groupIsDestroyed: ( + threadVariant == .group && + LibSession.groupIsDestroyed(groupSessionId: SessionId(.group, hex: threadId), using: dependencies) + ), + using: dependencies ) ) self.pagedDataObserver = nil @@ -194,9 +251,10 @@ public class ConversationViewModel: OWSAudioPlayerDelegate, NavigatableStateHold // distinct stutter) self.pagedDataObserver = self.setupPagedObserver( for: threadId, - userPublicKey: (initialData?.currentUserPublicKey ?? getUserHexEncodedPublicKey()), - blinded15PublicKey: initialData?.blinded15Key, - blinded25PublicKey: initialData?.blinded25Key + userSessionId: (initialData?.userSessionId ?? dependencies[cache: .general].sessionId), + blinded15SessionId: initialData?.blinded15SessionId, + blinded25SessionId: initialData?.blinded25SessionId, + using: dependencies ) // Run the initial query on a background thread so we don't block the push transition @@ -237,21 +295,38 @@ public class ConversationViewModel: OWSAudioPlayerDelegate, NavigatableStateHold private func setupObservableThreadData(for threadId: String) -> ThreadObservation { return ValueObservation - .trackingConstantRegion { [weak self] db -> SessionThreadViewModel? in - let userPublicKey: String = getUserHexEncodedPublicKey(db) + .trackingConstantRegion { [weak self, dependencies] db -> SessionThreadViewModel? in + let userSessionId: SessionId = dependencies[cache: .general].sessionId let recentReactionEmoji: [String] = try Emoji.getRecent(db, withDefaultEmoji: true) - let oldThreadData: SessionThreadViewModel? = self?._threadData.wrappedValue let threadViewModel: SessionThreadViewModel? = try SessionThreadViewModel - .conversationQuery(threadId: threadId, userPublicKey: userPublicKey) + .conversationQuery(threadId: threadId, userSessionId: userSessionId) .fetchOne(db) return threadViewModel .map { $0.with(recentReactionEmoji: recentReactionEmoji) } .map { viewModel -> SessionThreadViewModel in - viewModel.populatingCurrentUserBlindedKeys( + let wasKickedFromGroup: Bool = ( + viewModel.threadVariant == .group && + LibSession.wasKickedFromGroup( + groupSessionId: SessionId(.group, hex: viewModel.threadId), + using: dependencies + ) + ) + let groupIsDestroyed: Bool = ( + viewModel.threadVariant == .group && + LibSession.groupIsDestroyed( + groupSessionId: SessionId(.group, hex: viewModel.threadId), + using: dependencies + ) + ) + + return viewModel.populatingCurrentUserBlindedIds( db, - currentUserBlinded15PublicKeyForThisThread: oldThreadData?.currentUserBlinded15PublicKey, - currentUserBlinded25PublicKeyForThisThread: oldThreadData?.currentUserBlinded25PublicKey + currentUserBlinded15SessionIdForThisThread: self?.threadData.currentUserBlinded15SessionId, + currentUserBlinded25SessionIdForThisThread: self?.threadData.currentUserBlinded25SessionId, + wasKickedFromGroup: wasKickedFromGroup, + groupIsDestroyed: groupIsDestroyed, + using: dependencies ) } } @@ -290,11 +365,52 @@ public class ConversationViewModel: OWSAudioPlayerDelegate, NavigatableStateHold } } + public func emptyStateText(for threadData: SessionThreadViewModel) -> String { + let blocksCommunityMessageRequests: Bool = (threadData.profile?.blocksCommunityMessageRequests == true) + let wasKickedFromGroup: Bool = ( + threadData.threadVariant == .group && + LibSession.wasKickedFromGroup(groupSessionId: SessionId(.group, hex: threadData.threadId), using: dependencies) + ) + let groupIsDestroyed: Bool = ( + threadData.threadVariant == .group && + LibSession.groupIsDestroyed(groupSessionId: SessionId(.group, hex: threadData.threadId), using: dependencies) + ) + + switch (threadData.threadIsNoteToSelf, threadData.canWrite(using: dependencies), blocksCommunityMessageRequests, wasKickedFromGroup, groupIsDestroyed) { + case (true, _, _, _, _): return "noteToSelfEmpty".localized() + case (_, false, true, _, _): + return "messageRequestsTurnedOff" + .put(key: "name", value: threadData.displayName) + .localized() + + case (_, _, _, _, true): + return "groupDeletedMemberDescription" + .put(key: "group_name", value: threadData.displayName) + .localized() + + case (_, _, _, true, _): + return "groupRemovedYou" + .put(key: "group_name", value: threadData.displayName) + .localized() + + case (_, false, false, _, _): + return "conversationsEmpty" + .put(key: "conversation_name", value: threadData.displayName) + .localized() + + default: + return "groupNoMessages" + .put(key: "group_name", value: threadData.displayName) + .localized() + } + } + private func setupPagedObserver( for threadId: String, - userPublicKey: String, - blinded15PublicKey: String?, - blinded25PublicKey: String? + userSessionId: SessionId, + blinded15SessionId: SessionId?, + blinded25SessionId: SessionId?, + using dependencies: Dependencies ) -> PagedDatabaseObserver { return PagedDatabaseObserver( pagedTable: Interaction.self, @@ -360,9 +476,9 @@ public class ConversationViewModel: OWSAudioPlayerDelegate, NavigatableStateHold groupSQL: MessageViewModel.groupSQL, orderSQL: MessageViewModel.orderSQL, dataQuery: MessageViewModel.baseQuery( - userPublicKey: userPublicKey, - blinded15PublicKey: blinded15PublicKey, - blinded25PublicKey: blinded25PublicKey, + userSessionId: userSessionId, + blinded15SessionId: blinded15SessionId, + blinded25SessionId: blinded25SessionId, orderSQL: MessageViewModel.orderSQL, groupSQL: MessageViewModel.groupSQL ), @@ -422,7 +538,8 @@ public class ConversationViewModel: OWSAudioPlayerDelegate, NavigatableStateHold self?.unobservedInteractionDataChanges = updatedData } ) - } + }, + using: dependencies ) } @@ -466,15 +583,16 @@ public class ConversationViewModel: OWSAudioPlayerDelegate, NavigatableStateHold isLastOutgoing: ( cellViewModel.id == sortedData .filter { - $0.authorId == threadData.currentUserPublicKey || - $0.authorId == threadData.currentUserBlinded15PublicKey || - $0.authorId == threadData.currentUserBlinded25PublicKey + $0.authorId == threadData.currentUserSessionId || + $0.authorId == threadData.currentUserBlinded15SessionId || + $0.authorId == threadData.currentUserBlinded25SessionId } .last? .id ), - currentUserBlinded15PublicKey: threadData.currentUserBlinded15PublicKey, - currentUserBlinded25PublicKey: threadData.currentUserBlinded25PublicKey + currentUserBlinded15SessionId: threadData.currentUserBlinded15SessionId, + currentUserBlinded25SessionId: threadData.currentUserBlinded25SessionId, + using: dependencies ) } .reduce([]) { result, message in @@ -520,7 +638,7 @@ public class ConversationViewModel: OWSAudioPlayerDelegate, NavigatableStateHold id: UUID, messageViewModel: MessageViewModel, interaction: Interaction, - attachmentData: Attachment.PreparedData?, + attachmentData: [Attachment]?, linkPreviewDraft: LinkPreviewDraft?, linkPreviewAttachment: Attachment?, quoteModel: QuotedReplyModel? @@ -539,32 +657,34 @@ public class ConversationViewModel: OWSAudioPlayerDelegate, NavigatableStateHold // Generate the optimistic data let optimisticMessageId: UUID = UUID() let threadData: SessionThreadViewModel = self._threadData.wrappedValue - let currentUserProfile: Profile = Profile.fetchOrCreateCurrentUser() + let currentUserProfile: Profile = Profile.fetchOrCreateCurrentUser(using: dependencies) let interaction: Interaction = Interaction( threadId: threadData.threadId, threadVariant: threadData.threadVariant, - authorId: (threadData.currentUserBlinded15PublicKey ?? threadData.currentUserPublicKey), + authorId: (threadData.currentUserBlinded15SessionId ?? threadData.currentUserSessionId), variant: .standardOutgoing, body: text, timestampMs: sentTimestampMs, hasMention: Interaction.isUserMentioned( publicKeysToCheck: [ - threadData.currentUserPublicKey, - threadData.currentUserBlinded15PublicKey, - threadData.currentUserBlinded25PublicKey + threadData.currentUserSessionId, + threadData.currentUserBlinded15SessionId, + threadData.currentUserBlinded25SessionId ].compactMap { $0 }, body: text ), expiresInSeconds: threadData.disappearingMessagesConfiguration?.durationSeconds, expiresStartedAtMs: (threadData.disappearingMessagesConfiguration?.type == .disappearAfterSend ? Double(sentTimestampMs) : nil), - linkPreviewUrl: linkPreviewDraft?.urlString + linkPreviewUrl: linkPreviewDraft?.urlString, + using: dependencies ) - let optimisticAttachments: Attachment.PreparedData? = attachments - .map { Attachment.prepare(attachments: $0) } + let optimisticAttachments: [Attachment]? = attachments + .map { Attachment.prepare(attachments: $0, using: dependencies) } let linkPreviewAttachment: Attachment? = linkPreviewDraft.map { draft in try? LinkPreview.generateAttachmentIfPossible( imageData: draft.jpegImageData, - type: .jpeg + type: .jpeg, + using: dependencies ) } @@ -585,8 +705,8 @@ public class ConversationViewModel: OWSAudioPlayerDelegate, NavigatableStateHold body: interaction.body, expiresStartedAtMs: interaction.expiresStartedAtMs, expiresInSeconds: interaction.expiresInSeconds, - isSenderOpenGroupModerator: OpenGroupManager.isUserModeratorOrAdmin( - threadData.currentUserPublicKey, + isSenderOpenGroupModerator: dependencies[singleton: .openGroupManager].isUserModeratorOrAdmin( + publicKey: threadData.currentUserSessionId, for: threadData.openGroupRoomToken, on: threadData.openGroupServer ), @@ -606,11 +726,12 @@ public class ConversationViewModel: OWSAudioPlayerDelegate, NavigatableStateHold LinkPreview( url: draft.urlString, title: draft.title, - attachmentId: nil // Can't save to db optimistically + attachmentId: nil, // Can't save to db optimistically + using: dependencies ) }, linkPreviewAttachment: linkPreviewAttachment, - attachments: optimisticAttachments?.attachments + attachments: optimisticAttachments ) let optimisticData: OptimisticMessageData = ( optimisticMessageId, @@ -701,9 +822,9 @@ public class ConversationViewModel: OWSAudioPlayerDelegate, NavigatableStateHold public func mentions(for query: String = "") -> [MentionInfo] { let threadData: SessionThreadViewModel = self._threadData.wrappedValue - return Storage.shared - .read { db -> [MentionInfo] in - let userPublicKey: String = getUserHexEncodedPublicKey(db) + return dependencies[singleton: .storage] + .read { [dependencies] db -> [MentionInfo] in + let userSessionId: SessionId = dependencies[cache: .general].sessionId let pattern: FTS5Pattern? = try? SessionThreadViewModel.pattern(db, searchTerm: query, forTable: Profile.self) let capabilities: Set = (threadData.threadVariant != .community ? nil : @@ -721,7 +842,7 @@ public class ConversationViewModel: OWSAudioPlayerDelegate, NavigatableStateHold return (try MentionInfo .query( - userPublicKey: userPublicKey, + userPublicKey: userSessionId.hexString, threadId: threadData.threadId, threadVariant: threadData.threadVariant, targetPrefixes: targetPrefixes, @@ -737,7 +858,7 @@ public class ConversationViewModel: OWSAudioPlayerDelegate, NavigatableStateHold public func updateDraft(to draft: String) { let threadId: String = self.threadId - let currentDraft: String = Storage.shared + let currentDraft: String = dependencies[singleton: .storage] .read { db in try SessionThread .select(.messageDraft) @@ -750,7 +871,7 @@ public class ConversationViewModel: OWSAudioPlayerDelegate, NavigatableStateHold // Only write the updated draft to the database if it's changed (avoid unnecessary writes) guard draft != currentDraft else { return } - Storage.shared.writeAsync { db in + dependencies[singleton: .storage].writeAsync { db in try SessionThread .filter(id: threadId) .updateAll(db, SessionThread.Columns.messageDraft.set(to: draft)) @@ -782,18 +903,18 @@ public class ConversationViewModel: OWSAudioPlayerDelegate, NavigatableStateHold markAsReadPublisher = markAsReadTrigger .throttle(for: .milliseconds(100), scheduler: DispatchQueue.global(qos: .userInitiated), latest: true) .handleEvents( - receiveOutput: { [weak self] target, timestampMs in + receiveOutput: { [weak self, dependencies] target, timestampMs in let threadData: SessionThreadViewModel? = self?._threadData.wrappedValue switch target { - case .thread: threadData?.markAsRead(target: target) + case .thread: threadData?.markAsRead(target: target, using: dependencies) case .threadAndInteractions(let interactionId): guard timestampMs == nil || (self?.lastInteractionTimestampMsMarkedAsRead ?? 0) < (timestampMs ?? 0) || (self?.lastInteractionIdMarkedAsRead ?? 0) < (interactionId ?? 0) else { - threadData?.markAsRead(target: .thread) + threadData?.markAsRead(target: .thread, using: dependencies) return } @@ -804,7 +925,7 @@ public class ConversationViewModel: OWSAudioPlayerDelegate, NavigatableStateHold } self?.lastInteractionIdMarkedAsRead = (interactionId ?? threadData?.interactionId) - threadData?.markAsRead(target: target) + threadData?.markAsRead(target: target, using: dependencies) } } ) @@ -822,9 +943,10 @@ public class ConversationViewModel: OWSAudioPlayerDelegate, NavigatableStateHold self.observableThreadData = self.setupObservableThreadData(for: updatedThreadId) self.pagedDataObserver = self.setupPagedObserver( for: updatedThreadId, - userPublicKey: getUserHexEncodedPublicKey(), - blinded15PublicKey: nil, - blinded25PublicKey: nil + userSessionId: dependencies[cache: .general].sessionId, + blinded15SessionId: nil, + blinded25SessionId: nil, + using: dependencies ) // Try load everything up to the initial visible message, fallback to just the initial page of messages @@ -838,9 +960,7 @@ public class ConversationViewModel: OWSAudioPlayerDelegate, NavigatableStateHold public func trustContact() { guard self._threadData.wrappedValue.threadVariant == .contact else { return } - let threadId: String = self.threadId - - Storage.shared.writeAsync { db in + dependencies[singleton: .storage].writeAsync { [threadId, dependencies] db in try Contact .filter(id: threadId) .updateAll(db, Contact.Columns.isTrusted.set(to: true)) @@ -851,7 +971,7 @@ public class ConversationViewModel: OWSAudioPlayerDelegate, NavigatableStateHold .stateInfo(authorId: threadId, state: .pendingDownload) .fetchAll(db) .forEach { attachmentDownloadInfo in - JobRunner.add( + dependencies[singleton: .jobRunner].add( db, job: Job( variant: .attachmentDownload, @@ -860,7 +980,8 @@ public class ConversationViewModel: OWSAudioPlayerDelegate, NavigatableStateHold details: AttachmentDownloadJob.Details( attachmentId: attachmentDownloadInfo.attachmentId ) - ) + ), + canStartJob: true ) } } @@ -869,13 +990,15 @@ public class ConversationViewModel: OWSAudioPlayerDelegate, NavigatableStateHold public func unblockContact() { guard self._threadData.wrappedValue.threadVariant == .contact else { return } - let threadId: String = self.threadId - let displayName: String = self._threadData.wrappedValue.displayName - - Storage.shared.writeAsync { db in + dependencies[singleton: .storage].writeAsync { [threadId, dependencies] db in try Contact .filter(id: threadId) - .updateAllAndConfig(db, Contact.Columns.isBlocked.set(to: false)) + .updateAllAndConfig( + db, + Contact.Columns.isBlocked.set(to: false), + calledFromConfig: nil, + using: dependencies + ) } } @@ -887,6 +1010,14 @@ public class ConversationViewModel: OWSAudioPlayerDelegate, NavigatableStateHold reactionExpandedInteractionIds.remove(interactionId) } + public func deletionActions(for cellViewModels: [MessageViewModel]) -> MessageViewModel.DeletionBehaviours? { + return MessageViewModel.DeletionBehaviours.deletionActions( + for: cellViewModels, + with: self._threadData.wrappedValue, + using: dependencies + ) + } + // MARK: - Audio Playback public struct PlaybackInfo { @@ -934,8 +1065,8 @@ public class ConversationViewModel: OWSAudioPlayerDelegate, NavigatableStateHold let attachment: Attachment = viewModel.attachments?.first, attachment.isAudio, attachment.isValid, - let originalFilePath: String = attachment.originalFilePath, - FileManager.default.fileExists(atPath: originalFilePath) + let originalFilePath: String = attachment.originalFilePath(using: dependencies), + dependencies[singleton: .fileManager].fileExists(atPath: originalFilePath) else { return nil } // Create the info with the update callback @@ -962,8 +1093,8 @@ public class ConversationViewModel: OWSAudioPlayerDelegate, NavigatableStateHold guard let attachment: Attachment = viewModel.attachments?.first, - let originalFilePath: String = attachment.originalFilePath, - FileManager.default.fileExists(atPath: originalFilePath) + let originalFilePath: String = attachment.originalFilePath(using: dependencies), + dependencies[singleton: .fileManager].fileExists(atPath: originalFilePath) else { return } // If the user interacted with the currently playing item @@ -1113,7 +1244,7 @@ public class ConversationViewModel: OWSAudioPlayerDelegate, NavigatableStateHold .firstIndex(where: { $0.id == interactionId }), currentIndex < (messageSection.elements.count - 1), messageSection.elements[currentIndex + 1].cellType == .voiceMessage, - Storage.shared[.shouldAutoPlayConsecutiveAudioMessages] == true + dependencies[singleton: .storage, key: .shouldAutoPlayConsecutiveAudioMessages] else { return } let nextItem: MessageViewModel = messageSection.elements[currentIndex + 1] diff --git a/Session/Conversations/Emoji Picker/EmojiPickerCollectionView.swift b/Session/Conversations/Emoji Picker/EmojiPickerCollectionView.swift index 06611a28ee5..b366b5e2a87 100644 --- a/Session/Conversations/Emoji Picker/EmojiPickerCollectionView.swift +++ b/Session/Conversations/Emoji Picker/EmojiPickerCollectionView.swift @@ -10,6 +10,7 @@ protocol EmojiPickerCollectionViewDelegate: AnyObject { } class EmojiPickerCollectionView: UICollectionView { + private let dependencies: Dependencies let layout: UICollectionViewFlowLayout weak var pickerDelegate: EmojiPickerCollectionViewDelegate? @@ -46,7 +47,9 @@ class EmojiPickerCollectionView: UICollectionView { // MARK: - Initialization - init() { + init(using dependencies: Dependencies) { + self.dependencies = dependencies + layout = UICollectionViewFlowLayout() layout.itemSize = CGSize(width: Self.emojiWidth, height: Self.emojiWidth) layout.minimumInteritemSpacing = EmojiPickerCollectionView.minimumSpacing @@ -70,7 +73,7 @@ class EmojiPickerCollectionView: UICollectionView { tapGestureRecognizer.delegate = self // Fetch the emoji data from the database - let maybeEmojiData: (recent: [EmojiWithSkinTones], allGrouped: [Emoji.Category: [EmojiWithSkinTones]])? = Storage.shared.read { db in + let maybeEmojiData: (recent: [EmojiWithSkinTones], allGrouped: [Emoji.Category: [EmojiWithSkinTones]])? = dependencies[singleton: .storage].read { db in // Some emoji have two different code points but identical appearances. Let's remove them! // If we normalize to a different emoji than the one currently in our array, we want to drop // the non-normalized variant if the normalized variant already exists. Otherwise, map to the @@ -180,9 +183,9 @@ class EmojiPickerCollectionView: UICollectionView { guard let cell = cellForItem(at: indexPath) else { return } currentSkinTonePicker?.dismiss() - currentSkinTonePicker = EmojiSkinTonePicker.present(referenceView: cell, emoji: emoji) { [weak self] emoji in + currentSkinTonePicker = EmojiSkinTonePicker.present(referenceView: cell, emoji: emoji) { [weak self, dependencies] emoji in if let emoji: EmojiWithSkinTones = emoji { - Storage.shared.writeAsync { db in + dependencies[singleton: .storage].writeAsync { db in emoji.baseEmoji?.setPreferredSkinTones( db, preferredSkinTonePermutation: emoji.skinTones @@ -241,7 +244,7 @@ extension EmojiPickerCollectionView: UICollectionViewDataSource { } func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell { - let cell = dequeue(type: EmojiCell.self, for: indexPath) + let cell: EmojiCell = dequeue(type: EmojiCell.self, for: indexPath) guard let emoji = emojiForIndexPath(indexPath) else { Log.error("[EmojiPickerCollectionView] unexpected indexPath") @@ -254,8 +257,7 @@ extension EmojiPickerCollectionView: UICollectionViewDataSource { } func collectionView(_ collectionView: UICollectionView, viewForSupplementaryElementOfKind kind: String, at indexPath: IndexPath) -> UICollectionReusableView { - let sectionHeader = dequeue(type: EmojiSectionHeader.self, ofKind: kind, for: indexPath) - + let sectionHeader: EmojiSectionHeader = dequeue(type: EmojiSectionHeader.self, ofKind: kind, for: indexPath) sectionHeader.label.text = nameForSection(indexPath.section) return sectionHeader diff --git a/Session/Conversations/Emoji Picker/EmojiPickerSheet.swift b/Session/Conversations/Emoji Picker/EmojiPickerSheet.swift index 89cdebba5bc..b4c1ab521e4 100644 --- a/Session/Conversations/Emoji Picker/EmojiPickerSheet.swift +++ b/Session/Conversations/Emoji Picker/EmojiPickerSheet.swift @@ -2,8 +2,10 @@ import UIKit import SessionUIKit +import SessionUtilitiesKit class EmojiPickerSheet: BaseVC { + private let dependencies: Dependencies let completionHandler: (EmojiWithSkinTones?) -> Void let dismissHandler: () -> Void @@ -40,7 +42,7 @@ class EmojiPickerSheet: BaseVC { return result }() - private let collectionView = EmojiPickerCollectionView() + private lazy var collectionView = EmojiPickerCollectionView(using: dependencies) private lazy var searchBar: SearchBar = { let result = SearchBar() @@ -53,7 +55,8 @@ class EmojiPickerSheet: BaseVC { // MARK: - Initialization - init(completionHandler: @escaping (EmojiWithSkinTones?) -> Void, dismissHandler: @escaping () -> Void) { + init(completionHandler: @escaping (EmojiWithSkinTones?) -> Void, dismissHandler: @escaping () -> Void, using dependencies: Dependencies) { + self.dependencies = dependencies self.completionHandler = completionHandler self.dismissHandler = dismissHandler diff --git a/Session/Conversations/Input View/ExpandingAttachmentsButton.swift b/Session/Conversations/Input View/ExpandingAttachmentsButton.swift index 11eb67aefe0..0a1b1c71039 100644 --- a/Session/Conversations/Input View/ExpandingAttachmentsButton.swift +++ b/Session/Conversations/Input View/ExpandingAttachmentsButton.swift @@ -2,6 +2,7 @@ import UIKit import SessionUIKit +import SessionUtilitiesKit final class ExpandingAttachmentsButton: UIView, InputViewButtonDelegate { private weak var delegate: ExpandingAttachmentsButtonDelegate? diff --git a/Session/Conversations/Input View/InputTextView.swift b/Session/Conversations/Input View/InputTextView.swift index 61889cacf4a..af2890ee540 100644 --- a/Session/Conversations/Input View/InputTextView.swift +++ b/Session/Conversations/Input View/InputTextView.swift @@ -2,6 +2,7 @@ import UIKit import SessionUIKit +import SessionUtilitiesKit public final class InputTextView: UITextView, UITextViewDelegate { private weak var snDelegate: InputTextViewDelegate? diff --git a/Session/Conversations/Input View/InputView.swift b/Session/Conversations/Input View/InputView.swift index 21bb2263c88..2b2b40dee89 100644 --- a/Session/Conversations/Input View/InputView.swift +++ b/Session/Conversations/Input View/InputView.swift @@ -13,6 +13,7 @@ final class InputView: UIView, InputViewButtonDelegate, InputTextViewDelegate, M private static let linkPreviewViewInset: CGFloat = 6 private var disposables: Set = Set() + private let dependencies: Dependencies private let threadVariant: SessionThread.Variant private weak var delegate: InputViewDelegate? @@ -82,7 +83,7 @@ final class InputView: UIView, InputViewButtonDelegate, InputTextViewDelegate, M private lazy var voiceMessageButtonContainer = container(for: voiceMessageButton) private lazy var mentionsView: MentionSelectionView = { - let result: MentionSelectionView = MentionSelectionView() + let result: MentionSelectionView = MentionSelectionView(using: dependencies) result.delegate = self return result @@ -144,7 +145,8 @@ final class InputView: UIView, InputViewButtonDelegate, InputTextViewDelegate, M // MARK: - Initialization - init(threadVariant: SessionThread.Variant, delegate: InputViewDelegate) { + init(threadVariant: SessionThread.Variant, delegate: InputViewDelegate, using dependencies: Dependencies) { + self.dependencies = dependencies self.threadVariant = threadVariant self.delegate = delegate @@ -263,11 +265,12 @@ final class InputView: UIView, InputViewButtonDelegate, InputTextViewDelegate, M authorId: quoteDraftInfo.model.authorId, quotedText: quoteDraftInfo.model.body, threadVariant: threadVariant, - currentUserPublicKey: quoteDraftInfo.model.currentUserPublicKey, - currentUserBlinded15PublicKey: quoteDraftInfo.model.currentUserBlinded15PublicKey, - currentUserBlinded25PublicKey: quoteDraftInfo.model.currentUserBlinded25PublicKey, + currentUserSessionId: quoteDraftInfo.model.currentUserSessionId, + currentUserBlinded15SessionId: quoteDraftInfo.model.currentUserBlinded15SessionId, + currentUserBlinded25SessionId: quoteDraftInfo.model.currentUserBlinded25SessionId, direction: (quoteDraftInfo.isOutgoing ? .outgoing : .incoming), - attachment: quoteDraftInfo.model.attachment + attachment: quoteDraftInfo.model.attachment, + using: dependencies ) { [weak self] in self?.quoteDraftInfo = nil } @@ -286,15 +289,15 @@ final class InputView: UIView, InputViewButtonDelegate, InputTextViewDelegate, M // Suggest that the user enable link previews if they haven't already and we haven't // told them about link previews yet let text = inputTextView.text! - let areLinkPreviewsEnabled: Bool = Storage.shared[.areLinkPreviewsEnabled] + let areLinkPreviewsEnabled: Bool = dependencies[singleton: .storage, key: .areLinkPreviewsEnabled] if !LinkPreview.allPreviewUrls(forMessageBodyText: text).isEmpty && !areLinkPreviewsEnabled && - !UserDefaults.standard[.hasSeenLinkPreviewSuggestion] + !dependencies[defaults: .standard, key: .hasSeenLinkPreviewSuggestion] { delegate?.showLinkPreviewSuggestionModal() - UserDefaults.standard[.hasSeenLinkPreviewSuggestion] = true + dependencies[defaults: .standard, key: .hasSeenLinkPreviewSuggestion] = true return } // Check that link previews are enabled @@ -306,7 +309,7 @@ final class InputView: UIView, InputViewButtonDelegate, InputTextViewDelegate, M func autoGenerateLinkPreview() { // Check that a valid URL is present - guard let linkPreviewURL = LinkPreview.previewUrl(for: text, selectedRange: inputTextView.selectedRange) else { + guard let linkPreviewURL = LinkPreview.previewUrl(for: text, selectedRange: inputTextView.selectedRange, using: dependencies) else { return } @@ -319,7 +322,7 @@ final class InputView: UIView, InputViewButtonDelegate, InputTextViewDelegate, M // Set the state to loading linkPreviewInfo = (url: linkPreviewURL, draft: nil) - linkPreviewView.update(with: LinkPreview.LoadingState(), isOutgoing: false) + linkPreviewView.update(with: LinkPreview.LoadingState(), isOutgoing: false, using: dependencies) // Add the link preview view additionalContentContainer.addSubview(linkPreviewView) @@ -329,7 +332,7 @@ final class InputView: UIView, InputViewButtonDelegate, InputTextViewDelegate, M linkPreviewView.pin(.bottom, to: .bottom, of: additionalContentContainer, withInset: -4) // Build the link preview - LinkPreview.tryToBuildPreviewInfo(previewUrl: linkPreviewURL) + LinkPreview.tryToBuildPreviewInfo(previewUrl: linkPreviewURL, using: dependencies) .subscribe(on: DispatchQueue.global(qos: .userInitiated)) .receive(on: DispatchQueue.main) .sink( @@ -343,11 +346,15 @@ final class InputView: UIView, InputViewButtonDelegate, InputTextViewDelegate, M self?.additionalContentContainer.subviews.forEach { $0.removeFromSuperview() } } }, - receiveValue: { [weak self] draft in + receiveValue: { [weak self, dependencies] draft in guard self?.linkPreviewInfo?.url == linkPreviewURL else { return } // Obsolete self?.linkPreviewInfo = (url: linkPreviewURL, draft: draft) - self?.linkPreviewView.update(with: LinkPreview.DraftState(linkPreviewDraft: draft), isOutgoing: false) + self?.linkPreviewView.update( + with: LinkPreview.DraftState(linkPreviewDraft: draft), + isOutgoing: false, + using: dependencies + ) } ) .store(in: &disposables) @@ -420,7 +427,7 @@ final class InputView: UIView, InputViewButtonDelegate, InputTextViewDelegate, M // because if something goes wrong it'll trigger `hideVoiceMessageUI` and we don't want it to // end up in a state with the input content hidden showVoiceMessageUI() - delegate?.startVoiceMessageRecording(using: dependencies) + delegate?.startVoiceMessageRecording() } func handleInputViewButtonLongPressMoved(_ inputViewButton: InputViewButton, with touch: UITouch?) { diff --git a/Session/Conversations/Input View/InputViewButton.swift b/Session/Conversations/Input View/InputViewButton.swift index 246510fed66..17a1789475c 100644 --- a/Session/Conversations/Input View/InputViewButton.swift +++ b/Session/Conversations/Input View/InputViewButton.swift @@ -28,7 +28,13 @@ final class InputViewButton: UIView { // MARK: - Lifecycle - init(icon: UIImage?, isSendButton: Bool = false, delegate: InputViewButtonDelegate? = nil, hasOpaqueBackground: Bool = false, onTap: (() -> Void)? = nil) { + init( + icon: UIImage?, + isSendButton: Bool = false, + delegate: InputViewButtonDelegate? = nil, + hasOpaqueBackground: Bool = false, + onTap: (() -> Void)? = nil + ) { self.icon = icon self.isSendButton = isSendButton self.delegate = delegate @@ -138,9 +144,7 @@ final class InputViewButton: UIView { // We want to detect both taps and long presses - override func touchesBegan(_ touches: Set, with event: UIEvent?) { onTouchesBegan() } - - private func onTouchesBegan(using dependencies: Dependencies = Dependencies()) { + override func touchesBegan(_ touches: Set, with event: UIEvent?) { guard isUserInteractionEnabled else { return } UIImpactFeedbackGenerator(style: .heavy).impactOccurred() @@ -148,7 +152,7 @@ final class InputViewButton: UIView { invalidateLongPressIfNeeded() longPressTimer = Timer.scheduledTimer(withTimeInterval: 0.5, repeats: false, block: { [weak self] _ in self?.isLongPress = true - self?.delegate?.handleInputViewButtonLongPressBegan(self, using: dependencies) + self?.delegate?.handleInputViewButtonLongPressBegan(self) }) } @@ -188,13 +192,13 @@ final class InputViewButton: UIView { protocol InputViewButtonDelegate: AnyObject { func handleInputViewButtonTapped(_ inputViewButton: InputViewButton) - func handleInputViewButtonLongPressBegan(_ inputViewButton: InputViewButton?, using dependencies: Dependencies) + func handleInputViewButtonLongPressBegan(_ inputViewButton: InputViewButton?) func handleInputViewButtonLongPressMoved(_ inputViewButton: InputViewButton, with touch: UITouch?) func handleInputViewButtonLongPressEnded(_ inputViewButton: InputViewButton, with touch: UITouch?) } extension InputViewButtonDelegate { - func handleInputViewButtonLongPressBegan(_ inputViewButton: InputViewButton?, using dependencies: Dependencies) { } + func handleInputViewButtonLongPressBegan(_ inputViewButton: InputViewButton?) { } func handleInputViewButtonLongPressMoved(_ inputViewButton: InputViewButton, with touch: UITouch?) { } func handleInputViewButtonLongPressEnded(_ inputViewButton: InputViewButton, with touch: UITouch?) { } } diff --git a/Session/Conversations/Input View/MentionSelectionView.swift b/Session/Conversations/Input View/MentionSelectionView.swift index 290c08740e0..2dc932f248a 100644 --- a/Session/Conversations/Input View/MentionSelectionView.swift +++ b/Session/Conversations/Input View/MentionSelectionView.swift @@ -2,10 +2,12 @@ import UIKit import SessionUIKit +import SessionMessagingKit import SessionUtilitiesKit import SignalUtilitiesKit final class MentionSelectionView: UIView, UITableViewDataSource, UITableViewDelegate { + private let dependencies: Dependencies var candidates: [MentionInfo] = [] { didSet { tableView.isScrollEnabled = (candidates.count > 4) @@ -36,17 +38,18 @@ final class MentionSelectionView: UIView, UITableViewDataSource, UITableViewDele // MARK: - Initialization - override init(frame: CGRect) { - super.init(frame: frame) + init(using dependencies: Dependencies) { + self.dependencies = dependencies - setUpViewHierarchy() - } - - required init?(coder: NSCoder) { - super.init(coder: coder) + super.init(frame: .zero) setUpViewHierarchy() } + + @available(*, unavailable, message: "use other init(using:) instead.") + required public init(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } private func setUpViewHierarchy() { // Table view @@ -84,12 +87,13 @@ final class MentionSelectionView: UIView, UITableViewDataSource, UITableViewDele cell.update( with: candidates[indexPath.row].profile, threadVariant: candidates[indexPath.row].threadVariant, - isUserModeratorOrAdmin: OpenGroupManager.isUserModeratorOrAdmin( - candidates[indexPath.row].profile.id, + isUserModeratorOrAdmin: dependencies[singleton: .openGroupManager].isUserModeratorOrAdmin( + publicKey: candidates[indexPath.row].profile.id, for: candidates[indexPath.row].openGroupRoomToken, on: candidates[indexPath.row].openGroupServer ), - isLast: (indexPath.row == (candidates.count - 1)) + isLast: (indexPath.row == (candidates.count - 1)), + using: dependencies ) cell.accessibilityIdentifier = "Contact" cell.accessibilityLabel = candidates[indexPath.row].profile.displayName( @@ -183,15 +187,17 @@ private extension MentionSelectionView { with profile: Profile, threadVariant: SessionThread.Variant, isUserModeratorOrAdmin: Bool, - isLast: Bool + isLast: Bool, + using dependencies: Dependencies ) { displayNameLabel.text = profile.displayName(for: threadVariant) profilePictureView.update( publicKey: profile.id, threadVariant: .contact, // Always show the display picture in 'contact' mode - customImageData: nil, + displayPictureFilename: nil, profile: profile, - profileIcon: (isUserModeratorOrAdmin ? .crown : .none) + profileIcon: (isUserModeratorOrAdmin ? .crown : .none), + using: dependencies ) separator.isHidden = isLast } diff --git a/Session/Conversations/Input View/VoiceMessageRecordingView.swift b/Session/Conversations/Input View/VoiceMessageRecordingView.swift index 571f132daad..1478d406c33 100644 --- a/Session/Conversations/Input View/VoiceMessageRecordingView.swift +++ b/Session/Conversations/Input View/VoiceMessageRecordingView.swift @@ -62,7 +62,7 @@ final class VoiceMessageRecordingView: UIView { private lazy var chevronImageView: UIImageView = { let result: UIImageView = UIImageView( - image: (Singleton.hasAppContext && Singleton.appContext.isRTL ? + image: (Dependencies.isRTL ? UIImage(named: "small_chevron_left")?.withHorizontallyFlippedOrientation() : UIImage(named: "small_chevron_left") )? @@ -277,9 +277,9 @@ final class VoiceMessageRecordingView: UIView { // MARK: - Interaction func handleLongPressMoved(to location: CGPoint) { - if (Singleton.hasAppContext && (!Singleton.appContext.isRTL && location.x < bounds.center.x) || (Singleton.appContext.isRTL && location.x > bounds.center.x)) { + if ((!Dependencies.isRTL && location.x < bounds.center.x) || (Dependencies.isRTL && location.x > bounds.center.x)) { let translationX = location.x - bounds.center.x - let sign: CGFloat = (Singleton.appContext.isRTL ? 1 : -1) + let sign: CGFloat = (Dependencies.isRTL ? 1 : -1) let chevronDamping: CGFloat = 4 let labelDamping: CGFloat = 3 let chevronX = (chevronDamping * (sqrt(abs(translationX)) / sqrt(chevronDamping))) * sign @@ -311,9 +311,9 @@ final class VoiceMessageRecordingView: UIView { } } - func handleLongPressEnded(at location: CGPoint, using dependencies: Dependencies = Dependencies()) { + func handleLongPressEnded(at location: CGPoint) { if pulseView.frame.contains(location) { - delegate?.endVoiceMessageRecording(using: dependencies) + delegate?.endVoiceMessageRecording() } else if isValidLockViewLocation(location) { let tapGestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(onCircleViewTap)) @@ -333,10 +333,8 @@ final class VoiceMessageRecordingView: UIView { } } - @objc private func onCircleViewTap() { handleCircleViewTap() } - - private func handleCircleViewTap(using dependencies: Dependencies = Dependencies()) { - delegate?.endVoiceMessageRecording(using: dependencies) + @objc private func onCircleViewTap() { + delegate?.endVoiceMessageRecording() } @objc private func handleCancelButtonTapped() { @@ -477,7 +475,7 @@ extension VoiceMessageRecordingView { // MARK: - Delegate protocol VoiceMessageRecordingViewDelegate: AnyObject { - func startVoiceMessageRecording(using dependencies: Dependencies) - func endVoiceMessageRecording(using dependencies: Dependencies) + func startVoiceMessageRecording() + func endVoiceMessageRecording() func cancelVoiceMessageRecording() } diff --git a/Session/Conversations/Message Cells/CallMessageCell.swift b/Session/Conversations/Message Cells/CallMessageCell.swift index 7476d2f9db0..bc3ef6456d9 100644 --- a/Session/Conversations/Message Cells/CallMessageCell.swift +++ b/Session/Conversations/Message Cells/CallMessageCell.swift @@ -128,7 +128,8 @@ final class CallMessageCell: MessageCell { mediaCache: NSCache, playbackInfo: ConversationViewModel.PlaybackInfo?, showExpandedReactions: Bool, - lastSearchText: String? + lastSearchText: String?, + using dependencies: Dependencies ) { guard cellViewModel.variant == .infoCall, @@ -139,6 +140,7 @@ final class CallMessageCell: MessageCell { ) else { return } + self.dependencies = dependencies self.accessibilityIdentifier = "Control message" self.isAccessibilityElement = true self.viewModel = cellViewModel @@ -166,7 +168,7 @@ final class CallMessageCell: MessageCell { let shouldShowInfoIcon: Bool = ( ( messageInfo.state == .permissionDenied && - !Storage.shared[.areCallsEnabled] + !dependencies[singleton: .storage, key: .areCallsEnabled] ) || ( messageInfo.state == .permissionDeniedMicrophone && AVAudioSession.sharedInstance().recordPermission != .granted @@ -186,7 +188,8 @@ final class CallMessageCell: MessageCell { timerView.configure( expirationTimestampMs: expirationTimestampMs, - initialDurationSeconds: expiresInSeconds + initialDurationSeconds: expiresInSeconds, + using: dependencies ) timerView.themeTintColor = .textSecondary timerViewContainer.isHidden = false @@ -214,10 +217,11 @@ final class CallMessageCell: MessageCell { @objc private func handleTap(_ gestureRecognizer: UITapGestureRecognizer) { guard + let dependencies: Dependencies = self.dependencies, let cellViewModel: MessageViewModel = self.viewModel, cellViewModel.variant == .infoCall, let infoMessageData: Data = (cellViewModel.rawBody ?? "").data(using: .utf8), - let messageInfo: CallMessage.MessageInfo = try? JSONDecoder().decode( + let messageInfo: CallMessage.MessageInfo = try? JSONDecoder(using: dependencies).decode( CallMessage.MessageInfo.self, from: infoMessageData ) @@ -227,7 +231,7 @@ final class CallMessageCell: MessageCell { guard ( messageInfo.state == .permissionDenied && - !Storage.shared[.areCallsEnabled] + !dependencies[singleton: .storage, key: .areCallsEnabled] ) || ( messageInfo.state == .permissionDeniedMicrophone && AVAudioSession.sharedInstance().recordPermission != .granted diff --git a/Session/Conversations/Message Cells/Content Views/DeletedMessageView.swift b/Session/Conversations/Message Cells/Content Views/DeletedMessageView.swift index e0218e65806..9c955627743 100644 --- a/Session/Conversations/Message Cells/Content Views/DeletedMessageView.swift +++ b/Session/Conversations/Message Cells/Content Views/DeletedMessageView.swift @@ -1,9 +1,10 @@ // Copyright © 2022 Rangeproof Pty Ltd. All rights reserved. import UIKit +import SessionUIKit import SignalUtilitiesKit +import SessionMessagingKit import SessionUtilitiesKit -import SessionUIKit final class DeletedMessageView: UIView { private static let iconSize: CGFloat = 18 @@ -11,11 +12,11 @@ final class DeletedMessageView: UIView { // MARK: - Lifecycle - init(textColor: ThemeValue) { + init(textColor: ThemeValue, variant: Interaction.Variant) { super.init(frame: CGRect.zero) accessibilityIdentifier = "Deleted message" isAccessibilityElement = true - setUpViewHierarchy(textColor: textColor) + setUpViewHierarchy(textColor: textColor, variant: variant) } override init(frame: CGRect) { @@ -26,32 +27,36 @@ final class DeletedMessageView: UIView { preconditionFailure("Use init(textColor:) instead.") } - private func setUpViewHierarchy(textColor: ThemeValue) { + private func setUpViewHierarchy(textColor: ThemeValue, variant: Interaction.Variant) { // Image view - let icon = UIImage(named: "ic_trash")? - .resized(to: CGSize( - width: DeletedMessageView.iconSize, - height: DeletedMessageView.iconSize - ))? - .withRenderingMode(.alwaysTemplate) + let imageContainerView: UIView = UIView() + imageContainerView.set(.width, to: DeletedMessageView.iconImageViewSize) + imageContainerView.set(.height, to: DeletedMessageView.iconImageViewSize) - let imageView = UIImageView(image: icon) + let imageView = UIImageView(image: UIImage(named: "ic_trash")?.withRenderingMode(.alwaysTemplate)) imageView.themeTintColor = textColor - imageView.contentMode = .center - imageView.set(.width, to: DeletedMessageView.iconImageViewSize) - imageView.set(.height, to: DeletedMessageView.iconImageViewSize) + imageView.contentMode = .scaleAspectFit + imageView.set(.width, to: DeletedMessageView.iconSize) + imageView.set(.height, to: DeletedMessageView.iconSize) + imageContainerView.addSubview(imageView) + imageView.center(in: imageContainerView) // Body label let titleLabel = UILabel() titleLabel.font = .systemFont(ofSize: Values.smallFontSize) - titleLabel.text = "deleteMessageDeleted" - .putNumber(1) - .localized() + titleLabel.text = { + switch variant { + case .standardIncomingDeletedLocally, .standardOutgoingDeletedLocally: + return "deleteMessageDeletedLocally".localized() + + default: return "deleteMessageDeletedGlobally".localized() + } + }() titleLabel.themeTextColor = textColor titleLabel.lineBreakMode = .byTruncatingTail // Stack view - let stackView = UIStackView(arrangedSubviews: [ imageView, titleLabel ]) + let stackView = UIStackView(arrangedSubviews: [ imageContainerView, titleLabel ]) stackView.axis = .horizontal stackView.alignment = .center stackView.isLayoutMarginsRelativeArrangement = true diff --git a/Session/Conversations/Message Cells/Content Views/DisappearingMessageTimerView.swift b/Session/Conversations/Message Cells/Content Views/DisappearingMessageTimerView.swift index 9d0d6df73bd..3c975e8bdd2 100644 --- a/Session/Conversations/Message Cells/Content Views/DisappearingMessageTimerView.swift +++ b/Session/Conversations/Message Cells/Content Views/DisappearingMessageTimerView.swift @@ -2,6 +2,7 @@ import UIKit import SessionSnodeKit +import SessionUtilitiesKit class DisappearingMessageTimerView: UIView { private var initialDurationSeconds: Double = 0 @@ -17,35 +18,35 @@ class DisappearingMessageTimerView: UIView { // MARK: - Lifecycle init() { - super.init(frame: CGRect.zero) + super.init(frame: .zero) self.addSubview(iconImageView) iconImageView.pin(to: self, withInset: 1) } override init(frame: CGRect) { - preconditionFailure("Use init(viewItem:textColor:) instead.") + preconditionFailure("Use init() instead.") } required init?(coder: NSCoder) { - preconditionFailure("Use init(viewItem:textColor:) instead.") + preconditionFailure("Use init() instead.") } - public func configure(expirationTimestampMs: Double, initialDurationSeconds: Double) { + public func configure(expirationTimestampMs: Double, initialDurationSeconds: Double, using dependencies: Dependencies) { self.expirationTimestampMs = expirationTimestampMs self.initialDurationSeconds = initialDurationSeconds - self.updateProgress() - self.startAnimation() + self.updateProgress(using: dependencies) + self.startAnimation(using: dependencies) } - @objc private func updateProgress() { + private func updateProgress(using dependencies: Dependencies) { guard self.expirationTimestampMs > 0 else { self.progress = 12 return } - let timestampMs: Double = Double(SnodeAPI.currentOffsetTimestampMs()) + let timestampMs: Double = dependencies[cache: .snodeAPI].currentOffsetTimestampMs() let secondsLeft: Double = max((self.expirationTimestampMs - timestampMs) / 1000, 0) let progressRatio: Double = self.initialDurationSeconds > 0 ? secondsLeft / self.initialDurationSeconds : 0 @@ -59,15 +60,13 @@ class DisappearingMessageTimerView: UIView { self.iconImageView.image = UIImage(named: imageName)?.withRenderingMode(.alwaysTemplate) } - private func startAnimation() { + private func startAnimation(using dependencies: Dependencies) { self.clearAnimation() - self.animationTimer = Timer.weakScheduledTimer( + self.animationTimer = Timer.scheduledTimerOnMainThread( withTimeInterval: 0.1, - target: self, - selector: #selector(updateProgress), - userInfo: nil, - repeats: true - ) + repeats: true, + using: dependencies + ) { [weak self] _ in self?.updateProgress(using: dependencies) } } private func clearAnimation() { diff --git a/Session/Conversations/Message Cells/Content Views/LinkPreviewState.swift b/Session/Conversations/Message Cells/Content Views/LinkPreviewState.swift index 90486314e37..9ca6fc32e99 100644 --- a/Session/Conversations/Message Cells/Content Views/LinkPreviewState.swift +++ b/Session/Conversations/Message Cells/Content Views/LinkPreviewState.swift @@ -105,7 +105,7 @@ public extension LinkPreview { // Note: We don't check if the image is valid here because that can be confirmed // in 'imageState' and it's a little inefficient guard imageAttachment?.isImage == true else { return nil } - guard let imageData: Data = try? imageAttachment?.readDataFromFile() else { + guard let imageData: Data = try? imageAttachment?.readDataFromFile(using: dependencies) else { return nil } guard let image = UIImage(data: imageData) else { @@ -118,6 +118,7 @@ public extension LinkPreview { // MARK: - Type Specific + private let dependencies: Dependencies private let linkPreview: LinkPreview private let imageAttachment: Attachment? @@ -131,7 +132,8 @@ public extension LinkPreview { // MARK: - Initialization - init(linkPreview: LinkPreview, imageAttachment: Attachment?) { + init(linkPreview: LinkPreview, imageAttachment: Attachment?, using dependencies: Dependencies) { + self.dependencies = dependencies self.linkPreview = linkPreview self.imageAttachment = imageAttachment } diff --git a/Session/Conversations/Message Cells/Content Views/LinkPreviewView.swift b/Session/Conversations/Message Cells/Content Views/LinkPreviewView.swift index e6e33c8cde4..7da2453700b 100644 --- a/Session/Conversations/Message Cells/Content Views/LinkPreviewView.swift +++ b/Session/Conversations/Message Cells/Content Views/LinkPreviewView.swift @@ -4,6 +4,7 @@ import UIKit import NVActivityIndicatorView import SessionUIKit import SessionMessagingKit +import SessionUtilitiesKit final class LinkPreviewView: UIView { private static let loaderSize: CGFloat = 24 @@ -147,7 +148,8 @@ final class LinkPreviewView: UIView { delegate: TappableLabelDelegate? = nil, cellViewModel: MessageViewModel? = nil, bodyLabelTextColor: ThemeValue? = nil, - lastSearchText: String? = nil + lastSearchText: String? = nil, + using dependencies: Dependencies ) { cancelButton.removeFromSuperview() @@ -205,7 +207,8 @@ final class LinkPreviewView: UIView { with: maxWidth, textColor: (bodyLabelTextColor ?? .textPrimary), searchText: lastSearchText, - delegate: delegate + delegate: delegate, + using: dependencies ) self.bodyTappableLabel = bodyTappableLabel diff --git a/Session/Conversations/Message Cells/Content Views/MediaAlbumView.swift b/Session/Conversations/Message Cells/Content Views/MediaAlbumView.swift index e89315408e8..d201dd29c8d 100644 --- a/Session/Conversations/Message Cells/Content Views/MediaAlbumView.swift +++ b/Session/Conversations/Message Cells/Content Views/MediaAlbumView.swift @@ -23,7 +23,8 @@ public class MediaAlbumView: UIStackView { mediaCache: NSCache, items: [Attachment], isOutgoing: Bool, - maxMessageWidth: CGFloat + maxMessageWidth: CGFloat, + using dependencies: Dependencies ) { let itemsToDisplay: [Attachment] = MediaAlbumView.itemsToDisplay(forItems: items) @@ -41,7 +42,8 @@ public class MediaAlbumView: UIStackView { itemsToDisplay.count != items.count && (index == (itemsToDisplay.count - 1)) ), - cornerRadius: VisibleMessageCell.largeCornerRadius + cornerRadius: VisibleMessageCell.largeCornerRadius, + using: dependencies ) } diff --git a/Session/Conversations/Message Cells/Content Views/MediaPlaceholderView.swift b/Session/Conversations/Message Cells/Content Views/MediaPlaceholderView.swift index 19b043c6506..d70572f72ba 100644 --- a/Session/Conversations/Message Cells/Content Views/MediaPlaceholderView.swift +++ b/Session/Conversations/Message Cells/Content Views/MediaPlaceholderView.swift @@ -55,27 +55,24 @@ final class MediaPlaceholderView: UIView { default: return ( - "actionsheet_document_black", // stringlint:disable + "actionsheet_document_black", // stringlint:ignore "file".localized().lowercased() ) } }() // Image view - let imageView = UIImageView( - image: UIImage(named: iconName)? - .resized( - to: CGSize( - width: MediaPlaceholderView.iconSize, - height: MediaPlaceholderView.iconSize - ) - )? - .withRenderingMode(.alwaysTemplate) - ) + let imageContainerView: UIView = UIView() + imageContainerView.set(.width, to: MediaPlaceholderView.iconImageViewSize) + imageContainerView.set(.height, to: MediaPlaceholderView.iconImageViewSize) + + let imageView = UIImageView(image: UIImage(named: iconName)?.withRenderingMode(.alwaysTemplate)) imageView.themeTintColor = textColor - imageView.contentMode = .center - imageView.set(.width, to: MediaPlaceholderView.iconImageViewSize) - imageView.set(.height, to: MediaPlaceholderView.iconImageViewSize) + imageView.contentMode = .scaleAspectFit + imageView.set(.width, to: MediaPlaceholderView.iconSize) + imageView.set(.height, to: MediaPlaceholderView.iconSize) + imageContainerView.addSubview(imageView) + imageView.center(in: imageContainerView) // Body label let titleLabel = UILabel() @@ -87,7 +84,7 @@ final class MediaPlaceholderView: UIView { titleLabel.lineBreakMode = .byTruncatingTail // Stack view - let stackView = UIStackView(arrangedSubviews: [ imageView, titleLabel ]) + let stackView = UIStackView(arrangedSubviews: [ imageContainerView, titleLabel ]) stackView.axis = .horizontal stackView.alignment = .center addSubview(stackView) diff --git a/Session/Conversations/Message Cells/Content Views/MediaView.swift b/Session/Conversations/Message Cells/Content Views/MediaView.swift index c47a2c39440..94935c0d336 100644 --- a/Session/Conversations/Message Cells/Content Views/MediaView.swift +++ b/Session/Conversations/Message Cells/Content Views/MediaView.swift @@ -18,6 +18,7 @@ public class MediaView: UIView { // MARK: - + private let dependencies: Dependencies private let mediaCache: NSCache? public let attachment: Attachment private let isOutgoing: Bool @@ -52,8 +53,10 @@ public class MediaView: UIView { attachment: Attachment, isOutgoing: Bool, shouldSupressControls: Bool, - cornerRadius: CGFloat + cornerRadius: CGFloat, + using dependencies: Dependencies ) { + self.dependencies = dependencies self.mediaCache = mediaCache self.attachment = attachment self.isOutgoing = isOutgoing @@ -161,7 +164,7 @@ public class MediaView: UIView { animatedImageView.pin(to: self) _ = addUploadProgressIfNecessary(animatedImageView) - loadBlock = { [weak self] in + loadBlock = { [weak self, dependencies] in Log.assertOnMainThread() if animatedImageView.image != nil { @@ -174,7 +177,7 @@ public class MediaView: UIView { self?.configure(forError: .invalid) return } - guard let filePath: String = attachment.originalFilePath else { + guard let filePath: String = attachment.originalFilePath(using: dependencies) else { Log.error("[MediaView] Attachment stream missing original file path.") self?.configure(forError: .invalid) return @@ -218,7 +221,7 @@ public class MediaView: UIView { stillImageView.pin(to: self) _ = addUploadProgressIfNecessary(stillImageView) - loadBlock = { [weak self] in + loadBlock = { [weak self, dependencies] in Log.assertOnMainThread() if stillImageView.image != nil { @@ -234,6 +237,7 @@ public class MediaView: UIView { attachment.thumbnail( size: .large, + using: dependencies, success: { image, _ in applyMediaBlock(image) }, failure: { Log.error("[MediaView] Could not load thumbnail") @@ -308,7 +312,7 @@ public class MediaView: UIView { videoPlayButton.center(in: stillImageView) } - loadBlock = { [weak self] in + loadBlock = { [weak self, dependencies] in Log.assertOnMainThread() if stillImageView.image != nil { @@ -324,6 +328,7 @@ public class MediaView: UIView { attachment.thumbnail( size: .medium, + using: dependencies, success: { image, _ in applyMediaBlock(image) }, failure: { Log.error("[MediaView] Could not load thumbnail") @@ -503,6 +508,7 @@ import SwiftUI struct MediaView_SwiftUI: UIViewRepresentable { public typealias UIViewType = MediaView + private let dependencies: Dependencies private let mediaCache: NSCache? public let attachment: Attachment private let isOutgoing: Bool @@ -514,8 +520,10 @@ struct MediaView_SwiftUI: UIViewRepresentable { attachment: Attachment, isOutgoing: Bool, shouldSupressControls: Bool, - cornerRadius: CGFloat + cornerRadius: CGFloat, + using dependencies: Dependencies ) { + self.dependencies = dependencies self.mediaCache = mediaCache self.attachment = attachment self.isOutgoing = isOutgoing @@ -527,9 +535,10 @@ struct MediaView_SwiftUI: UIViewRepresentable { let mediaView = MediaView( mediaCache: mediaCache, attachment: attachment, - isOutgoing: isOutgoing, + isOutgoing: isOutgoing, shouldSupressControls: shouldSupressControls, - cornerRadius: cornerRadius + cornerRadius: cornerRadius, + using: dependencies ) return mediaView diff --git a/Session/Conversations/Message Cells/Content Views/OpenGroupInvitationView.swift b/Session/Conversations/Message Cells/Content Views/OpenGroupInvitationView.swift index ecda9c0c627..b076ac680af 100644 --- a/Session/Conversations/Message Cells/Content Views/OpenGroupInvitationView.swift +++ b/Session/Conversations/Message Cells/Content Views/OpenGroupInvitationView.swift @@ -63,24 +63,25 @@ final class OpenGroupInvitationView: UIView { labelStackView.axis = .vertical // Icon - let iconSize = OpenGroupInvitationView.iconSize + let iconContainerView: UIView = UIView() + iconContainerView.layer.cornerRadius = (OpenGroupInvitationView.iconImageViewSize / 2) + iconContainerView.themeBackgroundColor = (isOutgoing ? .messageBubble_overlay : .primary) + iconContainerView.set(.width, to: OpenGroupInvitationView.iconImageViewSize) + iconContainerView.set(.height, to: OpenGroupInvitationView.iconImageViewSize) + let iconName = (isOutgoing ? "Globe" : "Plus") // stringlint:ignore - let iconImageViewSize = OpenGroupInvitationView.iconImageViewSize let iconImageView = UIImageView( - image: UIImage(named: iconName)? - .resized(to: CGSize(width: iconSize, height: iconSize))? - .withRenderingMode(.alwaysTemplate) + image: UIImage(named: iconName)?.withRenderingMode(.alwaysTemplate) ) iconImageView.themeTintColor = (isOutgoing ? .messageBubble_outgoingText : .textPrimary) - iconImageView.contentMode = .center - iconImageView.layer.cornerRadius = iconImageViewSize / 2 - iconImageView.layer.masksToBounds = true - iconImageView.themeBackgroundColor = (isOutgoing ? .messageBubble_overlay : .primary) - iconImageView.set(.width, to: iconImageViewSize) - iconImageView.set(.height, to: iconImageViewSize) + iconImageView.contentMode = .scaleAspectFit + iconImageView.set(.width, to: OpenGroupInvitationView.iconSize) + iconImageView.set(.height, to: OpenGroupInvitationView.iconSize) + iconContainerView.addSubview(iconImageView) + iconImageView.center(in: iconContainerView) // Main stack - let mainStackView = UIStackView(arrangedSubviews: [ iconImageView, labelStackView ]) + let mainStackView = UIStackView(arrangedSubviews: [ iconContainerView, labelStackView ]) mainStackView.axis = .horizontal mainStackView.spacing = Values.mediumSpacing mainStackView.alignment = .center diff --git a/Session/Conversations/Message Cells/Content Views/QuoteView.swift b/Session/Conversations/Message Cells/Content Views/QuoteView.swift index ddbf3b56460..055b217c6fe 100644 --- a/Session/Conversations/Message Cells/Content Views/QuoteView.swift +++ b/Session/Conversations/Message Cells/Content Views/QuoteView.swift @@ -20,6 +20,7 @@ final class QuoteView: UIView { // MARK: - Variables + private let dependencies: Dependencies private let onCancel: (() -> ())? // MARK: - Lifecycle @@ -29,13 +30,15 @@ final class QuoteView: UIView { authorId: String, quotedText: String?, threadVariant: SessionThread.Variant, - currentUserPublicKey: String?, - currentUserBlinded15PublicKey: String?, - currentUserBlinded25PublicKey: String?, + currentUserSessionId: String?, + currentUserBlinded15SessionId: String?, + currentUserBlinded25SessionId: String?, direction: Direction, attachment: Attachment?, + using dependencies: Dependencies, onCancel: (() -> ())? = nil ) { + self.dependencies = dependencies self.onCancel = onCancel super.init(frame: CGRect.zero) @@ -45,9 +48,9 @@ final class QuoteView: UIView { authorId: authorId, quotedText: quotedText, threadVariant: threadVariant, - currentUserPublicKey: currentUserPublicKey, - currentUserBlinded15PublicKey: currentUserBlinded15PublicKey, - currentUserBlinded25PublicKey: currentUserBlinded25PublicKey, + currentUserSessionId: currentUserSessionId, + currentUserBlinded15SessionId: currentUserBlinded15SessionId, + currentUserBlinded25SessionId: currentUserBlinded25SessionId, direction: direction, attachment: attachment ) @@ -66,9 +69,9 @@ final class QuoteView: UIView { authorId: String, quotedText: String?, threadVariant: SessionThread.Variant, - currentUserPublicKey: String?, - currentUserBlinded15PublicKey: String?, - currentUserBlinded25PublicKey: String?, + currentUserSessionId: String?, + currentUserBlinded15SessionId: String?, + currentUserBlinded25SessionId: String?, direction: Direction, attachment: Attachment? ) { @@ -102,12 +105,17 @@ final class QuoteView: UIView { if let attachment: Attachment = attachment { let isAudio: Bool = attachment.isAudio let fallbackImageName: String = (isAudio ? "attachment_audio" : "actionsheet_document_black") // stringlint:ignore + let imageContainerView: UIView = UIView() + imageContainerView.themeBackgroundColor = .messageBubble_overlay + imageContainerView.layer.cornerRadius = VisibleMessageCell.smallCornerRadius + imageContainerView.layer.masksToBounds = true + imageContainerView.set(.width, to: thumbnailSize) + imageContainerView.set(.height, to: thumbnailSize) + mainStackView.addArrangedSubview(imageContainerView) + let imageView: UIImageView = UIImageView( - image: UIImage(named: fallbackImageName)? - .resized(to: CGSize(width: iconSize, height: iconSize))? - .withRenderingMode(.alwaysTemplate) + image: UIImage(named: fallbackImageName)?.withRenderingMode(.alwaysTemplate) ) - imageView.themeTintColor = { switch mode { case .regular: return (direction == .outgoing ? @@ -117,13 +125,11 @@ final class QuoteView: UIView { case .draft: return .textPrimary } }() - imageView.contentMode = .center - imageView.themeBackgroundColor = .messageBubble_overlay - imageView.layer.cornerRadius = VisibleMessageCell.smallCornerRadius - imageView.layer.masksToBounds = true - imageView.set(.width, to: thumbnailSize) - imageView.set(.height, to: thumbnailSize) - mainStackView.addArrangedSubview(imageView) + imageView.contentMode = .scaleAspectFit + imageView.set(.width, to: iconSize) + imageView.set(.height, to: iconSize) + imageContainerView.addSubview(imageView) + imageView.center(in: imageContainerView) if (body ?? "").isEmpty { body = attachment.shortDescription @@ -133,6 +139,7 @@ final class QuoteView: UIView { if attachment.isVisualMedia { attachment.thumbnail( size: .small, + using: dependencies, success: { [imageView] image, _ in guard Thread.isMainThread else { DispatchQueue.main.async { @@ -182,7 +189,7 @@ final class QuoteView: UIView { }() bodyLabel.font = .systemFont(ofSize: Values.smallFontSize) - ThemeManager.onThemeChange(observer: bodyLabel) { [weak bodyLabel] theme, primaryColor in + ThemeManager.onThemeChange(observer: bodyLabel) { [weak bodyLabel, dependencies] theme, primaryColor in guard let textColor: UIColor = theme.color(for: targetThemeColor) else { return } bodyLabel?.attributedText = body @@ -190,9 +197,9 @@ final class QuoteView: UIView { MentionUtilities.highlightMentions( in: $0, threadVariant: threadVariant, - currentUserPublicKey: currentUserPublicKey, - currentUserBlinded15PublicKey: currentUserBlinded15PublicKey, - currentUserBlinded25PublicKey: currentUserBlinded25PublicKey, + currentUserSessionId: currentUserSessionId, + currentUserBlinded15SessionId: currentUserBlinded15SessionId, + currentUserBlinded25SessionId: currentUserBlinded25SessionId, location: { switch (mode, direction) { case (.draft, _): return .quoteDraft @@ -205,7 +212,8 @@ final class QuoteView: UIView { primaryColor: primaryColor, attributes: [ .foregroundColor: textColor - ] + ], + using: dependencies ) } .defaulting( @@ -218,9 +226,9 @@ final class QuoteView: UIView { // Label stack view let isCurrentUser: Bool = [ - currentUserPublicKey, - currentUserBlinded15PublicKey, - currentUserBlinded25PublicKey + currentUserSessionId, + currentUserBlinded15SessionId, + currentUserBlinded25SessionId ] .compactMap { $0 } .asSet() @@ -234,13 +242,15 @@ final class QuoteView: UIView { // When we can't find the quoted message we want to hide the author label return Profile.displayNameNoFallback( id: authorId, - threadVariant: threadVariant + threadVariant: threadVariant, + using: dependencies ) } return Profile.displayName( id: authorId, - threadVariant: threadVariant + threadVariant: threadVariant, + using: dependencies ) }() authorLabel.themeTextColor = targetThemeColor diff --git a/Session/Conversations/Message Cells/Content Views/SwiftUI/OpenGroupInvitationView_SwiftUI.swift b/Session/Conversations/Message Cells/Content Views/SwiftUI/OpenGroupInvitationView_SwiftUI.swift index ee3b6797b0a..ecf4994cdc2 100644 --- a/Session/Conversations/Message Cells/Content Views/SwiftUI/OpenGroupInvitationView_SwiftUI.swift +++ b/Session/Conversations/Message Cells/Content Views/SwiftUI/OpenGroupInvitationView_SwiftUI.swift @@ -39,25 +39,23 @@ struct OpenGroupInvitationView_SwiftUI: View { spacing: Values.mediumSpacing ) { // Icon - let iconName = (isOutgoing ? "Globe" : "Plus") // stringlint:ignore - if let iconImage = UIImage(named: iconName)? - .resized(to: CGSize(width: Self.iconSize, height: Self.iconSize))? + if let iconImage = UIImage(named: isOutgoing ? "Globe" : "Plus")? .withRenderingMode(.alwaysTemplate) { - Image(uiImage: iconImage) - .foregroundColor(themeColor: (isOutgoing ? .messageBubble_outgoingText : .textPrimary)) - .background( - Circle() - .fill(themeColor: (isOutgoing ? .messageBubble_overlay : .primary)) - .frame( - width: Self.iconImageViewSize, - height: Self.iconImageViewSize - ) - ) + Circle() + .fill(themeColor: (isOutgoing ? .messageBubble_overlay : .primary)) .frame( width: Self.iconImageViewSize, height: Self.iconImageViewSize ) + .overlay { + Image(uiImage: iconImage) + .foregroundColor(themeColor: (isOutgoing ? .messageBubble_outgoingText : .textPrimary)) + .frame( + width: Self.iconSize, + height: Self.iconSize + ) + } } // Text diff --git a/Session/Conversations/Message Cells/Content Views/SwiftUI/QuoteView_SwiftUI.swift b/Session/Conversations/Message Cells/Content Views/SwiftUI/QuoteView_SwiftUI.swift index 17d0d896a9a..c35beabfb67 100644 --- a/Session/Conversations/Message Cells/Content Views/SwiftUI/QuoteView_SwiftUI.swift +++ b/Session/Conversations/Message Cells/Content Views/SwiftUI/QuoteView_SwiftUI.swift @@ -13,9 +13,9 @@ struct QuoteView_SwiftUI: View { var authorId: String var quotedText: String? var threadVariant: SessionThread.Variant - var currentUserPublicKey: String? - var currentUserBlinded15PublicKey: String? - var currentUserBlinded25PublicKey: String? + var currentUserSessionId: String? + var currentUserBlinded15SessionId: String? + var currentUserBlinded25SessionId: String? var direction: Direction var attachment: Attachment? } @@ -29,14 +29,15 @@ struct QuoteView_SwiftUI: View { private static let cancelButtonSize: CGFloat = 33 private static let cornerRadius: CGFloat = 4 + private let dependencies: Dependencies private var info: Info private var onCancel: (() -> ())? private var isCurrentUser: Bool { return [ - info.currentUserPublicKey, - info.currentUserBlinded15PublicKey, - info.currentUserBlinded25PublicKey + info.currentUserSessionId, + info.currentUserBlinded15SessionId, + info.currentUserBlinded25SessionId ] .compactMap { $0 } .asSet() @@ -59,22 +60,27 @@ struct QuoteView_SwiftUI: View { // When we can't find the quoted message we want to hide the author label return Profile.displayNameNoFallback( id: info.authorId, - threadVariant: info.threadVariant + threadVariant: info.threadVariant, + using: dependencies ) } return Profile.displayName( id: info.authorId, - threadVariant: info.threadVariant + threadVariant: info.threadVariant, + using: dependencies ) } - public init(info: Info, onCancel: (() -> ())? = nil) { + public init(info: Info, using dependencies: Dependencies, onCancel: (() -> ())? = nil) { + self.dependencies = dependencies self.info = info self.onCancel = onCancel + if let attachment = info.attachment, attachment.isVisualMedia { attachment.thumbnail( size: .small, + using: dependencies, success: { [self] image, _ in self.thumbnail = image }, @@ -97,26 +103,34 @@ struct QuoteView_SwiftUI: View { let fallbackImageName: String = (attachment.isAudio ? "attachment_audio" : "actionsheet_document_black") return UIImage(named: fallbackImageName)? - .resized(to: CGSize(width: Self.iconSize, height: Self.iconSize))? .withRenderingMode(.alwaysTemplate) }() { - Image(uiImage: image) - .foregroundColor(themeColor: { - switch info.mode { - case .regular: return (info.direction == .outgoing ? - .messageBubble_outgoingText : - .messageBubble_incomingText - ) - case .draft: return .textPrimary - } - }()) + ZStack() { + RoundedRectangle( + cornerRadius: Self.cornerRadius + ) + .fill(themeColor: .messageBubble_overlay) .frame( width: Self.thumbnailSize, - height: Self.thumbnailSize, - alignment: .center + height: Self.thumbnailSize ) - .backgroundColor(themeColor: .messageBubble_overlay) - .cornerRadius(Self.cornerRadius) + + Image(uiImage: image) + .foregroundColor(themeColor: { + switch info.mode { + case .regular: return (info.direction == .outgoing ? + .messageBubble_outgoingText : + .messageBubble_incomingText + ) + case .draft: return .textPrimary + } + }()) + .frame( + width: Self.iconSize, + height: Self.iconSize, + alignment: .center + ) + } } } else { // Line view @@ -159,9 +173,9 @@ struct QuoteView_SwiftUI: View { MentionUtilities.highlightMentions( in: quotedText, threadVariant: info.threadVariant, - currentUserPublicKey: info.currentUserPublicKey, - currentUserBlinded15PublicKey: info.currentUserBlinded15PublicKey, - currentUserBlinded25PublicKey: info.currentUserBlinded25PublicKey, + currentUserSessionId: info.currentUserSessionId, + currentUserBlinded15SessionId: info.currentUserBlinded15SessionId, + currentUserBlinded25SessionId: info.currentUserBlinded25SessionId, location: { switch (info.mode, info.direction) { case (.draft, _): return .quoteDraft @@ -175,7 +189,8 @@ struct QuoteView_SwiftUI: View { attributes: [ .foregroundColor: textColor, .font: UIFont.systemFont(ofSize: Values.smallFontSize) - ] + ], + using: dependencies ) ) .lineLimit(2) @@ -215,16 +230,35 @@ struct QuoteView_SwiftUI_Previews: PreviewProvider { static var previews: some View { ZStack { ThemeManager.currentTheme.colorSwiftUI(for: .backgroundPrimary).ignoresSafeArea() - - QuoteView_SwiftUI( - info: QuoteView_SwiftUI.Info( - mode: .draft, - authorId: "", - threadVariant: .contact, - direction: .outgoing + VStack(spacing: 20) { + QuoteView_SwiftUI( + info: QuoteView_SwiftUI.Info( + mode: .draft, + authorId: "", + threadVariant: .contact, + direction: .outgoing + ), + using: Dependencies.createEmpty() ) - ) - .frame(height: 40) + .frame(height: 40) + + QuoteView_SwiftUI( + info: QuoteView_SwiftUI.Info( + mode: .regular, + authorId: "", + threadVariant: .contact, + direction: .incoming, + attachment: Attachment( + variant: .standard, + state: .downloaded, + contentType: "audio/wav", + byteCount: 0 + ) + ), + using: Dependencies.createEmpty() + ) + .previewLayout(.sizeThatFits) + } } } } diff --git a/Session/Conversations/Message Cells/Content Views/TypingIndicatorView.swift b/Session/Conversations/Message Cells/Content Views/TypingIndicatorView.swift index 127349bdbe3..066263caa99 100644 --- a/Session/Conversations/Message Cells/Content Views/TypingIndicatorView.swift +++ b/Session/Conversations/Message Cells/Content Views/TypingIndicatorView.swift @@ -41,10 +41,12 @@ import SessionUtilitiesKit self.spacing = kDotMaxHSpacing self.alignment = .center - NotificationCenter.default.addObserver(self, - selector: #selector(didBecomeActive), - name: .sessionDidBecomeActive, - object: nil) + NotificationCenter.default.addObserver( + self, + selector: #selector(didBecomeActive), + name: .sessionDidBecomeActive, + object: nil + ) } deinit { diff --git a/Session/Conversations/Message Cells/DateHeaderCell.swift b/Session/Conversations/Message Cells/DateHeaderCell.swift index 0f37ed4e42c..bb8e1f1c907 100644 --- a/Session/Conversations/Message Cells/DateHeaderCell.swift +++ b/Session/Conversations/Message Cells/DateHeaderCell.swift @@ -44,7 +44,8 @@ final class DateHeaderCell: MessageCell { mediaCache: NSCache, playbackInfo: ConversationViewModel.PlaybackInfo?, showExpandedReactions: Bool, - lastSearchText: String? + lastSearchText: String?, + using dependencies: Dependencies ) { guard cellViewModel.cellType == .dateHeader else { return } diff --git a/Session/Conversations/Message Cells/InfoMessageCell.swift b/Session/Conversations/Message Cells/InfoMessageCell.swift index e1c7b61b587..f4c14c73706 100644 --- a/Session/Conversations/Message Cells/InfoMessageCell.swift +++ b/Session/Conversations/Message Cells/InfoMessageCell.swift @@ -3,6 +3,7 @@ import UIKit import SessionUIKit import SessionMessagingKit +import SessionUtilitiesKit final class InfoMessageCell: MessageCell { private static let iconSize: CGFloat = 12 @@ -93,12 +94,14 @@ final class InfoMessageCell: MessageCell { mediaCache: NSCache, playbackInfo: ConversationViewModel.PlaybackInfo?, showExpandedReactions: Bool, - lastSearchText: String? + lastSearchText: String?, + using dependencies: Dependencies ) { guard cellViewModel.variant.isInfoMessage else { return } self.accessibilityIdentifier = "Control message" self.isAccessibilityElement = true + self.dependencies = dependencies self.viewModel = cellViewModel self.actionLabel.isHidden = true @@ -127,14 +130,18 @@ final class InfoMessageCell: MessageCell { self.actionLabel.text = "disappearingMessagesFollowSetting".localized() } - self.label.themeTextColor = (cellViewModel.variant == .infoClosedGroupCurrentUserErrorLeaving) ? .danger : .textSecondary + self.label.themeTextColor = (cellViewModel.variant == .infoGroupCurrentUserErrorLeaving ? + .danger : + .textSecondary + ) let shouldShowIcon: Bool = (icon != nil) || ((cellViewModel.expiresInSeconds ?? 0) > 0) - iconContainerViewWidthConstraint.constant = shouldShowIcon ? InfoMessageCell.iconSize : 0 - iconContainerViewHeightConstraint.constant = shouldShowIcon ? InfoMessageCell.iconSize : 0 + iconContainerViewWidthConstraint.constant = (shouldShowIcon ? InfoMessageCell.iconSize : 0) + iconContainerViewHeightConstraint.constant = (shouldShowIcon ? InfoMessageCell.iconSize : 0) guard shouldShowIcon else { return } + // Timer if let expiresStartedAtMs: Double = cellViewModel.expiresStartedAtMs, @@ -144,7 +151,8 @@ final class InfoMessageCell: MessageCell { timerView.configure( expirationTimestampMs: expirationTimestampMs, - initialDurationSeconds: expiresInSeconds + initialDurationSeconds: expiresInSeconds, + using: dependencies ) timerView.themeTintColor = .textSecondary timerView.isHidden = false diff --git a/Session/Conversations/Message Cells/MessageCell.swift b/Session/Conversations/Message Cells/MessageCell.swift index 4f9ffd3170a..7746b7ae95d 100644 --- a/Session/Conversations/Message Cells/MessageCell.swift +++ b/Session/Conversations/Message Cells/MessageCell.swift @@ -11,6 +11,7 @@ public enum SwipeState { } public class MessageCell: UITableViewCell { + var dependencies: Dependencies? var viewModel: MessageViewModel? weak var delegate: MessageCellDelegate? open var contextSnapshotView: UIView? { return nil } @@ -45,12 +46,20 @@ public class MessageCell: UITableViewCell { // MARK: - Updating + public override func prepareForReuse() { + super.prepareForReuse() + + self.dependencies = nil + self.viewModel = nil + } + func update( with cellViewModel: MessageViewModel, mediaCache: NSCache, playbackInfo: ConversationViewModel.PlaybackInfo?, showExpandedReactions: Bool, - lastSearchText: String? + lastSearchText: String?, + using dependencies: Dependencies ) { preconditionFailure("Must be overridden by subclasses.") } @@ -69,13 +78,14 @@ public class MessageCell: UITableViewCell { guard viewModel.cellType != .unreadMarker else { return UnreadMarkerCell.self } switch viewModel.variant { - case .standardOutgoing, .standardIncoming, .standardIncomingDeleted: + case .standardOutgoing, .standardIncoming, .standardIncomingDeleted, .standardOutgoingDeleted, + .standardIncomingDeletedLocally, .standardOutgoingDeletedLocally: return VisibleMessageCell.self - case .infoClosedGroupCreated, .infoClosedGroupUpdated, - .infoClosedGroupCurrentUserLeft, .infoClosedGroupCurrentUserLeaving, .infoClosedGroupCurrentUserErrorLeaving, + case .infoLegacyGroupCreated, .infoLegacyGroupUpdated, .infoLegacyGroupCurrentUserLeft, + .infoGroupCurrentUserLeaving, .infoGroupCurrentUserErrorLeaving, .infoDisappearingMessagesUpdate, .infoScreenshotNotification, .infoMediaSavedNotification, - .infoMessageRequestAccepted: + .infoMessageRequestAccepted, .infoGroupInfoInvited, .infoGroupInfoUpdated, .infoGroupMembersUpdated: return InfoMessageCell.self case .infoCall: @@ -88,11 +98,11 @@ public class MessageCell: UITableViewCell { protocol MessageCellDelegate: ReactionDelegate { func handleItemLongPressed(_ cellViewModel: MessageViewModel) - func handleItemTapped(_ cellViewModel: MessageViewModel, cell: UITableViewCell, cellLocation: CGPoint, using dependencies: Dependencies) + func handleItemTapped(_ cellViewModel: MessageViewModel, cell: UITableViewCell, cellLocation: CGPoint) func handleItemDoubleTapped(_ cellViewModel: MessageViewModel) func handleItemSwiped(_ cellViewModel: MessageViewModel, state: SwipeState) func openUrl(_ urlString: String) - func handleReplyButtonTapped(for cellViewModel: MessageViewModel, using dependencies: Dependencies) + func handleReplyButtonTapped(for cellViewModel: MessageViewModel) func startThread(with sessionId: String, openGroupServer: String?, openGroupPublicKey: String?) func showReactionList(_ cellViewModel: MessageViewModel, selectedReaction: EmojiWithSkinTones?) func needsLayout(for cellViewModel: MessageViewModel, expandingReactions: Bool) @@ -100,6 +110,6 @@ protocol MessageCellDelegate: ReactionDelegate { extension MessageCellDelegate { func handleItemTapped(_ cellViewModel: MessageViewModel, cell: UITableViewCell, cellLocation: CGPoint) { - handleItemTapped(cellViewModel, cell: cell, cellLocation: cellLocation, using: Dependencies()) + handleItemTapped(cellViewModel, cell: cell, cellLocation: cellLocation) } } diff --git a/Session/Conversations/Message Cells/TypingIndicatorCell.swift b/Session/Conversations/Message Cells/TypingIndicatorCell.swift index 067ef9005f6..6d86630aa0f 100644 --- a/Session/Conversations/Message Cells/TypingIndicatorCell.swift +++ b/Session/Conversations/Message Cells/TypingIndicatorCell.swift @@ -3,6 +3,7 @@ import UIKit import SessionUIKit import SessionMessagingKit +import SessionUtilitiesKit // Assumptions // • We'll never encounter an outgoing typing indicator. @@ -44,10 +45,12 @@ final class TypingIndicatorCell: MessageCell { mediaCache: NSCache, playbackInfo: ConversationViewModel.PlaybackInfo?, showExpandedReactions: Bool, - lastSearchText: String? + lastSearchText: String?, + using dependencies: Dependencies ) { guard cellViewModel.cellType == .typingIndicator else { return } + self.dependencies = dependencies self.viewModel = cellViewModel // Bubble view diff --git a/Session/Conversations/Message Cells/UnreadMarkerCell.swift b/Session/Conversations/Message Cells/UnreadMarkerCell.swift index 80644d8f6aa..9392b1d295c 100644 --- a/Session/Conversations/Message Cells/UnreadMarkerCell.swift +++ b/Session/Conversations/Message Cells/UnreadMarkerCell.swift @@ -65,7 +65,8 @@ final class UnreadMarkerCell: MessageCell { mediaCache: NSCache, playbackInfo: ConversationViewModel.PlaybackInfo?, showExpandedReactions: Bool, - lastSearchText: String? + lastSearchText: String?, + using dependencies: Dependencies ) { guard cellViewModel.cellType == .unreadMarker else { return } } diff --git a/Session/Conversations/Message Cells/VisibleMessageCell.swift b/Session/Conversations/Message Cells/VisibleMessageCell.swift index 7c865094b80..f421bbe296a 100644 --- a/Session/Conversations/Message Cells/VisibleMessageCell.swift +++ b/Session/Conversations/Message Cells/VisibleMessageCell.swift @@ -113,13 +113,6 @@ final class VisibleMessageCell: MessageCell, TappableLabelDelegate { result.image = UIImage(named: "ic_reply")?.withRenderingMode(.alwaysTemplate) result.themeTintColor = .textPrimary - // Flip horizontally for RTL languages - result.transform = CGAffineTransform.identity - .scaledBy( - x: (Singleton.hasAppContext && Singleton.appContext.isRTL ? -1 : 1), - y: 1 - ) - return result }() @@ -282,8 +275,10 @@ final class VisibleMessageCell: MessageCell, TappableLabelDelegate { mediaCache: NSCache, playbackInfo: ConversationViewModel.PlaybackInfo?, showExpandedReactions: Bool, - lastSearchText: String? + lastSearchText: String?, + using dependencies: Dependencies ) { + self.dependencies = dependencies self.viewModel = cellViewModel // We want to add spacing between "clusters" of messages to indicate that time has @@ -313,29 +308,21 @@ final class VisibleMessageCell: MessageCell, TappableLabelDelegate { profilePictureView.update( publicKey: cellViewModel.authorId, threadVariant: .contact, // Always show the display picture in 'contact' mode - customImageData: nil, + displayPictureFilename: nil, profile: cellViewModel.profile, - profileIcon: (cellViewModel.isSenderOpenGroupModerator ? .crown : .none) + profileIcon: (cellViewModel.isSenderOpenGroupModerator ? .crown : .none), + using: dependencies ) // Bubble view - contentViewLeadingConstraint1.isActive = ( - cellViewModel.variant == .standardIncoming || - cellViewModel.variant == .standardIncomingDeleted - ) + contentViewLeadingConstraint1.isActive = cellViewModel.variant.isIncoming contentViewLeadingConstraint1.constant = (isGroupThread ? VisibleMessageCell.groupThreadHSpacing : VisibleMessageCell.contactThreadHSpacing) - contentViewLeadingConstraint2.isActive = (cellViewModel.variant == .standardOutgoing) + contentViewLeadingConstraint2.isActive = cellViewModel.variant.isOutgoing contentViewTopConstraint.constant = (cellViewModel.senderName == nil ? 0 : VisibleMessageCell.authorLabelBottomSpacing) - contentViewTrailingConstraint1.isActive = (cellViewModel.variant == .standardOutgoing) - contentViewTrailingConstraint2.isActive = ( - cellViewModel.variant == .standardIncoming || - cellViewModel.variant == .standardIncomingDeleted - ) + contentViewTrailingConstraint1.isActive = cellViewModel.variant.isOutgoing + contentViewTrailingConstraint2.isActive = cellViewModel.variant.isIncoming - let bubbleBackgroundColor: ThemeValue = (( - cellViewModel.variant == .standardIncoming || - cellViewModel.variant == .standardIncomingDeleted - ) ? .messageBubble_incomingBackground : .messageBubble_outgoingBackground) + let bubbleBackgroundColor: ThemeValue = (cellViewModel.variant.isIncoming ? .messageBubble_incomingBackground : .messageBubble_outgoingBackground) bubbleView.themeBackgroundColor = bubbleBackgroundColor bubbleBackgroundView.themeBackgroundColor = bubbleBackgroundColor updateBubbleViewCorners() @@ -345,7 +332,8 @@ final class VisibleMessageCell: MessageCell, TappableLabelDelegate { for: cellViewModel, mediaCache: mediaCache, playbackInfo: playbackInfo, - lastSearchText: lastSearchText + lastSearchText: lastSearchText, + using: dependencies ) bubbleView.accessibilityIdentifier = "Message body" @@ -362,6 +350,13 @@ final class VisibleMessageCell: MessageCell, TappableLabelDelegate { let authorLabelAvailableSpace = CGSize(width: authorLabelAvailableWidth, height: .greatestFiniteMagnitude) let authorLabelSize = authorLabel.sizeThatFits(authorLabelAvailableSpace) authorLabelHeightConstraint.constant = (cellViewModel.senderName != nil ? authorLabelSize.height : 0) + + // Flip horizontally for RTL languages + replyIconImageView.transform = CGAffineTransform.identity + .scaledBy( + x: (Dependencies.isRTL ? -1 : 1), + y: 1 + ) // Swipe to reply if ContextMenuVC.viewModelCanReply(cellViewModel) { @@ -372,14 +367,11 @@ final class VisibleMessageCell: MessageCell, TappableLabelDelegate { } // Under bubble content - underBubbleStackView.alignment = (cellViewModel.variant == .standardOutgoing ? - .trailing : - .leading - ) - underBubbleStackViewIncomingLeadingConstraint.isActive = (cellViewModel.variant != .standardOutgoing) - underBubbleStackViewIncomingTrailingConstraint.isActive = (cellViewModel.variant != .standardOutgoing) - underBubbleStackViewOutgoingLeadingConstraint.isActive = (cellViewModel.variant == .standardOutgoing) - underBubbleStackViewOutgoingTrailingConstraint.isActive = (cellViewModel.variant == .standardOutgoing) + underBubbleStackView.alignment = (cellViewModel.variant.isOutgoing ?.trailing : .leading) + underBubbleStackViewIncomingLeadingConstraint.isActive = !cellViewModel.variant.isOutgoing + underBubbleStackViewIncomingTrailingConstraint.isActive = !cellViewModel.variant.isOutgoing + underBubbleStackViewOutgoingLeadingConstraint.isActive = cellViewModel.variant.isOutgoing + underBubbleStackViewOutgoingTrailingConstraint.isActive = cellViewModel.variant.isOutgoing // Reaction view reactionContainerView.isHidden = (cellViewModel.reactionInfo?.isEmpty != false) @@ -405,7 +397,8 @@ final class VisibleMessageCell: MessageCell, TappableLabelDelegate { messageStatusImageView.themeTintColor = tintColor messageStatusContainerView.isHidden = ( (cellViewModel.expiresInSeconds ?? 0) == 0 && ( - cellViewModel.variant != .standardOutgoing || + !cellViewModel.variant.isOutgoing || + cellViewModel.variant.isDeletedMessage || cellViewModel.variant == .infoCall || ( cellViewModel.state == .sent && @@ -427,7 +420,8 @@ final class VisibleMessageCell: MessageCell, TappableLabelDelegate { timerView.configure( expirationTimestampMs: expirationTimestampMs, - initialDurationSeconds: expiresInSeconds + initialDurationSeconds: expiresInSeconds, + using: dependencies ) timerView.themeTintColor = tintColor timerView.isHidden = false @@ -438,16 +432,10 @@ final class VisibleMessageCell: MessageCell, TappableLabelDelegate { messageStatusImageView.isHidden = false } - timerViewOutgoingMessageConstraint.isActive = (cellViewModel.variant == .standardOutgoing) - timerViewIncomingMessageConstraint.isActive = ( - cellViewModel.variant == .standardIncoming || - cellViewModel.variant == .standardIncomingDeleted - ) - messageStatusLabelOutgoingMessageConstraint.isActive = (cellViewModel.variant == .standardOutgoing) - messageStatusLabelIncomingMessageConstraint.isActive = ( - cellViewModel.variant == .standardIncoming || - cellViewModel.variant == .standardIncomingDeleted - ) + timerViewOutgoingMessageConstraint.isActive = cellViewModel.variant.isOutgoing + timerViewIncomingMessageConstraint.isActive = cellViewModel.variant.isIncoming + messageStatusLabelOutgoingMessageConstraint.isActive = cellViewModel.variant.isOutgoing + messageStatusLabelIncomingMessageConstraint.isActive = cellViewModel.variant.isIncoming // Set the height of the underBubbleStackView to 0 if it has no content (need to do this // otherwise it can randomly stretch) @@ -460,17 +448,16 @@ final class VisibleMessageCell: MessageCell, TappableLabelDelegate { for cellViewModel: MessageViewModel, mediaCache: NSCache, playbackInfo: ConversationViewModel.PlaybackInfo?, - lastSearchText: String? + lastSearchText: String?, + using dependencies: Dependencies ) { - let bodyLabelTextColor: ThemeValue = (cellViewModel.variant == .standardOutgoing ? - .messageBubble_outgoingText : - .messageBubble_incomingText - ) - - snContentView.alignment = (cellViewModel.variant == .standardOutgoing ? - .trailing : - .leading + let isOutgoing: Bool = ( + cellViewModel.variant == .standardOutgoing || + cellViewModel.variant == .standardOutgoingDeleted || + cellViewModel.variant == .standardOutgoingDeletedLocally ) + let bodyLabelTextColor: ThemeValue = (isOutgoing ? .messageBubble_outgoingText : .messageBubble_incomingText) + snContentView.alignment = (isOutgoing ? .trailing : .leading) for subview in snContentView.arrangedSubviews { snContentView.removeArrangedSubview(subview) @@ -486,8 +473,11 @@ final class VisibleMessageCell: MessageCell, TappableLabelDelegate { bodyTappableLabel = nil // Handle the deleted state first (it's much simpler than the others) - guard cellViewModel.variant != .standardIncomingDeleted else { - let deletedMessageView: DeletedMessageView = DeletedMessageView(textColor: bodyLabelTextColor) + guard !cellViewModel.variant.isDeletedMessage else { + let deletedMessageView: DeletedMessageView = DeletedMessageView( + textColor: bodyLabelTextColor, + variant: cellViewModel.variant + ) bubbleView.addSubview(deletedMessageView) deletedMessageView.pin(to: bubbleView) snContentView.addArrangedSubview(bubbleBackgroundView) @@ -517,13 +507,15 @@ final class VisibleMessageCell: MessageCell, TappableLabelDelegate { linkPreviewView.update( with: LinkPreview.SentState( linkPreview: linkPreview, - imageAttachment: cellViewModel.linkPreviewAttachment + imageAttachment: cellViewModel.linkPreviewAttachment, + using: dependencies ), - isOutgoing: (cellViewModel.variant == .standardOutgoing), + isOutgoing: isOutgoing, delegate: self, cellViewModel: cellViewModel, bodyLabelTextColor: bodyLabelTextColor, - lastSearchText: lastSearchText + lastSearchText: lastSearchText, + using: dependencies ) self.linkPreviewView = linkPreviewView bubbleView.addSubview(linkPreviewView) @@ -536,7 +528,7 @@ final class VisibleMessageCell: MessageCell, TappableLabelDelegate { name: (linkPreview.title ?? ""), url: linkPreview.url, textColor: bodyLabelTextColor, - isOutgoing: (cellViewModel.variant == .standardOutgoing) + isOutgoing: isOutgoing ) openGroupInvitationView.isAccessibilityElement = true openGroupInvitationView.accessibilityIdentifier = "Community invitation" @@ -560,14 +552,12 @@ final class VisibleMessageCell: MessageCell, TappableLabelDelegate { authorId: quote.authorId, quotedText: quote.body, threadVariant: cellViewModel.threadVariant, - currentUserPublicKey: cellViewModel.currentUserPublicKey, - currentUserBlinded15PublicKey: cellViewModel.currentUserBlinded15PublicKey, - currentUserBlinded25PublicKey: cellViewModel.currentUserBlinded25PublicKey, - direction: (cellViewModel.variant == .standardOutgoing ? - .outgoing : - .incoming - ), - attachment: cellViewModel.quoteAttachment + currentUserSessionId: cellViewModel.currentUserSessionId, + currentUserBlinded15SessionId: cellViewModel.currentUserBlinded15SessionId, + currentUserBlinded25SessionId: cellViewModel.currentUserBlinded25SessionId, + direction: (isOutgoing ? .outgoing : .incoming), + attachment: cellViewModel.quoteAttachment, + using: dependencies ) self.quoteView = quoteView let quoteViewContainer = UIView(wrapping: quoteView, withInsets: UIEdgeInsets(top: 0, leading: hInset, bottom: 0, trailing: hInset)) @@ -580,7 +570,8 @@ final class VisibleMessageCell: MessageCell, TappableLabelDelegate { with: maxWidth, textColor: bodyLabelTextColor, searchText: lastSearchText, - delegate: self + delegate: self, + using: dependencies ) self.bodyTappableLabel = bodyTappableLabel stackView.addArrangedSubview(bodyTappableLabel) @@ -602,7 +593,8 @@ final class VisibleMessageCell: MessageCell, TappableLabelDelegate { with: maxWidth, textColor: bodyLabelTextColor, searchText: lastSearchText, - delegate: self + delegate: self, + using: dependencies ) self.bodyTappableLabel = bodyTappableLabel @@ -618,8 +610,9 @@ final class VisibleMessageCell: MessageCell, TappableLabelDelegate { items: (cellViewModel.attachments? .filter { $0.isVisualMedia }) .defaulting(to: []), - isOutgoing: (cellViewModel.variant == .standardOutgoing), - maxMessageWidth: maxMessageWidth + isOutgoing: isOutgoing, + maxMessageWidth: maxMessageWidth, + using: dependencies ) self.albumView = albumView let size = getSize(for: cellViewModel) @@ -674,7 +667,8 @@ final class VisibleMessageCell: MessageCell, TappableLabelDelegate { with: maxWidth, textColor: bodyLabelTextColor, searchText: lastSearchText, - delegate: self + delegate: self, + using: dependencies ) self.bodyTappableLabel = bodyTappableLabel @@ -703,7 +697,7 @@ final class VisibleMessageCell: MessageCell, TappableLabelDelegate { return } - let isSelfSend: Bool = (reactionInfo.reaction.authorId == cellViewModel.currentUserPublicKey) + let isSelfSend: Bool = (reactionInfo.reaction.authorId == cellViewModel.currentUserSessionId) if let value: ReactionViewModel = result.value(forKey: emoji) { result.replace( @@ -754,7 +748,7 @@ final class VisibleMessageCell: MessageCell, TappableLabelDelegate { } override func dynamicUpdate(with cellViewModel: MessageViewModel, playbackInfo: ConversationViewModel.PlaybackInfo?) { - guard cellViewModel.variant != .standardIncomingDeleted else { return } + guard !cellViewModel.variant.isDeletedMessage else { return } // If it's an incoming media message and the thread isn't trusted then show the placeholder view if cellViewModel.cellType != .textOnlyMessage && cellViewModel.variant == .standardIncoming && !cellViewModel.threadIsTrusted { @@ -800,8 +794,8 @@ final class VisibleMessageCell: MessageCell, TappableLabelDelegate { // Only allow swipes to the left; allowing swipes to the right gets in the way of // the default iOS swipe to go back gesture guard - (Singleton.hasAppContext && Singleton.appContext.isRTL && v.x > 0) || - (!Singleton.hasAppContext || !Singleton.appContext.isRTL && v.x < 0) + (Dependencies.isRTL && v.x > 0) || + (!Dependencies.isRTL && v.x < 0) else { return false } return abs(v.x) > abs(v.y) // It has to be more horizontal than vertical @@ -865,9 +859,7 @@ final class VisibleMessageCell: MessageCell, TappableLabelDelegate { isHandlingLongPress = true } - @objc private func handleTap(_ gestureRecognizer: UITapGestureRecognizer) { onTap(gestureRecognizer) } - - private func onTap(_ gestureRecognizer: UITapGestureRecognizer, using dependencies: Dependencies = Dependencies()) { + @objc private func handleTap(_ gestureRecognizer: UITapGestureRecognizer) { guard let cellViewModel: MessageViewModel = self.viewModel else { return } let location = gestureRecognizer.location(in: self) @@ -903,10 +895,10 @@ final class VisibleMessageCell: MessageCell, TappableLabelDelegate { if reactionContainerView.convert(reactionView.frame, from: reactionView.superview).contains(convertedLocation) { if reactionView.viewModel.showBorder { - delegate?.removeReact(cellViewModel, for: reactionView.viewModel.emoji, using: dependencies) + delegate?.removeReact(cellViewModel, for: reactionView.viewModel.emoji) } else { - delegate?.react(cellViewModel, with: reactionView.viewModel.emoji, using: dependencies) + delegate?.react(cellViewModel, with: reactionView.viewModel.emoji) } return } @@ -923,7 +915,7 @@ final class VisibleMessageCell: MessageCell, TappableLabelDelegate { } } else if snContentView.bounds.contains(snContentView.convert(location, from: self)) { - delegate?.handleItemTapped(cellViewModel, cell: self, cellLocation: location, using: dependencies) + delegate?.handleItemTapped(cellViewModel, cell: self, cellLocation: location) } } @@ -940,8 +932,8 @@ final class VisibleMessageCell: MessageCell, TappableLabelDelegate { .translation(in: self) .x .clamp( - (Singleton.hasAppContext && Singleton.appContext.isRTL ? 0 : -CGFloat.greatestFiniteMagnitude), - (Singleton.hasAppContext && Singleton.appContext.isRTL ? CGFloat.greatestFiniteMagnitude : 0) + (Dependencies.isRTL ? 0 : -CGFloat.greatestFiniteMagnitude), + (Dependencies.isRTL ? CGFloat.greatestFiniteMagnitude : 0) ) switch gestureRecognizer.state { @@ -950,7 +942,7 @@ final class VisibleMessageCell: MessageCell, TappableLabelDelegate { case .changed: // The idea here is to asymptotically approach a maximum drag distance let damping: CGFloat = 20 - let sign: CGFloat = (Singleton.hasAppContext && Singleton.appContext.isRTL ? 1 : -1) + let sign: CGFloat = (Dependencies.isRTL ? 1 : -1) let x = (damping * (sqrt(abs(translationX)) / sqrt(damping))) * sign viewsToMoveForReply.forEach { $0.transform = CGAffineTransform(translationX: x, y: 0) } @@ -991,11 +983,11 @@ final class VisibleMessageCell: MessageCell, TappableLabelDelegate { } } - private func reply(using dependencies: Dependencies = Dependencies()) { + private func reply() { guard let cellViewModel: MessageViewModel = self.viewModel else { return } resetReply() - delegate?.handleReplyButtonTapped(for: cellViewModel, using: dependencies) + delegate?.handleReplyButtonTapped(for: cellViewModel) } // MARK: - Convenience @@ -1080,18 +1072,18 @@ final class VisibleMessageCell: MessageCell, TappableLabelDelegate { let oppositeEdgePadding: CGFloat = (includingOppositeGutter ? gutterSize : contactThreadHSpacing) switch cellViewModel.variant { - case .standardOutgoing: + case .standardOutgoing, .standardOutgoingDeleted, .standardOutgoingDeletedLocally: return (width - contactThreadHSpacing - oppositeEdgePadding) - case .standardIncoming, .standardIncomingDeleted: + case .standardIncoming, .standardIncomingDeleted, .standardIncomingDeletedLocally: let isGroupThread = ( cellViewModel.threadVariant == .community || cellViewModel.threadVariant == .legacyGroup || cellViewModel.threadVariant == .group ) - let leftGutterSize = (isGroupThread ? leftGutterSize : contactThreadHSpacing) + let leftEdgeGutterSize = (isGroupThread ? leftGutterSize : contactThreadHSpacing) - return (width - leftGutterSize - oppositeEdgePadding) + return (width - leftEdgeGutterSize - oppositeEdgePadding) default: preconditionFailure() } @@ -1103,9 +1095,9 @@ final class VisibleMessageCell: MessageCell, TappableLabelDelegate { theme: Theme, primaryColor: Theme.PrimaryColor, textColor: ThemeValue, - searchText: String? - ) -> NSMutableAttributedString? - { + searchText: String?, + using dependencies: Dependencies + ) -> NSMutableAttributedString? { guard let body: String = cellViewModel.body, !body.isEmpty, @@ -1120,9 +1112,9 @@ final class VisibleMessageCell: MessageCell, TappableLabelDelegate { attributedString: MentionUtilities.highlightMentions( in: body, threadVariant: cellViewModel.threadVariant, - currentUserPublicKey: cellViewModel.currentUserPublicKey, - currentUserBlinded15PublicKey: cellViewModel.currentUserBlinded15PublicKey, - currentUserBlinded25PublicKey: cellViewModel.currentUserBlinded25PublicKey, + currentUserSessionId: cellViewModel.currentUserSessionId, + currentUserBlinded15SessionId: cellViewModel.currentUserBlinded15SessionId, + currentUserBlinded25SessionId: cellViewModel.currentUserBlinded25SessionId, location: (isOutgoing ? .outgoingMessage : .incomingMessage), textColor: actualTextColor, theme: theme, @@ -1130,7 +1122,8 @@ final class VisibleMessageCell: MessageCell, TappableLabelDelegate { attributes: [ .foregroundColor: actualTextColor, .font: UIFont.systemFont(ofSize: getFontSize(for: cellViewModel)) - ] + ], + using: dependencies ) ) @@ -1205,7 +1198,7 @@ final class VisibleMessageCell: MessageCell, TappableLabelDelegate { // we only highlight those cases) normalizedBody .ranges( - of: (Singleton.appContext.isRTL ? + of: (Dependencies.isRTL ? "(\(part.lowercased()))(^|[^a-zA-Z0-9])" : "(^|[^a-zA-Z0-9])(\(part.lowercased()))" ), @@ -1240,7 +1233,8 @@ final class VisibleMessageCell: MessageCell, TappableLabelDelegate { with availableWidth: CGFloat, textColor: ThemeValue, searchText: String?, - delegate: TappableLabelDelegate? + delegate: TappableLabelDelegate?, + using dependencies: Dependencies ) -> TappableLabel { let result: TappableLabel = TappableLabel() result.setContentCompressionResistancePriority(.required, for: .vertical) @@ -1252,12 +1246,13 @@ final class VisibleMessageCell: MessageCell, TappableLabelDelegate { ThemeManager.onThemeChange(observer: result) { [weak result] theme, primaryColor in let hasPreviousSetText: Bool = ((result?.attributedText?.length ?? 0) > 0) - result?.attributedText = Self.getBodyAttributedText( + result?.attributedText = VisibleMessageCell.getBodyAttributedText( for: cellViewModel, theme: theme, primaryColor: primaryColor, textColor: textColor, - searchText: searchText + searchText: searchText, + using: dependencies ) if let result: TappableLabel = result, !hasPreviousSetText { diff --git a/Session/Conversations/Settings/ThreadDisappearingMessagesSettingsViewModel.swift b/Session/Conversations/Settings/ThreadDisappearingMessagesSettingsViewModel.swift index 74cc4b82b92..1118131fd4a 100644 --- a/Session/Conversations/Settings/ThreadDisappearingMessagesSettingsViewModel.swift +++ b/Session/Conversations/Settings/ThreadDisappearingMessagesSettingsViewModel.swift @@ -22,9 +22,8 @@ class ThreadDisappearingMessagesSettingsViewModel: SessionTableViewModel, Naviga private var isNoteToSelf: Bool private let currentUserIsClosedGroupMember: Bool? private let currentUserIsClosedGroupAdmin: Bool? - private let config: DisappearingMessagesConfiguration - private var currentSelection: CurrentValueSubject - private var shouldShowConfirmButton: CurrentValueSubject + private let originalConfig: DisappearingMessagesConfiguration + private var configSubject: CurrentValueSubject // MARK: - Initialization @@ -34,17 +33,16 @@ class ThreadDisappearingMessagesSettingsViewModel: SessionTableViewModel, Naviga currentUserIsClosedGroupMember: Bool?, currentUserIsClosedGroupAdmin: Bool?, config: DisappearingMessagesConfiguration, - using dependencies: Dependencies = Dependencies() + using dependencies: Dependencies ) { self.dependencies = dependencies self.threadId = threadId self.threadVariant = threadVariant - self.isNoteToSelf = (threadId == getUserHexEncodedPublicKey(using: dependencies)) + self.isNoteToSelf = (threadId == dependencies[cache: .general].sessionId.hexString) self.currentUserIsClosedGroupMember = currentUserIsClosedGroupMember self.currentUserIsClosedGroupAdmin = currentUserIsClosedGroupAdmin - self.config = config - self.currentSelection = CurrentValueSubject(config) - self.shouldShowConfirmButton = CurrentValueSubject(false) + self.originalConfig = config + self.configSubject = CurrentValueSubject(config) } // MARK: - Config @@ -96,7 +94,15 @@ class ThreadDisappearingMessagesSettingsViewModel: SessionTableViewModel, Naviga } }() - lazy var footerButtonInfo: AnyPublisher = shouldShowConfirmButton + lazy var footerButtonInfo: AnyPublisher = configSubject + .map { [originalConfig] currentConfig -> Bool in + // Need to explicitly compare values because 'lastChangeTimestampMs' will differ + return ( + currentConfig.isEnabled != originalConfig.isEnabled || + currentConfig.durationSeconds != originalConfig.durationSeconds || + currentConfig.type != originalConfig.type + ) + } .removeDuplicates() .map { [weak self] shouldShowConfirmButton -> SessionButton.Info? in guard shouldShowConfirmButton else { return nil } @@ -119,282 +125,305 @@ class ThreadDisappearingMessagesSettingsViewModel: SessionTableViewModel, Naviga .eraseToAnyPublisher() lazy var observation: TargetObservation = ObservationBuilder - .subject(currentSelection) - .map { [weak self, threadVariant, isNoteToSelf, config, currentUserIsClosedGroupMember, currentUserIsClosedGroupAdmin] currentSelection -> [SectionModel] in - switch (threadVariant, isNoteToSelf) { - case (.contact, false): - return [ - SectionModel( - model: .type, - elements: [ - SessionCell.Info( - id: "off".localized(), - title: "off".localized(), - rightAccessory: .radio( - isSelected: { (self?.currentSelection.value.isEnabled == false) }, - accessibility: Accessibility( - identifier: "Off - Radio" - ) - ), + .subject(configSubject) + .compactMap { [weak self] currentConfig -> [SectionModel]? in self?.content(currentConfig) } + + private func content(_ currentConfig: DisappearingMessagesConfiguration) -> [SectionModel] { + switch (threadVariant, isNoteToSelf) { + case (.contact, false): + return [ + SectionModel( + model: .type, + elements: [ + SessionCell.Info( + id: "off".localized(), + title: "off".localized(), + trailingAccessory: .radio( + isSelected: !currentConfig.isEnabled, accessibility: Accessibility( - identifier: "Disable disappearing messages (Off option)", - label: "Disable disappearing messages (Off option)" - ), - onTap: { - let updatedConfig: DisappearingMessagesConfiguration = currentSelection - .with( - isEnabled: false, - durationSeconds: DisappearingMessagesConfiguration.DefaultDuration.off.seconds - ) - self?.shouldShowConfirmButton.send(updatedConfig != config) - self?.currentSelection.send(updatedConfig) - } + identifier: "Off - Radio" + ) ), - SessionCell.Info( - id: "disappearingMessagesDisappearAfterRead".localized(), - title: "disappearingMessagesDisappearAfterRead".localized(), - subtitle: "disappearingMessagesDisappearAfterReadDescription".localized(), - rightAccessory: .radio( - isSelected: { - (self?.currentSelection.value.isEnabled == true) && - (self?.currentSelection.value.type == .disappearAfterRead) - }, - accessibility: Accessibility( - identifier: "Disappear After Read - Radio" + accessibility: Accessibility( + identifier: "Disable disappearing messages (Off option)", + label: "Disable disappearing messages (Off option)" + ), + onTap: { [weak self] in + self?.configSubject.send( + currentConfig.with( + isEnabled: false, + durationSeconds: DisappearingMessagesConfiguration.DefaultDuration.off.seconds ) + ) + } + ), + SessionCell.Info( + id: "disappearingMessagesDisappearAfterRead".localized(), + title: "disappearingMessagesDisappearAfterRead".localized(), + subtitle: "disappearingMessagesDisappearAfterReadDescription".localized(), + trailingAccessory: .radio( + isSelected: ( + currentConfig.isEnabled && + currentConfig.type == .disappearAfterRead ), accessibility: Accessibility( - identifier: "Disappear after read option", - label: "Disappear after read option" - ), - onTap: { - let updatedConfig: DisappearingMessagesConfiguration = { - if (config.isEnabled == true && config.type == .disappearAfterRead) { - return config - } - return currentSelection - .with( - isEnabled: true, - durationSeconds: DisappearingMessagesConfiguration.DefaultDuration.disappearAfterRead.seconds, - type: .disappearAfterRead - ) - }() - self?.shouldShowConfirmButton.send(updatedConfig != config) - self?.currentSelection.send(updatedConfig) - } + identifier: "Disappear After Read - Radio" + ) ), - SessionCell.Info( - id: "disappearingMessagesDisappearAfterSend".localized(), - title: "disappearingMessagesDisappearAfterSend".localized(), - subtitle: "disappearingMessagesDisappearAfterSendDescription".localized(), - rightAccessory: .radio( - isSelected: { - (self?.currentSelection.value.isEnabled == true) && - (self?.currentSelection.value.type == .disappearAfterSend) - }, - accessibility: Accessibility( - identifier: "Disappear After Send - Radio" + accessibility: Accessibility( + identifier: "Disappear after read option", + label: "Disappear after read option" + ), + onTap: { [weak self, originalConfig] in + switch (originalConfig.isEnabled, originalConfig.type) { + case (true, .disappearAfterRead): self?.configSubject.send(originalConfig) + default: self?.configSubject.send( + currentConfig.with( + isEnabled: true, + durationSeconds: DisappearingMessagesConfiguration.DefaultDuration.disappearAfterRead.seconds, + type: .disappearAfterRead + ) ) + } + } + ), + SessionCell.Info( + id: "disappearingMessagesDisappearAfterSend".localized(), + title: "disappearingMessagesDisappearAfterSend".localized(), + subtitle: "disappearingMessagesDisappearAfterSendDescription".localized(), + trailingAccessory: .radio( + isSelected: ( + currentConfig.isEnabled && + currentConfig.type == .disappearAfterSend ), accessibility: Accessibility( - identifier: "Disappear after send option", - label: "Disappear after send option" - ), - onTap: { - let updatedConfig: DisappearingMessagesConfiguration = { - if (config.isEnabled == true && config.type == .disappearAfterSend) { - return config - } - return currentSelection - .with( - isEnabled: true, - durationSeconds: DisappearingMessagesConfiguration.DefaultDuration.disappearAfterSend.seconds, - type: .disappearAfterSend - ) - }() - self?.shouldShowConfirmButton.send(updatedConfig != config) - self?.currentSelection.send(updatedConfig) - } - ) - ].compactMap { $0 } - ), - (currentSelection.isEnabled == false ? nil : - SectionModel( - model: (currentSelection.type == .disappearAfterSend ? - .timerDisappearAfterSend : - .timerDisappearAfterRead + identifier: "Disappear After Send - Radio" + ) ), - elements: DisappearingMessagesConfiguration - .validDurationsSeconds(currentSelection.type ?? .disappearAfterSend) - .map { duration in - let title: String = duration.formatted(format: .long) - - return SessionCell.Info( - id: title, - title: title, - rightAccessory: .radio( - isSelected: { - (self?.currentSelection.value.isEnabled == true) && - (self?.currentSelection.value.durationSeconds == duration) - }, - accessibility: Accessibility( - identifier: "\(title) - Radio" - ) - ), - accessibility: Accessibility( - identifier: "Time option", - label: "Time option" - ), - onTap: { - let updatedConfig: DisappearingMessagesConfiguration = currentSelection.with(durationSeconds: duration) - self?.shouldShowConfirmButton.send(updatedConfig != config) - self?.currentSelection.send(updatedConfig) - } + accessibility: Accessibility( + identifier: "Disappear after send option", + label: "Disappear after send option" + ), + onTap: { [weak self, originalConfig] in + switch (originalConfig.isEnabled, originalConfig.type) { + case (true, .disappearAfterSend): self?.configSubject.send(originalConfig) + default: self?.configSubject.send( + currentConfig.with( + isEnabled: true, + durationSeconds: DisappearingMessagesConfiguration.DefaultDuration.disappearAfterSend.seconds, + type: .disappearAfterSend + ) ) } + } ) - ) - ].compactMap { $0 } - - case (.legacyGroup, _), (.group, _), (_, true): - return [ + ].compactMap { $0 } + ), + (!currentConfig.isEnabled ? nil : SectionModel( - model: (isNoteToSelf ? .noteToSelf : .group), - elements: [ - SessionCell.Info( - id: "off".localized(), - title: "off".localized(), - rightAccessory: .radio( - isSelected: { (self?.currentSelection.value.isEnabled == false) }, - accessibility: Accessibility( - identifier: "Off - Radio" - ) - ), - isEnabled: ( - isNoteToSelf || - currentUserIsClosedGroupAdmin == true - ), - accessibility: Accessibility( - identifier: "Disable disappearing messages (Off option)", - label: "Disable disappearing messages (Off option)" - ), - onTap: { - let updatedConfig: DisappearingMessagesConfiguration = currentSelection - .with( - isEnabled: false, - durationSeconds: DisappearingMessagesConfiguration.DefaultDuration.off.seconds - ) - self?.shouldShowConfirmButton.send(updatedConfig != config) - self?.currentSelection.send(updatedConfig) - } - ) - ] - .compactMap { $0 } - .appending( - contentsOf: DisappearingMessagesConfiguration - .validDurationsSeconds(.disappearAfterSend) - .map { duration in - let title: String = duration.formatted(format: .long) + model: (currentConfig.type == .disappearAfterSend ? + .timerDisappearAfterSend : + .timerDisappearAfterRead + ), + elements: DisappearingMessagesConfiguration + .validDurationsSeconds(currentConfig.type ?? .disappearAfterSend, using: dependencies) + .map { duration in + let title: String = duration.formatted(format: .long) - return SessionCell.Info( - id: title, - title: title, - rightAccessory: .radio( - isSelected: { - (self?.currentSelection.value.isEnabled == true) && - (self?.currentSelection.value.durationSeconds == duration) - }, - accessibility: Accessibility( - identifier: "\(title) - Radio" - ) + return SessionCell.Info( + id: title, + title: title, + trailingAccessory: .radio( + isSelected: ( + currentConfig.isEnabled && + currentConfig.durationSeconds == duration ), - isEnabled: (isNoteToSelf || currentUserIsClosedGroupAdmin == true), accessibility: Accessibility( - identifier: "Time option", - label: "Time option" - ), - onTap: { - // If the new disappearing messages config feature flag isn't - // enabled then the 'isEnabled' and 'type' values are set via - // the first section so pass `nil` values to keep the existing - // setting - let updatedConfig: DisappearingMessagesConfiguration = currentSelection - .with( - isEnabled: true, - durationSeconds: duration, - type: .disappearAfterSend - ) - self?.shouldShowConfirmButton.send(updatedConfig != config) - self?.currentSelection.send(updatedConfig) - } + identifier: "\(title) - Radio" + ) + ), + accessibility: Accessibility( + identifier: "Time option", + label: "Time option" + ), + onTap: { [weak self] in + self?.configSubject.send( + currentConfig.with( + durationSeconds: duration + ) + ) + } + ) + } + ) + ) + ].compactMap { $0 } + + case (.legacyGroup, _), (.group, _), (_, true): + return [ + SectionModel( + model: (isNoteToSelf ? .noteToSelf : .group), + elements: [ + SessionCell.Info( + id: "off".localized(), + title: "off".localized(), + trailingAccessory: .radio( + isSelected: !currentConfig.isEnabled, + accessibility: Accessibility( + identifier: "Off - Radio" + ) + ), + isEnabled: ( + isNoteToSelf || + currentUserIsClosedGroupAdmin == true + ), + accessibility: Accessibility( + identifier: "Disable disappearing messages (Off option)", + label: "Disable disappearing messages (Off option)" + ), + onTap: { [weak self] in + self?.configSubject.send( + currentConfig.with( + isEnabled: false, + durationSeconds: DisappearingMessagesConfiguration.DefaultDuration.off.seconds ) - } + ) + } ) + ] + .compactMap { $0 } + .appending( + contentsOf: DisappearingMessagesConfiguration + .validDurationsSeconds(.disappearAfterSend, using: dependencies) + .map { duration in + let title: String = duration.formatted(format: .long) + + return SessionCell.Info( + id: title, + title: title, + trailingAccessory: .radio( + isSelected: ( + currentConfig.isEnabled && + currentConfig.durationSeconds == duration + ), + accessibility: Accessibility( + identifier: "\(title) - Radio" + ) + ), + isEnabled: (isNoteToSelf || currentUserIsClosedGroupAdmin == true), + accessibility: Accessibility( + identifier: "Time option", + label: "Time option" + ), + onTap: { [weak self] in + self?.configSubject.send( + currentConfig.with( + isEnabled: true, + durationSeconds: duration, + type: .disappearAfterSend + ) + ) + } + ) + } ) - ].compactMap { $0 } + ) + ].compactMap { $0 } - case (.community, _): - return [] // Should not happen - } + case (.community, _): + return [] // Should not happen } + } // MARK: - Functions private func saveChanges() { - let updatedConfig: DisappearingMessagesConfiguration = self.currentSelection.value + let updatedConfig: DisappearingMessagesConfiguration = self.configSubject.value - guard self.config != updatedConfig else { return } + guard self.originalConfig != updatedConfig else { return } - dependencies.storage.writeAsync(using: dependencies) { [threadId, threadVariant, dependencies] db in - let userPublicKey: String = getUserHexEncodedPublicKey(db, using: dependencies) - let currentTimestampMs: Int64 = SnodeAPI.currentOffsetTimestampMs() + dependencies[singleton: .storage].writeAsync { [threadId, threadVariant, dependencies] db in + // Update the local state + try updatedConfig.upserted(db) + let currentOffsetTimestampMs: Int64 = dependencies[cache: .snodeAPI].currentOffsetTimestampMs() let interactionId = try updatedConfig .saved(db) .insertControlMessage( db, threadVariant: threadVariant, - authorId: userPublicKey, - timestampMs: currentTimestampMs, + authorId: dependencies[cache: .general].sessionId.hexString, + timestampMs: currentOffsetTimestampMs, serverHash: nil, - serverExpirationTimestamp: nil + serverExpirationTimestamp: nil, + using: dependencies ) - let expirationTimerUpdateMessage: ExpirationTimerUpdate = ExpirationTimerUpdate() - .with(sentTimestamp: UInt64(currentTimestampMs)) - .with(updatedConfig) - - try MessageSender.send( - db, - message: expirationTimerUpdateMessage, - interactionId: interactionId, - threadId: threadId, - threadVariant: threadVariant, - using: dependencies - ) - } - - // Contacts & legacy closed groups need to update the LibSession - dependencies.storage.writeAsync(using: dependencies) { [threadId, threadVariant] db in + // Update libSession switch threadVariant { case .contact: - try LibSession - .update( - db, - sessionId: threadId, - disappearingMessagesConfig: updatedConfig - ) + try LibSession.update( + db, + sessionId: threadId, + disappearingMessagesConfig: updatedConfig, + using: dependencies + ) case .legacyGroup: - try LibSession - .update( - db, - groupPublicKey: threadId, - disappearingConfig: updatedConfig - ) + try LibSession.update( + db, + legacyGroupSessionId: threadId, + disappearingConfig: updatedConfig, + using: dependencies + ) + + case .group: + try LibSession.update( + db, + groupSessionId: SessionId(.group, hex: threadId), + disappearingConfig: updatedConfig, + using: dependencies + ) default: break } + + // Send a control message that the disappearing messages setting changed + switch threadVariant { + case .group: + try MessageSender.send( + db, + message: GroupUpdateInfoChangeMessage( + changeType: .disappearingMessages, + updatedExpiration: UInt32(updatedConfig.isEnabled ? updatedConfig.durationSeconds : 0), + sentTimestampMs: UInt64(currentOffsetTimestampMs), + authMethod: try Authentication.with( + db, + swarmPublicKey: threadId, + using: dependencies + ), + using: dependencies + ), + interactionId: nil, + threadId: threadId, + threadVariant: .group, + using: dependencies + ) + + default: + try MessageSender.send( + db, + message: ExpirationTimerUpdate() + .with(sentTimestampMs: UInt64(currentOffsetTimestampMs)) + .with(updatedConfig), + interactionId: interactionId, + threadId: threadId, + threadVariant: threadVariant, + using: dependencies + ) + } } } } + +extension String: Differentiable {} diff --git a/Session/Conversations/Settings/ThreadSettingsViewModel.swift b/Session/Conversations/Settings/ThreadSettingsViewModel.swift index 61ffd19f8a9..774fe44733e 100644 --- a/Session/Conversations/Settings/ThreadSettingsViewModel.swift +++ b/Session/Conversations/Settings/ThreadSettingsViewModel.swift @@ -11,18 +11,24 @@ import SignalUtilitiesKit import SessionUtilitiesKit import SessionSnodeKit -class ThreadSettingsViewModel: SessionTableViewModel, NavigationItemSource, NavigatableStateHolder, EditableStateHolder, ObservableTableSource { +class ThreadSettingsViewModel: SessionTableViewModel, NavigatableStateHolder, ObservableTableSource { public let dependencies: Dependencies public let navigatableState: NavigatableState = NavigatableState() - public let editableState: EditableState = EditableState() public let state: TableDataState = TableDataState() public let observableState: ObservableTableSourceState = ObservableTableSourceState() private let threadId: String private let threadVariant: SessionThread.Variant private let didTriggerSearch: () -> () - private var oldDisplayName: String? - private var editedDisplayName: String? + private var updatedName: String? + private var updatedDescription: String? + private var onDisplayPictureSelected: ((ConfirmationModal.ValueUpdate) -> Void)? + private lazy var imagePickerHandler: ImagePickerHandler = ImagePickerHandler( + onTransition: { [weak self] in self?.transitionToScreen($0, transitionType: $1) }, + onImageDataPicked: { [weak self] resultImageData in + self?.onDisplayPictureSelected?(.image(resultImageData)) + } + ) // MARK: - Initialization @@ -30,22 +36,12 @@ class ThreadSettingsViewModel: SessionTableViewModel, NavigationItemSource, Navi threadId: String, threadVariant: SessionThread.Variant, didTriggerSearch: @escaping () -> (), - using dependencies: Dependencies = Dependencies() + using dependencies: Dependencies ) { self.dependencies = dependencies self.threadId = threadId self.threadVariant = threadVariant self.didTriggerSearch = didTriggerSearch - self.oldDisplayName = (threadVariant != .contact ? - nil : - dependencies.storage.read { db in - try Profile - .filter(id: threadId) - .select(.nickname) - .asRequest(of: String.self) - .fetchOne(db) - } - ) } // MARK: - Config @@ -64,11 +60,21 @@ class ThreadSettingsViewModel: SessionTableViewModel, NavigationItemSource, Navi public enum Section: SessionTableSection { case conversationInfo case content + case adminActions + case destructiveActions + + public var style: SessionTableSectionStyle { + switch self { + case .destructiveActions: return .padding + default: return .none + } + } } public enum TableItem: Differentiable { case avatar - case nickname + case displayName + case threadDescription case sessionId case copyThreadId @@ -77,110 +83,22 @@ class ThreadSettingsViewModel: SessionTableViewModel, NavigationItemSource, Navi case addToOpenGroup case disappearingMessages case disappearingMessagesDuration + case groupMembers case editGroup + case promoteAdmins case leaveGroup - case notificationSound case notificationMentionsOnly case notificationMute case blockUser + + case debugDeleteBeforeNow + case debugDeleteAttachmentsBeforeNow } - // MARK: - Navigation - - lazy var navState: AnyPublisher = { - Publishers - .CombineLatest( - isEditing, - textChanged - .handleEvents( - receiveOutput: { [weak self] value, _ in - self?.editedDisplayName = value - } - ) - .filter { _ in false } - .prepend((nil, .nickname)) - ) - .map { isEditing, _ -> NavState in (isEditing ? .editing : .standard) } - .removeDuplicates() - .prepend(.standard) // Initial value - .shareReplay(1) - .eraseToAnyPublisher() - }() - - lazy var leftNavItems: AnyPublisher<[SessionNavItem], Never> = navState - .map { [weak self] navState -> [SessionNavItem] in - // Only show the 'Edit' button if it's a contact thread - guard self?.threadVariant == .contact else { return [] } - guard navState == .editing else { return [] } - - return [ - SessionNavItem( - id: .cancel, - systemItem: .cancel, - accessibilityIdentifier: "Cancel button" - ) { [weak self] in - self?.setIsEditing(false) - self?.editedDisplayName = self?.oldDisplayName - } - ] - } - .eraseToAnyPublisher() - - lazy var rightNavItems: AnyPublisher<[SessionNavItem], Never> = navState - .map { [weak self, dependencies] navState -> [SessionNavItem] in - // Only show the 'Edit' button if it's a contact thread - guard self?.threadVariant == .contact else { return [] } - - switch navState { - case .editing: - return [ - SessionNavItem( - id: .done, - systemItem: .done, - accessibilityIdentifier: "Done" - ) { [weak self] in - self?.setIsEditing(false) - - guard - self?.threadVariant == .contact, - let threadId: String = self?.threadId, - let editedDisplayName: String = self?.editedDisplayName - else { return } - - let updatedNickname: String = editedDisplayName - .trimmingCharacters(in: .whitespacesAndNewlines) - self?.oldDisplayName = (updatedNickname.isEmpty ? nil : editedDisplayName) - - dependencies.storage.writeAsync(using: dependencies) { db in - try Profile - .filter(id: threadId) - .updateAllAndConfig( - db, - Profile.Columns.nickname - .set(to: (updatedNickname.isEmpty ? nil : editedDisplayName)) - ) - } - } - ] - - case .standard: - return [ - SessionNavItem( - id: .edit, - systemItem: .edit, - accessibilityIdentifier: "Edit button", - accessibilityLabel: "Edit user nickname" - ) { [weak self] in self?.setIsEditing(true) } - ] - } - } - .eraseToAnyPublisher() - // MARK: - Content private struct State: Equatable { let threadViewModel: SessionThreadViewModel? - let notificationSound: Preferences.Sound let disappearingMessagesConfig: DisappearingMessagesConfiguration } @@ -193,26 +111,16 @@ class ThreadSettingsViewModel: SessionTableViewModel, NavigationItemSource, Navi lazy var observation: TargetObservation = ObservationBuilder .databaseObservation(self) { [dependencies, threadId = self.threadId] db -> State in - let userPublicKey: String = getUserHexEncodedPublicKey(db, using: dependencies) + let userSessionId: SessionId = dependencies[cache: .general].sessionId let threadViewModel: SessionThreadViewModel? = try SessionThreadViewModel - .conversationSettingsQuery(threadId: threadId, userPublicKey: userPublicKey) - .fetchOne(db) - - let fallbackSound: Preferences.Sound = db[.defaultNotificationSound] - .defaulting(to: Preferences.Sound.defaultNotificationSound) - let notificationSound: Preferences.Sound = try SessionThread - .filter(id: threadId) - .select(.notificationSound) - .asRequest(of: Preferences.Sound.self) + .conversationSettingsQuery(threadId: threadId, userSessionId: userSessionId) .fetchOne(db) - .defaulting(to: fallbackSound) let disappearingMessagesConfig: DisappearingMessagesConfiguration = try DisappearingMessagesConfiguration .fetchOne(db, id: threadId) .defaulting(to: DisappearingMessagesConfiguration.defaultWith(threadId)) return State( threadViewModel: threadViewModel, - notificationSound: notificationSound, disappearingMessagesConfig: disappearingMessagesConfig ) } @@ -240,518 +148,680 @@ class ThreadSettingsViewModel: SessionTableViewModel, NavigationItemSource, Navi ) && threadViewModel.currentUserIsClosedGroupAdmin == true ) - let editIcon: UIImage? = UIImage(named: "icon_edit") + let editIcon: UIImage? = UIImage(systemName: "pencil") + let canEditDisplayName: Bool = ( + threadViewModel.threadIsNoteToSelf != true && ( + threadViewModel.threadVariant == .contact || + currentUserIsClosedGroupAdmin + ) + ) - return [ - SectionModel( - model: .conversationInfo, - elements: [ + let conversationInfoSection: SectionModel = SectionModel( + model: .conversationInfo, + elements: [ + SessionCell.Info( + id: .avatar, + accessory: .profile( + id: threadViewModel.id, + size: .hero, + threadVariant: threadViewModel.threadVariant, + displayPictureFilename: threadViewModel.displayPictureFilename, + profile: threadViewModel.profile, + profileIcon: { + guard + threadViewModel.threadVariant == .group && + currentUserIsClosedGroupAdmin && + dependencies[feature: .updatedGroupsAllowDisplayPicture] + else { return .none } + + // If we already have a display picture then the main profile gets the icon + return (threadViewModel.displayPictureFilename != nil ? .rightPlus : .none) + }(), + additionalProfile: threadViewModel.additionalProfile, + additionalProfileIcon: { + guard + threadViewModel.threadVariant == .group && + currentUserIsClosedGroupAdmin && + dependencies[feature: .updatedGroupsAllowDisplayPicture] + else { return .none } + + // No display picture means the dual-profile so the additionalProfile gets the icon + return .rightPlus + }(), + accessibility: nil + ), + styling: SessionCell.StyleInfo( + alignment: .centerHugging, + customPadding: SessionCell.Padding(bottom: Values.smallSpacing), + backgroundStyle: .noBackground + ), + onTap: { [weak self] in + switch (threadViewModel.threadVariant, threadViewModel.displayPictureFilename, currentUserIsClosedGroupAdmin) { + case (.contact, _, _): self?.viewDisplayPicture(threadViewModel: threadViewModel) + case (.group, _, true): + self?.updateGroupDisplayPicture(currentFileName: threadViewModel.displayPictureFilename) + + case (_, .some, _): self?.viewDisplayPicture(threadViewModel: threadViewModel) + default: break + } + + } + ), + SessionCell.Info( + id: .displayName, + leadingAccessory: (!canEditDisplayName ? nil : + .icon( + editIcon?.withRenderingMode(.alwaysTemplate), + size: .mediumAspectFill, + customTint: .textSecondary, + shouldFill: true + ) + ), + title: SessionCell.TextInfo( + threadViewModel.displayName, + font: .titleLarge, + alignment: .center + ), + styling: SessionCell.StyleInfo( + alignment: .centerHugging, + customPadding: SessionCell.Padding( + top: Values.smallSpacing, + leading: (!canEditDisplayName ? nil : + -((IconSize.medium.size + (Values.smallSpacing * 2)) / 2) + ), + bottom: { + guard threadViewModel.threadVariant != .contact else { return Values.smallSpacing } + guard threadViewModel.threadDescription == nil else { return Values.smallSpacing } + + return Values.largeSpacing + }() + ), + backgroundStyle: .noBackground + ), + accessibility: Accessibility( + identifier: "Username", + label: threadViewModel.displayName + ), + onTap: { [weak self] in + guard !threadViewModel.threadIsNoteToSelf else { return } + + switch (threadViewModel.threadVariant, currentUserIsClosedGroupAdmin) { + case (.contact, _): + self?.updateNickname( + current: threadViewModel.profile?.nickname, + displayName: ( + /// **Note:** We want to use the `profile` directly rather than `threadViewModel.displayName` + /// as the latter would use the `nickname` here which is incorrect + threadViewModel.profile?.displayName(ignoringNickname: true) ?? + Profile.truncated(id: threadViewModel.threadId, truncating: .middle) + ) + ) + + case (.group, true), (.legacyGroup, true): + self?.updateGroupNameAndDescription( + currentName: threadViewModel.displayName, + currentDescription: threadViewModel.threadDescription, + isUpdatedGroup: (threadViewModel.threadVariant == .group) + ) + + case (.community, _), (.legacyGroup, false), (.group, false): break + } + } + ), + + threadViewModel.threadDescription.map { threadDescription in SessionCell.Info( - id: .avatar, - accessory: .profile( - id: threadViewModel.id, - size: .hero, - threadVariant: threadViewModel.threadVariant, - customImageData: threadViewModel.openGroupProfilePictureData, - profile: threadViewModel.profile, - profileIcon: .none, - additionalProfile: threadViewModel.additionalProfile, - additionalProfileIcon: .none, - accessibility: nil + id: .threadDescription, + subtitle: SessionCell.TextInfo( + threadDescription, + font: .subtitle, + alignment: .center ), styling: SessionCell.StyleInfo( - alignment: .centerHugging, - customPadding: SessionCell.Padding(bottom: Values.smallSpacing), + tintColor: .textSecondary, + customPadding: SessionCell.Padding( + top: 0, + bottom: (threadViewModel.threadVariant != .contact ? Values.largeSpacing : nil) + ), backgroundStyle: .noBackground ), - onTap: { [weak self] in self?.viewProfilePicture(threadViewModel: threadViewModel) } - ), + accessibility: Accessibility( + identifier: "Description", + label: threadDescription + ) + ) + }, + + (threadViewModel.threadVariant != .contact ? nil : SessionCell.Info( - id: .nickname, - leftAccessory: (threadViewModel.threadVariant != .contact ? nil : - .icon( - editIcon?.withRenderingMode(.alwaysTemplate), - size: .fit, - customTint: .textSecondary - ) - ), - title: SessionCell.TextInfo( - threadViewModel.displayName, - font: .titleLarge, + id: .sessionId, + subtitle: SessionCell.TextInfo( + threadViewModel.id, + font: .monoSmall, alignment: .center, - editingPlaceholder: "nicknameEnter".localized(), - interaction: (threadViewModel.threadVariant == .contact ? .editable : .none) + interaction: .copy ), styling: SessionCell.StyleInfo( - alignment: .centerHugging, customPadding: SessionCell.Padding( top: Values.smallSpacing, - trailing: (threadViewModel.threadVariant != .contact ? - nil : - -(((editIcon?.size.width ?? 0) + (Values.smallSpacing * 2)) / 2) - ), - bottom: (threadViewModel.threadVariant != .contact ? - nil : - Values.smallSpacing - ), - interItem: 0 + bottom: Values.largeSpacing ), backgroundStyle: .noBackground ), accessibility: Accessibility( - identifier: "Username", - label: threadViewModel.displayName + identifier: "Session ID", + label: threadViewModel.id + ) + ) + ) + ].compactMap { $0 } + ) + let standardActionsSection: SectionModel = SectionModel( + model: .content, + elements: [ + (threadViewModel.threadVariant == .legacyGroup || threadViewModel.threadVariant == .group ? nil : + SessionCell.Info( + id: .copyThreadId, + leadingAccessory: .icon( + UIImage(named: "ic_copy")? + .withRenderingMode(.alwaysTemplate) + ), + title: (threadViewModel.threadVariant == .community ? + "communityUrlCopy".localized() : + "accountIDCopy".localized() + ), + accessibility: Accessibility( + identifier: "\(ThreadSettingsViewModel.self).copy_thread_id", + label: "Copy Session ID" ), onTap: { [weak self] in - self?.textChanged(self?.oldDisplayName, for: .nickname) - self?.setIsEditing(true) - } - ), + switch threadViewModel.threadVariant { + case .contact, .legacyGroup, .group: + UIPasteboard.general.string = threadViewModel.threadId - (threadViewModel.threadVariant != .contact ? nil : - SessionCell.Info( - id: .sessionId, - subtitle: SessionCell.TextInfo( - threadViewModel.id, - font: .monoSmall, - alignment: .center, - interaction: .copy - ), - styling: SessionCell.StyleInfo( - customPadding: SessionCell.Padding( - top: Values.smallSpacing, - bottom: Values.largeSpacing - ), - backgroundStyle: .noBackground - ), - accessibility: Accessibility( - identifier: "Session ID", - label: threadViewModel.id - ) - ) - ) - ].compactMap { $0 } - ), - SectionModel( - model: .content, - elements: [ - (threadViewModel.threadVariant == .legacyGroup || threadViewModel.threadVariant == .group ? nil : - SessionCell.Info( - id: .copyThreadId, - leftAccessory: .icon( - UIImage(named: "ic_copy")? - .withRenderingMode(.alwaysTemplate) - ), - title: (threadViewModel.threadVariant == .community ? - "communityUrlCopy".localized() : - "accountIDCopy".localized() - ), - accessibility: Accessibility( - identifier: "\(ThreadSettingsViewModel.self).copy_thread_id", - label: "Copy Session ID" - ), - onTap: { [weak self] in - switch threadViewModel.threadVariant { - case .contact, .legacyGroup, .group: - UIPasteboard.general.string = threadViewModel.threadId + case .community: + guard + let urlString: String = LibSession.communityUrlFor( + server: threadViewModel.openGroupServer, + roomToken: threadViewModel.openGroupRoomToken, + publicKey: threadViewModel.openGroupPublicKey + ) + else { return } - case .community: - guard - let urlString: String = LibSession.communityUrlFor( - server: threadViewModel.openGroupServer, - roomToken: threadViewModel.openGroupRoomToken, - publicKey: threadViewModel.openGroupPublicKey - ) - else { return } + UIPasteboard.general.string = urlString + } - UIPasteboard.general.string = urlString - } + self?.showToast( + text: "copied".localized(), + backgroundColor: .backgroundSecondary + ) + } + ) + ), - self?.showToast( - text: "copied".localized(), - backgroundColor: .backgroundSecondary - ) - } + SessionCell.Info( + id: .allMedia, + leadingAccessory: .icon( + UIImage(named: "actionsheet_camera_roll_black")? + .withRenderingMode(.alwaysTemplate) + ), + title: "conversationsSettingsAllMedia".localized(), + accessibility: Accessibility( + identifier: "\(ThreadSettingsViewModel.self).all_media", + label: "All media" + ), + onTap: { [weak self, dependencies] in + self?.transitionToScreen( + MediaGalleryViewModel.createAllMediaViewController( + threadId: threadViewModel.threadId, + threadVariant: threadViewModel.threadVariant, + focusedAttachmentId: nil, + using: dependencies + ) ) + } + ), + + SessionCell.Info( + id: .searchConversation, + leadingAccessory: .icon( + UIImage(named: "conversation_settings_search")? + .withRenderingMode(.alwaysTemplate) ), + title: "searchConversation".localized(), + accessibility: Accessibility( + identifier: "\(ThreadSettingsViewModel.self).search", + label: "Search" + ), + onTap: { [weak self] in self?.didTriggerSearch() } + ), + (threadViewModel.threadVariant != .community ? nil : SessionCell.Info( - id: .allMedia, - leftAccessory: .icon( - UIImage(named: "actionsheet_camera_roll_black")? + id: .addToOpenGroup, + leadingAccessory: .icon( + UIImage(named: "ic_plus_24")? .withRenderingMode(.alwaysTemplate) ), - title: "conversationsSettingsAllMedia".localized(), + title: "membersInvite".localized(), accessibility: Accessibility( - identifier: "\(ThreadSettingsViewModel.self).all_media", - label: "All media" + identifier: "\(ThreadSettingsViewModel.self).add_to_open_group" ), - onTap: { [weak self] in + onTap: { [weak self] in self?.inviteUsersToCommunity(threadViewModel: threadViewModel) } + ) + ), + + (threadViewModel.threadVariant == .community || threadViewModel.threadIsBlocked == true ? nil : + SessionCell.Info( + id: .disappearingMessages, + leadingAccessory: .icon( + UIImage(systemName: "timer")? + .withRenderingMode(.alwaysTemplate) + ), + title: "disappearingMessages".localized(), + subtitle: { + guard current.disappearingMessagesConfig.isEnabled else { + return "off".localized() + } + + return (current.disappearingMessagesConfig.type ?? .unknown) + .localizedState( + durationString: current.disappearingMessagesConfig.durationString + ) + }(), + accessibility: Accessibility( + identifier: "Disappearing messages", + label: "\(ThreadSettingsViewModel.self).disappearing_messages" + ), + onTap: { [weak self, dependencies] in self?.transitionToScreen( - MediaGalleryViewModel.createAllMediaViewController( - threadId: threadViewModel.threadId, - threadVariant: threadViewModel.threadVariant, - focusedAttachmentId: nil + SessionTableViewController( + viewModel: ThreadDisappearingMessagesSettingsViewModel( + threadId: threadViewModel.threadId, + threadVariant: threadViewModel.threadVariant, + currentUserIsClosedGroupMember: threadViewModel.currentUserIsClosedGroupMember, + currentUserIsClosedGroupAdmin: threadViewModel.currentUserIsClosedGroupAdmin, + config: current.disappearingMessagesConfig, + using: dependencies + ) ) ) } - ), + ) + ), + (!currentUserIsClosedGroupMember ? nil : SessionCell.Info( - id: .searchConversation, - leftAccessory: .icon( - UIImage(named: "conversation_settings_search")? + id: .groupMembers, + leadingAccessory: .icon( + UIImage(named: "icon_members")? .withRenderingMode(.alwaysTemplate) ), - title: "searchConversation".localized(), + title: "groupMembers".localized(), accessibility: Accessibility( - identifier: "\(ThreadSettingsViewModel.self).search", - label: "Search" + identifier: "Group members", + label: "Group members" ), - onTap: { [weak self] in - self?.didTriggerSearch() - } - ), + onTap: { [weak self] in self?.viewMembers() } + ) + ), - (threadViewModel.threadVariant != .community ? nil : - SessionCell.Info( - id: .addToOpenGroup, - leftAccessory: .icon( - UIImage(named: "ic_plus_24")? - .withRenderingMode(.alwaysTemplate) - ), - title: "membersInvite".localized(), - accessibility: Accessibility( - identifier: "\(ThreadSettingsViewModel.self).add_to_open_group" - ), - onTap: { [weak self] in - self?.transitionToScreen( - UserSelectionVC( - with: "membersInvite".localized(), - excluding: Set() - ) { [weak self] selectedUsers in - self?.addUsersToOpenGoup( - threadViewModel: threadViewModel, - selectedUsers: selectedUsers - ) - } + (!currentUserIsClosedGroupAdmin ? nil : + SessionCell.Info( + id: .editGroup, + leadingAccessory: .icon( + UIImage(named: "table_ic_group_edit")? + .withRenderingMode(.alwaysTemplate) + ), + title: "groupEdit".localized(), + accessibility: Accessibility( + identifier: "Edit group", + label: "Edit group" + ), + onTap: { [weak self, dependencies] in + self?.transitionToScreen( + SessionTableViewController( + viewModel: EditGroupViewModel( + threadId: threadViewModel.threadId, + using: dependencies + ) ) - } - ) - ), + ) + } + ) + ), + + (!currentUserIsClosedGroupAdmin || !dependencies[feature: .updatedGroupsAllowPromotions] ? nil : + SessionCell.Info( + id: .promoteAdmins, + leadingAccessory: .icon( + UIImage(named: "table_ic_group_edit")? + .withRenderingMode(.alwaysTemplate) + ), + title: "adminPromote".localized(), + accessibility: Accessibility( + identifier: "Promote admins", + label: "Promote admins" + ), + onTap: { [weak self] in self?.promoteAdmins() } + ) + ), - (threadViewModel.threadVariant == .community || threadViewModel.threadIsBlocked == true ? nil : - SessionCell.Info( - id: .disappearingMessages, - leftAccessory: .icon( - UIImage(systemName: "timer")? - .withRenderingMode(.alwaysTemplate) - ), - title: "disappearingMessages".localized(), - subtitle: { - guard current.disappearingMessagesConfig.isEnabled else { - return "off".localized() - } - - return (current.disappearingMessagesConfig.type ?? .unknown) - .localizedState( - durationString: current.disappearingMessagesConfig.durationString - ) - }(), - accessibility: Accessibility( - identifier: "Disappearing messages", - label: "\(ThreadSettingsViewModel.self).disappearing_messages" + (!currentUserIsClosedGroupMember ? nil : + SessionCell.Info( + id: .leaveGroup, + leadingAccessory: .icon( + UIImage(named: "table_ic_group_leave")? + .withRenderingMode(.alwaysTemplate) + ), + title: "groupLeave".localized(), + accessibility: Accessibility( + identifier: "Leave group", + label: "Leave group" + ), + confirmationInfo: ConfirmationModal.Info( + title: "groupLeave".localized(), + body: (currentUserIsClosedGroupAdmin ? + .attributedText( + "groupLeaveDescriptionAdmin" + .put(key: "group_name", value: threadViewModel.displayName) + .localizedFormatted(baseFont: .boldSystemFont(ofSize: Values.smallFontSize)) + ) : + .attributedText( + "groupLeaveDescription" + .put(key: "group_name", value: threadViewModel.displayName) + .localizedFormatted(baseFont: .boldSystemFont(ofSize: Values.smallFontSize)) + ) ), - onTap: { [weak self] in - self?.transitionToScreen( - SessionTableViewController( - viewModel: ThreadDisappearingMessagesSettingsViewModel( - threadId: threadViewModel.threadId, - threadVariant: threadViewModel.threadVariant, - currentUserIsClosedGroupMember: threadViewModel.currentUserIsClosedGroupMember, - currentUserIsClosedGroupAdmin: threadViewModel.currentUserIsClosedGroupAdmin, - config: current.disappearingMessagesConfig - ) - ) + confirmTitle: "leave".localized(), + confirmStyle: .danger, + cancelStyle: .alert_text + ), + onTap: { [dependencies] in + dependencies[singleton: .storage].write { db in + try SessionThread.deleteOrLeave( + db, + type: .leaveGroupAsync, + threadId: threadViewModel.threadId, + threadVariant: threadViewModel.threadVariant, + calledFromConfig: nil, + using: dependencies ) } - ) - ), - - (!currentUserIsClosedGroupMember ? nil : - SessionCell.Info( - id: .editGroup, - leftAccessory: .icon( - UIImage(named: "table_ic_group_edit")? - .withRenderingMode(.alwaysTemplate) - ), - title: "groupEdit".localized(), + } + ) + ), + + (threadViewModel.threadVariant == .contact ? nil : + SessionCell.Info( + id: .notificationMentionsOnly, + leadingAccessory: .icon( + UIImage(named: "NotifyMentions")? + .withRenderingMode(.alwaysTemplate) + ), + title: "deleteAfterGroupPR1MentionsOnly".localized(), + subtitle: "deleteAfterGroupPR1MentionsOnlyDescription".localized(), + trailingAccessory: .toggle( + threadViewModel.threadOnlyNotifyForMentions == true, + oldValue: (previous?.threadViewModel?.threadOnlyNotifyForMentions == true), accessibility: Accessibility( - identifier: "Edit group", - label: "Edit group" - ), - onTap: { [weak self] in - self?.transitionToScreen( - EditClosedGroupVC( - threadId: threadViewModel.threadId, - threadVariant: threadViewModel.threadVariant + identifier: "Notify for Mentions Only - Switch" + ) + ), + isEnabled: ( + ( + threadViewModel.threadVariant != .legacyGroup && + threadViewModel.threadVariant != .group + ) || + currentUserIsClosedGroupMember + ), + accessibility: Accessibility( + identifier: "Mentions only notification setting", + label: "Mentions only" + ), + onTap: { [dependencies] in + let newValue: Bool = !(threadViewModel.threadOnlyNotifyForMentions == true) + + dependencies[singleton: .storage].writeAsync { db in + try SessionThread + .filter(id: threadViewModel.threadId) + .updateAll( + db, + SessionThread.Columns.onlyNotifyForMentions + .set(to: newValue) ) - ) } - ) - ), - - (!currentUserIsClosedGroupMember ? nil : - SessionCell.Info( - id: .leaveGroup, - leftAccessory: .icon( - UIImage(named: "table_ic_group_leave")? - .withRenderingMode(.alwaysTemplate) - ), - title: "groupLeave".localized(), + } + ) + ), + + (threadViewModel.threadIsNoteToSelf ? nil : + SessionCell.Info( + id: .notificationMute, + leadingAccessory: .icon( + UIImage(named: "Mute")? + .withRenderingMode(.alwaysTemplate) + ), + title: "notificationsMute".localized(), + trailingAccessory: .toggle( + threadViewModel.threadMutedUntilTimestamp != nil, + oldValue: (previous?.threadViewModel?.threadMutedUntilTimestamp != nil), accessibility: Accessibility( - identifier: "Leave group", - label: "Leave group" - ), - confirmationInfo: ConfirmationModal.Info( - title: "groupLeave".localized(), - body: (currentUserIsClosedGroupAdmin ? - .attributedText( - "groupDeleteDescription" - .put(key: "group_name", value: threadViewModel.displayName) - .localizedFormatted(baseFont: .boldSystemFont(ofSize: Values.smallFontSize)) - ) : - .attributedText( - "groupLeaveDescription" - .put(key: "group_name", value: threadViewModel.displayName) - .localizedFormatted(baseFont: .boldSystemFont(ofSize: Values.smallFontSize)) - ) - ), - confirmTitle: "leave".localized(), - confirmStyle: .danger, - cancelStyle: .alert_text - ), - onTap: { [dependencies] in - dependencies.storage.write { db in - try SessionThread.deleteOrLeave( + identifier: "Mute - Switch" + ) + ), + isEnabled: ( + ( + threadViewModel.threadVariant != .legacyGroup && + threadViewModel.threadVariant != .group + ) || + currentUserIsClosedGroupMember + ), + accessibility: Accessibility( + identifier: "\(ThreadSettingsViewModel.self).mute", + label: "Mute notifications" + ), + onTap: { [dependencies] in + dependencies[singleton: .storage].writeAsync { db in + let currentValue: TimeInterval? = try SessionThread + .filter(id: threadViewModel.threadId) + .select(.mutedUntilTimestamp) + .asRequest(of: TimeInterval.self) + .fetchOne(db) + + try SessionThread + .filter(id: threadViewModel.threadId) + .updateAll( db, - type: .leaveGroupAsync, - threadId: threadViewModel.threadId, - calledFromConfigHandling: false + SessionThread.Columns.mutedUntilTimestamp.set( + to: (currentValue == nil ? + Date.distantFuture.timeIntervalSince1970 : + nil + ) + ) ) - } } - ) - ), - - (threadViewModel.threadIsNoteToSelf ? nil : - SessionCell.Info( - id: .notificationSound, - leftAccessory: .icon( - UIImage(named: "table_ic_notification_sound")? - .withRenderingMode(.alwaysTemplate) - ), - title: "deleteAfterGroupPR1MessageSound".localized(), - rightAccessory: .dropDown( - .dynamicString { current.notificationSound.displayName } - ), - onTap: { [weak self] in - self?.transitionToScreen( - SessionTableViewController( - viewModel: NotificationSoundViewModel(threadId: threadViewModel.threadId) + } + ) + ), + + (threadViewModel.threadIsNoteToSelf || threadViewModel.threadVariant != .contact ? nil : + SessionCell.Info( + id: .blockUser, + leadingAccessory: .icon( + UIImage(named: "table_ic_block")? + .withRenderingMode(.alwaysTemplate) + ), + title: "deleteAfterGroupPR1BlockThisUser".localized(), + trailingAccessory: .toggle( + threadViewModel.threadIsBlocked == true, + oldValue: (previous?.threadViewModel?.threadIsBlocked == true), + accessibility: Accessibility( + identifier: "Block This User - Switch" + ) + ), + accessibility: Accessibility( + identifier: "\(ThreadSettingsViewModel.self).block", + label: "Block" + ), + confirmationInfo: ConfirmationModal.Info( + title: { + guard threadViewModel.threadIsBlocked == true else { + return String( + format: "block".localized(), + threadViewModel.displayName ) + } + + return String( + format: "blockUnblock".localized(), + threadViewModel.displayName ) - } - ) - ), - - (threadViewModel.threadVariant == .contact ? nil : - SessionCell.Info( - id: .notificationMentionsOnly, - leftAccessory: .icon( - UIImage(named: "NotifyMentions")? - .withRenderingMode(.alwaysTemplate) - ), - title: "deleteAfterGroupPR1MentionsOnly".localized(), - subtitle: "deleteAfterGroupPR1MentionsOnlyDescription".localized(), - rightAccessory: .toggle( - .boolValue( - threadViewModel.threadOnlyNotifyForMentions == true, - oldValue: ((previous?.threadViewModel ?? threadViewModel).threadOnlyNotifyForMentions == true) - ), - accessibility: Accessibility( - identifier: "Notify for Mentions Only - Switch" + }(), + body: (threadViewModel.threadIsBlocked == true ? + .attributedText( + "blockUnblockName" + .put(key: "name", value: threadViewModel.displayName) + .localizedFormatted(baseFont: .systemFont(ofSize: Values.smallFontSize)) + ) : + .attributedText( + "blockDescription" + .put(key: "name", value: threadViewModel.displayName) + .localizedFormatted(baseFont: .systemFont(ofSize: Values.smallFontSize)) ) ), - isEnabled: ( - ( - threadViewModel.threadVariant != .legacyGroup && - threadViewModel.threadVariant != .group - ) || - currentUserIsClosedGroupMember - ), - accessibility: Accessibility( - identifier: "Mentions only notification setting", - label: "Mentions only" + confirmTitle: (threadViewModel.threadIsBlocked == true ? + "blockUnblock".localized() : + "block".localized() ), - onTap: { [dependencies] in - let newValue: Bool = !(threadViewModel.threadOnlyNotifyForMentions == true) - - dependencies.storage.writeAsync { db in - try SessionThread - .filter(id: threadViewModel.threadId) - .updateAll( - db, - SessionThread.Columns.onlyNotifyForMentions - .set(to: newValue) - ) - } - } - ) - ), - - (threadViewModel.threadIsNoteToSelf ? nil : + confirmStyle: .danger, + cancelStyle: .alert_text + ), + onTap: { [weak self] in + let isBlocked: Bool = (threadViewModel.threadIsBlocked == true) + + self?.updateBlockedState( + from: isBlocked, + isBlocked: !isBlocked, + threadId: threadViewModel.threadId, + displayName: threadViewModel.displayName + ) + } + ) + ) + ].compactMap { $0 } + ) + let adminActionsSection: SectionModel? = nil + let destructiveActionsSection: SectionModel? + + if dependencies[feature: .updatedGroupsDeleteBeforeNow] || dependencies[feature: .updatedGroupsDeleteAttachmentsBeforeNow] { + destructiveActionsSection = SectionModel( + model: .destructiveActions, + elements: [ + // FIXME: [GROUPS REBUILD] Need to build this properly in a future release + (!dependencies[feature: .updatedGroupsDeleteBeforeNow] || threadViewModel.threadVariant != .group ? nil : SessionCell.Info( - id: .notificationMute, - leftAccessory: .icon( - UIImage(named: "Mute")? - .withRenderingMode(.alwaysTemplate) - ), - title: "notificationsMute".localized(), - rightAccessory: .toggle( - .boolValue( - threadViewModel.threadMutedUntilTimestamp != nil, - oldValue: ((previous?.threadViewModel ?? threadViewModel).threadMutedUntilTimestamp != nil) - ), - accessibility: Accessibility( - identifier: "Mute - Switch" - ) + id: .debugDeleteBeforeNow, + leadingAccessory: .icon( + UIImage(named: "ic_bin")? + .withRenderingMode(.alwaysTemplate), + customTint: .danger ), - isEnabled: ( - ( - threadViewModel.threadVariant != .legacyGroup && - threadViewModel.threadVariant != .group - ) || - currentUserIsClosedGroupMember + title: "[DEBUG] Delete all messages before now", // stringlint:disable + styling: SessionCell.StyleInfo( + tintColor: .danger ), - accessibility: Accessibility( - identifier: "\(ThreadSettingsViewModel.self).mute", - label: "Mute notifications" + confirmationInfo: ConfirmationModal.Info( + title: "delete".localized(), + body: .text("Are you sure you want to delete all messages sent before now for all group members?"), // stringlint:disable + confirmTitle: "delete".localized(), + confirmStyle: .danger, + cancelStyle: .alert_text ), - onTap: { [dependencies] in - dependencies.storage.writeAsync { db in - let currentValue: TimeInterval? = try SessionThread - .filter(id: threadViewModel.threadId) - .select(.mutedUntilTimestamp) - .asRequest(of: TimeInterval.self) - .fetchOne(db) - - try SessionThread - .filter(id: threadViewModel.threadId) - .updateAll( - db, - SessionThread.Columns.mutedUntilTimestamp.set( - to: (currentValue == nil ? - Date.distantFuture.timeIntervalSince1970 : - nil - ) - ) - ) - } - } + onTap: { [weak self] in self?.deleteAllMessagesBeforeNow() } ) ), - - (threadViewModel.threadIsNoteToSelf || threadViewModel.threadVariant != .contact ? nil : + // FIXME: [GROUPS REBUILD] Need to build this properly in a future release + (!dependencies[feature: .updatedGroupsDeleteAttachmentsBeforeNow] || threadViewModel.threadVariant != .group ? nil : SessionCell.Info( - id: .blockUser, - leftAccessory: .icon( - UIImage(named: "table_ic_block")? - .withRenderingMode(.alwaysTemplate) - ), - title: "deleteAfterGroupPR1BlockThisUser".localized(), - rightAccessory: .toggle( - .boolValue( - threadViewModel.threadIsBlocked == true, - oldValue: ((previous?.threadViewModel ?? threadViewModel).threadIsBlocked == true) - ), - accessibility: Accessibility( - identifier: "Block This User - Switch" - ) + id: .debugDeleteAttachmentsBeforeNow, + leadingAccessory: .icon( + UIImage(named: "ic_bin")? + .withRenderingMode(.alwaysTemplate), + customTint: .danger ), - accessibility: Accessibility( - identifier: "\(ThreadSettingsViewModel.self).block", - label: "Block" + title: "[DEBUG] Delete all arrachments before now", // stringlint:disable + styling: SessionCell.StyleInfo( + tintColor: .danger ), confirmationInfo: ConfirmationModal.Info( - title: { - guard threadViewModel.threadIsBlocked == true else { - return String( - format: "block".localized(), - threadViewModel.displayName - ) - } - - return String( - format: "blockUnblock".localized(), - threadViewModel.displayName - ) - }(), - body: (threadViewModel.threadIsBlocked == true ? - .attributedText( - "blockUnblockName" - .put(key: "name", value: threadViewModel.displayName) - .localizedFormatted(baseFont: .systemFont(ofSize: Values.smallFontSize)) - ) : - .attributedText( - "blockDescription" - .put(key: "name", value: threadViewModel.displayName) - .localizedFormatted(baseFont: .systemFont(ofSize: Values.smallFontSize)) - ) - ), - confirmTitle: (threadViewModel.threadIsBlocked == true ? - "blockUnblock".localized() : - "block".localized() - ), + title: "delete".localized(), + body: .text("Are you sure you want to delete all attachments (and their associated messages) sent before now for all group members?"), // stringlint:disable + confirmTitle: "delete".localized(), confirmStyle: .danger, cancelStyle: .alert_text ), - onTap: { [weak self] in - let isBlocked: Bool = (threadViewModel.threadIsBlocked == true) - - self?.updateBlockedState( - from: isBlocked, - isBlocked: !isBlocked, - threadId: threadViewModel.threadId, - displayName: threadViewModel.displayName - ) - } + onTap: { [weak self] in self?.deleteAllAttachmentsBeforeNow() } ) - ) + ) ].compactMap { $0 } ) - ] + } + else { + destructiveActionsSection = nil + } + + return [ + conversationInfoSection, + standardActionsSection, + adminActionsSection, + destructiveActionsSection + ].compactMap { $0 } } // MARK: - Functions - private func viewProfilePicture(threadViewModel: SessionThreadViewModel) { - guard - threadViewModel.threadVariant == .contact, - let profile: Profile = threadViewModel.profile, - let profileData: Data = ProfileManager.profileAvatar(profile: profile) - else { return } + private func viewDisplayPicture(threadViewModel: SessionThreadViewModel) { + let displayPictureData: Data + let ownerId: DisplayPictureManager.OwnerId = { + switch threadViewModel.threadVariant { + case .contact: .user(threadViewModel.threadId) + case .group, .legacyGroup: .group(threadViewModel.threadId) + case .community: .community(threadViewModel.threadId) + } + }() - let format: ImageFormat = profileData.guessedImageFormat + switch threadViewModel.threadVariant { + case .legacyGroup: return // No display pictures for legacy groups + case .contact: + guard + let profile: Profile = threadViewModel.profile, + let imageData: Data = DisplayPictureManager.displayPicture(owner: .user(profile), using: dependencies) + else { return } + + displayPictureData = imageData + + default: + guard + threadViewModel.displayPictureFilename != nil, + let imageData: Data = dependencies[singleton: .storage].read({ [dependencies] db in + DisplayPictureManager.displayPicture(db, id: ownerId, using: dependencies) + }) + else { return } + + displayPictureData = imageData + } + + let format: ImageFormat = displayPictureData.guessedImageFormat let navController: UINavigationController = StyledNavigationController( rootViewController: ProfilePictureVC( image: (format == .gif || format == .webp ? nil : - UIImage(data: profileData) + UIImage(data: displayPictureData) ), animatedImage: (format != .gif && format != .webp ? nil : - YYImage(data: profileData) + YYImage(data: displayPictureData) ), title: threadViewModel.displayName ) @@ -761,7 +831,7 @@ class ThreadSettingsViewModel: SessionTableViewModel, NavigationItemSource, Navi self.transitionToScreen(navController, transitionType: .present) } - private func addUsersToOpenGoup(threadViewModel: SessionThreadViewModel, selectedUsers: Set) { + private func inviteUsersToCommunity(threadViewModel: SessionThreadViewModel) { guard let name: String = threadViewModel.openGroupName, let communityUrl: String = LibSession.communityUrlFor( @@ -771,56 +841,578 @@ class ThreadSettingsViewModel: SessionTableViewModel, NavigationItemSource, Navi ) else { return } - dependencies.storage.writeAsync { [dependencies] db in - let currentUserSessionId: String = getUserHexEncodedPublicKey(db, using: dependencies) - try selectedUsers.forEach { userId in - let thread: SessionThread = try SessionThread - .fetchOrCreate(db, id: userId, variant: .contact, shouldBeVisible: nil) - - try LinkPreview( - url: communityUrl, - variant: .openGroupInvitation, - title: name + self.transitionToScreen( + SessionTableViewController( + viewModel: UserListViewModel( + title: "membersInvite".localized(), + emptyState: "contactNone".localized(), + showProfileIcons: false, + request: Contact + .filter(Contact.Columns.isApproved == true) + .filter(Contact.Columns.didApproveMe == true) + .filter(Contact.Columns.id != threadViewModel.currentUserSessionId), + footerTitle: "membersInvite".localized(), + onSubmit: .publisher { [dependencies] _, selectedUserInfo in + dependencies[singleton: .storage] + .writePublisher { db in + try selectedUserInfo.forEach { userInfo in + let thread: SessionThread = try SessionThread.fetchOrCreate( + db, + id: userInfo.profileId, + variant: .contact, + creationDateTimestamp: (dependencies[cache: .snodeAPI].currentOffsetTimestampMs() / 1000), + shouldBeVisible: nil, + calledFromConfig: nil, + using: dependencies + ) + + try LinkPreview( + url: communityUrl, + variant: .openGroupInvitation, + title: name, + using: dependencies + ) + .upsert(db) + + let interaction: Interaction = try Interaction( + threadId: thread.id, + threadVariant: thread.variant, + authorId: userInfo.profileId, + variant: .standardOutgoing, + timestampMs: dependencies[cache: .snodeAPI].currentOffsetTimestampMs(), + expiresInSeconds: try? DisappearingMessagesConfiguration + .select(.durationSeconds) + .filter(id: userInfo.profileId) + .filter(DisappearingMessagesConfiguration.Columns.isEnabled == true) + .asRequest(of: TimeInterval.self) + .fetchOne(db), + linkPreviewUrl: communityUrl, + using: dependencies + ) + .inserted(db) + + try MessageSender.send( + db, + interaction: interaction, + threadId: thread.id, + threadVariant: thread.variant, + using: dependencies + ) + + // Trigger disappear after read + dependencies[singleton: .jobRunner].upsert( + db, + job: DisappearingMessagesJob.updateNextRunIfNeeded( + db, + interaction: interaction, + startedAtMs: dependencies[cache: .snodeAPI].currentOffsetTimestampMs(), + using: dependencies + ), + canStartJob: true + ) + } + } + .mapError { UserListError.error($0.localizedDescription) } + .eraseToAnyPublisher() + }, + using: dependencies ) - .save(db) - - let interaction: Interaction = try Interaction( - threadId: thread.id, - threadVariant: thread.variant, - authorId: currentUserSessionId, - variant: .standardOutgoing, - timestampMs: SnodeAPI.currentOffsetTimestampMs(), - expiresInSeconds: try? DisappearingMessagesConfiguration - .select(.durationSeconds) - .filter(id: userId) - .filter(DisappearingMessagesConfiguration.Columns.isEnabled == true) - .asRequest(of: TimeInterval.self) - .fetchOne(db), - linkPreviewUrl: communityUrl + ), + transitionType: .push + ) + } + + private func viewMembers() { + self.transitionToScreen( + SessionTableViewController( + viewModel: UserListViewModel( + title: "groupMembers".localized(), + showProfileIcons: true, + request: GroupMember + .select( + GroupMember.Columns.groupId, + GroupMember.Columns.profileId, + max(GroupMember.Columns.role).forKey(GroupMember.Columns.role.name), + GroupMember.Columns.roleStatus, + GroupMember.Columns.isHidden + ) + .filter(GroupMember.Columns.groupId == threadId) + .group(GroupMember.Columns.profileId), + onTap: .callback { [weak self, dependencies] _, memberInfo in + dependencies[singleton: .storage].write { db in + try SessionThread.fetchOrCreate( + db, + id: memberInfo.profileId, + variant: .contact, + creationDateTimestamp: (dependencies[cache: .snodeAPI].currentOffsetTimestampMs() / 1000), + shouldBeVisible: nil, + calledFromConfig: nil, + using: dependencies + ) + } + + self?.transitionToScreen( + ConversationVC( + threadId: memberInfo.profileId, + threadVariant: .contact, + using: dependencies + ), + transitionType: .push + ) + }, + using: dependencies ) - .inserted(db) - - try MessageSender.send( - db, - interaction: interaction, - threadId: thread.id, - threadVariant: thread.variant, + ) + ) + } + + private func promoteAdmins() { + guard dependencies[feature: .updatedGroupsAllowPromotions] else { return } + + let groupMember: TypedTableAlias = TypedTableAlias() + + /// Submitting and resending using the same logic + func send( + _ viewModel: UserListViewModel?, + _ memberInfo: [(id: String, profile: Profile?)], + isResend: Bool + ) { + MessageSender.promoteGroupMembers( + groupSessionId: SessionId(.group, hex: threadId), + members: memberInfo, + sendAdminChangedMessage: !isResend, + using: dependencies + ) + viewModel?.showToast( + text: "adminSendingPromotion" + .putNumber(memberInfo.count) + .localized(), + backgroundColor: .backgroundSecondary + ) + } + + /// Show the selection list + self.transitionToScreen( + SessionTableViewController( + viewModel: UserListViewModel( + title: "promote".localized(), + // FIXME: Localise this + emptyState: "There are no group members which can be promoted.", + showProfileIcons: true, + request: SQLRequest(""" + SELECT \(groupMember.allColumns) + FROM \(groupMember) + WHERE ( + \(groupMember[.groupId]) == \(threadId) AND ( + \(groupMember[.role]) == \(GroupMember.Role.admin) OR + ( + \(groupMember[.role]) != \(GroupMember.Role.admin) AND + \(groupMember[.roleStatus]) == \(GroupMember.RoleStatus.accepted) + ) + ) + ) + GROUP BY \(groupMember[.profileId]) + """), + footerTitle: "promote".localized(), + onTap: .conditionalAction( + action: { memberInfo in + guard memberInfo.profileId != memberInfo.currentUserSessionId.hexString else { + return .none + } + + switch (memberInfo.value.role, memberInfo.value.roleStatus) { + case (.standard, _): return .radio + default: + return .custom( + trailingAccessory: { _ in + .highlightingBackgroundLabel( + title: "resend".localized() + ) + }, + onTap: { viewModel, info in + send(viewModel, [(info.profileId, info.profile)], isResend: true) + } + ) + } + } + ), + onSubmit: .callback { viewModel, selectedInfo in + send(viewModel, selectedInfo.map { ($0.profileId, $0.profile) }, isResend: false) + }, using: dependencies ) - - // Trigger disappear after read - dependencies.jobRunner.upsert( - db, - job: DisappearingMessagesJob.updateNextRunIfNeeded( - db, - interaction: interaction, - startedAtMs: TimeInterval(SnodeAPI.currentOffsetTimestampMs()) + ), + transitionType: .push + ) + } + + private func updateNickname(current: String?, displayName: String) { + /// Set `updatedName` to `current` so we can disable the "save" button when there are no changes and don't need to worry + /// about retrieving them in the confirmation closure + self.updatedName = current + self.transitionToScreen( + ConfirmationModal( + info: ConfirmationModal.Info( + title: "nicknameSet".localized(), + body: .input( + explanation: "nicknameDescription" + .put(key: "name", value: displayName) + .localizedFormatted(baseFont: ConfirmationModal.explanationFont), + info: ConfirmationModal.Info.Body.InputInfo( + placeholder: "nicknameEnter".localized(), + initialValue: current + ), + onChange: { [weak self] updatedName in self?.updatedName = updatedName } + ), + confirmTitle: "save".localized(), + confirmEnabled: .afterChange { [weak self] _ in + self?.updatedName != current && + self?.updatedName?.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty == false + }, + cancelTitle: "remove".localized(), + cancelStyle: .danger, + cancelEnabled: .bool(current?.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty == false), + hasCloseButton: true, + dismissOnConfirm: false, + onConfirm: { [weak self, dependencies, threadId] modal in + guard + let finalNickname: String = (self?.updatedName ?? "") + .trimmingCharacters(in: .whitespacesAndNewlines) + .nullIfEmpty + else { return } + + /// Check if the data violates the size constraints + guard !Profile.isTooLong(profileName: finalNickname) else { + self?.transitionToScreen( + ConfirmationModal( + info: ConfirmationModal.Info( + title: "theError".localized(), + body: .text("nicknameErrorShorter".localized()), + cancelTitle: "okay".localized(), + cancelStyle: .alert_text, + dismissType: .single + ) + ), + transitionType: .present + ) + return + } + + /// Update the nickname + dependencies[singleton: .storage].writeAsync { db in + try Profile + .filter(id: threadId) + .updateAllAndConfig( + db, + Profile.Columns.nickname.set(to: finalNickname), + calledFromConfig: nil, + using: dependencies + ) + } + modal.dismiss(animated: true) + }, + onCancel: { [dependencies, threadId] modal in + /// Remove the nickname + dependencies[singleton: .storage].writeAsync { db in + try Profile + .filter(id: threadId) + .updateAllAndConfig( + db, + Profile.Columns.nickname.set(to: nil), + calledFromConfig: nil, + using: dependencies + ) + } + modal.dismiss(animated: true) + } + ) + ), + transitionType: .present + ) + } + + private func updateGroupNameAndDescription( + currentName: String, + currentDescription: String?, + isUpdatedGroup: Bool + ) { + /// Set the `updatedName` and `updatedDescription` values to the current values so we can disable the "save" button when there are + /// no changes and don't need to worry about retrieving them in the confirmation closure + self.updatedName = currentName + self.updatedDescription = currentDescription + self.transitionToScreen( + ConfirmationModal( + info: ConfirmationModal.Info( + title: "groupInformationSet".localized(), + body: { [weak self, dependencies] in + guard isUpdatedGroup && dependencies[feature: .updatedGroupsAllowDescriptionEditing] else { + return .input( + explanation: NSAttributedString(string: "groupNameVisible".localized()), + info: ConfirmationModal.Info.Body.InputInfo( + placeholder: "groupNameEnter".localized(), + initialValue: currentName + ), + onChange: { updatedName in self?.updatedName = updatedName } + ) + } + + return .dualInput( + // FIXME: Localise this + explanation: NSAttributedString(string: "Group name and description are visible to all group members."), + firstInfo: ConfirmationModal.Info.Body.InputInfo( + placeholder: "groupNameEnter".localized(), + initialValue: currentName + ), + secondInfo: ConfirmationModal.Info.Body.InputInfo( + placeholder: "groupDescriptionEnter".localized(), + initialValue: currentDescription + ), + onChange: { updatedName, updatedDescription in + self?.updatedName = updatedName + self?.updatedDescription = updatedDescription + } + ) + }(), + confirmTitle: "save".localized(), + confirmEnabled: .afterChange { [weak self] _ in + self?.updatedName?.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty == false && ( + self?.updatedName != currentName || + self?.updatedDescription != currentDescription + ) + }, + cancelStyle: .danger, + dismissOnConfirm: false, + onConfirm: { [weak self, dependencies, threadId] modal in + guard + let finalName: String = (self?.updatedName ?? "") + .trimmingCharacters(in: .whitespacesAndNewlines) + .nullIfEmpty + else { return } + + let finalDescription: String? = self?.updatedDescription + .map { $0.trimmingCharacters(in: .whitespacesAndNewlines) } + + /// Check if the data violates any of the size constraints + let maybeErrorString: String? = { + guard !LibSession.isTooLong(groupName: finalName) else { + return "groupNameEnterShorter".localized() + } + guard !LibSession.isTooLong(groupDescription: (finalDescription ?? "")) else { + // FIXME: Localise this + return "Please enter a shorter group description." + } + + return nil // No error has occurred + }() + + if let errorString: String = maybeErrorString { + self?.transitionToScreen( + ConfirmationModal( + info: ConfirmationModal.Info( + title: "theError".localized(), + body: .text(errorString), + cancelTitle: "okay".localized(), + cancelStyle: .alert_text, + dismissType: .single + ) + ), + transitionType: .present + ) + return + } + + /// Update the group appropriately + MessageSender + .updateGroup( + groupSessionId: threadId, + name: finalName, + groupDescription: finalDescription, + using: dependencies + ) + .sinkUntilComplete( + receiveCompletion: { [weak self] result in + switch result { + case .finished: modal.dismiss(animated: true) + case .failure: + self?.transitionToScreen( + ConfirmationModal( + info: ConfirmationModal.Info( + title: "theError".localized(), + body: .text("deleteAfterLegacyGroupsGroupUpdateErrorTitle".localized()), + cancelTitle: "okay".localized(), + cancelStyle: .alert_text + ) + ), + transitionType: .present + ) + } + } + ) + } + ) + ), + transitionType: .present + ) + } + + private func updateGroupDisplayPicture(currentFileName: String?) { + guard dependencies[feature: .updatedGroupsAllowDisplayPicture] else { return } + + let existingImageData: Data? = dependencies[singleton: .storage].read { [threadId, dependencies] db in + DisplayPictureManager.displayPicture(db, id: .group(threadId), using: dependencies) + } + self.transitionToScreen( + ConfirmationModal( + info: ConfirmationModal.Info( + title: "groupSetDisplayPicture".localized(), + body: .image( + placeholderData: UIImage(named: "profile_placeholder")?.pngData(), + valueData: existingImageData, + icon: .rightPlus, + style: .circular, + accessibility: Accessibility( + identifier: "Image picker", + label: "Image picker" + ), + onClick: { [weak self] onDisplayPictureSelected in + self?.onDisplayPictureSelected = onDisplayPictureSelected + self?.showPhotoLibraryForAvatar() + } ), - canStartJob: true, + confirmTitle: "save".localized(), + confirmEnabled: .afterChange { info in + switch info.body { + case .image(_, let valueData, _, _, _, _): return (valueData != nil) + default: return false + } + }, + cancelTitle: "remove".localized(), + cancelEnabled: .bool(existingImageData != nil), + hasCloseButton: true, + dismissOnConfirm: false, + onConfirm: { [weak self] modal in + switch modal.info.body { + case .image(_, .some(let valueData), _, _, _, _): + self?.updateGroupDisplayPicture( + displayPictureUpdate: .groupUploadImageData(valueData), + onComplete: { [weak modal] in modal?.close() } + ) + + default: modal.close() + } + }, + onCancel: { [weak self] modal in + self?.updateGroupDisplayPicture( + displayPictureUpdate: .groupRemove, + onComplete: { [weak modal] in modal?.close() } + ) + } + ) + ), + transitionType: .present + ) + } + + private func showPhotoLibraryForAvatar() { + Permissions.requestLibraryPermissionIfNeeded(isSavingMedia: false, using: dependencies) { [weak self] in + DispatchQueue.main.async { + let picker: UIImagePickerController = UIImagePickerController() + picker.sourceType = .photoLibrary + picker.mediaTypes = [ "public.image" ] // stringlint:disable + picker.delegate = self?.imagePickerHandler + + self?.transitionToScreen(picker, transitionType: .present) + } + } + } + + private func updateGroupDisplayPicture( + displayPictureUpdate: DisplayPictureManager.Update, + onComplete: @escaping () -> () + ) { + switch displayPictureUpdate { + case .none: onComplete() + default: break + } + + func performChanges(_ viewController: ModalActivityIndicatorViewController, _ displayPictureUpdate: DisplayPictureManager.Update) { + let existingFileName: String? = dependencies[singleton: .storage].read { [threadId] db in + try? ClosedGroup + .filter(id: threadId) + .select(.displayPictureFilename) + .asRequest(of: String.self) + .fetchOne(db) + } + + MessageSender + .updateGroup( + groupSessionId: threadId, + displayPictureUpdate: displayPictureUpdate, using: dependencies ) + .sinkUntilComplete( + receiveCompletion: { [dependencies] result in + // Remove any cached avatar image value + if let existingFileName: String = existingFileName { + dependencies.mutate(cache: .displayPicture) { $0.imageData[existingFileName] = nil } + } + + DispatchQueue.main.async { + viewController.dismiss(completion: { + onComplete() + }) + } + } + ) + } + + let viewController = ModalActivityIndicatorViewController(canCancel: false) { [weak self, dependencies] viewController in + switch displayPictureUpdate { + case .none, .currentUserRemove, .currentUserUploadImageData, .currentUserUpdateTo, + .contactRemove, .contactUpdateTo: + viewController.dismiss(animated: true) // Shouldn't get called + + case .groupRemove, .groupUpdateTo: performChanges(viewController, displayPictureUpdate) + case .groupUploadImageData(let data): + DisplayPictureManager.prepareAndUploadDisplayPicture( + queue: DispatchQueue.global(qos: .background), + imageData: data, + success: { url, fileName, key in + performChanges(viewController, .groupUpdateTo(url: url, key: key, fileName: fileName)) + }, + failure: { error in + DispatchQueue.main.async { + viewController.dismiss { + let message: String = { + switch (displayPictureUpdate, error) { + case (.groupRemove, _): return "profileDisplayPictureRemoveError".localized() + case (_, .uploadMaxFileSizeExceeded): + return "profileDisplayPictureSizeError".localized() + + default: return "errorConnection".localized() + } + }() + + self?.transitionToScreen( + ConfirmationModal( + info: ConfirmationModal.Info( + title: "deleteAfterLegacyGroupsGroupUpdateErrorTitle".localized(), + body: .text(message), + cancelTitle: "okay".localized(), + cancelStyle: .alert_text, + dismissType: .single + ) + ), + transitionType: .present + ) + } + } + }, + using: dependencies + ) } } + self.transitionToScreen(viewController, transitionType: .present) } private func updateBlockedState( @@ -831,13 +1423,41 @@ class ThreadSettingsViewModel: SessionTableViewModel, NavigationItemSource, Navi ) { guard oldBlockedState != isBlocked else { return } - dependencies.storage.writeAsync { db in + dependencies[singleton: .storage].writeAsync { [dependencies] db in try Contact .filter(id: threadId) .updateAllAndConfig( db, - Contact.Columns.isBlocked.set(to: isBlocked) + Contact.Columns.isBlocked.set(to: isBlocked), + calledFromConfig: nil, + using: dependencies ) } } + + private func deleteAllMessagesBeforeNow() { + guard threadVariant == .group else { return } + + dependencies[singleton: .storage].writeAsync { [threadId, dependencies] db in + try LibSession.deleteMessagesBefore( + db, + groupSessionId: SessionId(.group, hex: threadId), + timestamp: (dependencies[cache: .snodeAPI].currentOffsetTimestampMs() / 1000), + using: dependencies + ) + } + } + + private func deleteAllAttachmentsBeforeNow() { + guard threadVariant == .group else { return } + + dependencies[singleton: .storage].writeAsync { [threadId, dependencies] db in + try LibSession.deleteAttachmentsBefore( + db, + groupSessionId: SessionId(.group, hex: threadId), + timestamp: (dependencies[cache: .snodeAPI].currentOffsetTimestampMs() / 1000), + using: dependencies + ) + } + } } diff --git a/Session/Conversations/Views & Modals/ConversationTitleView.swift b/Session/Conversations/Views & Modals/ConversationTitleView.swift index fa66701c145..651d9bd2215 100644 --- a/Session/Conversations/Views & Modals/ConversationTitleView.swift +++ b/Session/Conversations/Views & Modals/ConversationTitleView.swift @@ -9,6 +9,7 @@ final class ConversationTitleView: UIView { private static let leftInset: CGFloat = 8 private static let leftInsetWithCallButton: CGFloat = 54 + private let dependencies: Dependencies private var oldSize: CGSize = .zero override var intrinsicContentSize: CGSize { @@ -39,7 +40,7 @@ final class ConversationTitleView: UIView { }() private lazy var labelCarouselView: SessionLabelCarouselView = { - let result = SessionLabelCarouselView() + let result = SessionLabelCarouselView(using: dependencies) return result }() @@ -53,7 +54,9 @@ final class ConversationTitleView: UIView { // MARK: - Initialization - init() { + init(using dependencies: Dependencies) { + self.dependencies = dependencies + super.init(frame: .zero) addSubview(stackView) @@ -76,11 +79,13 @@ final class ConversationTitleView: UIView { public func initialSetup( with threadVariant: SessionThread.Variant, - isNoteToSelf: Bool + isNoteToSelf: Bool, + isMessageRequest: Bool ) { self.update( with: " ", isNoteToSelf: isNoteToSelf, + isMessageRequest: isMessageRequest, threadVariant: threadVariant, mutedUntilTimestamp: nil, onlyNotifyForMentions: false, @@ -107,6 +112,7 @@ final class ConversationTitleView: UIView { public func update( with name: String, isNoteToSelf: Bool, + isMessageRequest: Bool, threadVariant: SessionThread.Variant, mutedUntilTimestamp: TimeInterval?, onlyNotifyForMentions: Bool, @@ -118,6 +124,7 @@ final class ConversationTitleView: UIView { self?.update( with: name, isNoteToSelf: isNoteToSelf, + isMessageRequest: isMessageRequest, threadVariant: threadVariant, mutedUntilTimestamp: mutedUntilTimestamp, onlyNotifyForMentions: onlyNotifyForMentions, @@ -129,10 +136,12 @@ final class ConversationTitleView: UIView { } let shouldHaveSubtitle: Bool = ( - Date().timeIntervalSince1970 <= (mutedUntilTimestamp ?? 0) || - onlyNotifyForMentions || - userCount != nil || - disappearingMessagesConfig?.isEnabled == true + !isMessageRequest && ( + Date().timeIntervalSince1970 <= (mutedUntilTimestamp ?? 0) || + onlyNotifyForMentions || + userCount != nil || + disappearingMessagesConfig?.isEnabled == true + ) ) self.titleLabel.text = name @@ -143,6 +152,22 @@ final class ConversationTitleView: UIView { Values.veryLargeFontSize ) ) + self.labelCarouselView.isHidden = !shouldHaveSubtitle + + // Contact threads also have the call button to compensate for + let shouldShowCallButton: Bool = ( + SessionCall.isEnabled && + !isNoteToSelf && + threadVariant == .contact + ) + self.stackViewLeadingConstraint.constant = (shouldShowCallButton ? + ConversationTitleView.leftInsetWithCallButton : + ConversationTitleView.leftInset + ) + self.stackViewTrailingConstraint.constant = 0 + + // No need to add themed subtitle content if we aren't adding the subtitle carousel + guard shouldHaveSubtitle else { return } ThemeManager.onThemeChange(observer: self.labelCarouselView) { [weak self] theme, _ in guard let textPrimary: UIColor = theme.color(for: .textPrimary) else { return } @@ -258,18 +283,6 @@ final class ConversationTitleView: UIView { self?.labelCarouselView.isHidden = (labelInfos.count == 0) } - - // Contact threads also have the call button to compensate for - let shouldShowCallButton: Bool = ( - SessionCall.isEnabled && - !isNoteToSelf && - threadVariant == .contact - ) - self.stackViewLeadingConstraint.constant = (shouldShowCallButton ? - ConversationTitleView.leftInsetWithCallButton : - ConversationTitleView.leftInset - ) - self.stackViewTrailingConstraint.constant = 0 } // MARK: - Interaction diff --git a/Session/Conversations/Views & Modals/InfoBanner.swift b/Session/Conversations/Views & Modals/InfoBanner.swift index ec6315f0d36..c56050fb869 100644 --- a/Session/Conversations/Views & Modals/InfoBanner.swift +++ b/Session/Conversations/Views & Modals/InfoBanner.swift @@ -1,36 +1,132 @@ // Copyright © 2022 Rangeproof Pty Ltd. All rights reserved. +// +// stringlint:disable import UIKit import SessionUIKit final class InfoBanner: UIView { + public enum Icon: Equatable, Hashable { + case none + case link + case close + + var image: UIImage? { + switch self { + case .none: return nil + case .link: return UIImage(systemName: "arrow.up.right.square")?.withRenderingMode(.alwaysTemplate) + case .close: + return UIImage( + systemName: "xmark", + withConfiguration: UIImage.SymbolConfiguration(pointSize: 12, weight: .bold) + )?.withRenderingMode(.alwaysTemplate) + } + } + } + public struct Info: Equatable, Hashable { + let font: UIFont let message: String + let icon: Icon + let tintColor: ThemeValue let backgroundColor: ThemeValue - let messageFont: UIFont - let messageTintColor: ThemeValue - let messageLabelAccessibilityLabel: String? - let height: CGFloat + let accessibility: Accessibility? + let labelAccessibility: Accessibility? + let height: CGFloat? + let onTap: (() -> Void)? + + static var empty: Info = Info(font: .systemFont(ofSize: Values.smallFontSize), message: "") + + public init( + font: UIFont, + message: String, + icon: Icon = .none, + tintColor: ThemeValue = .black, + backgroundColor: ThemeValue = .primary, + accessibility: Accessibility? = nil, + labelAccessibility: Accessibility? = nil, + height: CGFloat? = nil, + onTap: (() -> Void)? = nil + ) { + self.font = font + self.message = message + self.icon = icon + self.tintColor = tintColor + self.backgroundColor = backgroundColor + self.accessibility = accessibility + self.labelAccessibility = labelAccessibility + self.height = height + self.onTap = onTap + } func with( + font: UIFont? = nil, message: String? = nil, + icon: Icon? = nil, + tintColor: ThemeValue? = nil, backgroundColor: ThemeValue? = nil, - messageFont: UIFont? = nil, - messageTintColor: ThemeValue? = nil, - messageLabelAccessibilityLabel: String? = nil, - height: CGFloat? = nil + accessibility: Accessibility? = nil, + labelAccessibility: Accessibility? = nil, + height: CGFloat? = nil, + onTap: (() -> Void)? = nil ) -> Info { return Info( + font: font ?? self.font, message: message ?? self.message, + icon: icon ?? self.icon, + tintColor: tintColor ?? self.tintColor, backgroundColor: backgroundColor ?? self.backgroundColor, - messageFont: messageFont ?? self.messageFont, - messageTintColor: messageTintColor ?? self.messageTintColor, - messageLabelAccessibilityLabel: messageLabelAccessibilityLabel ?? self.messageLabelAccessibilityLabel, - height: height ?? self.height + accessibility: accessibility ?? self.accessibility, + labelAccessibility: labelAccessibility ?? self.labelAccessibility, + height: height ?? self.height, + onTap: onTap ?? self.onTap + ) + } + + public func hash(into hasher: inout Hasher) { + font.hash(into: &hasher) + message.hash(into: &hasher) + icon.hash(into: &hasher) + tintColor.hash(into: &hasher) + backgroundColor.hash(into: &hasher) + accessibility.hash(into: &hasher) + labelAccessibility.hash(into: &hasher) + height.hash(into: &hasher) + } + + public static func == (lhs: InfoBanner.Info, rhs: InfoBanner.Info) -> Bool { + return ( + lhs.font == rhs.font && + lhs.message == rhs.message && + lhs.icon == rhs.icon && + lhs.tintColor == rhs.tintColor && + lhs.backgroundColor == rhs.backgroundColor && + lhs.accessibility == rhs.accessibility && + lhs.labelAccessibility == rhs.labelAccessibility && + lhs.height == rhs.height ) } } + private lazy var stackView: UIStackView = { + let result: UIStackView = UIStackView() + result.axis = .horizontal + result.alignment = .center + result.distribution = .fill + result.spacing = Values.smallSpacing + + return result + }() + + private lazy var leftIconPadding: UIView = { + let result: UIView = UIView() + result.set(.width, to: 18) + result.set(.height, to: 18) + result.isHidden = true + + return result + }() + private lazy var label: UILabel = { let result: UILabel = UILabel() result.textAlignment = .center @@ -41,45 +137,40 @@ final class InfoBanner: UIView { return result }() - private lazy var closeButton: UIButton = { - let result: UIButton = UIButton() - result.translatesAutoresizingMaskIntoConstraints = false - result.setImage( - UIImage(systemName: "xmark", withConfiguration: UIImage.SymbolConfiguration(pointSize: 12, weight: .bold))? - .withRenderingMode(.alwaysTemplate), - for: .normal + private lazy var rightIconImageView: UIImageView = { + let result: UIImageView = UIImageView( + image: UIImage(systemName: "arrow.up.right.square")?.withRenderingMode(.alwaysTemplate) ) - result.contentMode = .center - result.addTarget(self, action: #selector(dismissBanner), for: .touchUpInside) + result.set(.width, to: 18) + result.set(.height, to: 18) + result.isHidden = true return result }() public var info: Info? - public var dismiss: (() -> Void)? + private var heightConstraint: NSLayoutConstraint? // MARK: - Initialization - init(info: Info, dismiss: (() -> Void)? = nil) { + init(info: Info) { super.init(frame: CGRect.zero) - addSubview(label) + addSubview(stackView) - label.pin(.top, to: .top, of: self) - label.pin(.bottom, to: .bottom, of: self) - label.pin(.leading, to: .leading, of: self, withInset: Values.veryLargeSpacing) - label.pin(.trailing, to: .trailing, of: self, withInset: -Values.veryLargeSpacing) + stackView.addArrangedSubview(leftIconPadding) + stackView.addArrangedSubview(label) + stackView.addArrangedSubview(rightIconImageView) - addSubview(closeButton) + stackView.pin(.top, to: .top, of: self, withInset: Values.verySmallSpacing) + stackView.pin(.bottom, to: .bottom, of: self, withInset: -Values.verySmallSpacing) + stackView.pin(.leading, to: .leading, of: self, withInset: Values.mediumSpacing) + stackView.pin(.trailing, to: .trailing, of: self, withInset: -Values.mediumSpacing) - let buttonSize: CGFloat = (12 + (Values.smallSpacing * 2)) - closeButton.center(.vertical, in: self) - closeButton.pin(.trailing, to: .trailing, of: self, withInset: -Values.smallSpacing) - closeButton.set(.width, to: buttonSize) - closeButton.set(.height, to: buttonSize) + self.update(with: info) - self.set(.height, to: info.height) - self.update(info, dismiss: dismiss) + let tapGestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(bannerTapped)) + self.addGestureRecognizer(tapGestureRecognizer) } override init(frame: CGRect) { @@ -90,47 +181,63 @@ final class InfoBanner: UIView { preconditionFailure("Use init(coder:) instead.") } - // MARK: Update + // MARK: - Interaction - private func update(_ info: InfoBanner.Info, dismiss: (() -> Void)?) { - self.info = info - self.dismiss = dismiss - - themeBackgroundColor = info.backgroundColor - - label.font = info.messageFont - label.text = info.message - label.themeTextColor = info.messageTintColor - label.accessibilityLabel = info.messageLabelAccessibilityLabel - - closeButton.themeTintColor = info.messageTintColor - closeButton.isHidden = (dismiss == nil) + @objc private func bannerTapped() { + info?.onTap?() } + // MARK: - Update + public func update( + font: UIFont? = nil, message: String? = nil, + icon: Icon = .none, + tintColor: ThemeValue? = nil, backgroundColor: ThemeValue? = nil, - messageFont: UIFont? = nil, - messageTintColor: ThemeValue? = nil, - messageLabelAccessibilityLabel: String? = nil, + accessibility: Accessibility? = nil, + labelAccessibility: Accessibility? = nil, height: CGFloat? = nil, - dismiss: (() -> Void)? = nil + onTap: (() -> Void)? = nil ) { - if let updatedInfo = self.info?.with( - message: message, - backgroundColor: backgroundColor, - messageFont: messageFont, - messageTintColor: messageTintColor, - messageLabelAccessibilityLabel: messageLabelAccessibilityLabel, - height: height - ) { - self.update(updatedInfo, dismiss: dismiss) - } + guard let currentInfo: Info = self.info else { return } + + self.update( + with: currentInfo.with( + font: font, + message: message, + icon: icon, + tintColor: tintColor, + backgroundColor: backgroundColor, + accessibility: accessibility, + labelAccessibility: labelAccessibility, + height: height, + onTap: onTap + ) + ) } - // MARK: - Actions - - @objc private func dismissBanner() { - self.dismiss?() + public func update(with info: InfoBanner.Info) { + self.info = info + self.heightConstraint?.isActive = false // Calling 'set' below will enable it + + switch info.height { + case .some(let fixedHeight): self.heightConstraint = self.set(.height, to: fixedHeight) + case .none: break + } + + themeBackgroundColor = info.backgroundColor + isAccessibilityElement = (info.accessibility != nil) + accessibilityIdentifier = info.accessibility?.identifier + accessibilityLabel = info.accessibility?.label + + label.font = info.font + label.text = info.message + label.themeTextColor = info.tintColor + label.accessibilityIdentifier = info.labelAccessibility?.identifier + label.accessibilityLabel = info.labelAccessibility?.label + rightIconImageView.image = info.icon.image + rightIconImageView.isHidden = (info.icon == .none) + rightIconImageView.themeTintColor = info.tintColor } } diff --git a/Session/Conversations/Views & Modals/MessageRequestFooterView.swift b/Session/Conversations/Views & Modals/MessageRequestFooterView.swift index 9ce29d2e68f..843a99ac652 100644 --- a/Session/Conversations/Views & Modals/MessageRequestFooterView.swift +++ b/Session/Conversations/Views & Modals/MessageRequestFooterView.swift @@ -2,6 +2,7 @@ import UIKit import SessionUIKit +import SessionMessagingKit import SessionUtilitiesKit class MessageRequestFooterView: UIView { @@ -62,7 +63,7 @@ class MessageRequestFooterView: UIView { result.translatesAutoresizingMaskIntoConstraints = false result.clipsToBounds = true result.titleLabel?.font = UIFont.boldSystemFont(ofSize: 16) - result.setTitle("deleteAfterGroupPR1BlockUser".localized(), for: .normal) + result.setTitle("block".localized(), for: .normal) result.setThemeTitleColor(.danger, for: .normal) result.addTarget(self, action: #selector(block), for: .touchUpInside) @@ -85,7 +86,7 @@ class MessageRequestFooterView: UIView { result.accessibilityLabel = "Delete message request" result.isAccessibilityElement = true result.translatesAutoresizingMaskIntoConstraints = false - result.setTitle("decline".localized(), for: .normal) + result.setTitle("delete".localized(), for: .normal) result.addTarget(self, action: #selector(decline), for: .touchUpInside) return result @@ -98,6 +99,7 @@ class MessageRequestFooterView: UIView { canWrite: Bool, threadIsMessageRequest: Bool, threadRequiresApproval: Bool, + closedGroupAdminProfile: Profile?, onBlock: @escaping () -> (), onAccept: @escaping () -> (), onDecline: @escaping () -> () @@ -113,7 +115,8 @@ class MessageRequestFooterView: UIView { threadVariant: threadVariant, canWrite: canWrite, threadIsMessageRequest: threadIsMessageRequest, - threadRequiresApproval: threadRequiresApproval + threadRequiresApproval: threadRequiresApproval, + closedGroupAdminProfile: closedGroupAdminProfile ) setupLayout() } @@ -154,17 +157,22 @@ class MessageRequestFooterView: UIView { threadVariant: SessionThread.Variant, canWrite: Bool, threadIsMessageRequest: Bool, - threadRequiresApproval: Bool + threadRequiresApproval: Bool, + closedGroupAdminProfile: Profile? ) { self.isHidden = (!canWrite || (!threadIsMessageRequest && !threadRequiresApproval)) - self.blockButton.isHidden = ( - threadVariant != .contact || - threadRequiresApproval - ) - self.descriptionLabel.text = (threadRequiresApproval ? - "messageRequestPendingDescription".localized() : - "messageRequestsAcceptDescription".localized() - ) + + switch threadVariant { + case .contact: self.blockButton.isHidden = threadRequiresApproval + case .group: self.blockButton.isHidden = (closedGroupAdminProfile != nil) + default: self.blockButton.isHidden = true + } + switch (threadVariant, threadRequiresApproval) { + case (.contact, false): self.descriptionLabel.text = "messageRequestsAcceptDescription".localized() + case (.contact, true): self.descriptionLabel.text = "messageRequestPendingDescription".localized() + case (.group, _): self.descriptionLabel.text = "messageRequestGroupInviteDescription".localized() + default: break + } self.actionStackView.isHidden = threadRequiresApproval self.messageRequestDescriptionLabelBottomConstraint?.constant = (threadRequiresApproval ? -4 : -20) } diff --git a/Session/Conversations/Views & Modals/ReactionListSheet.swift b/Session/Conversations/Views & Modals/ReactionListSheet.swift index 2f96317811a..eb3baba55b1 100644 --- a/Session/Conversations/Views & Modals/ReactionListSheet.swift +++ b/Session/Conversations/Views & Modals/ReactionListSheet.swift @@ -18,6 +18,7 @@ final class ReactionListSheet: BaseVC { } } + fileprivate let dependencies: Dependencies private let interactionId: Int64 private let onDismiss: (() -> ())? private var messageViewModel: MessageViewModel = MessageViewModel() @@ -103,9 +104,10 @@ final class ReactionListSheet: BaseVC { return result }() - // MARK: - Lifecycle + // MARK: - Initialization - init(for interactionId: Int64, onDismiss: (() -> ())? = nil) { + init(for interactionId: Int64, using dependencies: Dependencies, onDismiss: (() -> ())? = nil) { + self.dependencies = dependencies self.interactionId = interactionId self.onDismiss = onDismiss @@ -120,6 +122,8 @@ final class ReactionListSheet: BaseVC { preconditionFailure("Use init(for:) instead.") } + // MARK: - Lifecycle + override func viewDidLoad() { super.viewDidLoad() @@ -225,7 +229,7 @@ final class ReactionListSheet: BaseVC { return } - if reactionInfo.reaction.authorId == cellViewModel.currentUserPublicKey { + if reactionInfo.reaction.authorId == cellViewModel.currentUserSessionId { updatedValue.insert(reactionInfo, at: 0) } else { @@ -371,10 +375,10 @@ final class ReactionListSheet: BaseVC { @objc private func clearAllTapped() { clearAll() } - private func clearAll(using dependencies: Dependencies = Dependencies()) { + private func clearAll() { guard let selectedReaction: EmojiWithSkinTones = self.reactionSummaries.first(where: { $0.isSelected })?.emoji else { return } - delegate?.removeAllReactions(messageViewModel, for: selectedReaction.rawValue, using: dependencies) + delegate?.removeAllReactions(messageViewModel, for: selectedReaction.rawValue) } } @@ -435,7 +439,7 @@ extension ReactionListSheet: UITableViewDelegate, UITableViewDataSource { with: SessionCell.Info( id: cellViewModel, position: Position.with(indexPath.row, count: self.selectedReactionUserList.count), - leftAccessory: .profile(id: authorId, profile: cellViewModel.profile), + leadingAccessory: .profile(id: authorId, profile: cellViewModel.profile), title: ( cellViewModel.profile?.displayName() ?? Profile.truncated( @@ -443,7 +447,7 @@ extension ReactionListSheet: UITableViewDelegate, UITableViewDataSource { threadVariant: self.messageViewModel.threadVariant ) ), - rightAccessory: (authorId != self.messageViewModel.currentUserPublicKey ? nil : + trailingAccessory: (authorId != self.messageViewModel.currentUserSessionId ? nil : .icon( UIImage(named: "X")? .withRenderingMode(.alwaysTemplate), @@ -451,8 +455,9 @@ extension ReactionListSheet: UITableViewDelegate, UITableViewDataSource { ) ), styling: SessionCell.StyleInfo(backgroundStyle: .edgeToEdge), - isEnabled: (authorId == self.messageViewModel.currentUserPublicKey) - ) + isEnabled: (authorId == self.messageViewModel.currentUserSessionId) + ), + using: dependencies ) return cell @@ -470,7 +475,7 @@ extension ReactionListSheet: UITableViewDelegate, UITableViewDataSource { .first(where: { $0.isSelected })? .emoji, selectedReaction.rawValue == cellViewModel.reaction.emoji, - cellViewModel.reaction.authorId == self.messageViewModel.currentUserPublicKey + cellViewModel.reaction.authorId == self.messageViewModel.currentUserSessionId else { return } delegate?.removeReact(self.messageViewModel, for: selectedReaction) @@ -602,13 +607,7 @@ extension ReactionListSheet { // MARK: - Delegate protocol ReactionDelegate: AnyObject { - func react(_ cellViewModel: MessageViewModel, with emoji: EmojiWithSkinTones, using dependencies: Dependencies) - func removeReact(_ cellViewModel: MessageViewModel, for emoji: EmojiWithSkinTones, using dependencies: Dependencies) - func removeAllReactions(_ cellViewModel: MessageViewModel, for emoji: String, using dependencies: Dependencies) -} - -extension ReactionDelegate { - func removeReact(_ cellViewModel: MessageViewModel, for emoji: EmojiWithSkinTones) { - removeReact(cellViewModel, for: emoji, using: Dependencies()) - } + func react(_ cellViewModel: MessageViewModel, with emoji: EmojiWithSkinTones) + func removeReact(_ cellViewModel: MessageViewModel, for emoji: EmojiWithSkinTones) + func removeAllReactions(_ cellViewModel: MessageViewModel, for emoji: String) } diff --git a/Session/Conversations/Views & Modals/SessionLabelCarouselView.swift b/Session/Conversations/Views & Modals/SessionLabelCarouselView.swift index a10f5d7a88f..798f514219b 100644 --- a/Session/Conversations/Views & Modals/SessionLabelCarouselView.swift +++ b/Session/Conversations/Views & Modals/SessionLabelCarouselView.swift @@ -2,9 +2,12 @@ import UIKit import SessionUIKit +import SessionUtilitiesKit final class SessionLabelCarouselView: UIView, UIScrollViewDelegate { private static let autoScrollingTimeInterval: TimeInterval = 10 + + private let dependencies: Dependencies private var labelInfos: [LabelInfo] = [] private var labelSize: CGSize = .zero private var shouldAutoScroll: Bool = false @@ -88,8 +91,11 @@ final class SessionLabelCarouselView: UIView, UIScrollViewDelegate { // MARK: - Initialization - init(labelInfos: [LabelInfo] = [], labelSize: CGSize = .zero, shouldAutoScroll: Bool = false) { + init(labelInfos: [LabelInfo] = [], labelSize: CGSize = .zero, shouldAutoScroll: Bool = false, using dependencies: Dependencies) { + self.dependencies = dependencies + super.init(frame: .zero) + setUpViewHierarchy() self.update(with: labelInfos, labelSize: labelSize, shouldAutoScroll: shouldAutoScroll) } @@ -175,7 +181,7 @@ final class SessionLabelCarouselView: UIView, UIScrollViewDelegate { private func startScrolling() { timer?.invalidate() - timer = Timer.scheduledTimerOnMainThread(withTimeInterval: Self.autoScrollingTimeInterval, repeats: true) { _ in + timer = Timer.scheduledTimerOnMainThread(withTimeInterval: Self.autoScrollingTimeInterval, repeats: true, using: dependencies) { _ in guard self.labelInfos.count != 0 else { return } let targetPage = (self.pageControl.currentPage + 1) % self.labelInfos.count self.scrollView.scrollRectToVisible( diff --git a/SessionUIKit/Database/Migrations/_001_ThemePreferences.swift b/Session/Database/Migrations/_001_ThemePreferences.swift similarity index 58% rename from SessionUIKit/Database/Migrations/_001_ThemePreferences.swift rename to Session/Database/Migrations/_001_ThemePreferences.swift index 4cf398af121..6551ef8c8c5 100644 --- a/SessionUIKit/Database/Migrations/_001_ThemePreferences.swift +++ b/Session/Database/Migrations/_001_ThemePreferences.swift @@ -4,12 +4,17 @@ import Foundation import GRDB +import SessionUIKit import SessionUtilitiesKit /// This migration extracts an old theme preference from UserDefaults and saves it to the database as well as set the default for the other /// theme preferences +/// +/// **Note:** This migration used to live within `SessionUIKit` but we wanted to isolate it and remove dependencies from it so we +/// needed to extract this migration into the `Session` and `SessionShareExtension` targets (since both need theming they both +/// need to provide this migration as an option during setup) enum _001_ThemePreferences: Migration { - static let target: TargetMigrations.Identifier = .uiKit + static let target: TargetMigrations.Identifier = ._deprecatedUIKit static let identifier: String = "ThemePreferences" static let needsConfigSync: Bool = false static let minExpectedRunDuration: TimeInterval = 0.1 @@ -20,7 +25,7 @@ enum _001_ThemePreferences: Migration { static func migrate(_ db: Database, using dependencies: Dependencies) throws { // Determine if the user was matching the system setting (previously the absence of this value // indicated that the app should match the system setting) - let isExistingUser: Bool = Identity.userExists(db) + let isExistingUser: Bool = Identity.userExists(db, using: dependencies) let hadCustomLegacyThemeSetting: Bool = UserDefaults.standard.dictionaryRepresentation() .keys .contains("appMode") @@ -39,14 +44,27 @@ enum _001_ThemePreferences: Migration { db[.theme] = targetTheme db[.themePrimaryColor] = targetPrimaryColor - // Looks like the ThemeManager will load it's default values before this migration gets run - // as a result we need to update the ThemeManager to ensure the correct theme is applied - ThemeManager.setInitialThemeState( - theme: targetTheme, - primaryColor: targetPrimaryColor, - matchSystemNightModeSetting: matchSystemNightModeSetting + Storage.update(progress: 1, for: self, in: target, using: dependencies) + } +} + +extension Theme: @retroactive EnumStringSetting {} +extension Theme.PrimaryColor: @retroactive EnumStringSetting {} + +enum DeprecatedUIKitMigrationTarget: MigratableTarget { + public static func migrations() -> TargetMigrations { + return TargetMigrations( + identifier: ._deprecatedUIKit, + migrations: [ + // Want to ensure the initial DB stuff has been completed before doing any + // SNUIKit migrations + [], // Initial DB Creation + [], // YDB to GRDB Migration + [], // Legacy DB removal + [ + _001_ThemePreferences.self + ] + ] ) - - Storage.update(progress: 1, for: self, in: target) // In case this is the last migration } } diff --git a/Session/Emoji/Emoji+Available.swift b/Session/Emoji/Emoji+Available.swift index cf69a73ce48..3add42a26ef 100644 --- a/Session/Emoji/Emoji+Available.swift +++ b/Session/Emoji/Emoji+Available.swift @@ -2,21 +2,21 @@ // // stringlint:disable -import Foundation +import UIKit import SessionUtilitiesKit extension Emoji { private static let availableCache: Atomic<[Emoji:Bool]> = Atomic([:]) private static let iosVersionKey = "iosVersion" - private static let cacheUrl = URL(fileURLWithPath: FileManager.default.appSharedDataDirectoryPath) + private static let cacheUrl = URL(fileURLWithPath: SessionFileManager.nonInjectedAppSharedDataDirectoryPath) .appendingPathComponent("Library") .appendingPathComponent("Caches") .appendingPathComponent("emoji.plist") - static func warmAvailableCache() { + static func warmAvailableCache(using dependencies: Dependencies) { Log.assertOnMainThread() - guard Singleton.hasAppContext && Singleton.appContext.isMainAppAndActive else { return } + guard dependencies[singleton: .appContext].isMainAppAndActive else { return } var availableCache = [Emoji: Bool]() var uncachedEmoji = [Emoji]() @@ -58,10 +58,12 @@ extension Emoji { availableMap[iosVersionKey] = iosVersion do { - // Use FileManager.createDirectory directly because OWSFileSystem.ensureDirectoryExists + // Use FileManager.createDirectory directly because FileSystem.ensureDirectoryExists // can modify the protection, and this is a system-managed directory. - try FileManager.default.createDirectory(at: Self.cacheUrl.deletingLastPathComponent(), - withIntermediateDirectories: true) + try dependencies[singleton: .fileManager].createDirectory( + at: Self.cacheUrl.deletingLastPathComponent(), + withIntermediateDirectories: true + ) try availableMap.write(to: Self.cacheUrl) } catch { Log.warn("[Emoji] Failed to save emoji availability cache; it will be recomputed next time! \(error)") diff --git a/Session/Emoji/Emoji+Category.swift b/Session/Emoji/Emoji+Category.swift index 39233faa419..343d9a0e879 100644 --- a/Session/Emoji/Emoji+Category.swift +++ b/Session/Emoji/Emoji+Category.swift @@ -1,9 +1,11 @@ // This file is generated by EmojiGenerator.swift, do not manually edit it. - +// // swiftlint:disable all // stringlint:disable +import Foundation + extension Emoji { enum Category: String, CaseIterable, Equatable { case smileysAndPeople = "Smileys & People" diff --git a/Session/Home/GlobalSearch/EmptySearchResultCell.swift b/Session/Home/GlobalSearch/EmptySearchResultCell.swift index 8633e9f67b3..cd5217eae78 100644 --- a/Session/Home/GlobalSearch/EmptySearchResultCell.swift +++ b/Session/Home/GlobalSearch/EmptySearchResultCell.swift @@ -62,7 +62,7 @@ class EmptySearchResultCell: UITableViewCell { public func configure(isLoading: Bool) { if isLoading { // Calling stopAnimating() here is a workaround for - // the spinner won't change its colour as the theme changed. + // the spinner won't change its color as the theme changed. spinner.stopAnimating() spinner.startAnimating() messageLabel.isHidden = true diff --git a/Session/Home/GlobalSearch/GlobalSearchViewController.swift b/Session/Home/GlobalSearch/GlobalSearchViewController.swift index f6581a8c830..95294c2873f 100644 --- a/Session/Home/GlobalSearch/GlobalSearchViewController.swift +++ b/Session/Home/GlobalSearch/GlobalSearchViewController.swift @@ -43,9 +43,9 @@ class GlobalSearchViewController: BaseVC, LibSessionRespondingViewController, UI private let dependencies: Dependencies private lazy var defaultSearchResults: SearchResultData = { let nonalphabeticNameTitle: String = "#" // stringlint:ignore - let contacts: [SessionThreadViewModel] = Storage.shared.read { db -> [SessionThreadViewModel]? in + let contacts: [SessionThreadViewModel] = dependencies[singleton: .storage].read { [dependencies] db -> [SessionThreadViewModel]? in try SessionThreadViewModel - .defaultContactsQuery(userPublicKey: getUserHexEncodedPublicKey(db)) + .defaultContactsQuery(userSessionId: dependencies[cache: .general].sessionId) .fetchAll(db) } .defaulting(to: []) @@ -234,12 +234,15 @@ class GlobalSearchViewController: BaseVC, LibSessionRespondingViewController, UI private func refreshSearchResults() { refreshTimer?.invalidate() - refreshTimer = WeakTimer.scheduledTimer(timeInterval: 0.1, target: self, userInfo: nil, repeats: false) { [weak self] _ in + refreshTimer = Timer.scheduledTimerOnMainThread(withTimeInterval: 0.1, using: dependencies) { [weak self] _ in self?.updateSearchResults(searchText: (self?.searchText ?? "")) } } - private func updateSearchResults(searchText rawSearchText: String, force: Bool = false) { + private func updateSearchResults( + searchText rawSearchText: String, + force: Bool = false + ) { let searchText = rawSearchText.stripped guard searchText.count > 0 else { @@ -254,24 +257,24 @@ class GlobalSearchViewController: BaseVC, LibSessionRespondingViewController, UI lastSearchText = searchText - DispatchQueue.global(qos: .default).async { [weak self] in + DispatchQueue.global(qos: .default).async { [weak self, dependencies] in self?.readConnection.wrappedValue?.interrupt() - let result: Result<[SectionModel], Error>? = Storage.shared.read { db -> Result<[SectionModel], Error> in + let result: Result<[SectionModel], Error>? = dependencies[singleton: .storage].read { db -> Result<[SectionModel], Error> in self?.readConnection.mutate { $0 = db } do { - let userPublicKey: String = getUserHexEncodedPublicKey(db) + let userSessionId: SessionId = dependencies[cache: .general].sessionId let contactsAndGroupsResults: [SessionThreadViewModel] = try SessionThreadViewModel .contactsAndGroupsQuery( - userPublicKey: userPublicKey, + userSessionId: userSessionId, pattern: try SessionThreadViewModel.pattern(db, searchTerm: searchText), searchTerm: searchText ) .fetchAll(db) let messageResults: [SessionThreadViewModel] = try SessionThreadViewModel .messagesQuery( - userPublicKey: userPublicKey, + userSessionId: userSessionId, pattern: try SessionThreadViewModel.pattern(db, searchTerm: searchText) ) .fetchAll(db) @@ -393,12 +396,15 @@ extension GlobalSearchViewController { // If it's a one-to-one thread then make sure the thread exists before pushing to it (in case the // contact has been hidden) if threadVariant == .contact { - Storage.shared.write { db in + dependencies[singleton: .storage].write { [dependencies] db in try SessionThread.fetchOrCreate( db, id: threadId, variant: threadVariant, - shouldBeVisible: nil // Don't change current state + creationDateTimestamp: (dependencies[cache: .snodeAPI].currentOffsetTimestampMs() / 1000), + shouldBeVisible: nil, // Don't change current state + calledFromConfig: nil, + using: dependencies ) } } @@ -495,17 +501,25 @@ extension GlobalSearchViewController { switch section.model { case .contactsAndGroups: let cell: FullConversationCell = tableView.dequeue(type: FullConversationCell.self, for: indexPath) - cell.updateForContactAndGroupSearchResult(with: section.elements[indexPath.row], searchText: self.termForCurrentSearchResultSet) + cell.updateForContactAndGroupSearchResult( + with: section.elements[indexPath.row], + searchText: self.termForCurrentSearchResultSet, + using: dependencies + ) return cell case .messages: let cell: FullConversationCell = tableView.dequeue(type: FullConversationCell.self, for: indexPath) - cell.updateForMessageSearchResult(with: section.elements[indexPath.row], searchText: self.termForCurrentSearchResultSet) + cell.updateForMessageSearchResult( + with: section.elements[indexPath.row], + searchText: self.termForCurrentSearchResultSet, + using: dependencies + ) return cell case .groupedContacts: let cell: FullConversationCell = tableView.dequeue(type: FullConversationCell.self, for: indexPath) - cell.updateForDefaultContacts(with: section.elements[indexPath.row]) + cell.updateForDefaultContacts(with: section.elements[indexPath.row], using: dependencies) return cell } } diff --git a/Session/Home/HomeVC.swift b/Session/Home/HomeVC.swift index f9e5e732e5c..8e3ea9e8046 100644 --- a/Session/Home/HomeVC.swift +++ b/Session/Home/HomeVC.swift @@ -8,7 +8,7 @@ import SessionMessagingKit import SessionUtilitiesKit import SignalUtilitiesKit -final class HomeVC: BaseVC, LibSessionRespondingViewController, UITableViewDataSource, UITableViewDelegate, SeedReminderViewDelegate { +public final class HomeVC: BaseVC, LibSessionRespondingViewController, UITableViewDataSource, UITableViewDelegate, SeedReminderViewDelegate { private static let loadingHeaderHeight: CGFloat = 40 public static let newConversationButtonSize: CGFloat = 60 @@ -25,14 +25,15 @@ final class HomeVC: BaseVC, LibSessionRespondingViewController, UITableViewDataS // MARK: - LibSessionRespondingViewController - let isConversationList: Bool = true + public let isConversationList: Bool = true // MARK: - Intialization - init(flow: Onboarding.Flow? = nil, using dependencies: Dependencies) { + init(using dependencies: Dependencies) { self.viewModel = HomeViewModel(using: dependencies) - Storage.shared.addObserver(viewModel.pagedDataObserver) - self.flow = flow + + dependencies[singleton: .storage].addObserver(viewModel.pagedDataObserver) + super.init(nibName: nil, bundle: nil) } @@ -47,6 +48,8 @@ final class HomeVC: BaseVC, LibSessionRespondingViewController, UITableViewDataS // MARK: - UI private var tableViewTopConstraint: NSLayoutConstraint! + private var loadingConversationsLabelTopConstraint: NSLayoutConstraint! + private var navBarProfileView: ProfilePictureView? private lazy var seedReminderView: SeedReminderView = { let result = SeedReminderView() @@ -288,17 +291,10 @@ final class HomeVC: BaseVC, LibSessionRespondingViewController, UITableViewDataS // MARK: - Lifecycle - override func viewDidLoad() { + public override func viewDidLoad() { super.viewDidLoad() - // Note: This is a hack to ensure `isRTL` is initially gets run on the main thread so the value - // is cached (it gets called on background threads and if it hasn't cached the value then it can - // cause odd performance issues since it accesses UIKit) - if Singleton.hasAppContext { _ = Singleton.appContext.isRTL } - // Preparation - SessionApp.homeViewController.mutate { $0 = self } - updateNavBarButtons(userProfile: self.viewModel.state.userProfile) setUpNavBarSessionHeading() @@ -311,21 +307,23 @@ final class HomeVC: BaseVC, LibSessionRespondingViewController, UITableViewDataS // Loading conversations label view.addSubview(loadingConversationsLabel) - loadingConversationsLabel.pin(.top, to: .top, of: view, withInset: Values.veryLargeSpacing) loadingConversationsLabel.pin(.leading, to: .leading, of: view, withInset: 50) loadingConversationsLabel.pin(.trailing, to: .trailing, of: view, withInset: -50) // Table view view.addSubview(tableView) tableView.pin(.leading, to: .leading, of: view) + tableView.pin(.trailing, to: .trailing, of: view) + tableView.pin(.bottom, to: .bottom, of: view) + if self.viewModel.state.showViewedSeedBanner { + loadingConversationsLabelTopConstraint = loadingConversationsLabel.pin(.top, to: .bottom, of: seedReminderView, withInset: Values.mediumSpacing) tableViewTopConstraint = tableView.pin(.top, to: .bottom, of: seedReminderView) } else { + loadingConversationsLabelTopConstraint = loadingConversationsLabel.pin(.top, to: .top, of: view, withInset: Values.veryLargeSpacing) tableViewTopConstraint = tableView.pin(.top, to: .top, of: view) } - tableView.pin(.trailing, to: .trailing, of: view) - tableView.pin(.bottom, to: .bottom, of: view) // Empty state view view.addSubview(emptyStateStackView) @@ -353,28 +351,28 @@ final class HomeVC: BaseVC, LibSessionRespondingViewController, UITableViewDataS ) // Start polling if needed (i.e. if the user just created or restored their Session ID) - if Identity.userExists(), let appDelegate: AppDelegate = UIApplication.shared.delegate as? AppDelegate { + if Identity.userExists(using: viewModel.dependencies), let appDelegate: AppDelegate = UIApplication.shared.delegate as? AppDelegate { appDelegate.startPollersIfNeeded() } // Onion request path countries cache - IP2Country.populateCacheIfNeededAsync() + viewModel.dependencies.warmCache(cache: .ip2Country) } - override func viewWillAppear(_ animated: Bool) { + public override func viewWillAppear(_ animated: Bool) { super.viewWillAppear(animated) startObservingChanges() } - override func viewDidAppear(_ animated: Bool) { + public override func viewDidAppear(_ animated: Bool) { super.viewDidAppear(animated) self.viewHasAppeared = true self.autoLoadNextPageIfNeeded() } - override func viewWillDisappear(_ animated: Bool) { + public override func viewWillDisappear(_ animated: Bool) { super.viewWillDisappear(animated) stopObservingChanges() @@ -405,7 +403,7 @@ final class HomeVC: BaseVC, LibSessionRespondingViewController, UITableViewDataS runAndClearInitialChangeCallback = nil } - dataChangeObservable = Storage.shared.start( + dataChangeObservable = viewModel.dependencies[singleton: .storage].start( viewModel.observableState, onError: { _ in }, onChange: { [weak self] state in @@ -452,12 +450,15 @@ final class HomeVC: BaseVC, LibSessionRespondingViewController, UITableViewDataS // Update the 'view seed' UI if updatedState.showViewedSeedBanner != self.viewModel.state.showViewedSeedBanner { tableViewTopConstraint.isActive = false + loadingConversationsLabelTopConstraint.isActive = false seedReminderView.isHidden = !updatedState.showViewedSeedBanner if updatedState.showViewedSeedBanner { + loadingConversationsLabelTopConstraint = loadingConversationsLabel.pin(.top, to: .bottom, of: seedReminderView, withInset: Values.mediumSpacing) tableViewTopConstraint = tableView.pin(.top, to: .bottom, of: seedReminderView) } else { + loadingConversationsLabelTopConstraint = loadingConversationsLabel.pin(.top, to: .top, of: view, withInset: Values.veryLargeSpacing) tableViewTopConstraint = tableView.pin(.top, to: .top, of: view, withInset: Values.smallSpacing) } } @@ -576,18 +577,58 @@ final class HomeVC: BaseVC, LibSessionRespondingViewController, UITableViewDataS profilePictureView.update( publicKey: userProfile.id, threadVariant: .contact, - customImageData: nil, + displayPictureFilename: nil, profile: userProfile, - additionalProfile: nil + profileIcon: { + switch (viewModel.dependencies[feature: .serviceNetwork], viewModel.dependencies[feature: .forceOffline]) { + case (.testnet, false): return .letter("T", false) // stringlint:ignore + case (.testnet, true): return .letter("T", true) // stringlint:ignore + default: return .none + } + }(), + additionalProfile: nil, + using: viewModel.dependencies ) + navBarProfileView = profilePictureView let tapGestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(openSettings)) profilePictureView.addGestureRecognizer(tapGestureRecognizer) // Path status indicator - let pathStatusView = PathStatusView() + let pathStatusView = PathStatusView(using: viewModel.dependencies) pathStatusView.accessibilityLabel = "Current onion routing path indicator" + viewModel.dependencies.publisher(feature: .serviceNetwork) + .subscribe(on: DispatchQueue.global(qos: .background), using: viewModel.dependencies) + .receive(on: DispatchQueue.main, using: viewModel.dependencies) + .sink( + receiveCompletion: { [weak self] _ in + /// If the stream completes it means the network cache was reset in which case we want to + /// re-register for updates in the next run loop (as the new cache should be created by then) + DispatchQueue.main.async { + self?.updateNavBarButtons(userProfile: userProfile) + } + }, + receiveValue: { [weak profilePictureView, dependencies = viewModel.dependencies] value in + profilePictureView?.update( + publicKey: userProfile.id, + threadVariant: .contact, + displayPictureFilename: nil, + profile: userProfile, + profileIcon: { + switch (dependencies[feature: .serviceNetwork], dependencies[feature: .forceOffline]) { + case (.testnet, false): return .letter("T", false) // stringlint:ignore + case (.testnet, true): return .letter("T", true) // stringlint:ignore + default: return .none + } + }(), + additionalProfile: nil, + using: dependencies + ) + } + ) + .store(in: &profilePictureView.disposables) + // Container view let profilePictureViewContainer = UIView() profilePictureViewContainer.addSubview(profilePictureView) @@ -610,17 +651,17 @@ final class HomeVC: BaseVC, LibSessionRespondingViewController, UITableViewDataS // MARK: - UITableViewDataSource - func numberOfSections(in tableView: UITableView) -> Int { + public func numberOfSections(in tableView: UITableView) -> Int { return viewModel.threadData.count } - func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { + public func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { let section: HomeViewModel.SectionModel = viewModel.threadData[section] return section.elements.count } - func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { + public func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { let section: HomeViewModel.SectionModel = viewModel.threadData[indexPath.section] switch section.model { @@ -635,7 +676,7 @@ final class HomeVC: BaseVC, LibSessionRespondingViewController, UITableViewDataS case .threads: let threadViewModel: SessionThreadViewModel = section.elements[indexPath.row] let cell: FullConversationCell = tableView.dequeue(type: FullConversationCell.self, for: indexPath) - cell.update(with: threadViewModel) + cell.update(with: threadViewModel, using: viewModel.dependencies) cell.accessibilityIdentifier = "Conversation list item" cell.accessibilityLabel = threadViewModel.displayName return cell @@ -644,7 +685,7 @@ final class HomeVC: BaseVC, LibSessionRespondingViewController, UITableViewDataS } } - func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? { + public func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? { let section: HomeViewModel.SectionModel = viewModel.threadData[section] switch section.model { @@ -666,7 +707,7 @@ final class HomeVC: BaseVC, LibSessionRespondingViewController, UITableViewDataS // MARK: - UITableViewDelegate - func tableView(_ tableView: UITableView, heightForHeaderInSection section: Int) -> CGFloat { + public func tableView(_ tableView: UITableView, heightForHeaderInSection section: Int) -> CGFloat { let section: HomeViewModel.SectionModel = viewModel.threadData[section] switch section.model { @@ -675,7 +716,7 @@ final class HomeVC: BaseVC, LibSessionRespondingViewController, UITableViewDataS } } - func tableView(_ tableView: UITableView, willDisplayHeaderView view: UIView, forSection section: Int) { + public func tableView(_ tableView: UITableView, willDisplayHeaderView view: UIView, forSection section: Int) { guard self.hasLoadedInitialThreadData && self.viewHasAppeared && !self.isLoadingMore else { return } let section: HomeViewModel.SectionModel = self.viewModel.threadData[section] @@ -692,7 +733,7 @@ final class HomeVC: BaseVC, LibSessionRespondingViewController, UITableViewDataS } } - func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { + public func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { tableView.deselectRow(at: indexPath, animated: true) let section: HomeViewModel.SectionModel = self.viewModel.threadData[indexPath.section] @@ -700,38 +741,37 @@ final class HomeVC: BaseVC, LibSessionRespondingViewController, UITableViewDataS switch section.model { case .messageRequests: let viewController: SessionTableViewController = SessionTableViewController( - viewModel: MessageRequestsViewModel() + viewModel: MessageRequestsViewModel(using: viewModel.dependencies) ) self.navigationController?.pushViewController(viewController, animated: true) case .threads: let threadViewModel: SessionThreadViewModel = section.elements[indexPath.row] - show( - threadViewModel.threadId, - variant: threadViewModel.threadVariant, - isMessageRequest: (threadViewModel.threadIsMessageRequest == true), - with: .none, + let viewController: ConversationVC = ConversationVC( + threadId: threadViewModel.threadId, + threadVariant: threadViewModel.threadVariant, focusedInteractionInfo: nil, - animated: true + using: viewModel.dependencies ) + self.navigationController?.pushViewController(viewController, animated: true) default: break } } - func tableView(_ tableView: UITableView, canEditRowAt indexPath: IndexPath) -> Bool { + public func tableView(_ tableView: UITableView, canEditRowAt indexPath: IndexPath) -> Bool { return true } - func tableView(_ tableView: UITableView, willBeginEditingRowAt indexPath: IndexPath) { + public func tableView(_ tableView: UITableView, willBeginEditingRowAt indexPath: IndexPath) { UIContextualAction.willBeginEditing(indexPath: indexPath, tableView: tableView) } - func tableView(_ tableView: UITableView, didEndEditingRowAt indexPath: IndexPath?) { + public func tableView(_ tableView: UITableView, didEndEditingRowAt indexPath: IndexPath?) { UIContextualAction.didEndEditing(indexPath: indexPath, tableView: tableView) } - func tableView(_ tableView: UITableView, leadingSwipeActionsConfigurationForRowAt indexPath: IndexPath) -> UISwipeActionsConfiguration? { + public func tableView(_ tableView: UITableView, leadingSwipeActionsConfigurationForRowAt indexPath: IndexPath) -> UISwipeActionsConfiguration? { let section: HomeViewModel.SectionModel = self.viewModel.threadData[indexPath.section] let threadViewModel: SessionThreadViewModel = section.elements[indexPath.row] @@ -751,7 +791,8 @@ final class HomeVC: BaseVC, LibSessionRespondingViewController, UITableViewDataS tableView: tableView, threadViewModel: threadViewModel, viewController: self, - navigatableStateHolder: viewModel + navigatableStateHolder: viewModel, + using: viewModel.dependencies ) ) @@ -759,7 +800,7 @@ final class HomeVC: BaseVC, LibSessionRespondingViewController, UITableViewDataS } } - func tableView(_ tableView: UITableView, trailingSwipeActionsConfigurationForRowAt indexPath: IndexPath) -> UISwipeActionsConfiguration? { + public func tableView(_ tableView: UITableView, trailingSwipeActionsConfigurationForRowAt indexPath: IndexPath) -> UISwipeActionsConfiguration? { let section: HomeViewModel.SectionModel = self.viewModel.threadData[indexPath.section] let threadViewModel: SessionThreadViewModel = section.elements[indexPath.row] @@ -773,12 +814,13 @@ final class HomeVC: BaseVC, LibSessionRespondingViewController, UITableViewDataS tableView: tableView, threadViewModel: threadViewModel, viewController: self, - navigatableStateHolder: viewModel + navigatableStateHolder: viewModel, + using: viewModel.dependencies ) ) case .threads: - let sessionIdPrefix: SessionId.Prefix? = (try? SessionId(from: threadViewModel.threadId))?.prefix + let sessionIdPrefix: SessionId.Prefix? = try? SessionId.Prefix(from: threadViewModel.threadId) // Cannot properly sync outgoing blinded message requests so only provide valid options let shouldHavePinAction: Bool = ( @@ -820,7 +862,8 @@ final class HomeVC: BaseVC, LibSessionRespondingViewController, UITableViewDataS tableView: tableView, threadViewModel: threadViewModel, viewController: self, - navigatableStateHolder: viewModel + navigatableStateHolder: viewModel, + using: viewModel.dependencies ) ) @@ -831,11 +874,7 @@ final class HomeVC: BaseVC, LibSessionRespondingViewController, UITableViewDataS // MARK: - Interaction func handleContinueButtonTapped(from seedReminderView: SeedReminderView) { - if let recoveryPasswordView: RecoveryPasswordScreen = try? RecoveryPasswordScreen() { - let viewController: SessionHostingViewController = SessionHostingViewController(rootView: recoveryPasswordView) - viewController.setNavBarTitle("sessionRecoveryPassword".localized()) - self.navigationController?.pushViewController(viewController, animated: true) - } else { + guard let recoveryPasswordView: RecoveryPasswordScreen = try? RecoveryPasswordScreen(using: viewModel.dependencies) else { let targetViewController: UIViewController = ConfirmationModal( info: ConfirmationModal.Info( title: "theError".localized(), @@ -845,42 +884,17 @@ final class HomeVC: BaseVC, LibSessionRespondingViewController, UITableViewDataS ) ) present(targetViewController, animated: true, completion: nil) + return } - } - - func show( - _ threadId: String, - variant: SessionThread.Variant, - isMessageRequest: Bool, - with action: ConversationViewModel.Action, - focusedInteractionInfo: Interaction.TimestampInfo?, - animated: Bool - ) { - if let presentedVC = self.presentedViewController { - presentedVC.dismiss(animated: false, completion: nil) - } - - let finalViewControllers: [UIViewController] = [ - self, - ( - (isMessageRequest && action != .compose) ? - SessionTableViewController(viewModel: MessageRequestsViewModel()) : - nil - ), - ConversationVC( - threadId: threadId, - threadVariant: variant, - focusedInteractionInfo: focusedInteractionInfo, - using: viewModel.dependencies - ) - ].compactMap { $0 } - - self.navigationController?.setViewControllers(finalViewControllers, animated: animated) + + let viewController: SessionHostingViewController = SessionHostingViewController(rootView: recoveryPasswordView) + viewController.setNavBarTitle("sessionRecoveryPassword".localized()) + self.navigationController?.pushViewController(viewController, animated: true) } @objc private func openSettings() { let settingsViewController: SessionTableViewController = SessionTableViewController( - viewModel: SettingsViewModel() + viewModel: SettingsViewModel(using: viewModel.dependencies) ) let navigationController = StyledNavigationController(rootViewController: settingsViewController) navigationController.modalPresentationStyle = .fullScreen @@ -895,24 +909,14 @@ final class HomeVC: BaseVC, LibSessionRespondingViewController, UITableViewDataS self.navigationController?.setViewControllers([ self, searchController ], animated: true) } - @objc func createNewConversation() { - let viewController = SessionHostingViewController( - rootView: StartConversationScreen(), - customizedNavigationBackground: .backgroundSecondary - ) - viewController.setNavBarTitle("conversationsStart".localized()) - viewController.setUpDismissingButton(on: .right) - - let navigationController = StyledNavigationController(rootViewController: viewController) - if UIDevice.current.isIPad { - navigationController.modalPresentationStyle = .fullScreen - } - navigationController.modalPresentationCapturesStatusBarAppearance = true - present(navigationController, animated: true, completion: nil) + @objc private func createNewConversation() { + viewModel.dependencies[singleton: .app].createNewConversation() } func createNewDMFromDeepLink(sessionId: String) { - let viewController: SessionHostingViewController = SessionHostingViewController(rootView: NewMessageScreen(accountId: sessionId)) + let viewController: SessionHostingViewController = SessionHostingViewController( + rootView: NewMessageScreen(accountId: sessionId, using: viewModel.dependencies) + ) viewController.setNavBarTitle( "messageNew" .putNumber(1) diff --git a/Session/Home/HomeViewModel.swift b/Session/Home/HomeViewModel.swift index 66ae93bb94e..f74452a8fa8 100644 --- a/Session/Home/HomeViewModel.swift +++ b/Session/Home/HomeViewModel.swift @@ -25,6 +25,7 @@ public class HomeViewModel: NavigatableStateHolder { public static let pageSize: Int = (UIDevice.current.isIPad ? 20 : 15) public struct State: Equatable { + let userSessionId: SessionId let showViewedSeedBanner: Bool let hasHiddenMessageRequests: Bool let unreadMessageRequestThreadCount: Int @@ -36,26 +37,17 @@ public class HomeViewModel: NavigatableStateHolder { // MARK: - Initialization init(using dependencies: Dependencies) { - typealias InitialData = ( - showViewedSeedBanner: Bool, - hasHiddenMessageRequests: Bool, - profile: Profile - ) - - let initialData: InitialData? = Storage.shared.read { db -> InitialData in - ( - !db[.hasViewedSeed], - db[.hasHiddenMessageRequests], - Profile.fetchOrCreateCurrentUser(db) - ) + let initialState: State? = dependencies[singleton: .storage].read { db -> State in + try HomeViewModel.retrieveState(db, excludingMessageRequestThreadCount: true, using: dependencies) } self.dependencies = dependencies self.state = State( - showViewedSeedBanner: (initialData?.showViewedSeedBanner ?? true), - hasHiddenMessageRequests: (initialData?.hasHiddenMessageRequests ?? false), + userSessionId: (initialState?.userSessionId ?? dependencies[cache: .general].sessionId), + showViewedSeedBanner: (initialState?.showViewedSeedBanner ?? true), + hasHiddenMessageRequests: (initialState?.hasHiddenMessageRequests ?? false), unreadMessageRequestThreadCount: 0, - userProfile: (initialData?.profile ?? Profile.fetchOrCreateCurrentUser()) + userProfile: (initialState?.userProfile ?? Profile.fetchOrCreateCurrentUser(using: dependencies)) ) self.pagedDataObserver = nil @@ -63,7 +55,7 @@ public class HomeViewModel: NavigatableStateHolder { // 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) - let userPublicKey: String = self.state.userProfile.id + let userSessionId: SessionId = self.state.userSessionId let thread: TypedTableAlias = TypedTableAlias() self.pagedDataObserver = PagedDatabaseObserver( pagedTable: SessionThread.self, @@ -126,7 +118,7 @@ public class HomeViewModel: NavigatableStateHolder { WHERE ( \(groupMember[.groupId]) = \(thread[.id]) AND \(SQL("\(groupMember[.role]) = \(targetRole)")) AND - \(groupMember[.profileId]) != \(userPublicKey) + \(groupMember[.profileId]) != \(userSessionId.hexString) ) ) OR profile.id = ( -- Back profile @@ -136,10 +128,10 @@ public class HomeViewModel: NavigatableStateHolder { WHERE ( \(groupMember[.groupId]) = \(thread[.id]) AND \(SQL("\(groupMember[.role]) = \(targetRole)")) AND - \(groupMember[.profileId]) != \(userPublicKey) + \(groupMember[.profileId]) != \(userSessionId.hexString) ) ) OR ( -- Fallback profile - profile.id = \(userPublicKey) AND + profile.id = \(userSessionId.hexString) AND ( SELECT COUNT(\(groupMember[.profileId])) FROM \(GroupMember.self) @@ -147,7 +139,7 @@ public class HomeViewModel: NavigatableStateHolder { WHERE ( \(groupMember[.groupId]) = \(thread[.id]) AND \(SQL("\(groupMember[.role]) = \(targetRole)")) AND - \(groupMember[.profileId]) != \(userPublicKey) + \(groupMember[.profileId]) != \(userSessionId.hexString) ) ) = 1 ) @@ -159,7 +151,7 @@ public class HomeViewModel: NavigatableStateHolder { ), PagedData.ObservedChanges( table: ClosedGroup.self, - columns: [.name], + columns: [.name, .invited, .displayPictureFilename], joinToPagedType: { let closedGroup: TypedTableAlias = TypedTableAlias() @@ -168,7 +160,7 @@ public class HomeViewModel: NavigatableStateHolder { ), PagedData.ObservedChanges( table: OpenGroup.self, - columns: [.name, .imageData], + columns: [.name, .displayPictureFilename], joinToPagedType: { let openGroup: TypedTableAlias = TypedTableAlias() @@ -188,11 +180,11 @@ public class HomeViewModel: NavigatableStateHolder { /// **Note:** This `optimisedJoinSQL` value includes the required minimum joins needed for the query but differs /// from the JOINs that are actually used for performance reasons as the basic logic can be simpler for where it's used joinSQL: SessionThreadViewModel.optimisedJoinSQL, - filterSQL: SessionThreadViewModel.homeFilterSQL(userPublicKey: userPublicKey), + filterSQL: SessionThreadViewModel.homeFilterSQL(userSessionId: userSessionId), groupSQL: SessionThreadViewModel.groupSQL, orderSQL: SessionThreadViewModel.homeOrderSQL, dataQuery: SessionThreadViewModel.baseQuery( - userPublicKey: userPublicKey, + userSessionId: userSessionId, groupSQL: SessionThreadViewModel.groupSQL, orderSQL: SessionThreadViewModel.homeOrderSQL ), @@ -207,7 +199,8 @@ public class HomeViewModel: NavigatableStateHolder { ) self?.hasReceivedInitialThreadData = true - } + }, + using: dependencies ) // Run the initial query on a background thread so we don't block the main thread @@ -230,20 +223,30 @@ public class HomeViewModel: NavigatableStateHolder { /// fetch (after the ones in `ValueConcurrentObserver.asyncStart`/`ValueConcurrentObserver.syncStart`) /// just in case the database has changed between the two reads - unfortunately it doesn't look like there is a way to prevent this public lazy var observableState = ValueObservation - .trackingConstantRegion { db -> State in try HomeViewModel.retrieveState(db) } + .trackingConstantRegion { [dependencies] db -> State in + try HomeViewModel.retrieveState(db, using: dependencies) + } .removeDuplicates() .handleEvents(didFail: { SNLog("[HomeViewModel] Observation failed with error: \($0)") }) - private static func retrieveState(_ db: Database) throws -> State { + private static func retrieveState( + _ db: Database, + excludingMessageRequestThreadCount: Bool = false, + using dependencies: Dependencies + ) throws -> State { + let userSessionId: SessionId = dependencies[cache: .general].sessionId let hasViewedSeed: Bool = db[.hasViewedSeed] let hasHiddenMessageRequests: Bool = db[.hasHiddenMessageRequests] - let userProfile: Profile = Profile.fetchOrCreateCurrentUser(db) - let unreadMessageRequestThreadCount: Int = try SessionThread - .unreadMessageRequestsCountQuery(userPublicKey: userProfile.id) - .fetchOne(db) - .defaulting(to: 0) + let userProfile: Profile = Profile.fetchOrCreateCurrentUser(db, using: dependencies) + let unreadMessageRequestThreadCount: Int = (excludingMessageRequestThreadCount ? 0 : + try SessionThread + .unreadMessageRequestsCountQuery(userSessionId: userSessionId) + .fetchOne(db) + .defaulting(to: 0) + ) return State( + userSessionId: userSessionId, showViewedSeedBanner: !hasViewedSeed, hasHiddenMessageRequests: hasHiddenMessageRequests, unreadMessageRequestThreadCount: unreadMessageRequestThreadCount, @@ -330,7 +333,8 @@ public class HomeViewModel: NavigatableStateHolder { elements: [ SessionThreadViewModel( threadId: SessionThreadViewModel.messageRequestsSectionId, - unreadCount: UInt(finalUnreadMessageRequestCount) + unreadCount: UInt(finalUnreadMessageRequestCount), + using: dependencies ) ] )] @@ -351,13 +355,28 @@ public class HomeViewModel: NavigatableStateHolder { return lhs.lastInteractionDate > rhs.lastInteractionDate } .map { viewModel -> SessionThreadViewModel in - viewModel.populatingCurrentUserBlindedKeys( - currentUserBlinded15PublicKeyForThisThread: groupedOldData[viewModel.threadId]? + viewModel.populatingCurrentUserBlindedIds( + currentUserBlinded15SessionIdForThisThread: groupedOldData[viewModel.threadId]? .first? - .currentUserBlinded15PublicKey, - currentUserBlinded25PublicKeyForThisThread: groupedOldData[viewModel.threadId]? + .currentUserBlinded15SessionId, + currentUserBlinded25SessionIdForThisThread: groupedOldData[viewModel.threadId]? .first? - .currentUserBlinded25PublicKey + .currentUserBlinded25SessionId, + wasKickedFromGroup: ( + viewModel.threadVariant == .group && + LibSession.wasKickedFromGroup( + groupSessionId: SessionId(.group, hex: viewModel.threadId), + using: dependencies + ) + ), + groupIsDestroyed: ( + viewModel.threadVariant == .group && + LibSession.groupIsDestroyed( + groupSessionId: SessionId(.group, hex: viewModel.threadId), + using: dependencies + ) + ), + using: dependencies ) } ) diff --git a/Session/Home/Message Requests/MessageRequestsViewModel.swift b/Session/Home/Message Requests/MessageRequestsViewModel.swift index 407bf0ebf46..4bf06c04877 100644 --- a/Session/Home/Message Requests/MessageRequestsViewModel.swift +++ b/Session/Home/Message Requests/MessageRequestsViewModel.swift @@ -5,6 +5,7 @@ import Combine import GRDB import DifferenceKit import SessionUIKit +import SessionMessagingKit import SessionUtilitiesKit import SignalUtilitiesKit @@ -23,7 +24,7 @@ class MessageRequestsViewModel: SessionTableViewModel, NavigatableStateHolder, O // MARK: - Initialization - init(using dependencies: Dependencies = Dependencies()) { + init(using dependencies: Dependencies) { self.dependencies = dependencies self.pagedDataObserver = nil @@ -31,8 +32,9 @@ class MessageRequestsViewModel: SessionTableViewModel, NavigatableStateHolder, O // 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) - let userPublicKey: String = getUserHexEncodedPublicKey(using: dependencies) + let userSessionId: SessionId = dependencies[cache: .general].sessionId let thread: TypedTableAlias = TypedTableAlias() + self.pagedDataObserver = PagedDatabaseObserver( pagedTable: SessionThread.self, pageSize: MessageRequestsViewModel.pageSize, @@ -80,11 +82,11 @@ class MessageRequestsViewModel: SessionTableViewModel, NavigatableStateHolder, O /// **Note:** This `optimisedJoinSQL` value includes the required minimum joins needed for the query but differs /// from the JOINs that are actually used for performance reasons as the basic logic can be simpler for where it's used joinSQL: SessionThreadViewModel.optimisedJoinSQL, - filterSQL: SessionThreadViewModel.messageRequestsFilterSQL(userPublicKey: userPublicKey), + filterSQL: SessionThreadViewModel.messageRequestsFilterSQL(userSessionId: userSessionId), groupSQL: SessionThreadViewModel.groupSQL, orderSQL: SessionThreadViewModel.messageRequetsOrderSQL, dataQuery: SessionThreadViewModel.baseQuery( - userPublicKey: userPublicKey, + userSessionId: userSessionId, groupSQL: SessionThreadViewModel.groupSQL, orderSQL: SessionThreadViewModel.messageRequetsOrderSQL ), @@ -94,7 +96,8 @@ class MessageRequestsViewModel: SessionTableViewModel, NavigatableStateHolder, O } self?.pendingTableDataSubject.send(data) - } + }, + using: dependencies ) // Run the initial query on a background thread so we don't block the push transition @@ -140,17 +143,32 @@ class MessageRequestsViewModel: SessionTableViewModel, NavigatableStateHolder, O section: .threads, elements: data .sorted { lhs, rhs -> Bool in lhs.lastInteractionDate > rhs.lastInteractionDate } - .map { viewModel -> SessionCell.Info in + .map { [dependencies] viewModel -> SessionCell.Info in SessionCell.Info( - id: viewModel.populatingCurrentUserBlindedKeys( - currentUserBlinded15PublicKeyForThisThread: groupedOldData[viewModel.threadId]? + id: viewModel.populatingCurrentUserBlindedIds( + currentUserBlinded15SessionIdForThisThread: groupedOldData[viewModel.threadId]? .first? .id - .currentUserBlinded15PublicKey, - currentUserBlinded25PublicKeyForThisThread: groupedOldData[viewModel.threadId]? + .currentUserBlinded15SessionId, + currentUserBlinded25SessionIdForThisThread: groupedOldData[viewModel.threadId]? .first? .id - .currentUserBlinded25PublicKey + .currentUserBlinded25SessionId, + wasKickedFromGroup: ( + viewModel.threadVariant == .group && + LibSession.wasKickedFromGroup( + groupSessionId: SessionId(.group, hex: viewModel.threadId), + using: dependencies + ) + ), + groupIsDestroyed: ( + viewModel.threadVariant == .group && + LibSession.groupIsDestroyed( + groupSessionId: SessionId(.group, hex: viewModel.threadId), + using: dependencies + ) + ), + using: dependencies ), accessibility: Accessibility( identifier: "Message request" @@ -200,7 +218,7 @@ class MessageRequestsViewModel: SessionTableViewModel, NavigatableStateHolder, O cancelStyle: .alert_text, onConfirm: { _ in // Clear the requests - dependencies.storage.write { db in + dependencies[singleton: .storage].write { db in // Remove the one-to-one requests try SessionThread.deleteOrLeave( db, @@ -208,17 +226,21 @@ class MessageRequestsViewModel: SessionTableViewModel, NavigatableStateHolder, O threadIds: threadInfo .filter { _, variant in variant == .contact } .map { id, _ in id }, - calledFromConfigHandling: false + threadVariant: .contact, + calledFromConfig: nil, + using: dependencies ) - // Remove the group requests + // Remove the group invites try SessionThread.deleteOrLeave( db, type: .deleteGroupAndContent, threadIds: threadInfo .filter { _, variant in variant == .legacyGroup || variant == .group } .map { id, _ in id }, - calledFromConfigHandling: false + threadVariant: .group, + calledFromConfig: nil, + using: dependencies ) } } @@ -248,16 +270,14 @@ class MessageRequestsViewModel: SessionTableViewModel, NavigatableStateHolder, O return UIContextualAction.configuration( for: UIContextualAction.generateSwipeActions( - [ - (threadViewModel.threadVariant != .contact ? nil : .block), - .delete - ].compactMap { $0 }, + [.block, .delete], for: .trailing, indexPath: indexPath, tableView: tableView, threadViewModel: threadViewModel, viewController: viewController, - navigatableStateHolder: nil + navigatableStateHolder: nil, + using: dependencies ) ) diff --git a/Session/Home/New Conversation/InviteAFriendScreen.swift b/Session/Home/New Conversation/InviteAFriendScreen.swift index fd2773d0ea8..9bc9390229e 100644 --- a/Session/Home/New Conversation/InviteAFriendScreen.swift +++ b/Session/Home/New Conversation/InviteAFriendScreen.swift @@ -9,10 +9,14 @@ struct InviteAFriendScreen: View { @EnvironmentObject var host: HostWrapper @State private var copied: Bool = false - private let accountId: String = getUserHexEncodedPublicKey() + private let accountId: String static private let cornerRadius: CGFloat = 13 + init(accountId: String) { + self.accountId = accountId + } + var body: some View { ZStack(alignment: .center) { VStack( @@ -145,5 +149,5 @@ struct InviteAFriendScreen: View { } #Preview { - InviteAFriendScreen() + InviteAFriendScreen(accountId: "050000000000000000000000000000000000000000000000000000000000000000") } diff --git a/Session/Home/New Conversation/NewMessageScreen.swift b/Session/Home/New Conversation/NewMessageScreen.swift index d4d5db3639d..4717950006b 100644 --- a/Session/Home/New Conversation/NewMessageScreen.swift +++ b/Session/Home/New Conversation/NewMessageScreen.swift @@ -9,12 +9,14 @@ import SessionSnodeKit struct NewMessageScreen: View { @EnvironmentObject var host: HostWrapper + private let dependencies: Dependencies @State var tabIndex = 0 @State private var accountIdOrONS: String @State private var errorString: String? = nil - init(accountId: String = "") { + init(accountId: String = "", using dependencies: Dependencies) { + self.dependencies = dependencies self.accountIdOrONS = accountId } @@ -42,7 +44,8 @@ struct NewMessageScreen: View { ScanQRCodeScreen( $accountIdOrONS, error: $errorString, - continueAction: continueWithAccountIdFromQRCode + continueAction: continueWithAccountIdFromQRCode, + using: dependencies ) } } @@ -85,7 +88,7 @@ struct NewMessageScreen: View { ModalActivityIndicatorViewController .present(fromViewController: self.host.controller?.navigationController!, canCancel: false) { modalActivityIndicator in SnodeAPI - .getSessionID(for: accountIdOrONS) + .getSessionID(for: accountIdOrONS, using: dependencies) .subscribe(on: DispatchQueue.global(qos: .userInitiated)) .receive(on: DispatchQueue.main) .sinkUntilComplete( @@ -117,7 +120,7 @@ struct NewMessageScreen: View { } private func startNewDM(with sessionId: String) { - SessionApp.presentConversationCreatingIfNeeded( + dependencies[singleton: .app].presentConversationCreatingIfNeeded( for: sessionId, variant: .contact, action: .compose, @@ -202,5 +205,5 @@ struct EnterAccountIdScreen: View { } #Preview { - NewMessageScreen() + NewMessageScreen(using: Dependencies.createEmpty()) } diff --git a/Session/Home/New Conversation/StartConversationScreen.swift b/Session/Home/New Conversation/StartConversationScreen.swift index f3ea227cb90..5937053c4ef 100644 --- a/Session/Home/New Conversation/StartConversationScreen.swift +++ b/Session/Home/New Conversation/StartConversationScreen.swift @@ -7,6 +7,11 @@ import SessionUtilitiesKit struct StartConversationScreen: View { @EnvironmentObject var host: HostWrapper + private let dependencies: Dependencies + + init(using dependencies: Dependencies) { + self.dependencies = dependencies + } var body: some View { ZStack(alignment: .topLeading) { @@ -23,10 +28,12 @@ struct StartConversationScreen: View { .putNumber(1) .localized() NewConversationCell( - image: "Message", + image: "Message", // stringlint:ignore title: title ) { - let viewController: SessionHostingViewController = SessionHostingViewController(rootView: NewMessageScreen()) + let viewController: SessionHostingViewController = SessionHostingViewController( + rootView: NewMessageScreen(using: dependencies) + ) viewController.setNavBarTitle(title) viewController.setUpDismissingButton(on: .right) self.host.controller?.navigationController?.pushViewController(viewController, animated: true) @@ -46,7 +53,7 @@ struct StartConversationScreen: View { image: "Group", title: "groupCreate".localized() ) { - let viewController = NewClosedGroupVC() + let viewController = NewClosedGroupVC(using: dependencies) self.host.controller?.navigationController?.pushViewController(viewController, animated: true) } .accessibility( @@ -64,7 +71,7 @@ struct StartConversationScreen: View { image: "Globe", // stringlint:ignore title: "communityJoin".localized() ) { - let viewController = JoinOpenGroupVC() + let viewController = JoinOpenGroupVC(using: dependencies) self.host.controller?.navigationController?.pushViewController(viewController, animated: true) } .accessibility( @@ -82,7 +89,9 @@ struct StartConversationScreen: View { image: "icon_invite", // stringlint:ignore title: "sessionInviteAFriend".localized() ) { - let viewController: SessionHostingViewController = SessionHostingViewController(rootView: InviteAFriendScreen()) + let viewController: SessionHostingViewController = SessionHostingViewController( + rootView: InviteAFriendScreen(accountId: dependencies[cache: .general].sessionId.hexString) + ) viewController.setNavBarTitle("sessionInviteAFriend".localized()) viewController.setUpDismissingButton(on: .right) self.host.controller?.navigationController?.pushViewController(viewController, animated: true) @@ -105,7 +114,7 @@ struct StartConversationScreen: View { .foregroundColor(themeColor: .textSecondary) QRCodeView( - string: getUserHexEncodedPublicKey(), + string: dependencies[cache: .general].sessionId.hexString, hasBackground: false, logo: "SessionWhite40", // stringlint:ignore themeStyle: ThemeManager.currentTheme.interfaceStyle @@ -155,5 +164,5 @@ fileprivate struct NewConversationCell: View { } #Preview { - StartConversationScreen() + StartConversationScreen(using: Dependencies.createEmpty()) } diff --git a/Session/Media Viewing & Editing/AllMediaViewController.swift b/Session/Media Viewing & Editing/AllMediaViewController.swift index fe38bc123bd..8cb0278b1cb 100644 --- a/Session/Media Viewing & Editing/AllMediaViewController.swift +++ b/Session/Media Viewing & Editing/AllMediaViewController.swift @@ -5,9 +5,11 @@ import QuartzCore import GRDB import DifferenceKit import SessionUIKit +import SessionUtilitiesKit import SignalUtilitiesKit public class AllMediaViewController: UIViewController, UIPageViewControllerDataSource, UIPageViewControllerDelegate { + private let dependencies: Dependencies private let pageVC = UIPageViewController(transitionStyle: .scroll, navigationOrientation: .horizontal, options: nil) private var pages: [UIViewController] = [] private var targetVCIndex: Int? @@ -38,7 +40,8 @@ public class AllMediaViewController: UIViewController, UIPageViewControllerDataS private var mediaTitleViewController: MediaTileViewController private var documentTitleViewController: DocumentTileViewController - init(mediaTitleViewController: MediaTileViewController, documentTitleViewController: DocumentTileViewController) { + init(mediaTitleViewController: MediaTileViewController, documentTitleViewController: DocumentTileViewController, using dependencies: Dependencies) { + self.dependencies = dependencies self.mediaTitleViewController = mediaTitleViewController self.documentTitleViewController = documentTitleViewController @@ -63,7 +66,7 @@ public class AllMediaViewController: UIViewController, UIPageViewControllerDataS // Add a custom back button if this is the only view controller if self.navigationController?.viewControllers.first == self { - let backButton = UIViewController.createOWSBackButton(target: self, selector: #selector(didPressDismissButton)) + let backButton = UIViewController.createOWSBackButton(target: self, selector: #selector(didPressDismissButton), using: dependencies) self.navigationItem.leftBarButtonItem = backButton } diff --git a/Session/Media Viewing & Editing/CropScaleImageViewController.swift b/Session/Media Viewing & Editing/CropScaleImageViewController.swift index 80f3c1c74c3..98defd4a7ae 100644 --- a/Session/Media Viewing & Editing/CropScaleImageViewController.swift +++ b/Session/Media Viewing & Editing/CropScaleImageViewController.swift @@ -194,10 +194,10 @@ import SessionUtilitiesKit imageLayer.contents = srcImage.cgImage imageView.layer.addSublayer(imageLayer) - let maskingView = OWSBezierPathView() + let maskingView = BezierPathView() contentView.addSubview(maskingView) - maskingView.configureShapeLayerBlock = { [weak self] layer, bounds in + maskingView.configureShapeLayer = { [weak self] layer, bounds in guard let strongSelf = self else { return } diff --git a/Session/Media Viewing & Editing/DocumentTitleViewController.swift b/Session/Media Viewing & Editing/DocumentTitleViewController.swift index 83cb35a78eb..138429f0939 100644 --- a/Session/Media Viewing & Editing/DocumentTitleViewController.swift +++ b/Session/Media Viewing & Editing/DocumentTitleViewController.swift @@ -7,6 +7,7 @@ import GRDB import DifferenceKit import SessionUIKit import SignalUtilitiesKit +import SessionMessagingKit import SessionUtilitiesKit public class DocumentTileViewController: UIViewController, UITableViewDelegate, UITableViewDataSource { @@ -19,6 +20,7 @@ public class DocumentTileViewController: UIViewController, UITableViewDelegate, static let footerBarHeight: CGFloat = 40 static let loadMoreHeaderHeight: CGFloat = 100 + private let dependencies: Dependencies private let viewModel: MediaGalleryViewModel private var hasLoadedInitialData: Bool = false private var didFinishInitialLayout: Bool = false @@ -29,9 +31,10 @@ public class DocumentTileViewController: UIViewController, UITableViewDelegate, // MARK: - Initialization - init(viewModel: MediaGalleryViewModel) { + init(viewModel: MediaGalleryViewModel, using dependencies: Dependencies) { + self.dependencies = dependencies self.viewModel = viewModel - Storage.shared.addObserver(viewModel.pagedDataObserver) + dependencies[singleton: .storage].addObserver(viewModel.pagedDataObserver) super.init(nibName: nil, bundle: nil) } @@ -76,7 +79,7 @@ public class DocumentTileViewController: UIViewController, UITableViewDelegate, // Add a custom back button if this is the only view controller if self.navigationController?.viewControllers.first == self { - let backButton = UIViewController.createOWSBackButton(target: self, selector: #selector(didPressDismissButton)) + let backButton = UIViewController.createOWSBackButton(target: self, selector: #selector(didPressDismissButton), using: dependencies) self.navigationItem.leftBarButtonItem = backButton } @@ -331,7 +334,7 @@ public class DocumentTileViewController: UIViewController, UITableViewDelegate, public func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { tableView.deselectRow(at: indexPath, animated: false) let attachment: Attachment = self.viewModel.galleryData[indexPath.section].elements[indexPath.row].attachment - guard let originalFilePath: String = attachment.originalFilePath else { return } + guard let originalFilePath: String = attachment.originalFilePath(using: dependencies) else { return } let fileUrl: URL = URL(fileURLWithPath: originalFilePath) diff --git a/Session/Media Viewing & Editing/GIFs/GifPickerCell.swift b/Session/Media Viewing & Editing/GIFs/GifPickerCell.swift index c085b88beaf..d3102176af7 100644 --- a/Session/Media Viewing & Editing/GIFs/GifPickerCell.swift +++ b/Session/Media Viewing & Editing/GIFs/GifPickerCell.swift @@ -4,6 +4,7 @@ import Foundation import Combine import UniformTypeIdentifiers import YYImage +import SessionSnodeKit import SignalUtilitiesKit import SessionUtilitiesKit @@ -11,6 +12,7 @@ class GifPickerCell: UICollectionViewCell { // MARK: Properties + var dependencies: Dependencies? var imageInfo: GiphyImageInfo? { didSet { Log.assertOnMainThread() @@ -58,6 +60,7 @@ class GifPickerCell: UICollectionViewCell { override func prepareForReuse() { super.prepareForReuse() + dependencies = nil imageInfo = nil isCellVisible = false stillAsset = nil @@ -112,7 +115,7 @@ class GifPickerCell: UICollectionViewCell { // Record high quality animated rendition, but to save bandwidth, don't start downloading // until it's selected. guard let highQualityAnimatedRendition = imageInfo.pickSendingRendition() else { - Log.warn("[GitPickerCell] could not pick gif rendition: \(imageInfo.giphyId)") + Log.warn(.giphy, "Cell could not pick gif rendition: \(imageInfo.giphyId)") clearAssetRequests() return } @@ -121,12 +124,12 @@ class GifPickerCell: UICollectionViewCell { // The Giphy API returns a slew of "renditions" for a given image. // It's critical that we carefully "pick" the best rendition to use. guard let animatedRendition = imageInfo.pickPreviewRendition() else { - Log.warn("[GitPickerCell] could not pick gif rendition: \(imageInfo.giphyId)") + Log.warn(.giphy, "Cell could not pick gif rendition: \(imageInfo.giphyId)") clearAssetRequests() return } guard let stillRendition = imageInfo.pickStillRendition() else { - Log.warn("[GitPickerCell] could not pick still rendition: \(imageInfo.giphyId)") + Log.warn(.giphy, "Cell could not pick still rendition: \(imageInfo.giphyId)") clearAssetRequests() return } @@ -135,12 +138,12 @@ class GifPickerCell: UICollectionViewCell { if stillAsset != nil || animatedAsset != nil { clearStillAssetRequest() } else if stillAssetRequest == nil { - stillAssetRequest = GiphyDownloader.giphyDownloader.requestAsset( + stillAssetRequest = dependencies?[singleton: .giphyDownloader].requestAsset( assetDescription: stillRendition, priority: .high, success: { [weak self] assetRequest, asset in if assetRequest != nil && assetRequest != self?.stillAssetRequest { - Log.error("[GitPickerCell] Obsolete request callback.") + Log.error(.giphy, "Cell received obsolete request callback.") return } @@ -150,7 +153,7 @@ class GifPickerCell: UICollectionViewCell { }, failure: { [weak self] assetRequest in if assetRequest != self?.stillAssetRequest { - Log.error("[GitPickerCell] Obsolete request callback.") + Log.error(.giphy, "Cell received obsolete request callback.") return } self?.clearStillAssetRequest() @@ -162,12 +165,12 @@ class GifPickerCell: UICollectionViewCell { if animatedAsset != nil { clearAnimatedAssetRequest() } else if animatedAssetRequest == nil { - animatedAssetRequest = GiphyDownloader.giphyDownloader.requestAsset( + animatedAssetRequest = dependencies?[singleton: .giphyDownloader].requestAsset( assetDescription: animatedRendition, priority: .low, success: { [weak self] assetRequest, asset in if assetRequest != nil && assetRequest != self?.animatedAssetRequest { - Log.error("[GitPickerCell] Obsolete request callback.") + Log.error(.giphy, "Cell received obsolete request callback.") return } @@ -178,7 +181,7 @@ class GifPickerCell: UICollectionViewCell { }, failure: { [weak self] assetRequest in if assetRequest != self?.animatedAssetRequest { - Log.error("[GitPickerCell] Obsolete request callback.") + Log.error(.giphy, "Cell received obsolete request callback.") return } @@ -198,13 +201,13 @@ class GifPickerCell: UICollectionViewCell { clearViewState() return } - guard Data.isValidImage(at: asset.filePath, type: .gif) else { - Log.error("[GitPickerCell] Invalid asset.") + guard let dependencies: Dependencies = dependencies, Data.isValidImage(at: asset.filePath, type: .gif, using: dependencies) else { + Log.error(.giphy, "Cell received invalid asset.") clearViewState() return } guard let image = YYImage(contentsOfFile: asset.filePath) else { - Log.error("[GitPickerCell] Could not load asset.") + Log.error(.giphy, "Cell could not load asset.") clearViewState() return } @@ -215,7 +218,7 @@ class GifPickerCell: UICollectionViewCell { imageView.pin(to: contentView) } guard let imageView = imageView else { - Log.error("[GitPickerCell] Missing imageview.") + Log.error(.giphy, "Cell missing imageview.") clearViewState() return } @@ -249,21 +252,25 @@ class GifPickerCell: UICollectionViewCell { public func requestRenditionForSending() -> AnyPublisher { guard let renditionForSending = self.renditionForSending else { - Log.error("[GitPickerCell] renditionForSending was unexpectedly nil") + Log.error(.giphy, "Cell renditionForSending was unexpectedly nil") return Fail(error: GiphyError.assertionError(description: "renditionForSending was unexpectedly nil")) .eraseToAnyPublisher() } + guard let dependencies: Dependencies = self.dependencies else { + return Fail(error: GiphyError.assertionError(description: "dependencies was unexpectedly nil")) + .eraseToAnyPublisher() + } // We don't retain a handle on the asset request, since there will only ever // be one selected asset, and we never want to cancel it. - return GiphyDownloader.giphyDownloader + return dependencies[singleton: .giphyDownloader] .requestAsset( assetDescription: renditionForSending, priority: .high ) .mapError { _ -> Error in // TODO: GiphyDownloader API should pass through a useful failing error so we can pass it through here - Log.error("[GitPickerCell] request failed") + Log.error(.giphy, "Cell request failed") return GiphyError.fetchFailure } .map { asset, _ in asset } diff --git a/Session/Media Viewing & Editing/GIFs/GifPickerLayout.swift b/Session/Media Viewing & Editing/GIFs/GifPickerLayout.swift index 9ac27f50ea6..bb22fd0eb9b 100644 --- a/Session/Media Viewing & Editing/GIFs/GifPickerLayout.swift +++ b/Session/Media Viewing & Editing/GIFs/GifPickerLayout.swift @@ -1,6 +1,6 @@ // Copyright (c) 2019 Open Whisper Systems. All rights reserved. -import Foundation +import UIKit protocol GifPickerLayoutDelegate: AnyObject { func imageInfosForLayout() -> [GiphyImageInfo] diff --git a/Session/Media Viewing & Editing/GIFs/GifPickerViewController.swift b/Session/Media Viewing & Editing/GIFs/GifPickerViewController.swift index 56fedbab581..d749e46e42c 100644 --- a/Session/Media Viewing & Editing/GIFs/GifPickerViewController.swift +++ b/Session/Media Viewing & Editing/GIFs/GifPickerViewController.swift @@ -4,6 +4,7 @@ import UIKit import Combine import SignalUtilitiesKit import SessionUIKit +import SessionMessagingKit import SessionUtilitiesKit class GifPickerViewController: OWSViewController, UISearchBarDelegate, UICollectionViewDataSource, UICollectionViewDelegate, GifPickerLayoutDelegate { @@ -16,7 +17,7 @@ class GifPickerViewController: OWSViewController, UISearchBarDelegate, UICollect private var viewMode = ViewMode.idle { didSet { - Log.debug("[GifPickerViewController] viewMode: \(viewMode)") + Log.debug(.giphy, "ViewController viewMode: \(viewMode)") updateContents() } @@ -24,6 +25,7 @@ class GifPickerViewController: OWSViewController, UISearchBarDelegate, UICollect var lastQuery: String = "" + private let dependencies: Dependencies public weak var delegate: GifPickerViewControllerDelegate? let searchBar: SearchBar @@ -40,7 +42,6 @@ class GifPickerViewController: OWSViewController, UISearchBarDelegate, UICollect var progressiveSearchTimer: Timer? private var disposables: Set = Set() - private var networkStatusCallbackId: UUID? // MARK: - Initialization @@ -49,7 +50,8 @@ class GifPickerViewController: OWSViewController, UISearchBarDelegate, UICollect fatalError("init(coder:) has not been implemented") } - required init() { + required init(using dependencies: Dependencies) { + self.dependencies = dependencies self.searchBar = SearchBar() self.layout = GifPickerLayout() self.collectionView = UICollectionView(frame: CGRect.zero, collectionViewLayout: self.layout) @@ -60,7 +62,6 @@ class GifPickerViewController: OWSViewController, UISearchBarDelegate, UICollect } deinit { - LibSession.removeNetworkChangedCallback(callbackId: networkStatusCallbackId) NotificationCenter.default.removeObserver(self) progressiveSearchTimer?.invalidate() @@ -76,7 +77,7 @@ class GifPickerViewController: OWSViewController, UISearchBarDelegate, UICollect func ensureCellState() { for cell in self.collectionView.visibleCells { guard let cell = cell as? GifPickerCell else { - Log.error("[GifPickerViewController] unexpected cell.") + Log.error(.giphy, "ViewController unexpected cell.") return } cell.ensureCellState() @@ -103,12 +104,13 @@ class GifPickerViewController: OWSViewController, UISearchBarDelegate, UICollect createViews() - networkStatusCallbackId = LibSession.onNetworkStatusChanged { [weak self] _ in - DispatchQueue.main.async { + dependencies[cache: .libSessionNetwork].networkStatus + .receive(on: DispatchQueue.main, using: dependencies) + .sink(receiveValue: { [weak self] _ in // Prod cells to try to load when connectivity changes. self?.ensureCellState() - } - } + }) + .store(in: &disposables) NotificationCenter.default.addObserver( self, @@ -220,15 +222,15 @@ class GifPickerViewController: OWSViewController, UISearchBarDelegate, UICollect private func updateContents() { guard let noResultsView = self.noResultsView else { - Log.error("[GifPickerViewController] Missing noResultsView") + Log.error(.giphy, "ViewController missing noResultsView") return } guard let searchErrorView = self.searchErrorView else { - Log.error("[GifPickerViewController] Missing searchErrorView") + Log.error(.giphy, "ViewController missing searchErrorView") return } guard let activityIndicator = self.activityIndicator else { - Log.error("[GifPickerViewController] Missing activityIndicator") + Log.error(.giphy, "ViewController missing activityIndicator") return } @@ -289,15 +291,16 @@ class GifPickerViewController: OWSViewController, UISearchBarDelegate, UICollect let cell = collectionView.dequeueReusableCell(withReuseIdentifier: kCellReuseIdentifier, for: indexPath) guard indexPath.row < imageInfos.count else { - Log.warn("[GifPickerViewController] indexPath: \(indexPath.row) out of range for imageInfo count: \(imageInfos.count) ") + Log.warn(.giphy, "ViewController indexPath: \(indexPath.row) out of range for imageInfo count: \(imageInfos.count) ") return cell } let imageInfo = imageInfos[indexPath.row] guard let gifCell = cell as? GifPickerCell else { - Log.error("[GifPickerViewController] Unexpected cell type.") + Log.error(.giphy, "ViewController unexpected cell type.") return cell } + gifCell.dependencies = dependencies gifCell.imageInfo = imageInfo return cell } @@ -306,30 +309,30 @@ class GifPickerViewController: OWSViewController, UISearchBarDelegate, UICollect public func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) { guard let cell = collectionView.cellForItem(at: indexPath) as? GifPickerCell else { - Log.error("[GifPickerViewController] unexpected cell.") + Log.error(.giphy, "ViewController unexpected cell.") return } guard cell.stillAsset != nil || cell.animatedAsset != nil else { // we don't want to let the user blindly select a gray cell - Log.debug("[GifPickerViewController] ignoring selection of cell with no preview") + Log.debug(.giphy, "ViewController ignoring selection of cell with no preview") return } guard self.hasSelectedCell == false else { - Log.error("[GifPickerViewController] Already selected cell") + Log.error(.giphy, "ViewController already selected cell") return } self.hasSelectedCell = true // Fade out all cells except the selected one. - let maskingView = OWSBezierPathView() + let maskingView = BezierPathView() // Selecting cell behind searchbar masks part of search bar. // So we insert mask *behind* the searchbar. self.view.insertSubview(maskingView, belowSubview: searchBar) let cellRect = self.collectionView.convert(cell.frame, to: self.view) - maskingView.configureShapeLayerBlock = { layer, bounds in + maskingView.configureShapeLayer = { layer, bounds in let path = UIBezierPath(rect: bounds) path.append(UIBezierPath(rect: cellRect)) @@ -347,7 +350,7 @@ class GifPickerViewController: OWSViewController, UISearchBarDelegate, UICollect } public func getFileForCell(_ cell: GifPickerCell) { - GiphyDownloader.giphyDownloader.cancelAllRequests() + dependencies[singleton: .giphyDownloader].cancelAllRequests() cell .requestRenditionForSending() @@ -374,14 +377,14 @@ class GifPickerViewController: OWSViewController, UISearchBarDelegate, UICollect self?.present(modal, animated: true) } }, - receiveValue: { [weak self] asset in + receiveValue: { [weak self, dependencies] asset in guard let rendition = asset.assetDescription as? GiphyRendition else { - Log.error("[GifPickerViewController] Invalid asset description.") + Log.error(.giphy, "ViewController invalid asset description.") return } - let dataSource = DataSourcePath(filePath: asset.filePath, shouldDeleteOnDeinit: false) - let attachment = SignalAttachment.attachment(dataSource: dataSource, type: rendition.type, imageQuality: .medium) + let dataSource = DataSourcePath(filePath: asset.filePath, shouldDeleteOnDeinit: false, using: dependencies) + let attachment = SignalAttachment.attachment(dataSource: dataSource, type: rendition.type, imageQuality: .medium, using: dependencies) self?.dismiss(animated: true) { // Delegate presents view controllers, so it's important that *this* controller be dismissed before that occurs. @@ -394,7 +397,7 @@ class GifPickerViewController: OWSViewController, UISearchBarDelegate, UICollect public func collectionView(_ collectionView: UICollectionView, willDisplay cell: UICollectionViewCell, forItemAt indexPath: IndexPath) { guard let cell = cell as? GifPickerCell else { - Log.error("[GifPickerViewController] unexpected cell.") + Log.error(.giphy, "ViewController unexpected cell.") return } // We only want to load the cells which are on-screen. @@ -403,7 +406,7 @@ class GifPickerViewController: OWSViewController, UISearchBarDelegate, UICollect public func collectionView(_ collectionView: UICollectionView, didEndDisplaying cell: UICollectionViewCell, forItemAt indexPath: IndexPath) { guard let cell = cell as? GifPickerCell else { - Log.error("[GifPickerViewController] unexpected cell.") + Log.error(.giphy, "ViewController unexpected cell.") return } cell.isCellVisible = false @@ -427,7 +430,7 @@ class GifPickerViewController: OWSViewController, UISearchBarDelegate, UICollect progressiveSearchTimer?.invalidate() progressiveSearchTimer = nil let kProgressiveSearchDelaySeconds = 1.0 - progressiveSearchTimer = WeakTimer.scheduledTimer(timeInterval: kProgressiveSearchDelaySeconds, target: self, userInfo: nil, repeats: true) { [weak self] _ in + progressiveSearchTimer = Timer.scheduledTimerOnMainThread(withTimeInterval: kProgressiveSearchDelaySeconds, repeats: true, using: dependencies) { [weak self] _ in self?.tryToSearch() } } @@ -460,7 +463,7 @@ class GifPickerViewController: OWSViewController, UISearchBarDelegate, UICollect let query: String = text.trimmingCharacters(in: .whitespacesAndNewlines) if (viewMode == .searching || viewMode == .results) && lastQuery == query { - Log.debug("[GifPickerViewController] ignoring duplicate search: \(query)") + Log.debug(.giphy, "ViewController ignoring duplicate search: \(query)") return } @@ -485,18 +488,18 @@ class GifPickerViewController: OWSViewController, UISearchBarDelegate, UICollect case .finished: break case .failure(let error): // Don't both showing error UI feedback for default "trending" results. - Log.error("[GifPickerViewController] error: \(error)") + Log.error(.giphy, "ViewController error: \(error)") } }, receiveValue: { [weak self] imageInfos in - Log.debug("[GifPickerViewController] showing trending") + Log.debug(.giphy, "ViewController showing trending") if imageInfos.count > 0 { self?.imageInfos = imageInfos self?.viewMode = .results } else { - Log.error("[GifPickerViewController] trending results was unexpectedly empty") + Log.error(.giphy, "ViewController trending results was unexpectedly empty") } } ) @@ -504,7 +507,7 @@ class GifPickerViewController: OWSViewController, UISearchBarDelegate, UICollect } private func search(query: String) { - Log.verbose("[GifPickerViewController] searching: \(query)") + Log.verbose(.giphy, "ViewController searching: \(query)") progressiveSearchTimer?.invalidate() progressiveSearchTimer = nil @@ -522,13 +525,13 @@ class GifPickerViewController: OWSViewController, UISearchBarDelegate, UICollect switch result { case .finished: break case .failure: - Log.verbose("[GifPickerViewController] search failed.") + Log.verbose(.giphy, "ViewController search failed.") // TODO: Present this error to the user. self?.viewMode = .error } }, receiveValue: { [weak self] imageInfos in - Log.verbose("[GifPickerViewController] search complete") + Log.verbose(.giphy, "ViewController search complete") self?.imageInfos = imageInfos if imageInfos.count > 0 { diff --git a/Session/Media Viewing & Editing/GIFs/GiphyAPI.swift b/Session/Media Viewing & Editing/GIFs/GiphyAPI.swift index 96851195fb2..2e9b72936b2 100644 --- a/Session/Media Viewing & Editing/GIFs/GiphyAPI.swift +++ b/Session/Media Viewing & Editing/GIFs/GiphyAPI.swift @@ -5,8 +5,31 @@ import Combine import CoreServices import UniformTypeIdentifiers import SignalUtilitiesKit +import SessionSnodeKit import SessionUtilitiesKit +// MARK: - Singleton + +public extension Singleton { + static let giphyDownloader: SingletonConfig = Dependencies.create( + identifier: "giphyDownloader", + createInstance: { dependencies in + ProxiedContentDownloader( + downloadFolderName: "GIFs", // stringlint:ignore + using: dependencies + ) + } + ) +} + +// MARK: - Log.Category + +public extension Log.Category { + static let giphy: Log.Category = .create("Giphy", defaultLevel: .info) +} + +// MARK: - GiphyFormat + // There's no UTI type for webp! enum GiphyFormat { case gif, mp4, jpg @@ -34,12 +57,14 @@ class GiphyRendition: ProxiedContentAssetDescription { let height: UInt let fileSize: UInt - init?(format: GiphyFormat, - name: String, - width: UInt, - height: UInt, - fileSize: UInt, - url: NSURL) { + init?( + format: GiphyFormat, + name: String, + width: UInt, + height: UInt, + fileSize: UInt, + url: NSURL + ) { self.format = format self.name = name self.width = width @@ -75,7 +100,19 @@ class GiphyRendition: ProxiedContentAssetDescription { } public func log() { - Log.verbose("[GiphyRendition] \t \(format), \(name), \(width), \(height), \(fileSize)") + Log.verbose(.giphy, "\t \(format), \(name), \(width), \(height), \(fileSize)") + } + + public static func == (lhs: GiphyRendition, rhs: GiphyRendition) -> Bool { + return ( + lhs.url == rhs.url && + lhs.fileExtension == rhs.fileExtension && + lhs.format == rhs.format && + lhs.name == rhs.name && + lhs.width == rhs.width && + lhs.height == rhs.height && + lhs.fileSize == rhs.fileSize + ) } } @@ -107,7 +144,7 @@ class GiphyImageInfo: NSObject { } public func log() { - Log.verbose("[GiphyImageInfo] giphyId: \(giphyId), \(renditions.count)") + Log.verbose(.giphy, "GiphyId: \(giphyId), \(renditions.count)") for rendition in renditions { rendition.log() } @@ -284,16 +321,16 @@ enum GiphyAPI { return urlSession .dataTaskPublisher(for: url) .mapError { urlError in - Log.verbose("[GiphyAPI] Search request failed: \(urlError)") + Log.verbose(.giphy, "Search request failed: \(urlError)") // URLError codes are negative values return NetworkError.unknown } .map { data, _ in - Log.verbose("[GiphyAPI] Search request succeeded") + Log.verbose(.giphy, "Search request succeeded") guard let imageInfos = self.parseGiphyImages(responseData: data) else { - Log.error("[GiphyAPI] Unable to parse trending images") + Log.error(.giphy, "Unable to parse trending images") return [] } @@ -325,7 +362,7 @@ enum GiphyAPI { var request: URLRequest = URLRequest(url: url) guard ContentProxy.configureProxiedRequest(request: &request) else { - SNLog("Could not configure query: \(query).") + Log.error(.giphy, "Could not configure query: \(query).") return Fail(error: NetworkError.invalidPreparedRequest) .eraseToAnyPublisher() } @@ -333,13 +370,13 @@ enum GiphyAPI { return urlSession .dataTaskPublisher(for: request) .mapError { urlError in - Log.error("[GiphyAPI] Search request failed: \(urlError)") + Log.error(.giphy, "Search request failed: \(urlError)") // URLError codes are negative values return NetworkError.unknown } .tryMap { data, _ -> [GiphyImageInfo] in - Log.verbose("[GiphyAPI] Search request succeeded") + Log.verbose(.giphy, "Search request succeeded") guard let imageInfos = self.parseGiphyImages(responseData: data) else { throw NetworkError.invalidResponse @@ -355,16 +392,16 @@ enum GiphyAPI { // stringlint:ignore_contents private static func parseGiphyImages(responseData: Data?) -> [GiphyImageInfo]? { guard let responseData: Data = responseData else { - Log.error("[GiphyAPI] Missing response.") + Log.error(.giphy, "Missing response.") return nil } guard let responseDict: [String: Any] = try? JSONSerialization .jsonObject(with: responseData, options: [ .fragmentsAllowed ]) as? [String: Any] else { - Log.error("[GiphyAPI] Invalid response.") + Log.error(.giphy, "Invalid response.") return nil } guard let imageDicts = responseDict["data"] as? [[String: Any]] else { - Log.error("[GiphyAPI] Invalid response data.") + Log.error(.giphy, "Invalid response data.") return nil } return imageDicts.compactMap { imageDict in @@ -376,21 +413,21 @@ enum GiphyAPI { // stringlint:ignore_contents private static func parseGiphyImage(imageDict: [String: Any]) -> GiphyImageInfo? { guard let giphyId = imageDict["id"] as? String else { - Log.warn("[GiphyAPI] Image dict missing id.") + Log.warn(.giphy, "Image dict missing id.") return nil } guard giphyId.count > 0 else { - Log.warn("[GiphyAPI] Image dict has invalid id.") + Log.warn(.giphy, "Image dict has invalid id.") return nil } guard let renditionDicts = imageDict["images"] as? [String: Any] else { - Log.warn("[GiphyAPI] Image dict missing renditions.") + Log.warn(.giphy, "Image dict missing renditions.") return nil } var renditions = [GiphyRendition]() for (renditionName, renditionDict) in renditionDicts { guard let renditionDict = renditionDict as? [String: Any] else { - Log.warn("[GiphyAPI] Invalid rendition dict.") + Log.warn(.giphy, "Invalid rendition dict.") continue } guard let rendition = parseGiphyRendition(renditionName: renditionName, @@ -400,12 +437,12 @@ enum GiphyAPI { renditions.append(rendition) } guard renditions.count > 0 else { - Log.warn("[GiphyAPI] Image has no valid renditions.") + Log.warn(.giphy, "Image has no valid renditions.") return nil } guard let originalRendition = findOriginalRendition(renditions: renditions) else { - Log.warn("[GiphyAPI] Image has no original rendition.") + Log.warn(.giphy, "Image has no original rendition.") return nil } @@ -444,15 +481,15 @@ enum GiphyAPI { return nil } guard urlString.count > 0 else { - Log.warn("[GiphyAPI] Rendition has invalid url.") + Log.warn(.giphy, "Rendition has invalid url.") return nil } guard let url = NSURL(string: urlString) else { - Log.warn("[GiphyAPI] Rendition url could not be parsed.") + Log.warn(.giphy, "Rendition url could not be parsed.") return nil } guard let fileExtension = url.pathExtension?.lowercased() else { - Log.warn("[GiphyAPI] Rendition url missing file extension.") + Log.warn(.giphy, "Rendition url missing file extension.") return nil } var format = GiphyFormat.gif @@ -465,7 +502,7 @@ enum GiphyAPI { } else if fileExtension == "webp" { return nil } else { - Log.warn("[GiphyAPI] Invalid file extension: \(fileExtension).") + Log.warn(.giphy, "Invalid file extension: \(fileExtension).") return nil } @@ -490,7 +527,7 @@ enum GiphyAPI { return nil } guard parsedValue > 0 else { - Log.verbose("[GiphyAPI] \(typeName) has non-positive \(key): \(parsedValue).") + Log.verbose(.giphy, "\(typeName) has non-positive \(key): \(parsedValue).") return nil } return parsedValue diff --git a/Session/Media Viewing & Editing/GIFs/GiphyDownloader.swift b/Session/Media Viewing & Editing/GIFs/GiphyDownloader.swift deleted file mode 100644 index d215aab9b30..00000000000 --- a/Session/Media Viewing & Editing/GIFs/GiphyDownloader.swift +++ /dev/null @@ -1,14 +0,0 @@ -// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved. -// -// stringlint:disable - -import Foundation -import SignalUtilitiesKit -import SessionUtilitiesKit - -public class GiphyDownloader: ProxiedContentDownloader { - - // MARK: - Properties - - public static let giphyDownloader = GiphyDownloader(downloadFolderName: "GIFs") -} diff --git a/Session/Media Viewing & Editing/ImagePickerController.swift b/Session/Media Viewing & Editing/ImagePickerController.swift index 1c4272013de..2d09d864db2 100644 --- a/Session/Media Viewing & Editing/ImagePickerController.swift +++ b/Session/Media Viewing & Editing/ImagePickerController.swift @@ -6,6 +6,7 @@ import Photos import PhotosUI import SessionUIKit import SignalUtilitiesKit +import SessionMessagingKit import SessionUtilitiesKit protocol ImagePickerGridControllerDelegate: AnyObject { @@ -25,6 +26,7 @@ class ImagePickerGridController: UICollectionViewController, PhotoLibraryDelegat weak var delegate: ImagePickerGridControllerDelegate? + private let dependencies: Dependencies private let library: PhotoLibrary = PhotoLibrary() private var photoCollection: PhotoCollection private var photoCollectionContents: PhotoCollectionContents @@ -34,7 +36,8 @@ class ImagePickerGridController: UICollectionViewController, PhotoLibraryDelegat var collectionViewFlowLayout: UICollectionViewFlowLayout var titleView: TitleView! - init() { + init(using dependencies: Dependencies) { + self.dependencies = dependencies collectionViewFlowLayout = type(of: self).buildLayout() photoCollection = library.defaultPhotoCollection() photoCollectionContents = photoCollection.contents() @@ -186,7 +189,7 @@ class ImagePickerGridController: UICollectionViewController, PhotoLibraryDelegat delegate.imagePicker( self, didSelectAsset: asset, - attachmentPublisher: photoCollectionContents.outgoingAttachment(for: asset) + attachmentPublisher: photoCollectionContents.outgoingAttachment(for: asset, using: dependencies) ) collectionView.selectItem(at: indexPath, animated: true, scrollPosition: []) case .deselect: @@ -395,7 +398,7 @@ class ImagePickerGridController: UICollectionViewController, PhotoLibraryDelegat var isShowingCollectionPickerController: Bool = false lazy var collectionPickerController: SessionTableViewController = SessionTableViewController( - viewModel: PhotoCollectionPickerViewModel(library: library) { [weak self] collection in + viewModel: PhotoCollectionPickerViewModel(library: library, using: dependencies) { [weak self] collection in guard self?.photoCollection != collection else { self?.hideCollectionPicker() return @@ -488,7 +491,7 @@ class ImagePickerGridController: UICollectionViewController, PhotoLibraryDelegat delegate.imagePicker( self, didSelectAsset: asset, - attachmentPublisher: photoCollectionContents.outgoingAttachment(for: asset) + attachmentPublisher: photoCollectionContents.outgoingAttachment(for: asset, using: dependencies) ) firstSelectedIndexPath = nil diff --git a/Session/Media Viewing & Editing/MediaDetailViewController.swift b/Session/Media Viewing & Editing/MediaDetailViewController.swift index 19143eea2ff..2c644c7c647 100644 --- a/Session/Media Viewing & Editing/MediaDetailViewController.swift +++ b/Session/Media Viewing & Editing/MediaDetailViewController.swift @@ -15,6 +15,7 @@ public enum MediaGalleryOption { } class MediaDetailViewController: OWSViewController, UIScrollViewDelegate { + private let dependencies: Dependencies public let galleryItem: MediaGalleryViewModel.Item public weak var delegate: MediaDetailViewControllerDelegate? private var image: UIImage? @@ -56,8 +57,10 @@ class MediaDetailViewController: OWSViewController, UIScrollViewDelegate { init( galleryItem: MediaGalleryViewModel.Item, - delegate: MediaDetailViewControllerDelegate? = nil + delegate: MediaDetailViewControllerDelegate? = nil, + using dependencies: Dependencies ) { + self.dependencies = dependencies self.galleryItem = galleryItem self.delegate = delegate @@ -66,6 +69,7 @@ class MediaDetailViewController: OWSViewController, UIScrollViewDelegate { // We cache the image data in case the attachment stream is deleted. galleryItem.attachment.thumbnail( size: .large, + using: dependencies, success: { [weak self] image, _ in // Only reload the content if the view has already loaded (if it // hasn't then it'll load with the image immediately) @@ -199,7 +203,7 @@ class MediaDetailViewController: OWSViewController, UIScrollViewDelegate { self.scrollView.zoomScale = 1 if self.galleryItem.attachment.isAnimated { - if self.galleryItem.attachment.isValid, let originalFilePath: String = self.galleryItem.attachment.originalFilePath { + if self.galleryItem.attachment.isValid, let originalFilePath: String = self.galleryItem.attachment.originalFilePath(using: dependencies) { let animatedView: YYAnimatedImageView = YYAnimatedImageView() animatedView.autoPlayAnimatedImage = false animatedView.image = YYImage(contentsOfFile: originalFilePath) @@ -344,8 +348,8 @@ class MediaDetailViewController: OWSViewController, UIScrollViewDelegate { @objc public func playVideo() { guard - let originalFilePath: String = self.galleryItem.attachment.originalFilePath, - FileManager.default.fileExists(atPath: originalFilePath) + let originalFilePath: String = self.galleryItem.attachment.originalFilePath(using: dependencies), + dependencies[singleton: .fileManager].fileExists(atPath: originalFilePath) else { return SNLog("Missing video file") } let videoUrl: URL = URL(fileURLWithPath: originalFilePath) diff --git a/Session/Media Viewing & Editing/MediaGalleryNavigationController.swift b/Session/Media Viewing & Editing/MediaGalleryNavigationController.swift index ebe751962e9..0e6f79ea948 100644 --- a/Session/Media Viewing & Editing/MediaGalleryNavigationController.swift +++ b/Session/Media Viewing & Editing/MediaGalleryNavigationController.swift @@ -30,7 +30,7 @@ class MediaGalleryNavigationController: UINavigationController { view.themeBackgroundColor = .newConversation_background - // Insert a view to ensure the nav bar colour goes to the top of the screen + // Insert a view to ensure the nav bar color goes to the top of the screen relayoutBackgroundView() } diff --git a/Session/Media Viewing & Editing/MediaGalleryViewModel.swift b/Session/Media Viewing & Editing/MediaGalleryViewModel.swift index 9ded18540f1..811e053bce7 100644 --- a/Session/Media Viewing & Editing/MediaGalleryViewModel.swift +++ b/Session/Media Viewing & Editing/MediaGalleryViewModel.swift @@ -4,6 +4,7 @@ import Foundation import GRDB import DifferenceKit import SignalUtilitiesKit +import SessionMessagingKit import SessionUtilitiesKit import SwiftUI @@ -27,6 +28,7 @@ public class MediaGalleryViewModel { // MARK: - Variables + public let dependencies: Dependencies public let threadId: String public let threadVariant: SessionThread.Variant private var focusedAttachmentId: String? @@ -72,8 +74,10 @@ public class MediaGalleryViewModel { mediaType: MediaType, pageSize: Int = 1, focusedAttachmentId: String? = nil, - performInitialQuerySync: Bool = false + performInitialQuerySync: Bool = false, + using dependencies: Dependencies ) { + self.dependencies = dependencies self.threadId = threadId self.threadVariant = threadVariant self.focusedAttachmentId = focusedAttachmentId @@ -109,7 +113,8 @@ public class MediaGalleryViewModel { self?.unobservedGalleryDataChanges = updatedData } ) - } + }, + using: dependencies ) // Run the initial query on a backgorund thread so we don't block the push transition @@ -350,8 +355,8 @@ public class MediaGalleryViewModel { return Item.baseQuery(orderSQL: orderSQL, customFilters: customFilters)([]) } - func thumbnailImage(async: @escaping (UIImage) -> ()) { - attachment.thumbnail(size: .small, success: { image, _ in async(image) }, failure: {}) + func thumbnailImage(using dependencies: Dependencies, async: @escaping (UIImage) -> ()) { + attachment.thumbnail(size: .small, using: dependencies, success: { image, _ in async(image) }, failure: {}) } } @@ -398,7 +403,7 @@ public class MediaGalleryViewModel { // Note: It's possible we already have cached album data for this interaction // but to avoid displaying stale data we re-fetch from the database anyway - let maybeAlbumInfo: AlbumInfo? = Storage.shared.read { db -> AlbumInfo in + let maybeAlbumInfo: AlbumInfo? = dependencies[singleton: .storage].read { db -> AlbumInfo in let attachment: TypedTableAlias = TypedTableAlias() let interaction: TypedTableAlias = TypedTableAlias() let interactionAttachment: TypedTableAlias = TypedTableAlias() @@ -534,7 +539,8 @@ public class MediaGalleryViewModel { interactionId: Int64, selectedAttachmentId: String, options: [MediaGalleryOption], - useTransitioningDelegate: Bool = true + useTransitioningDelegate: Bool = true, + using dependencies: Dependencies ) -> UIViewController? { // Load the data for the album immediately (needed before pushing to the screen so // transitions work nicely) @@ -542,7 +548,8 @@ public class MediaGalleryViewModel { threadId: threadId, threadVariant: threadVariant, isPagedData: false, - mediaType: .media + mediaType: .media, + using: dependencies ) viewModel.loadAndCacheAlbumData(for: interactionId, in: threadId) viewModel.replaceAlbumObservation(toObservationFor: interactionId) @@ -574,7 +581,8 @@ public class MediaGalleryViewModel { threadId: String, threadVariant: SessionThread.Variant, focusedAttachmentId: String?, - performInitialQuerySync: Bool = false + performInitialQuerySync: Bool = false, + using dependencies: Dependencies ) -> MediaTileViewController { let viewModel: MediaGalleryViewModel = MediaGalleryViewModel( threadId: threadId, @@ -583,11 +591,13 @@ public class MediaGalleryViewModel { mediaType: .media, pageSize: MediaTileViewController.itemPageSize, focusedAttachmentId: focusedAttachmentId, - performInitialQuerySync: performInitialQuerySync + performInitialQuerySync: performInitialQuerySync, + using: dependencies ) return MediaTileViewController( - viewModel: viewModel + viewModel: viewModel, + using: dependencies ) } @@ -595,7 +605,8 @@ public class MediaGalleryViewModel { threadId: String, threadVariant: SessionThread.Variant, focusedAttachmentId: String?, - performInitialQuerySync: Bool = false + performInitialQuerySync: Bool = false, + using dependencies: Dependencies ) -> DocumentTileViewController { let viewModel: MediaGalleryViewModel = MediaGalleryViewModel( threadId: threadId, @@ -604,11 +615,13 @@ public class MediaGalleryViewModel { mediaType: .document, pageSize: MediaTileViewController.itemPageSize, focusedAttachmentId: focusedAttachmentId, - performInitialQuerySync: performInitialQuerySync + performInitialQuerySync: performInitialQuerySync, + using: dependencies ) return DocumentTileViewController( - viewModel: viewModel + viewModel: viewModel, + using: dependencies ) } @@ -616,25 +629,29 @@ public class MediaGalleryViewModel { threadId: String, threadVariant: SessionThread.Variant, focusedAttachmentId: String?, - performInitialQuerySync: Bool = false + performInitialQuerySync: Bool = false, + using dependencies: Dependencies ) -> AllMediaViewController { let mediaTitleViewController = createMediaTileViewController( threadId: threadId, threadVariant: threadVariant, focusedAttachmentId: focusedAttachmentId, - performInitialQuerySync: performInitialQuerySync + performInitialQuerySync: performInitialQuerySync, + using: dependencies ) let documentTitleViewController = createDocumentTitleViewController( threadId: threadId, threadVariant: threadVariant, focusedAttachmentId: focusedAttachmentId, - performInitialQuerySync: performInitialQuerySync + performInitialQuerySync: performInitialQuerySync, + using: dependencies ) return AllMediaViewController( mediaTitleViewController: mediaTitleViewController, - documentTitleViewController: documentTitleViewController + documentTitleViewController: documentTitleViewController, + using: dependencies ) } } diff --git a/Session/Media Viewing & Editing/MediaPageViewController.swift b/Session/Media Viewing & Editing/MediaPageViewController.swift index f5078835d2e..897ef6ba36d 100644 --- a/Session/Media Viewing & Editing/MediaPageViewController.swift +++ b/Session/Media Viewing & Editing/MediaPageViewController.swift @@ -67,7 +67,7 @@ class MediaPageViewController: UIPageViewController, UIPageViewControllerDataSou self.viewModel = viewModel self.showAllMediaButton = options.contains(.showAllMediaButton) self.sliderEnabled = options.contains(.sliderEnabled) - self.initialPage = MediaDetailViewController(galleryItem: initialItem) + self.initialPage = MediaDetailViewController(galleryItem: initialItem, using: viewModel.dependencies) super.init( transitionStyle: .scroll, @@ -127,7 +127,7 @@ class MediaPageViewController: UIPageViewController, UIPageViewControllerDataSou // Navigation - let backButton = UIViewController.createOWSBackButton(target: self, selector: #selector(didPressDismissButton)) + let backButton = UIViewController.createOWSBackButton(target: self, selector: #selector(didPressDismissButton), using: viewModel.dependencies) self.navigationItem.leftBarButtonItem = backButton self.navigationItem.titleView = portraitHeaderView @@ -375,6 +375,7 @@ class MediaPageViewController: UIPageViewController, UIPageViewControllerDataSou galleryRailView.configureCellViews( album: (self.viewModel.albumData[item.interactionId] ?? []), focusedItem: currentItem, + using: viewModel.dependencies, cellViewBuilder: { _ in return GalleryRailCellView() } ) } @@ -385,7 +386,7 @@ class MediaPageViewController: UIPageViewController, UIPageViewControllerDataSou guard dataChangeObservable == nil else { return } // Start observing for data changes - dataChangeObservable = Storage.shared.start( + dataChangeObservable = viewModel.dependencies[singleton: .storage].start( viewModel.observableAlbumData, onError: { _ in }, onChange: { [weak self] albumData in @@ -487,7 +488,8 @@ class MediaPageViewController: UIPageViewController, UIPageViewControllerDataSou threadId: self.viewModel.threadId, threadVariant: self.viewModel.threadVariant, focusedAttachmentId: currentItem.attachment.id, - performInitialQuerySync: true + performInitialQuerySync: true, + using: viewModel.dependencies ) let navController: MediaGalleryNavigationController = MediaGalleryNavigationController() @@ -508,12 +510,12 @@ class MediaPageViewController: UIPageViewController, UIPageViewControllerDataSou @objc public func didPressShare(_ sender: Any) { share() } - public func share(using dependencies: Dependencies = Dependencies()) { + public func share() { guard let currentViewController = self.viewControllers?[0] as? MediaDetailViewController else { Log.error("[MediaPageViewController] currentViewController was unexpectedly nil") return } - guard let originalFilePath: String = currentViewController.galleryItem.attachment.originalFilePath else { + guard let originalFilePath: String = currentViewController.galleryItem.attachment.originalFilePath(using: viewModel.dependencies) else { return } @@ -526,7 +528,7 @@ class MediaPageViewController: UIPageViewController, UIPageViewControllerDataSou shareVC.popoverPresentationController?.sourceRect = self.view.bounds } - shareVC.completionWithItemsHandler = { activityType, completed, returnedItems, activityError in + shareVC.completionWithItemsHandler = { [dependencies = viewModel.dependencies] activityType, completed, returnedItems, activityError in if let activityError = activityError { Log.error("[MediaPageViewController] Failed to share with activityError: \(activityError)") } @@ -544,14 +546,14 @@ class MediaPageViewController: UIPageViewController, UIPageViewControllerDataSou let threadId: String = self.viewModel.threadId let threadVariant: SessionThread.Variant = self.viewModel.threadVariant - Storage.shared.write { db in + dependencies[singleton: .storage].write { db in try MessageSender.send( db, message: DataExtractionNotification( kind: .mediaSaved( timestamp: UInt64(currentViewController.galleryItem.interactionTimestampMs) ), - sentTimestamp: UInt64(SnodeAPI.currentOffsetTimestampMs()) + sentTimestampMs: dependencies[cache: .snodeAPI].currentOffsetTimestampMs() ) .with(DisappearingMessagesConfiguration .fetchOne(db, id: threadId)? @@ -573,14 +575,14 @@ class MediaPageViewController: UIPageViewController, UIPageViewControllerDataSou let deleteAction = UIAlertAction( title: "clearMessagesForMe".localized(), style: .destructive - ) { _ in - Storage.shared.writeAsync { db in + ) { [dependencies = viewModel.dependencies] _ in + dependencies[singleton: .storage].writeAsync { db in _ = try Attachment .filter(id: itemToDelete.attachment.id) .deleteAll(db) // Add the garbage collection job to delete orphaned attachment files - JobRunner.add( + dependencies[singleton: .jobRunner].add( db, job: Job( variant: .garbageCollection, @@ -588,7 +590,8 @@ class MediaPageViewController: UIPageViewController, UIPageViewControllerDataSou details: GarbageCollectionJob.Details( typesToCollect: [.orphanedAttachmentFiles] ) - ) + ), + canStartJob: true ) // Delete any interactions which had all of their attachments removed @@ -767,7 +770,7 @@ class MediaPageViewController: UIPageViewController, UIPageViewControllerDataSou } cachedPages[galleryItem.interactionId] = (cachedPages[galleryItem.interactionId] ?? [:]) - .setting(galleryItem, MediaDetailViewController(galleryItem: galleryItem, delegate: self)) + .setting(galleryItem, MediaDetailViewController(galleryItem: galleryItem, delegate: self, using: viewModel.dependencies)) return cachedPages[galleryItem.interactionId]?[galleryItem] } @@ -866,12 +869,13 @@ class MediaPageViewController: UIPageViewController, UIPageViewControllerDataSou let name: String = { switch targetItem.interactionVariant { case .standardIncoming: - return Storage.shared - .read { db in + return viewModel.dependencies[singleton: .storage] + .read { [dependencies = viewModel.dependencies] db in Profile.displayName( db, id: targetItem.interactionAuthorId, - threadVariant: threadVariant + threadVariant: threadVariant, + using: dependencies ) } .defaulting(to: Profile.truncated(id: targetItem.interactionAuthorId, truncating: .middle)) @@ -908,11 +912,11 @@ class MediaPageViewController: UIPageViewController, UIPageViewControllerDataSou } extension MediaGalleryViewModel.Item: GalleryRailItem { - public func buildRailItemView() -> UIView { + public func buildRailItemView(using dependencies: Dependencies) -> UIView { let imageView: UIImageView = UIImageView() imageView.contentMode = .scaleAspectFill - self.thumbnailImage { [weak imageView] image in + self.thumbnailImage(using: dependencies) { [weak imageView] image in DispatchQueue.main.async { imageView?.image = image } @@ -977,14 +981,14 @@ extension MediaPageViewController: UIViewControllerTransitioningDelegate { public func animationController(forPresented presented: UIViewController, presenting: UIViewController, source: UIViewController) -> UIViewControllerAnimatedTransitioning? { guard self == presented || self.navigationController == presented else { return nil } - return MediaZoomAnimationController(galleryItem: currentItem) + return MediaZoomAnimationController(galleryItem: currentItem, using: viewModel.dependencies) } public func animationController(forDismissed dismissed: UIViewController) -> UIViewControllerAnimatedTransitioning? { guard self == dismissed || self.navigationController == dismissed else { return nil } guard !self.viewModel.albumData.isEmpty else { return nil } - let animationController = MediaDismissAnimationController(galleryItem: currentItem, interactionController: mediaInteractiveDismiss) + let animationController = MediaDismissAnimationController(galleryItem: currentItem, interactionController: mediaInteractiveDismiss, using: viewModel.dependencies) mediaInteractiveDismiss?.interactiveDismissDelegate = animationController return animationController diff --git a/Session/Media Viewing & Editing/MediaTileViewController.swift b/Session/Media Viewing & Editing/MediaTileViewController.swift index c25ac30e641..992c061fefd 100644 --- a/Session/Media Viewing & Editing/MediaTileViewController.swift +++ b/Session/Media Viewing & Editing/MediaTileViewController.swift @@ -6,6 +6,7 @@ import GRDB import DifferenceKit import SessionUIKit import SignalUtilitiesKit +import SessionMessagingKit import SessionUtilitiesKit public class MediaTileViewController: UIViewController, UICollectionViewDataSource, UICollectionViewDelegate, UICollectionViewDelegateFlowLayout { @@ -18,6 +19,7 @@ public class MediaTileViewController: UIViewController, UICollectionViewDataSour static let footerBarHeight: CGFloat = 40 static let loadMoreHeaderHeight: CGFloat = 100 + private let dependencies: Dependencies public let viewModel: MediaGalleryViewModel private var hasLoadedInitialData: Bool = false private var didFinishInitialLayout: Bool = false @@ -36,9 +38,10 @@ public class MediaTileViewController: UIViewController, UICollectionViewDataSour // MARK: - Initialization - init(viewModel: MediaGalleryViewModel) { + init(viewModel: MediaGalleryViewModel, using dependencies: Dependencies) { + self.dependencies = dependencies self.viewModel = viewModel - Storage.shared.addObserver(viewModel.pagedDataObserver) + dependencies[singleton: .storage].addObserver(viewModel.pagedDataObserver) super.init(nibName: nil, bundle: nil) } @@ -126,7 +129,7 @@ public class MediaTileViewController: UIViewController, UICollectionViewDataSour // Add a custom back button if this is the only view controller if self.navigationController?.viewControllers.first == self { - let backButton = UIViewController.createOWSBackButton(target: self, selector: #selector(didPressDismissButton)) + let backButton = UIViewController.createOWSBackButton(target: self, selector: #selector(didPressDismissButton), using: dependencies) self.navigationItem.leftBarButtonItem = backButton } @@ -451,7 +454,8 @@ public class MediaTileViewController: UIViewController, UICollectionViewDataSour let cell: PhotoGridViewCell = collectionView.dequeue(type: PhotoGridViewCell.self, for: indexPath) cell.configure( item: GalleryGridCellItem( - galleryItem: section.elements[indexPath.row] + galleryItem: section.elements[indexPath.row], + using: dependencies ) ) @@ -544,7 +548,8 @@ public class MediaTileViewController: UIViewController, UICollectionViewDataSour threadVariant: self.viewModel.threadVariant, interactionId: galleryItem.interactionId, selectedAttachmentId: galleryItem.attachment.id, - options: [ .sliderEnabled ] + options: [ .sliderEnabled ], + using: dependencies ) guard let detailViewController: UIViewController = detailViewController else { return } @@ -684,8 +689,8 @@ public class MediaTileViewController: UIViewController, UICollectionViewDataSour .putNumber(indexPaths.count) .localized() - let deleteAction = UIAlertAction(title: confirmationTitle, style: .destructive) { [weak self] _ in - Storage.shared.writeAsync { db in + let deleteAction = UIAlertAction(title: confirmationTitle, style: .destructive) { [weak self, dependencies = viewModel.dependencies] _ in + dependencies[singleton: .storage].writeAsync { db in let interactionIds: Set = items .map { $0.interactionId } .asSet() @@ -695,7 +700,7 @@ public class MediaTileViewController: UIViewController, UICollectionViewDataSour .deleteAll(db) // Add the garbage collection job to delete orphaned attachment files - JobRunner.add( + dependencies[singleton: .jobRunner].add( db, job: Job( variant: .garbageCollection, @@ -703,7 +708,8 @@ public class MediaTileViewController: UIViewController, UICollectionViewDataSour details: GarbageCollectionJob.Details( typesToCollect: [.orphanedAttachmentFiles] ) - ) + ), + canStartJob: true ) // Delete any interactions which had all of their attachments removed @@ -838,9 +844,11 @@ private class MediaGalleryStaticHeader: UICollectionViewCell { } class GalleryGridCellItem: PhotoGridItem { + private let dependencies: Dependencies let galleryItem: MediaGalleryViewModel.Item - init(galleryItem: MediaGalleryViewModel.Item) { + init(galleryItem: MediaGalleryViewModel.Item, using dependencies: Dependencies) { + self.dependencies = dependencies self.galleryItem = galleryItem } @@ -857,7 +865,7 @@ class GalleryGridCellItem: PhotoGridItem { } func asyncThumbnail(completion: @escaping (UIImage?) -> Void) { - galleryItem.thumbnailImage(async: completion) + galleryItem.thumbnailImage(using: dependencies, async: completion) } } @@ -874,7 +882,8 @@ extension MediaTileViewController: UIViewControllerTransitioningDelegate { guard let focusedIndexPath: IndexPath = self.viewModel.focusedIndexPath else { return nil } return MediaDismissAnimationController( - galleryItem: self.viewModel.galleryData[focusedIndexPath.section].elements[focusedIndexPath.item] + galleryItem: self.viewModel.galleryData[focusedIndexPath.section].elements[focusedIndexPath.item], + using: dependencies ) } @@ -889,7 +898,8 @@ extension MediaTileViewController: UIViewControllerTransitioningDelegate { return MediaZoomAnimationController( galleryItem: self.viewModel.galleryData[focusedIndexPath.section].elements[focusedIndexPath.item], - shouldBounce: false + shouldBounce: false, + using: dependencies ) } } @@ -898,7 +908,7 @@ extension MediaTileViewController: UIViewControllerTransitioningDelegate { extension MediaTileViewController: MediaPresentationContextProvider { func mediaPresentationContext(mediaItem: Media, in coordinateSpace: UICoordinateSpace) -> MediaPresentationContext? { - guard case let .gallery(galleryItem) = mediaItem else { return nil } + guard case let .gallery(galleryItem, _) = mediaItem else { return nil } // Note: According to Apple's docs the 'indexPathsForVisibleRows' method returns an // unsorted array which means we can't use it to determine the desired 'visibleCell' diff --git a/Session/Media Viewing & Editing/MessageInfoScreen.swift b/Session/Media Viewing & Editing/MessageInfoScreen.swift index 05749d057a6..b69a0af8026 100644 --- a/Session/Media Viewing & Editing/MessageInfoScreen.swift +++ b/Session/Media Viewing & Editing/MessageInfoScreen.swift @@ -15,6 +15,7 @@ struct MessageInfoScreen: View { var actions: [ContextMenuVC.Action] var messageViewModel: MessageViewModel + let dependencies: Dependencies var isMessageFailed: Bool { return [.failed, .failedToSync].contains(messageViewModel.state) } @@ -28,12 +29,13 @@ struct MessageInfoScreen: View { ) { // Message bubble snapshot MessageBubble( - messageViewModel: messageViewModel + messageViewModel: messageViewModel, + dependencies: dependencies ) .background( RoundedRectangle(cornerRadius: Self.cornerRadius) .fill( - themeColor: (messageViewModel.variant == .standardIncoming || messageViewModel.variant == .standardIncomingDeleted ? + themeColor: (messageViewModel.variant == .standardIncoming || messageViewModel.variant == .standardIncomingDeleted || messageViewModel.variant == .standardIncomingDeletedLocally ? .messageBubble_incomingBackground : .messageBubble_outgoingBackground) ) @@ -87,7 +89,8 @@ struct MessageInfoScreen: View { SessionCarouselView_SwiftUI( index: $index, isOutgoing: (messageViewModel.variant == .standardOutgoing), - contentInfos: attachments + contentInfos: attachments, + using: dependencies ) .frame( maxWidth: .infinity, @@ -99,7 +102,8 @@ struct MessageInfoScreen: View { attachment: attachments[0], isOutgoing: (messageViewModel.variant == .standardOutgoing), shouldSupressControls: true, - cornerRadius: 0 + cornerRadius: 0, + using: dependencies ) .frame( maxWidth: .infinity, @@ -239,9 +243,10 @@ struct MessageInfoScreen: View { size: .message, publicKey: messageViewModel.authorId, threadVariant: .contact, // Always show the display picture in 'contact' mode - customImageData: nil, + displayPictureFilename: nil, profile: messageViewModel.profile, - profileIcon: (messageViewModel.isSenderOpenGroupModerator ? .crown : .none) + profileIcon: (messageViewModel.isSenderOpenGroupModerator ? .crown : .none), + using: dependencies ) let size: ProfilePictureView.Size = .list @@ -357,7 +362,8 @@ struct MessageInfoScreen: View { interactionId: messageViewModel.id, selectedAttachmentId: attachment.id, options: [ .sliderEnabled ], - useTransitioningDelegate: false + useTransitioningDelegate: false, + using: dependencies ) { self.host.controller?.present(mediaGalleryView, animated: true) } @@ -375,6 +381,7 @@ struct MessageBubble: View { static private let inset: CGFloat = 12 let messageViewModel: MessageViewModel + let dependencies: Dependencies var bodyLabelTextColor: ThemeValue { messageViewModel.variant == .standardOutgoing ? @@ -398,7 +405,8 @@ struct MessageBubble: View { LinkPreviewView_SwiftUI( state: LinkPreview.SentState( linkPreview: linkPreview, - imageAttachment: messageViewModel.linkPreviewAttachment + imageAttachment: messageViewModel.linkPreviewAttachment, + using: dependencies ), isOutgoing: (messageViewModel.variant == .standardOutgoing), maxWidth: maxWidth, @@ -423,12 +431,13 @@ struct MessageBubble: View { authorId: quote.authorId, quotedText: quote.body, threadVariant: messageViewModel.threadVariant, - currentUserPublicKey: messageViewModel.currentUserPublicKey, - currentUserBlinded15PublicKey: messageViewModel.currentUserBlinded15PublicKey, - currentUserBlinded25PublicKey: messageViewModel.currentUserBlinded25PublicKey, + currentUserSessionId: messageViewModel.currentUserSessionId, + currentUserBlinded15SessionId: messageViewModel.currentUserBlinded15SessionId, + currentUserBlinded25SessionId: messageViewModel.currentUserBlinded25SessionId, direction: (messageViewModel.variant == .standardOutgoing ? .outgoing : .incoming), attachment: messageViewModel.quoteAttachment - ) + ), + using: dependencies ) .fixedSize(horizontal: false, vertical: true) .padding(.top, Self.inset) @@ -442,7 +451,8 @@ struct MessageBubble: View { theme: ThemeManager.currentTheme, primaryColor: ThemeManager.primaryColor, textColor: bodyLabelTextColor, - searchText: nil + searchText: nil, + using: dependencies ) { AttributedText(bodyText) .padding(.all, Self.inset) @@ -454,7 +464,8 @@ struct MessageBubble: View { theme: ThemeManager.currentTheme, primaryColor: ThemeManager.primaryColor, textColor: bodyLabelTextColor, - searchText: nil + searchText: nil, + using: dependencies ) { AttributedText(bodyText) .padding(.all, Self.inset) @@ -486,7 +497,8 @@ struct MessageBubble: View { theme: ThemeManager.currentTheme, primaryColor: ThemeManager.primaryColor, textColor: bodyLabelTextColor, - searchText: nil + searchText: nil, + using: dependencies ) { ZStack{ AttributedText(bodyText) @@ -533,10 +545,15 @@ struct InfoBlock: View where Content: View { } final class MessageInfoViewController: SessionHostingViewController { - init(actions: [ContextMenuVC.Action], messageViewModel: MessageViewModel) { + init( + actions: [ContextMenuVC.Action], + messageViewModel: MessageViewModel, + using dependencies: Dependencies + ) { let messageInfoView = MessageInfoScreen( actions: actions, - messageViewModel: messageViewModel + messageViewModel: messageViewModel, + dependencies: dependencies ) super.init(rootView: messageInfoView) @@ -556,6 +573,7 @@ final class MessageInfoViewController: SessionHostingViewController = TableDataState() public let observableState: ObservableTableSourceState = ObservableTableSourceState() @@ -23,8 +21,8 @@ class PhotoCollectionPickerViewModel: SessionTableViewModel, ObservableTableSour init( library: PhotoLibrary, - onCollectionSelected: @escaping (PhotoCollection) -> Void, - using dependencies: Dependencies = Dependencies() + using dependencies: Dependencies, + onCollectionSelected: @escaping (PhotoCollection) -> Void ) { self.dependencies = dependencies self.library = library @@ -37,6 +35,25 @@ class PhotoCollectionPickerViewModel: SessionTableViewModel, ObservableTableSour public enum Section: SessionTableSection { case content } + + public struct TableItem: Hashable, Differentiable { + public typealias DifferenceIdentifier = String + + private let collection: PhotoCollection + public var differenceIdentifier: String { collection.id } + + init(collection: PhotoCollection) { + self.collection = collection + } + + public func isContentEqual(to source: TableItem) -> Bool { + return (collection.id == source.collection.id) + } + + public func hash(into hasher: inout Hasher) { + collection.id.hash(into: &hasher) + } + } // MARK: - Content @@ -59,8 +76,8 @@ class PhotoCollectionPickerViewModel: SessionTableViewModel, ObservableTableSour let lastAssetItem: PhotoPickerAssetItem? = contents.lastAssetItem(photoMediaSize: photoMediaSize) return SessionCell.Info( - id: collection.id, - leftAccessory: .iconAsync(size: .extraLarge, shouldFill: true) { imageView in + id: TableItem(collection: collection), + leadingAccessory: .iconAsync(size: .extraLarge, shouldFill: true) { imageView in // Note: We need to capture 'lastAssetItem' otherwise it'll be released and we won't // be able to load the thumbnail lastAssetItem?.asyncThumbnail { [weak imageView] image in diff --git a/Session/Media Viewing & Editing/PhotoLibrary.swift b/Session/Media Viewing & Editing/PhotoLibrary.swift index e31f1c60474..089564fa935 100644 --- a/Session/Media Viewing & Editing/PhotoLibrary.swift +++ b/Session/Media Viewing & Editing/PhotoLibrary.swift @@ -6,6 +6,7 @@ import Photos import CoreServices import UniformTypeIdentifiers import SignalUtilitiesKit +import SessionMessagingKit import SessionUtilitiesKit protocol PhotoLibraryDelegate: AnyObject { @@ -138,10 +139,9 @@ class PhotoCollectionContents { _ = imageManager.requestImage(for: asset, targetSize: thumbnailSize, contentMode: .aspectFill, options: nil, resultHandler: resultHandler) } - private func requestImageDataSource(for asset: PHAsset) -> AnyPublisher<(dataSource: (any DataSource), type: UTType), Error> { + private func requestImageDataSource(for asset: PHAsset, using dependencies: Dependencies) -> AnyPublisher<(dataSource: (any DataSource), type: UTType), Error> { return Deferred { Future { [weak self] resolver in - let options: PHImageRequestOptions = PHImageRequestOptions() options.isNetworkAccessAllowed = true @@ -157,7 +157,7 @@ class PhotoCollectionContents { return } - guard let dataSource = DataSourceValue(data: imageData, dataType: type) else { + guard let dataSource = DataSourceValue(data: imageData, dataType: type, using: dependencies) else { resolver(Result.failure(PhotoLibraryError.assertionError(description: "dataSource was unexpectedly nil"))) return } @@ -169,10 +169,9 @@ class PhotoCollectionContents { .eraseToAnyPublisher() } - private func requestVideoDataSource(for asset: PHAsset) -> AnyPublisher<(dataSource: (any DataSource), type: UTType), Error> { + private func requestVideoDataSource(for asset: PHAsset, using dependencies: Dependencies) -> AnyPublisher<(dataSource: (any DataSource), type: UTType), Error> { return Deferred { Future { [weak self] resolver in - let options: PHVideoRequestOptions = PHVideoRequestOptions() options.isNetworkAccessAllowed = true @@ -186,7 +185,7 @@ class PhotoCollectionContents { exportSession.outputFileType = AVFileType.mp4 exportSession.metadataItemFilter = AVMetadataItemFilter.forSharing() - let exportPath = FileSystem.temporaryFilePath(fileExtension: "mp4") // stringlint:ignore + let exportPath = dependencies[singleton: .fileManager].temporaryFilePath(fileExtension: "mp4") // stringlint:ignore let exportURL = URL(fileURLWithPath: exportPath) exportSession.outputURL = exportURL @@ -196,7 +195,7 @@ class PhotoCollectionContents { guard exportSession?.status == .completed, - let dataSource = DataSourcePath(fileUrl: exportURL, shouldDeleteOnDeinit: true) + let dataSource = DataSourcePath(fileUrl: exportURL, shouldDeleteOnDeinit: true, using: dependencies) else { resolver(Result.failure(PhotoLibraryError.assertionError(description: "Failed to build data source for exported video URL"))) return @@ -210,21 +209,19 @@ class PhotoCollectionContents { .eraseToAnyPublisher() } - func outgoingAttachment(for asset: PHAsset) -> AnyPublisher { + func outgoingAttachment(for asset: PHAsset, using dependencies: Dependencies) -> AnyPublisher { switch asset.mediaType { case .image: - return requestImageDataSource(for: asset) + return requestImageDataSource(for: asset, using: dependencies) .map { (dataSource: DataSource, type: UTType) in - SignalAttachment - .attachment(dataSource: dataSource, type: type, imageQuality: .medium) + SignalAttachment.attachment(dataSource: dataSource, type: type, imageQuality: .medium, using: dependencies) } .eraseToAnyPublisher() case .video: - return requestVideoDataSource(for: asset) + return requestVideoDataSource(for: asset, using: dependencies) .map { (dataSource: DataSource, type: UTType) in - SignalAttachment - .attachment(dataSource: dataSource, type: type) + SignalAttachment.attachment(dataSource: dataSource, type: type, using: dependencies) } .eraseToAnyPublisher() diff --git a/Session/Media Viewing & Editing/SendMediaNavigationController.swift b/Session/Media Viewing & Editing/SendMediaNavigationController.swift index 61c567a47a2..594cc1fa77c 100644 --- a/Session/Media Viewing & Editing/SendMediaNavigationController.swift +++ b/Session/Media Viewing & Editing/SendMediaNavigationController.swift @@ -5,6 +5,7 @@ import Combine import Photos import SignalUtilitiesKit import SessionUIKit +import SessionMessagingKit import SessionUtilitiesKit class SendMediaNavigationController: UINavigationController { @@ -146,7 +147,7 @@ class SendMediaNavigationController: UINavigationController { } private func didTapCameraModeButton() { - Permissions.requestCameraPermissionIfNeeded { [weak self] in + Permissions.requestCameraPermissionIfNeeded(using: dependencies) { [weak self] in DispatchQueue.main.async { self?.fadeTo(viewControllers: ((self?.captureViewController).map { [$0] } ?? [])) } @@ -154,7 +155,7 @@ class SendMediaNavigationController: UINavigationController { } private func didTapMediaLibraryModeButton() { - Permissions.requestLibraryPermissionIfNeeded(isSavingMedia: false) { [weak self] in + Permissions.requestLibraryPermissionIfNeeded(isSavingMedia: false, using: dependencies) { [weak self] in DispatchQueue.main.async { self?.fadeTo(viewControllers: ((self?.mediaLibraryViewController).map { [$0] } ?? [])) } @@ -211,14 +212,14 @@ class SendMediaNavigationController: UINavigationController { // MARK: Child VC's private lazy var captureViewController: PhotoCaptureViewController = { - let vc = PhotoCaptureViewController() + let vc = PhotoCaptureViewController(using: dependencies) vc.delegate = self return vc }() private lazy var mediaLibraryViewController: ImagePickerGridController = { - let vc = ImagePickerGridController() + let vc = ImagePickerGridController(using: dependencies) vc.delegate = self vc.collectionView.accessibilityLabel = "Images" @@ -232,7 +233,7 @@ class SendMediaNavigationController: UINavigationController { } guard - let approvalViewController = AttachmentApprovalViewController( + let approvalViewController: AttachmentApprovalViewController = AttachmentApprovalViewController( mode: .sharedNavigation, threadId: self.threadId, threadVariant: self.threadVariant, @@ -646,7 +647,7 @@ private class DoneButton: UIView { private lazy var chevron: UIView = { let image: UIImage = { - guard Singleton.hasAppContext && Singleton.appContext.isRTL else { return #imageLiteral(resourceName: "small_chevron_right") } + guard Dependencies.isRTL else { return #imageLiteral(resourceName: "small_chevron_right") } return #imageLiteral(resourceName: "small_chevron_left") }() diff --git a/Session/Media Viewing & Editing/Transitions/MediaDismissAnimationController.swift b/Session/Media Viewing & Editing/Transitions/MediaDismissAnimationController.swift index 76e880278e5..d6de7bb38dc 100644 --- a/Session/Media Viewing & Editing/Transitions/MediaDismissAnimationController.swift +++ b/Session/Media Viewing & Editing/Transitions/MediaDismissAnimationController.swift @@ -2,6 +2,7 @@ import UIKit import SessionUIKit +import SessionUtilitiesKit class MediaDismissAnimationController: NSObject { private let mediaItem: Media @@ -14,8 +15,8 @@ class MediaDismissAnimationController: NSObject { var fromMediaFrame: CGRect? var pendingCompletion: (() -> ())? - init(galleryItem: MediaGalleryViewModel.Item, interactionController: MediaInteractiveDismiss? = nil) { - self.mediaItem = .gallery(galleryItem) + init(galleryItem: MediaGalleryViewModel.Item, interactionController: MediaInteractiveDismiss? = nil, using dependencies: Dependencies) { + self.mediaItem = .gallery(galleryItem, dependencies) self.interactionController = interactionController } diff --git a/Session/Media Viewing & Editing/Transitions/MediaPresentationContext.swift b/Session/Media Viewing & Editing/Transitions/MediaPresentationContext.swift index 93ca9b5f925..ea3fc1a2ea2 100644 --- a/Session/Media Viewing & Editing/Transitions/MediaPresentationContext.swift +++ b/Session/Media Viewing & Editing/Transitions/MediaPresentationContext.swift @@ -1,18 +1,20 @@ // Copyright © 2022 Rangeproof Pty Ltd. All rights reserved. -import Foundation +import UIKit +import SessionMessagingKit +import SessionUtilitiesKit enum Media { - case gallery(MediaGalleryViewModel.Item) + case gallery(MediaGalleryViewModel.Item, Dependencies) case image(UIImage) var image: UIImage? { switch self { - case let .gallery(item): + case let .gallery(item, dependencies): // For videos attempt to load a large thumbnail, for other items just try to load // the source file directly - guard !item.isVideo else { return item.attachment.existingThumbnail(size: .large) } - guard let originalFilePath: String = item.attachment.originalFilePath else { return nil } + guard !item.isVideo else { return item.attachment.existingThumbnail(size: .large, using: dependencies) } + guard let originalFilePath: String = item.attachment.originalFilePath(using: dependencies) else { return nil } return UIImage(contentsOfFile: originalFilePath) diff --git a/Session/Media Viewing & Editing/Transitions/MediaZoomAnimationController.swift b/Session/Media Viewing & Editing/Transitions/MediaZoomAnimationController.swift index 7dd7a4f0b39..90ae359a64e 100644 --- a/Session/Media Viewing & Editing/Transitions/MediaZoomAnimationController.swift +++ b/Session/Media Viewing & Editing/Transitions/MediaZoomAnimationController.swift @@ -2,13 +2,14 @@ import UIKit import SessionUIKit +import SessionUtilitiesKit class MediaZoomAnimationController: NSObject { private let mediaItem: Media private let shouldBounce: Bool - init(galleryItem: MediaGalleryViewModel.Item, shouldBounce: Bool = true) { - self.mediaItem = .gallery(galleryItem) + init(galleryItem: MediaGalleryViewModel.Item, shouldBounce: Bool = true, using dependencies: Dependencies) { + self.mediaItem = .gallery(galleryItem, dependencies) self.shouldBounce = shouldBounce } } diff --git a/Session/Meta/AppDelegate.swift b/Session/Meta/AppDelegate.swift index 86762ef3e42..4572ce42def 100644 --- a/Session/Meta/AppDelegate.swift +++ b/Session/Meta/AppDelegate.swift @@ -10,12 +10,20 @@ import SessionUtilitiesKit import SignalUtilitiesKit import SessionSnodeKit +// MARK: - Log.Category + +private extension Log.Category { + static let cat: Log.Category = .create("AppDelegate", defaultLevel: .info) +} + +// MARK: - AppDelegate + @UIApplicationMain class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterDelegate { private static let maxRootViewControllerInitialQueryDuration: TimeInterval = 10 /// The AppDelete is initialised by the OS so we should init an instance of `Dependencies` to be used throughout - let dependencies: Dependencies = Dependencies() + let dependencies: Dependencies = Dependencies.createEmpty() var window: UIWindow? var backgroundSnapshotBlockerWindow: UIWindow? var appStartupWindow: UIWindow? @@ -24,50 +32,73 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD var startTime: CFTimeInterval = 0 private var loadingViewController: LoadingViewController? - /// This needs to be a lazy variable to ensure it doesn't get initialized before it actually needs to be used - lazy var poller: CurrentUserPoller = CurrentUserPoller() - // MARK: - Lifecycle func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { - Log.info("[AppDelegate] didFinishLaunchingWithOptions called.") +#if DEBUG + /// If we are running a Preview then we don't want to setup the application (previews are generally self contained individual views so + /// doing all this application setup is a waste or work, and could even cause crashes for the preview) + if ProcessInfo.processInfo.environment["XCODE_RUNNING_FOR_PREVIEWS"] == "1" { // stringlint:ignore + return true + } +#endif + + Log.info(.cat, "didFinishLaunchingWithOptions called.") startTime = CACurrentMediaTime() // These should be the first things we do (the startup process can fail without them) - Singleton.setup(appContext: MainAppContext()) + dependencies.set(singleton: .appContext, to: MainAppContext(using: dependencies)) verifyDBKeysAvailableBeforeBackgroundLaunch() - _ = AppVersion.shared - AppEnvironment.shared.pushRegistrationManager.createVoipRegistryIfNecessary() + dependencies.warmCache(cache: .appVersion) + dependencies[singleton: .pushRegistrationManager].createVoipRegistryIfNecessary() // Prevent the device from sleeping during database view async registration // (e.g. long database upgrades). // // This block will be cleared in storageIsReady. - DeviceSleepManager.sharedInstance.addBlock(blockObject: self) + dependencies[singleton: .deviceSleepManager].addBlock(blockObject: self) let mainWindow: UIWindow = TraitObservingWindow(frame: UIScreen.main.bounds) self.loadingViewController = LoadingViewController() AppSetup.setupEnvironment( - appSpecificBlock: { - Log.setup(with: Logger(primaryPrefix: "Session", level: .info)) - Log.info("[AppDelegate] Setting up environment.") + additionalMigrationTargets: [DeprecatedUIKitMigrationTarget.self], + appSpecificBlock: { [dependencies] in + Log.setup(with: Logger(primaryPrefix: "Session", level: .info, using: dependencies)) + Log.info(.cat, "Setting up environment.") + + /// Create a proper `NotificationPresenter` for the main app (defaults to a no-op version) + dependencies.set(singleton: .notificationsManager, to: NotificationPresenter(using: dependencies)) + dependencies.set(singleton: .callManager, to: SessionCallManager(using: dependencies)) // Setup LibSession - LibSession.addLogger() - LibSession.createNetworkIfNeeded() + LibSession.setupLogger(using: dependencies) + dependencies.warmCache(cache: .libSessionNetwork) + + // Configure the different targets + SNUtilitiesKit.configure( + networkMaxFileSize: Network.maxFileSize, + localizedFormatted: { helper, font in SessionSNUIKitConfig.localizedFormatted(helper, font) }, + localizedDeformatted: { helper in SessionSNUIKitConfig.localizedDeformatted(helper) }, + using: dependencies + ) + SNMessagingKit.configure(using: dependencies) - // Create AppEnvironment - AppEnvironment.shared.setup() + // Update state of current call + if dependencies[singleton: .callManager].currentCall == nil { + dependencies[defaults: .appGroup, key: .isCallOngoing] = false + dependencies[defaults: .appGroup, key: .lastCallPreOffer] = nil + } // Note: Intentionally dispatching sync as we want to wait for these to complete before // continuing DispatchQueue.main.sync { - ScreenLockUI.shared.setupWithRootWindow(rootWindow: mainWindow) + ScreenLockUI.shared.setupWithRootWindow(rootWindow: mainWindow, using: dependencies) OWSWindowManager.shared().setup( withRootWindow: mainWindow, - screenBlockingWindow: ScreenLockUI.shared.screenBlockingWindow + screenBlockingWindow: ScreenLockUI.shared.screenBlockingWindow, + backgroundWindowLevel: .background ) ScreenLockUI.shared.startObserving() } @@ -78,35 +109,42 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD minEstimatedTotalTime: minEstimatedTotalTime ) }, - migrationsCompletion: { [weak self] result, needsConfigSync in + migrationsCompletion: { [weak self, dependencies] result, needsConfigSync in if case .failure(let error) = result { DispatchQueue.main.async { self?.initialLaunchFailed = true - self?.showFailedStartupAlert(calledFrom: .finishLaunching, error: .databaseError(error)) + self?.showFailedStartupAlert( + calledFrom: .finishLaunching, + error: .databaseError(error) + ) } return } - /// Store a weak reference in the ThemeManager so it can properly apply themes as needed - /// - /// **Note:** Need to do this after the db migrations because theme preferences are stored in the database and - /// we don't want to access it until after the migrations run - ThemeManager.mainWindow = mainWindow - self?.completePostMigrationSetup(calledFrom: .finishLaunching, needsConfigSync: needsConfigSync) + /// Because the `SessionUIKit` target doesn't depend on the `SessionUtilitiesKit` dependency (it shouldn't + /// need to since it should just be UI) but since the theme settings are stored in the database we need to pass these through + /// to `SessionUIKit` and expose a mechanism to save updated settings - this is done here (once the migrations complete) + SNUIKit.configure( + with: SessionSNUIKitConfig(using: dependencies), + themeSettings: dependencies[singleton: .storage].read { db in + (db[.theme], db[.themePrimaryColor], db[.themeMatchSystemDayNightCycle]) + } + ) + + /// Now that the theme settings have been applied we can complete the migrations + self?.completePostMigrationSetup( + calledFrom: .finishLaunching, + needsConfigSync: needsConfigSync + ) }, using: dependencies ) - if SessionEnvironment.shared?.callManager.wrappedValue?.currentCall == nil { - UserDefaults.sharedLokiProject?[.isCallOngoing] = false - UserDefaults.sharedLokiProject?[.lastCallPreOffer] = nil - } - // No point continuing if we are running tests guard !SNUtilitiesKit.isRunningTests else { return true } self.window = mainWindow - Singleton.appContext.setMainWindow(mainWindow) + dependencies[singleton: .appContext].setMainWindow(mainWindow) // Show LoadingViewController until the async database view registrations are complete. mainWindow.rootViewController = self.loadingViewController @@ -116,28 +154,22 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD // miss notifications. // Setting the delegate also seems to prevent us from getting the legacy notification // notification callbacks upon launch e.g. 'didReceiveLocalNotification' - UNUserNotificationCenter.current().delegate = self + dependencies[singleton: .notificationsManager].setDelegate(self) NotificationCenter.default.addObserver( self, - selector: #selector(registrationStateDidChange), - name: .registrationStateDidChange, - object: nil - ) - NotificationCenter.default.addObserver( - self, - selector: #selector(showMissedCallTipsIfNeeded(_:)), + selector: #selector(showMissedCallTipsIfNeededNotification(_:)), name: .missedCall, object: nil ) - Log.info("[AppDelegate] didFinishLaunchingWithOptions completed.") + Log.info(.cat, "didFinishLaunchingWithOptions completed.") return true } func applicationWillEnterForeground(_ application: UIApplication) { Log.appResumedExecution() - Log.info("[AppDelegate] applicationWillEnterForeground.") + Log.info(.cat, "applicationWillEnterForeground.") /// **Note:** We _shouldn't_ need to call this here but for some reason the OS doesn't seems to /// be calling the `userNotificationCenter(_:,didReceive:withCompletionHandler:)` @@ -145,21 +177,21 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD /// springboard without swapping to another app) - adding this here in addition to the one in /// `appDidFinishLaunching` seems to fix this odd behaviour (even though it doesn't match /// Apple's documentation on the matter) - UNUserNotificationCenter.current().delegate = self + dependencies[singleton: .notificationsManager].setDelegate(self) - dependencies.storage.resumeDatabaseAccess() - LibSession.resumeNetworkAccess() + dependencies[singleton: .storage].resumeDatabaseAccess() + dependencies.mutate(cache: .libSessionNetwork) { $0.resumeNetworkAccess() } // Reset the 'startTime' (since it would be invalid from the last launch) startTime = CACurrentMediaTime() // If we've already completed migrations at least once this launch then check // to see if any "delayed" migrations now need to run - if Storage.shared.hasCompletedMigrations { - Log.info("Checking for pending migrations") + if dependencies[singleton: .storage].hasCompletedMigrations { + Log.info(.cat, "Checking for pending migrations") let initialLaunchFailed: Bool = self.initialLaunchFailed - Singleton.appReadiness.invalidate() + dependencies[singleton: .appReadiness].invalidate() // If the user went to the background too quickly then the database can be suspended before // properly starting up, in this case an alert will be shown but we can recover from it so @@ -171,6 +203,7 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD // Dispatch async so things can continue to be progressed if a migration does need to run DispatchQueue.global(qos: .userInitiated).async { [weak self, dependencies] in AppSetup.runPostSetupMigrations( + additionalMigrationTargets: [DeprecatedUIKitMigrationTarget.self], migrationProgressChanged: { progress, minEstimatedTotalTime in self?.loadingViewController?.updateProgress( progress: progress, @@ -200,8 +233,8 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD } func applicationDidEnterBackground(_ application: UIApplication) { - if !hasInitialRootViewController { Log.info("Entered background before startup was completed") } - Log.info("[AppDelegate] applicationDidEnterBackground.") + if !hasInitialRootViewController { Log.info(.cat, "Entered background before startup was completed") } + Log.info(.cat, "applicationDidEnterBackground.") Log.flush() // NOTE: Fix an edge case where user taps on the callkit notification @@ -209,41 +242,41 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD stopPollers(shouldStopUserPoller: !self.hasCallOngoing()) // Stop all jobs except for message sending and when completed suspend the database - JobRunner.stopAndClearPendingJobs(exceptForVariant: .messageSend, using: dependencies) { [dependencies] neededBackgroundProcessing in - if !self.hasCallOngoing() && (!neededBackgroundProcessing || Singleton.hasAppContext && Singleton.appContext.isInBackground) { - LibSession.suspendNetworkAccess() - dependencies.storage.suspendDatabaseAccess() - Log.info("[AppDelegate] completed network and database shutdowns.") + dependencies[singleton: .jobRunner].stopAndClearPendingJobs(exceptForVariant: .messageSend) { [dependencies] neededBackgroundProcessing in + if !self.hasCallOngoing() && (!neededBackgroundProcessing || dependencies[singleton: .appContext].isInBackground) { + dependencies.mutate(cache: .libSessionNetwork) { $0.suspendNetworkAccess() } + dependencies[singleton: .storage].suspendDatabaseAccess() + Log.info(.cat, "completed network and database shutdowns.") Log.flush() } } } func applicationDidReceiveMemoryWarning(_ application: UIApplication) { - Log.warn("applicationDidReceiveMemoryWarning") + Log.warn(.cat, "applicationDidReceiveMemoryWarning") } func applicationWillTerminate(_ application: UIApplication) { - Log.info("[AppDelegate] applicationWillTerminate.") + Log.info(.cat, "applicationWillTerminate.") Log.flush() stopPollers() } func applicationDidBecomeActive(_ application: UIApplication) { - Log.info("[AppDelegate] applicationDidBecomeActive.") + Log.info(.cat, "applicationDidBecomeActive.") guard !SNUtilitiesKit.isRunningTests else { return } - Log.info("[AppDelegate] Setting 'isMainAppActive' to true.") - UserDefaults.sharedLokiProject?[.isMainAppActive] = true + Log.info(.cat, "Setting 'isMainAppActive' to true.") + dependencies[defaults: .appGroup, key: .isMainAppActive] = true // FIXME: Seems like there are some discrepancies between the expectations of how the iOS lifecycle methods work, we should look into them and ensure the code behaves as expected (in this case there were situations where these two wouldn't get called when returning from the background) - dependencies.storage.resumeDatabaseAccess() - LibSession.resumeNetworkAccess() + dependencies[singleton: .storage].resumeDatabaseAccess() + dependencies.mutate(cache: .libSessionNetwork) { $0.resumeNetworkAccess() } ensureRootViewController(calledFrom: .didBecomeActive) - Singleton.appReadiness.runNowOrWhenAppDidBecomeReady { [weak self] in + dependencies[singleton: .appReadiness].runNowOrWhenAppDidBecomeReady { [weak self] in self?.handleActivation() /// Clear all notifications whenever we become active once the app is ready @@ -252,23 +285,21 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD /// no longer always called before `applicationDidBecomeActive` we need to trigger the "clear notifications" logic /// within the `runNowOrWhenAppDidBecomeReady` callback and dispatch to the next run loop to ensure it runs after /// the notification has actually been handled - DispatchQueue.main.async { [weak self] in + DispatchQueue.main.async { self?.clearAllNotificationsAndRestoreBadgeCount() } } // On every activation, clear old temp directories. - guard Singleton.hasAppContext else { return } - - Singleton.appContext.clearOldTemporaryDirectories() + dependencies[singleton: .fileManager].clearOldTemporaryDirectories() } func applicationWillResignActive(_ application: UIApplication) { - Log.info("[AppDelegate] applicationWillResignActive.") + Log.info(.cat, "applicationWillResignActive.") clearAllNotificationsAndRestoreBadgeCount() - Log.info("[AppDelegate] Setting 'isMainAppActive' to false.") - UserDefaults.sharedLokiProject?[.isMainAppActive] = false + Log.info(.cat, "Setting 'isMainAppActive' to false.") + dependencies[defaults: .appGroup, key: .isMainAppActive] = false Log.flush() } @@ -287,9 +318,9 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD func application(_ application: UIApplication, performFetchWithCompletionHandler completionHandler: @escaping (UIBackgroundFetchResult) -> Void) { Log.appResumedExecution() - Log.info("Starting background fetch.") - dependencies.storage.resumeDatabaseAccess() - LibSession.resumeNetworkAccess() + Log.info(.backgroundPoller, "Starting background fetch.") + dependencies[singleton: .storage].resumeDatabaseAccess() + dependencies.mutate(cache: .libSessionNetwork) { $0.resumeNetworkAccess() } let queue: DispatchQueue = .global(qos: .userInitiated) let poller: BackgroundPoller = BackgroundPoller() @@ -301,18 +332,19 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD // before the background task is due to expire in order to do so let cancelTimer: Timer = Timer.scheduledTimerOnMainThread( withTimeInterval: (application.backgroundTimeRemaining - 5), - repeats: false + repeats: false, + using: dependencies ) { [poller, dependencies] timer in timer.invalidate() guard cancellable != nil else { return } - Log.info("Background poll failed due to manual timeout.") + Log.info(.backgroundPoller, "Background poll failed due to manual timeout.") cancellable?.cancel() - if Singleton.hasAppContext && Singleton.appContext.isInBackground { - LibSession.suspendNetworkAccess() - dependencies.storage.suspendDatabaseAccess() + if dependencies[singleton: .appContext].isInBackground { + dependencies.mutate(cache: .libSessionNetwork) { $0.suspendNetworkAccess() } + dependencies[singleton: .storage].suspendDatabaseAccess() Log.flush() } @@ -320,12 +352,12 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD completionHandler(.failed) } - Singleton.appReadiness.runNowOrWhenAppDidBecomeReady { [dependencies, poller] in + dependencies[singleton: .appReadiness].runNowOrWhenAppDidBecomeReady { [dependencies, poller] in // If the 'AppReadiness' process takes too long then it's possible for the user to open // the app after this closure is registered but before it's actually triggered - this can // result in the `BackgroundPoller` incorrectly getting called in the foreground, this check // is here to prevent that - guard Singleton.hasAppContext && Singleton.appContext.isInBackground else { return } + guard dependencies[singleton: .appContext].isInBackground else { return } cancellable = poller .poll(using: dependencies) @@ -336,9 +368,9 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD // Ensure we haven't timed out yet guard cancelTimer.isValid else { return } - if Singleton.hasAppContext && Singleton.appContext.isInBackground { - LibSession.suspendNetworkAccess() - dependencies.storage.suspendDatabaseAccess() + if dependencies[singleton: .appContext].isInBackground { + dependencies.mutate(cache: .libSessionNetwork) { $0.suspendNetworkAccess() } + dependencies[singleton: .storage].suspendDatabaseAccess() Log.flush() } @@ -357,31 +389,33 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD // MARK: - App Readiness - private func completePostMigrationSetup(calledFrom lifecycleMethod: LifecycleMethod, needsConfigSync: Bool) { - Log.info("Migrations completed, performing setup and ensuring rootViewController") - Configuration.performMainSetup() - JobRunner.setExecutor(SyncPushTokensJob.self, for: .syncPushTokens) + private func completePostMigrationSetup( + calledFrom lifecycleMethod: LifecycleMethod, + needsConfigSync: Bool + ) { + Log.info(.cat, "Migrations completed, performing setup and ensuring rootViewController") + dependencies[singleton: .jobRunner].setExecutor(SyncPushTokensJob.self, for: .syncPushTokens) /// We need to do a clean up for disappear after send messages that are received by push notifications before /// the app set up the main screen and load initial data to prevent a case when the PagedDatabaseObserver /// hasn't been setup yet then the conversation screen can show stale (ie. deleted) interactions incorrectly - DisappearingMessagesJob.cleanExpiredMessagesOnLaunch() + DisappearingMessagesJob.cleanExpiredMessagesOnLaunch(using: dependencies) // Setup the UI if needed, then trigger any post-UI setup actions - self.ensureRootViewController(calledFrom: lifecycleMethod) { [weak self] success in + self.ensureRootViewController(calledFrom: lifecycleMethod) { [weak self, dependencies] success in // If we didn't successfully ensure the rootViewController then don't continue as // the user is in an invalid state (and should have already been shown a modal) guard success else { return } - Log.info("RootViewController ready for state: \(Onboarding.State.current), readying remaining processes") + Log.info(.cat, "RootViewController ready for state: \(dependencies[cache: .onboarding].state), readying remaining processes") self?.initialLaunchFailed = false - /// Trigger any launch-specific jobs and start the JobRunner with `JobRunner.appDidFinishLaunching()` some + /// Trigger any launch-specific jobs and start the JobRunner with `jobRunner.appDidFinishLaunching(using:)` some /// of these jobs (eg. DisappearingMessages job) can impact the interactions which get fetched to display on the home /// screen, if the PagedDatabaseObserver hasn't been setup yet then the home screen can show stale (ie. deleted) /// interactions incorrectly if lifecycleMethod == .finishLaunching { - JobRunner.appDidFinishLaunching() + dependencies[singleton: .jobRunner].appDidFinishLaunching() } /// Flag that the app is ready via `AppReadiness.setAppIsReady()` @@ -391,18 +425,18 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD /// /// **Note:** This this does much more than set a flag - it will also run all deferred blocks (including the JobRunner /// `appDidBecomeActive` method hence why it **must** also come after calling - /// `JobRunner.appDidFinishLaunching()`) - Singleton.appReadiness.setAppReady() + /// `jobRunner.appDidFinishLaunching(using:)`) + dependencies[singleton: .appReadiness].setAppReady() /// Remove the sleep blocking once the startup is done (needs to run on the main thread and sleeping while /// doing the startup could suspend the database causing errors/crashes - DeviceSleepManager.sharedInstance.removeBlock(blockObject: self) + dependencies[singleton: .deviceSleepManager].removeBlock(blockObject: self) /// App launch hasn't really completed until the main screen is loaded so wait until then to register it - AppVersion.shared.mainAppLaunchDidComplete() + dependencies.mutate(cache: .appVersion) { $0.mainAppLaunchDidComplete() } /// App won't be ready for extensions and no need to enqueue a config sync unless we successfully completed startup - Storage.shared.writeAsync { db in + dependencies[singleton: .storage].writeAsync { db in // Increment the launch count (guaranteed to change which results in the write actually // doing something and outputting and error if the DB is suspended) db[.activeCounter] = ((db[.activeCounter] ?? 0) + 1) @@ -411,17 +445,14 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD // at least once in the post-SAE world. db[.isReadyForAppExtensions] = true - if Identity.userCompletedRequiredOnboarding(db) { - let appVersion: AppVersion = AppVersion.shared - + if dependencies[cache: .onboarding].state == .completed { // If the device needs to sync config or the user updated to a new version - if - needsConfigSync || ( - (appVersion.lastAppVersion?.count ?? 0) > 0 && - appVersion.lastAppVersion != appVersion.currentAppVersion + if needsConfigSync || dependencies[cache: .appVersion].didJustUpdate { + ConfigurationSyncJob.enqueue( + db, + swarmPublicKey: dependencies[cache: .general].sessionId.hexString, + using: dependencies ) - { - ConfigurationSyncJob.enqueue(db, publicKey: getUserHexEncodedPublicKey(db)) } } } @@ -429,7 +460,7 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD // Add a log to track the proper startup time of the app so we know whether we need to // improve it in the future from user logs let startupDuration: CFTimeInterval = ((self?.startTime).map { CACurrentMediaTime() - $0 } ?? -1) - Log.info("\(lifecycleMethod.timingName) completed in \(.seconds(startupDuration), unit: .ms).") + Log.info(.cat, "\(lifecycleMethod.timingName) completed in \(.seconds(startupDuration), unit: .ms).") } // May as well run these on the background thread @@ -449,8 +480,8 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD message: error.message, preferredStyle: .alert ) - alert.addAction(UIAlertAction(title: "helpReportABugExportLogs".localized(), style: .default) { _ in - HelpViewModel.shareLogs(viewControllerToDismiss: alert) { [weak self] in + alert.addAction(UIAlertAction(title: "helpReportABugExportLogs".localized(), style: .default) { [dependencies] _ in + HelpViewModel.shareLogs(viewControllerToDismiss: alert, using: dependencies) { [weak self] in // Don't bother showing the "Failed Startup" modal again if we happen to now // have an initial view controller (this most likely means that the startup // completed while the user was sharing logs so we can just let the user use @@ -471,13 +502,14 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD case .databaseError: alert.addAction(UIAlertAction(title: "onboardingAccountExists".localized(), style: .destructive) { [dependencies] _ in // Reset the current database for a clean migration - dependencies.storage.resetForCleanMigration() + dependencies[singleton: .storage].resetForCleanMigration() // Hide the top banner if there was one TopBannerController.hide() // The re-run the migration (should succeed since there is no data) AppSetup.runPostSetupMigrations( + additionalMigrationTargets: [DeprecatedUIKitMigrationTarget.self], migrationProgressChanged: { [weak self] progress, minEstimatedTotalTime in self?.loadingViewController?.updateProgress( progress: progress, @@ -488,11 +520,17 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD switch result { case .failure: DispatchQueue.main.async { - self?.showFailedStartupAlert(calledFrom: lifecycleMethod, error: .failedToRestore) + self?.showFailedStartupAlert( + calledFrom: lifecycleMethod, + error: .failedToRestore + ) } case .success: - self?.completePostMigrationSetup(calledFrom: lifecycleMethod, needsConfigSync: needsConfigSync) + self?.completePostMigrationSetup( + calledFrom: lifecycleMethod, + needsConfigSync: needsConfigSync + ) } }, using: dependencies @@ -507,7 +545,7 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD exit(0) }) - Log.info("Showing startup alert due to error: \(error.description)") + Log.info(.cat, "Showing startup alert due to error: \(error.description)") self.window?.rootViewController?.present(alert, animated: animated, completion: presentationCompletion) } @@ -515,9 +553,9 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD private func verifyDBKeysAvailableBeforeBackgroundLaunch() { guard UIApplication.shared.applicationState == .background else { return } - guard !Storage.isDatabasePasswordAccessible else { return } // All good + guard !dependencies[singleton: .storage].isDatabasePasswordAccessible else { return } // All good - Log.info("Exiting because we are in the background and the database password is not accessible.") + Log.warn(.cat, "Exiting because we are in the background and the database password is not accessible.") let notificationContent: UNMutableNotificationContent = UNMutableNotificationContent() notificationContent.body = "notificationsIosRestart" @@ -542,7 +580,7 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD } private func enableBackgroundRefreshIfNecessary() { - Singleton.appReadiness.runNowOrWhenAppDidBecomeReady { + dependencies[singleton: .appReadiness].runNowOrWhenAppDidBecomeReady { UIApplication.shared.setMinimumBackgroundFetchInterval(UIApplication.backgroundFetchIntervalMinimum) } } @@ -551,20 +589,20 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD /// There is a _fun_ behaviour here where if the user launches the app, sends it to the background at the right time and then /// opens it again the `AppReadiness` closures can be triggered before `applicationDidBecomeActive` has been /// called again - this can result in odd behaviours so hold off on running this logic until it's properly called again - guard UserDefaults.sharedLokiProject?[.isMainAppActive] == true else { return } + guard dependencies[defaults: .appGroup, key: .isMainAppActive] == true else { return } /// There is a warning which can happen on launch because the Database read can be blocked by another database operation /// which could result in this blocking the main thread, as a result we want to check the identity exists on a background thread /// and then return to the main thread only when required - DispatchQueue.global(qos: .default).async { [weak self] in - guard Identity.userExists() else { return } + DispatchQueue.global(qos: .default).async { [weak self, dependencies] in + guard dependencies[cache: .onboarding].state == .completed else { return } self?.enableBackgroundRefreshIfNecessary() - JobRunner.appDidBecomeActive() + dependencies[singleton: .jobRunner].appDidBecomeActive() self?.startPollersIfNeeded() - if Singleton.hasAppContext && Singleton.appContext.isMainApp { + if dependencies[singleton: .appContext].isMainApp { DispatchQueue.main.async { self?.handleAppActivatedWithOngoingCallIfNeeded() } @@ -580,9 +618,9 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD // Always call the completion block and indicate whether we successfully created the UI guard - Storage.shared.isValid && + dependencies[singleton: .storage].isValid && ( - Singleton.appReadiness.isAppReady || + dependencies[singleton: .appReadiness].isAppReady || lifecycleMethod == .finishLaunching || lifecycleMethod == .enterForeground(initialLaunchFailed: true) ) && @@ -591,20 +629,26 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD /// Start a timeout for the creation of the rootViewController setup process (if it takes too long then we want to give the user /// the option to export their logs) - let populateHomeScreenTimer: Timer = Timer.scheduledTimerOnMainThread( - withTimeInterval: AppDelegate.maxRootViewControllerInitialQueryDuration, - repeats: false - ) { [weak self] timer in - timer.invalidate() - self?.showFailedStartupAlert(calledFrom: lifecycleMethod, error: .startupTimeout) - } + let longRunningStartupTimoutCancellable: AnyCancellable = Just(()) + .delay(for: .seconds(AppDelegate.maxRootViewControllerInitialQueryDuration), scheduler: DispatchQueue.main) + .sink( + receiveCompletion: { _ in }, + receiveValue: { [weak self] _ in + self?.showFailedStartupAlert(calledFrom: lifecycleMethod, error: .startupTimeout) + } + ) // All logic which needs to run after the 'rootViewController' is created - let rootViewControllerSetupComplete: (UIViewController) -> () = { [weak self] rootViewController in + let rootViewControllerSetupComplete: (UIViewController) -> () = { [weak self, dependencies] rootViewController in + /// `MainAppContext.determineDeviceRTL` uses UIKit to retrime `isRTL` so must be run on the main thread to prevent + /// lag/crashes on background threads + Dependencies.setIsRTLRetriever(requiresMainThread: true) { MainAppContext.determineDeviceRTL() } + + /// Setup the `TopBannerController` let presentedViewController: UIViewController? = self?.window?.rootViewController?.presentedViewController let targetRootViewController: UIViewController = TopBannerController( child: StyledNavigationController(rootViewController: rootViewController), - cachedWarning: UserDefaults.sharedLokiProject?[.topBannerWarningToShow] + cachedWarning: dependencies[defaults: .appGroup, key: .topBannerWarningToShow] .map { rawValue in TopBannerController.Warning(rawValue: rawValue) } ) @@ -621,7 +665,7 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD /// the `HomeVC` won't have completed loading it's view which means the `SessionApp.homeViewController` /// won't have been set - we set the value directly here to resolve this edge case if let homeViewController: HomeVC = rootViewController as? HomeVC { - SessionApp.homeViewController.mutate { $0 = homeViewController } + dependencies[singleton: .app].setHomeViewController(homeViewController) } /// If we were previously presenting a viewController but are no longer preseting it then present it again @@ -632,11 +676,15 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD case is UIAlertController, is ConfirmationModal: /// If the viewController we were presenting happened to be the "failed startup" modal then we can dismiss it /// automatically (while this seems redundant it's less jarring for the user than just instantly having it disappear) - self?.showFailedStartupAlert(calledFrom: lifecycleMethod, error: .startupTimeout, animated: false) { + self?.showFailedStartupAlert( + calledFrom: lifecycleMethod, + error: .startupTimeout, + animated: false + ) { self?.window?.rootViewController?.dismiss(animated: true) } - case is UIActivityViewController: HelpViewModel.shareLogs(animated: false) + case is UIActivityViewController: HelpViewModel.shareLogs(animated: false, using: dependencies) default: break } @@ -645,24 +693,36 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD } // Navigate to the approriate screen depending on the onboarding state - switch Onboarding.State.current { - case .newUser: + dependencies.warmCache(cache: .onboarding) + + switch dependencies[cache: .onboarding].state { + case .noUser, .noUserFailedIdentity: + if dependencies[cache: .onboarding].state == .noUserFailedIdentity { + Log.critical(.cat, "Failed to create an initial identity for a potentially new user.") + } + DispatchQueue.main.async { [dependencies] in - let viewController = SessionHostingViewController(rootView: LandingScreen(using: dependencies)) - viewController.setUpNavBarSessionIcon() - populateHomeScreenTimer.invalidate() + /// Once the onboarding process is complete we need to call `handleActivation` + let viewController = SessionHostingViewController(rootView: LandingScreen(using: dependencies) { [weak self] in + self?.handleActivation() + }) + viewController.setUpNavBarSessionIcon(using: dependencies) + longRunningStartupTimoutCancellable.cancel() rootViewControllerSetupComplete(viewController) } case .missingName: DispatchQueue.main.async { [dependencies] in - let viewController = SessionHostingViewController( - rootView: DisplayNameScreen(flow: .register, using: dependencies) - ) - viewController.setUpNavBarSessionIcon() - viewController.setUpClearDataBackButton(flow: .register) - populateHomeScreenTimer.invalidate() + let viewController = SessionHostingViewController(rootView: DisplayNameScreen(using: dependencies)) + viewController.setUpNavBarSessionIcon(using: dependencies) + longRunningStartupTimoutCancellable.cancel() rootViewControllerSetupComplete(viewController) + + /// Once the onboarding process is complete we need to call `handleActivation` + dependencies[cache: .onboarding].onboardingCompletePublisher + .subscribe(on: DispatchQueue.main, using: dependencies) + .receive(on: DispatchQueue.main, using: dependencies) + .sinkUntilComplete(receiveCompletion: { [weak self] _ in self?.handleActivation() }) } case .completed: @@ -673,7 +733,7 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD /// continue as we don't want to show a blank home screen DispatchQueue.global(qos: .userInitiated).async { viewController.startObservingChanges() { - populateHomeScreenTimer.invalidate() + longRunningStartupTimoutCancellable.cancel() DispatchQueue.main.async { rootViewControllerSetupComplete(viewController) @@ -687,36 +747,36 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD // MARK: - Notifications func application(_ application: UIApplication, didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data) { - PushRegistrationManager.shared.didReceiveVanillaPushToken(deviceToken) - Log.info("Registering for push notifications.") + dependencies[singleton: .pushRegistrationManager].didReceiveVanillaPushToken(deviceToken) + Log.info(.cat, "Registering for push notifications.") } func application(_ application: UIApplication, didFailToRegisterForRemoteNotificationsWithError error: Error) { - Log.error("Failed to register push token with error: \(error).") + Log.error(.cat, "Failed to register push token with error: \(error).") #if DEBUG - Log.warn("We're in debug mode. Faking success for remote registration with a fake push identifier.") - PushRegistrationManager.shared.didReceiveVanillaPushToken(Data(count: 32)) + Log.warn(.cat, "We're in debug mode. Faking success for remote registration with a fake push identifier.") + dependencies[singleton: .pushRegistrationManager].didReceiveVanillaPushToken(Data(count: 32)) #else - PushRegistrationManager.shared.didFailToReceiveVanillaPushToken(error: error) + dependencies[singleton: .pushRegistrationManager].didFailToReceiveVanillaPushToken(error: error) #endif } private func clearAllNotificationsAndRestoreBadgeCount() { - Singleton.appReadiness.runNowOrWhenAppDidBecomeReady { - AppEnvironment.shared.notificationPresenter.clearAllNotifications() + dependencies[singleton: .appReadiness].runNowOrWhenAppDidBecomeReady { [dependencies] in + dependencies[singleton: .notificationsManager].clearAllNotifications() - guard Singleton.hasAppContext && Singleton.appContext.isMainApp else { return } + guard dependencies[singleton: .appContext].isMainApp else { return } /// On application startup the `Storage.read` can be slightly slow while GRDB spins up it's database /// read pools (up to a few seconds), since this read is blocking we want to dispatch it to run async to ensure /// we don't block user interaction while it's running - DispatchQueue.global(qos: .default).async { - let unreadCount: Int = Storage.shared - .read { db in try Interaction.fetchUnreadCount(db) } + DispatchQueue.global(qos: .default).async(using: dependencies) { + let unreadCount: Int = dependencies[singleton: .storage] + .read { db in try Interaction.fetchUnreadCount(db, using: dependencies) } .defaulting(to: 0) - DispatchQueue.main.async { + DispatchQueue.main.async(using: dependencies) { UIApplication.shared.applicationIconBadgeNumber = unreadCount } } @@ -724,10 +784,10 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD } func application(_ application: UIApplication, performActionFor shortcutItem: UIApplicationShortcutItem, completionHandler: @escaping (Bool) -> Void) { - Singleton.appReadiness.runNowOrWhenAppDidBecomeReady { - guard Identity.userCompletedRequiredOnboarding() else { return } + dependencies[singleton: .appReadiness].runNowOrWhenAppDidBecomeReady { [dependencies] in + guard dependencies[cache: .onboarding].state == .completed else { return } - SessionApp.homeViewController.wrappedValue?.createNewConversation() + dependencies[singleton: .app].createNewConversation() completionHandler(true) } } @@ -739,17 +799,17 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD /// This decision should be based on whether the information in the notification is otherwise visible to the user. func userNotificationCenter(_ center: UNUserNotificationCenter, willPresent notification: UNNotification, withCompletionHandler completionHandler: @escaping (UNNotificationPresentationOptions) -> Void) { if notification.request.content.userInfo["remote"] != nil { - Log.info("Ignoring remote notifications while the app is in the foreground.") + Log.info(.cat, "Ignoring remote notifications while the app is in the foreground.") return } - Singleton.appReadiness.runNowOrWhenAppDidBecomeReady { + dependencies[singleton: .appReadiness].runNowOrWhenAppDidBecomeReady { // We need to respect the in-app notification sound preference. This method, which is called // for modern UNUserNotification users, could be a place to do that, but since we'd still // need to handle this behavior for legacy UINotification users anyway, we "allow" all // notification options here, and rely on the shared logic in NotificationPresenter to // honor notification sound preferences for both modern and legacy users. - completionHandler([.alert, .badge, .sound]) + completionHandler([.badge, .sound]) } } @@ -757,8 +817,11 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD /// the notification or choosing a UNNotificationAction. The delegate must be set before the application returns from /// application:didFinishLaunchingWithOptions:. func userNotificationCenter(_ center: UNUserNotificationCenter, didReceive response: UNNotificationResponse, withCompletionHandler completionHandler: @escaping () -> Void) { - Singleton.appReadiness.runNowOrWhenAppDidBecomeReady { [dependencies] in - AppEnvironment.shared.userNotificationActionHandler.handleNotificationResponse(response, completionHandler: completionHandler, using: dependencies) + dependencies[singleton: .appReadiness].runNowOrWhenAppDidBecomeReady { [dependencies] in + dependencies[singleton: .notificationActionHandler].handleNotificationResponse( + response, + completionHandler: completionHandler + ) } } @@ -771,58 +834,56 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD // MARK: - Notification Handling - @objc private func registrationStateDidChange() { - handleActivation() + @objc public func showMissedCallTipsIfNeededNotification(_ notification: Notification) { + showMissedCallTipsIfNeeded(notification) } - @objc public func showMissedCallTipsIfNeeded(_ notification: Notification) { - guard !UserDefaults.standard[.hasSeenCallMissedTips] else { return } + private func showMissedCallTipsIfNeeded(_ notification: Notification) { + guard + dependencies[singleton: .appContext].isValid, + !dependencies[defaults: .standard, key: .hasSeenCallMissedTips] + else { return } guard Thread.isMainThread else { DispatchQueue.main.async { self.showMissedCallTipsIfNeeded(notification) } return } - guard let callerId: String = notification.userInfo?[Notification.Key.senderId.rawValue] as? String else { - return - } - guard - Singleton.hasAppContext, - let presentingVC = Singleton.appContext.frontmostViewController - else { preconditionFailure() } + guard let callerId: String = notification.userInfo?[Notification.Key.senderId.rawValue] as? String else { return } + guard let presentingVC = dependencies[singleton: .appContext].frontMostViewController else { preconditionFailure() } let callMissedTipsModal: CallMissedTipsModal = CallMissedTipsModal( - caller: Profile.displayName(id: callerId) + caller: Profile.displayName(id: callerId, using: dependencies) ) presentingVC.present(callMissedTipsModal, animated: true, completion: nil) - UserDefaults.standard[.hasSeenCallMissedTips] = true + dependencies[defaults: .standard, key: .hasSeenCallMissedTips] = true } // MARK: - Polling public func startPollersIfNeeded(shouldStartGroupPollers: Bool = true) { - guard Identity.userExists() else { return } + guard dependencies[cache: .onboarding].state == .completed else { return } /// Start the pollers on a background thread so that any database queries they need to run don't /// block the main thread - DispatchQueue.global(qos: .background).async { [weak self] in - self?.poller.start() + DispatchQueue.global(qos: .background).async { [dependencies] in + dependencies[singleton: .currentUserPoller].startIfNeeded() guard shouldStartGroupPollers else { return } - ClosedGroupPoller.shared.start() - OpenGroupManager.shared.startPolling() + dependencies.mutate(cache: .groupPollers) { $0.startAllPollers() } + dependencies.mutate(cache: .communityPollers) { $0.startAllPollers() } } } public func stopPollers(shouldStopUserPoller: Bool = true) { if shouldStopUserPoller { - poller.stop() + dependencies[singleton: .currentUserPoller].stop() } - - ClosedGroupPoller.shared.stopAllPollers() - OpenGroupManager.shared.stopPolling() + + dependencies.mutate(cache: .groupPollers) { $0.stopAndRemoveAllPollers() } + dependencies.mutate(cache: .communityPollers) { $0.stopAndRemoveAllPollers() } } // MARK: - App Link @@ -858,32 +919,27 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD // MARK: - Call handling func hasIncomingCallWaiting() -> Bool { - guard let call = AppEnvironment.shared.callManager.currentCall else { return false } - - return !call.hasStartedConnecting + return (dependencies[singleton: .callManager].currentCall?.hasStartedConnecting == false) } func hasCallOngoing() -> Bool { - guard let call = AppEnvironment.shared.callManager.currentCall else { return false } - - return !call.hasEnded + return (dependencies[singleton: .callManager].currentCall?.hasEnded == false) } func handleAppActivatedWithOngoingCallIfNeeded() { guard - let call: SessionCall = (AppEnvironment.shared.callManager.currentCall as? SessionCall), - MiniCallView.current == nil, - Singleton.hasAppContext + let call: SessionCall = (dependencies[singleton: .callManager].currentCall as? SessionCall), + MiniCallView.current == nil else { return } - if let callVC = Singleton.appContext.frontmostViewController as? CallVC, callVC.call.uuid == call.uuid { + if let callVC = dependencies[singleton: .appContext].frontMostViewController as? CallVC, callVC.call.uuid == call.uuid { return } // FIXME: Handle more gracefully - guard let presentingVC = Singleton.appContext.frontmostViewController else { preconditionFailure() } + guard let presentingVC = dependencies[singleton: .appContext].frontMostViewController else { preconditionFailure() } - let callVC: CallVC = CallVC(for: call) + let callVC: CallVC = CallVC(for: call, using: dependencies) if let conversationVC: ConversationVC = (presentingVC as? TopBannerController)?.wrappedViewController() as? ConversationVC, diff --git a/Session/Meta/AppEnvironment.swift b/Session/Meta/AppEnvironment.swift deleted file mode 100644 index 6c9d0b27242..00000000000 --- a/Session/Meta/AppEnvironment.swift +++ /dev/null @@ -1,56 +0,0 @@ -// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved. -// -// stringlint:disable - -import Foundation -import SessionUtilitiesKit -import SignalUtilitiesKit -import SessionMessagingKit - -public class AppEnvironment { - - private static var _shared: AppEnvironment = AppEnvironment() - - public class var shared: AppEnvironment { - get { return _shared } - set { - guard SNUtilitiesKit.isRunningTests else { - Log.error("[AppEnvironment] Can only switch environments in tests.") - return - } - - _shared = newValue - } - } - - public var callManager: SessionCallManager - public var notificationPresenter: NotificationPresenter - public var pushRegistrationManager: PushRegistrationManager - - // Stored properties cannot be marked as `@available`, only classes and functions. - // Instead, store a private `Any` and wrap it with a public `@available` getter - private var _userNotificationActionHandler: Any? - - public var userNotificationActionHandler: UserNotificationActionHandler { - return _userNotificationActionHandler as! UserNotificationActionHandler - } - - private init() { - self.callManager = SessionCallManager() - self.notificationPresenter = NotificationPresenter() - self.pushRegistrationManager = PushRegistrationManager() - self._userNotificationActionHandler = UserNotificationActionHandler() - - SwiftSingletons.register(self) - } - - public func setup() { - // Hang certain singletons on Environment too. - SessionEnvironment.shared?.callManager.mutate { - $0 = callManager - } - SessionEnvironment.shared?.notificationsManager.mutate { - $0 = notificationPresenter - } - } -} diff --git a/Session/Meta/Images.xcassets/Settings/icon_members.imageset/Contents.json b/Session/Meta/Images.xcassets/Settings/icon_members.imageset/Contents.json new file mode 100644 index 00000000000..659504d4e24 --- /dev/null +++ b/Session/Meta/Images.xcassets/Settings/icon_members.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "icon_members.svg", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Session/Meta/Images.xcassets/Settings/icon_members.imageset/icon_members.svg b/Session/Meta/Images.xcassets/Settings/icon_members.imageset/icon_members.svg new file mode 100644 index 00000000000..545a11b526f --- /dev/null +++ b/Session/Meta/Images.xcassets/Settings/icon_members.imageset/icon_members.svg @@ -0,0 +1,18 @@ + + + + + + + + + + + + + + + + + + diff --git a/Session/Meta/MainAppContext.swift b/Session/Meta/MainAppContext.swift index 4f917da59c4..4ae9c3fd4d9 100644 --- a/Session/Meta/MainAppContext.swift +++ b/Session/Meta/MainAppContext.swift @@ -1,13 +1,14 @@ // Copyright © 2023 Rangeproof Pty Ltd. All rights reserved. import UIKit +import SessionUIKit import SessionUtilitiesKit final class MainAppContext: AppContext { - var _temporaryDirectory: String? + private let dependencies: Dependencies var reportedApplicationState: UIApplication.State - let appLaunchTime = Date() + var appLaunchTime: Date = Date() let isMainApp: Bool = true var isMainAppAndActive: Bool { var result: Bool = false @@ -22,18 +23,14 @@ final class MainAppContext: AppContext { return result } - var frontmostViewController: UIViewController? { UIApplication.shared.frontmostViewControllerIgnoringAlerts } + var frontMostViewController: UIViewController? { + UIApplication.shared.frontMostViewController(ignoringAlerts: true, using: dependencies) + } var backgroundTimeRemaining: TimeInterval { UIApplication.shared.backgroundTimeRemaining } var mainWindow: UIWindow? var wasWokenUpByPushNotification: Bool = false - private static var _isRTL: Bool = { - return (UIApplication.shared.userInterfaceLayoutDirection == .rightToLeft) - }() - - var isRTL: Bool { return MainAppContext._isRTL } - var statusBarHeight: CGFloat { UIApplication.shared.statusBarFrame.size.height } var openSystemSettingsAction: UIAlertAction? { let result = UIAlertAction( @@ -45,9 +42,14 @@ final class MainAppContext: AppContext { return result } + static func determineDeviceRTL() -> Bool { + return (UIApplication.shared.userInterfaceLayoutDirection == .rightToLeft) + } + // MARK: - Initialization - init() { + init(using dependencies: Dependencies) { + self.dependencies = dependencies self.reportedApplicationState = .inactive NotificationCenter.default.addObserver( @@ -130,18 +132,9 @@ final class MainAppContext: AppContext { func setMainWindow(_ mainWindow: UIWindow) { self.mainWindow = mainWindow - } - - func setStatusBarHidden(_ isHidden: Bool, animated isAnimated: Bool) { - UIApplication.shared.setStatusBarHidden(isHidden, with: (isAnimated ? .slide : .none)) - } - - func isAppForegroundAndActive() -> Bool { - return (reportedApplicationState == .active) - } - - func isInBackground() -> Bool { - return (reportedApplicationState == .background) + + // Store in SessionUIKit to avoid needing the SessionUtilitiesKit dependency + SNUIKit.setMainWindow(mainWindow) } func beginBackgroundTask(expirationHandler: @escaping () -> ()) -> UIBackgroundTaskIdentifier { @@ -169,52 +162,4 @@ final class MainAppContext: AppContext { } UIApplication.shared.isIdleTimerDisabled = shouldBeBlocking } - - func setNetworkActivityIndicatorVisible(_ value: Bool) { - UIApplication.shared.isNetworkActivityIndicatorVisible = value - } - - // MARK: - - - // stringlint:ignore_contents - func clearOldTemporaryDirectories() { - // We use the lowest priority queue for this, and wait N seconds - // to avoid interfering with app startup. - DispatchQueue.global(qos: .background).asyncAfter(deadline: .now() + .seconds(3)) { [weak self] in - guard - self?.isAppForegroundAndActive == true, // Abort if app not active - let thresholdDate: Date = self?.appLaunchTime - else { return } - - // Ignore the "current" temp directory. - let currentTempDirName: String = URL(fileURLWithPath: Singleton.appContext.temporaryDirectory).lastPathComponent - let dirPath = NSTemporaryDirectory() - - guard let fileNames: [String] = try? FileManager.default.contentsOfDirectory(atPath: dirPath) else { return } - - fileNames.forEach { fileName in - guard fileName != currentTempDirName else { return } - - // Delete files with either: - // - // a) "ows_temp" name prefix. - // b) modified time before app launch time. - let filePath: String = URL(fileURLWithPath: dirPath).appendingPathComponent(fileName).path - - if !fileName.hasPrefix("ows_temp") { - // It's fine if we can't get the attributes (the file may have been deleted since we found it), - // also don't delete files which were created in the last N minutes - guard - let attributes: [FileAttributeKey: Any] = try? FileManager.default.attributesOfItem(atPath: filePath), - let modificationDate: Date = attributes[.modificationDate] as? Date, - modificationDate.timeIntervalSince1970 <= thresholdDate.timeIntervalSince1970 - else { return } - } - - // This can happen if the app launches before the phone is unlocked. - // Clean up will occur when app becomes active. - try? FileSystem.deleteFile(at: filePath) - } - } - } } diff --git a/Session/Meta/Session+SNUIKit.swift b/Session/Meta/Session+SNUIKit.swift new file mode 100644 index 00000000000..6f52259d332 --- /dev/null +++ b/Session/Meta/Session+SNUIKit.swift @@ -0,0 +1,105 @@ +// Copyright © 2024 Rangeproof Pty Ltd. All rights reserved. + +import UIKit +import SessionUIKit +import SessionSnodeKit +import SessionUtilitiesKit + +// MARK: - SessionSNUIKitConfig + +internal struct SessionSNUIKitConfig: SNUIKit.ConfigType { + private let dependencies: Dependencies + + var maxFileSize: UInt { Network.maxFileSize } + var isStorageValid: Bool { dependencies[singleton: .storage].isValid } + + // MARK: - Initialization + + init(using dependencies: Dependencies) { + self.dependencies = dependencies + } + + // MARK: - Functions + + func themeChanged(_ theme: Theme, _ primaryColor: Theme.PrimaryColor, _ matchSystemNightModeSetting: Bool) { + dependencies[singleton: .storage].write { db in + db[.theme] = theme + db[.themePrimaryColor] = primaryColor + db[.themeMatchSystemDayNightCycle] = matchSystemNightModeSetting + } + } + + func persistentTopBannerChanged(warningKey: String?) { + dependencies[defaults: .appGroup, key: .topBannerWarningToShow] = warningKey + } + + func cachedContextualActionInfo(tableViewHash: Int, sideKey: String) -> [Int: Any]? { + dependencies[cache: .general].contextualActionLookupMap + .getting(tableViewHash)? + .getting(sideKey) + } + + func cacheContextualActionInfo(tableViewHash: Int, sideKey: String, actionIndex: Int, actionInfo: Any) { + dependencies.mutate(cache: .general) { cache in + let updatedLookup = (cache.contextualActionLookupMap[tableViewHash] ?? [:]) + .setting( + sideKey, + ((cache.contextualActionLookupMap[tableViewHash] ?? [:]).getting(sideKey) ?? [:]) + .setting(actionIndex, actionInfo) + ) + + cache.contextualActionLookupMap[tableViewHash] = updatedLookup + } + } + + func removeCachedContextualActionInfo(tableViewHash: Int, keys: [String]) { + dependencies.mutate(cache: .general) { cache in + keys.forEach { key in + cache.contextualActionLookupMap[tableViewHash]?[key] = nil + } + + if cache.contextualActionLookupMap[tableViewHash]?.isEmpty == true { + cache.contextualActionLookupMap[tableViewHash] = nil + } + } + } + + func placeholderIconCacher(cacheKey: String, generator: @escaping () -> UIImage) -> UIImage { + if let cachedIcon: UIImage = dependencies[cache: .general].placeholderCache.object(forKey: cacheKey as NSString) { + return cachedIcon + } + + let generatedImage: UIImage = generator() + dependencies.mutate(cache: .general) { + $0.placeholderCache.setObject(generatedImage, forKey: cacheKey as NSString) + } + + return generatedImage + } + + func localizedString(for key: String) -> String { + return key.localized() + } + + public static func localizedFormatted(_ helper: LocalizationHelper, _ baseFont: UIFont) -> NSAttributedString { + return NSAttributedString(stringWithHTMLTags: helper.localized(), font: baseFont) + } + + public static func localizedDeformatted(_ helper: LocalizationHelper) -> String { + return NSAttributedString(stringWithHTMLTags: helper.localized(), font: .systemFont(ofSize: 14)).string + } +} + +// MARK: - SNUIKit Localization + +public extension LocalizationHelper { + func localizedFormatted(in view: FontAccessible) -> NSAttributedString { + return localizedFormatted(baseFont: (view.fontValue ?? .systemFont(ofSize: 14))) + } +} + +public extension String { + func localizedFormatted(in view: FontAccessible) -> NSAttributedString { + return LocalizationHelper(template: self).localizedFormatted(in: view) + } +} diff --git a/Session/Meta/SessionApp.swift b/Session/Meta/SessionApp.swift index fc02eb667c4..69da9235b0c 100644 --- a/Session/Meta/SessionApp.swift +++ b/Session/Meta/SessionApp.swift @@ -7,10 +7,20 @@ import SessionUtilitiesKit import SessionMessagingKit import SessionUIKit -public struct SessionApp { - // FIXME: Refactor this to be protocol based for unit testing (or even dynamic based on view hierarchy - do want to avoid needing to use the main thread to access them though) - static let homeViewController: Atomic = Atomic(nil) - static let currentlyOpenConversationViewController: Atomic = Atomic(nil) +// MARK: - Singleton + +public extension Singleton { + static let app: SingletonConfig = Dependencies.create( + identifier: "app", + createInstance: { dependencies in SessionApp(using: dependencies) } + ) +} + +// MARK: - SessionApp + +public class SessionApp: SessionAppType { + private let dependencies: Dependencies + private var homeViewController: HomeVC? static var versionInfo: String { let buildNumber: String = (Bundle.main.infoDictionary?["CFBundleVersion"] as? String) @@ -23,38 +33,59 @@ public struct SessionApp { let versionInfo: [String] = [ "iOS \(UIDevice.current.systemVersion)", appVersion, - "libSession: \(LibSession.libSessionVersion)", + "libSession: \(LibSession.version)", commitInfo ].compactMap { $0 } return versionInfo.joined(separator: ", ") } - // MARK: - View Convenience Methods + // MARK: - Initialization + + init(using dependencies: Dependencies) { + self.dependencies = dependencies + } + + // MARK: - Functions + + public func setHomeViewController(_ homeViewController: HomeVC) { + self.homeViewController = homeViewController + } + + public func showHomeView() { + guard Thread.isMainThread else { + return DispatchQueue.main.async { + self.showHomeView() + } + } + + let homeViewController: HomeVC = HomeVC(using: dependencies) + let navController: UINavigationController = StyledNavigationController(rootViewController: homeViewController) + (UIApplication.shared.delegate as? AppDelegate)?.window?.rootViewController = navController + self.homeViewController = homeViewController + } - public static func presentConversationCreatingIfNeeded( + public func presentConversationCreatingIfNeeded( for threadId: String, variant: SessionThread.Variant, action: ConversationViewModel.Action = .none, dismissing presentingViewController: UIViewController?, animated: Bool ) { - let threadInfo: (threadExists: Bool, isMessageRequest: Bool)? = Storage.shared.read { db in + guard let homeViewController: HomeVC = self.homeViewController else { + Log.error("[SessionApp] Unable to present conversation due to missing HomeVC.") + return + } + + let threadInfo: (threadExists: Bool, isMessageRequest: Bool)? = dependencies[singleton: .storage].read { [dependencies] db in let isMessageRequest: Bool = { switch variant { - case .contact: + case .contact, .group: return SessionThread .isMessageRequest( - id: threadId, - variant: .contact, - currentUserPublicKey: getUserHexEncodedPublicKey(db), - shouldBeVisible: nil, - contactIsApproved: (try? Contact - .filter(id: threadId) - .select(.isApproved) - .asRequest(of: Bool.self) - .fetchOne(db)) - .defaulting(to: false), + db, + threadId: threadId, + userSessionId: dependencies[cache: .general].sessionId, includeNonVisible: true ) @@ -65,67 +96,62 @@ public struct SessionApp { return (SessionThread.filter(id: threadId).isNotEmpty(db), isMessageRequest) } - // Store the post-creation logic in a closure to avoid duplication - let afterThreadCreated: () -> () = { - presentingViewController?.dismiss(animated: true, completion: nil) - - homeViewController.wrappedValue?.show( - threadId, - variant: variant, - isMessageRequest: (threadInfo?.isMessageRequest == true), - with: action, - focusedInteractionInfo: nil, - animated: animated - ) - } - /// The thread should generally exist at the time of calling this method, but on the off chance it doesn't then we need to `fetchOrCreate` it and /// should do it on a background thread just in case something is keeping the DBWrite thread busy as in the past this could cause the app to hang - guard threadInfo?.threadExists == true else { - DispatchQueue.global(qos: .userInitiated).async { - Storage.shared.write { db in - try SessionThread.fetchOrCreate(db, id: threadId, variant: variant, shouldBeVisible: nil) - } - - // Send back to main thread for UI transitions - DispatchQueue.main.async { - afterThreadCreated() - } + creatingThreadIfNeededThenRunOnMain( + threadId: threadId, + variant: variant, + threadExists: (threadInfo?.threadExists == true), + onComplete: { [weak self] in + self?.showConversation( + threadId: threadId, + threadVariant: variant, + isMessageRequest: (threadInfo?.isMessageRequest == true), + action: action, + dismissing: presentingViewController, + homeViewController: homeViewController, + animated: animated + ) } - return - } + ) + } + + public func createNewConversation() { + guard let homeViewController: HomeVC = self.homeViewController else { return } - // Send to main thread if needed - guard Thread.isMainThread else { - DispatchQueue.main.async { - afterThreadCreated() - } - return - } + let viewController = SessionHostingViewController( + rootView: StartConversationScreen(using: dependencies), + customizedNavigationBackground: .backgroundSecondary + ) + viewController.setNavBarTitle("conversationsStart".localized()) + viewController.setUpDismissingButton(on: .right) - afterThreadCreated() + let navigationController = StyledNavigationController(rootViewController: viewController) + if UIDevice.current.isIPad { + navigationController.modalPresentationStyle = .fullScreen + } + navigationController.modalPresentationCapturesStatusBarAppearance = true + homeViewController.present(navigationController, animated: true, completion: nil) } - - // MARK: - Functions - public static func resetAppData( - using dependencies: Dependencies, - onReset: (() -> ())? = nil - ) { + public func resetData(onReset: (() -> ())) { + homeViewController = nil LibSession.clearLoggers() - LibSession.clearMemoryState(using: dependencies) - LibSession.clearSnodeCache() - LibSession.suspendNetworkAccess() - PushNotificationAPI.resetKeys() - dependencies.storage.resetAllStorage() - ProfileManager.resetProfileStorage() - Attachment.resetAttachmentStorage() - AppEnvironment.shared.notificationPresenter.clearAllNotifications() + dependencies.remove(cache: .libSession) + dependencies.mutate(cache: .libSessionNetwork) { + $0.clearSnodeCache() + $0.suspendNetworkAccess() + } + dependencies[singleton: .storage].resetAllStorage() + DisplayPictureManager.resetStorage(using: dependencies) + Attachment.resetAttachmentStorage(using: dependencies) + dependencies[singleton: .notificationsManager].clearAllNotifications() + try? dependencies[singleton: .keychain].removeAll() - onReset?() + onReset() Log.info("Data Reset Complete.") Log.flush() - + /// Wait until the next run loop to kill the app (hoping to avoid a crash due to the connection closes /// triggering logs) DispatchQueue.main.async { @@ -133,16 +159,94 @@ public struct SessionApp { } } - public static func showHomeView(using dependencies: Dependencies) { - guard Thread.isMainThread else { - DispatchQueue.main.async { - self.showHomeView(using: dependencies) + // MARK: - Internal Functions + + private func creatingThreadIfNeededThenRunOnMain( + threadId: String, + variant: SessionThread.Variant, + threadExists: Bool, + onComplete: @escaping () -> Void + ) { + guard !threadExists else { + switch Thread.isMainThread { + case true: return onComplete() + case false: return DispatchQueue.main.async(using: dependencies) { onComplete() } + } + } + guard !Thread.isMainThread else { + return DispatchQueue.global(qos: .userInitiated).async(using: dependencies) { [weak self] in + self?.creatingThreadIfNeededThenRunOnMain( + threadId: threadId, + variant: variant, + threadExists: threadExists, + onComplete: onComplete + ) } - return } - let homeViewController: HomeVC = HomeVC(using: dependencies) - let navController: UINavigationController = StyledNavigationController(rootViewController: homeViewController) - (UIApplication.shared.delegate as? AppDelegate)?.window?.rootViewController = navController + dependencies[singleton: .storage].write { [dependencies] db in + try SessionThread.fetchOrCreate( + db, + id: threadId, + variant: variant, + creationDateTimestamp: (dependencies[cache: .snodeAPI].currentOffsetTimestampMs() / 1000), + shouldBeVisible: nil, + calledFromConfig: nil, + using: dependencies + ) + } + + DispatchQueue.main.async(using: dependencies) { + onComplete() + } + } + + private func showConversation( + threadId: String, + threadVariant: SessionThread.Variant, + isMessageRequest: Bool, + action: ConversationViewModel.Action, + dismissing presentingViewController: UIViewController?, + homeViewController: HomeVC, + animated: Bool + ) { + presentingViewController?.dismiss(animated: true, completion: nil) + + homeViewController.navigationController?.setViewControllers( + [ + homeViewController, + (isMessageRequest && action != .compose ? + SessionTableViewController(viewModel: MessageRequestsViewModel(using: dependencies)) : + nil + ), + ConversationVC( + threadId: threadId, + threadVariant: threadVariant, + focusedInteractionInfo: nil, + using: dependencies + ) + ].compactMap { $0 }, + animated: animated + ) } } + +// MARK: - SessionAppType + +public protocol SessionAppType { + func setHomeViewController(_ homeViewController: HomeVC) + func showHomeView() + func presentConversationCreatingIfNeeded( + for threadId: String, + variant: SessionThread.Variant, + action: ConversationViewModel.Action, + dismissing presentingViewController: UIViewController?, + animated: Bool + ) + func createNewConversation() + func resetData(onReset: (() -> ())) +} + +public extension SessionAppType { + func resetData() { resetData(onReset: {}) } +} diff --git a/Session/Meta/Settings.bundle/Dependencies-settings-metadata.plist b/Session/Meta/Settings.bundle/Dependencies-settings-metadata.plist new file mode 100644 index 00000000000..886bd51d5d1 --- /dev/null +++ b/Session/Meta/Settings.bundle/Dependencies-settings-metadata.plist @@ -0,0 +1,683 @@ + + + + + PreferenceSpecifiers + + + FooterText + This application makes use of the following third party libraries: + Title + Acknowledgements + Type + PSGroupSpecifier + + + FooterText + + Title + xcbeautify + Type + PSGroupSpecifier + + + FooterText + BSD 3-Clause License + +Copyright (c) 2010-2022, Deusty, LLC +All rights reserved. + +Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. + +3. Neither the name of Deusty nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission of Deusty, LLC. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + + Title + CocoaLumberjack + Type + PSGroupSpecifier + + + FooterText + Copyright (C) 2015-2023 Gwendal Roué + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + Title + GRDB.swift + Type + PSGroupSpecifier + + + FooterText + Copyright (c) 2008, ZETETIC LLC +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + * Neither the name of the ZETETIC LLC nor the + names of its contributors may be used to endorse or promote products + derived from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY ZETETIC LLC ''AS IS'' AND ANY +EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL ZETETIC LLC BE LIABLE FOR ANY +DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND +ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + + Title + SQLCipher + Type + PSGroupSpecifier + + + FooterText + ISC License + +Copyright (c) 2014-2020, Frank Denis <j at pureftpd dot org> + +Permission to use, copy, modify, and/or distribute this software for any +purpose with or without fee is hereby granted, provided that the above +copyright notice and this permission notice appear in all copies. + +THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR +ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF +OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + + + Title + Sodium + Type + PSGroupSpecifier + + + FooterText + Copyright (c) 2011, The WebRTC project authors. All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + + * Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in + the documentation and/or other materials provided with the + distribution. + + * Neither the name of Google nor the names of its contributors may + be used to endorse or promote products derived from this software + without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + + Title + WebRTC-lib + Type + PSGroupSpecifier + + + FooterText + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + Title + DifferenceKit + Type + PSGroupSpecifier + + + FooterText + The MIT License (MIT) + +Copyright (c) 2016 Vinh Nguyen + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + + Title + NVActivityIndicatorView + Type + PSGroupSpecifier + + + FooterText + The MIT License (MIT) + +Copyright (c) 2015 ibireme <ibireme@gmail.com> + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + + + Title + YYImage + Type + PSGroupSpecifier + + + FooterText + Copyright (c) 2010, Google Inc. All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + + * Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in + the documentation and/or other materials provided with the + distribution. + + * Neither the name of Google nor the names of its contributors may + be used to endorse or promote products derived from this software + without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + + + Title + libwebp + Type + PSGroupSpecifier + + + FooterText + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + + + +## Runtime Library Exception to the Apache 2.0 License: ## + + + As an exception, if you use this Software to compile your source code and + portions of this Software are embedded into the binary product as a result, + you may redistribute such product without providing attribution as would + otherwise be required by Sections 4(a), 4(b) and 4(d) of the License. + + Title + SwiftProtobuf + Type + PSGroupSpecifier + + + StringsTable + Acknowledgements + Title + Acknowledgements + + diff --git a/Session/Meta/Settings.bundle/ThirdPartyLicenses.plist b/Session/Meta/Settings.bundle/ThirdPartyLicenses.plist index 1e94962d94c..6ca29b9545a 100644 --- a/Session/Meta/Settings.bundle/ThirdPartyLicenses.plist +++ b/Session/Meta/Settings.bundle/ThirdPartyLicenses.plist @@ -985,6 +985,88 @@ SOFTWARE. License The MIT License (MIT) +Copyright (c) 2016 swiftlyfalling (https://github.com/swiftlyfalling) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + + Title + session-grdb-swift + + + License + The author disclaims copyright to this source code. In place of +a legal notice, here is a blessing: + + * May you do good and not evil. + * May you find forgiveness for yourself and forgive others. + * May you share freely, never taking more than you give. + + Title + session-grdb-swift + + + License + /* +** LICENSE for the sqlite3 WebAssembly/JavaScript APIs. +** +** This bundle (typically released as sqlite3.js or sqlite3.mjs) +** is an amalgamation of JavaScript source code from two projects: +** +** 1) https://emscripten.org: the Emscripten "glue code" is covered by +** the terms of the MIT license and University of Illinois/NCSA +** Open Source License, as described at: +** +** https://emscripten.org/docs/introducing_emscripten/emscripten_license.html +** +** 2) https://sqlite.org: all code and documentation labeled as being +** from this source are released under the same terms as the sqlite3 +** C library: +** +** 2022-10-16 +** +** The author disclaims copyright to this source code. In place of a +** legal notice, here is a blessing: +** +** * May you do good and not evil. +** * May you find forgiveness for yourself and forgive others. +** * May you share freely, never taking more than you give. +*/ + + Title + session-grdb-swift + + + License + The author disclaims copyright to this source code. In place of +a legal notice, here is a blessing: + + May you do good and not evil. + May you find forgiveness for yourself and forgive others. + May you share freely, never taking more than you give. + + Title + session-grdb-swift + + + License + The MIT License (MIT) + Copyright (c) 2015 ibireme <ibireme@gmail.com> Permission is hereby granted, free of charge, to any person obtaining a copy diff --git a/Session/Meta/Signal-Bridging-Header.h b/Session/Meta/Signal-Bridging-Header.h deleted file mode 100644 index 51e3c026845..00000000000 --- a/Session/Meta/Signal-Bridging-Header.h +++ /dev/null @@ -1,7 +0,0 @@ -// -// Copyright (c) 2019 Open Whisper Systems. All rights reserved. -// -// Separate iOS Frameworks from other imports. -#import "OWSAudioPlayer.h" -#import "OWSBezierPathView.h" -#import "OWSWindowManager.h" diff --git a/Session/Meta/Translations/Localizable.xcstrings b/Session/Meta/Translations/Localizable.xcstrings index de86dad2bc8..f08574087c9 100644 --- a/Session/Meta/Translations/Localizable.xcstrings +++ b/Session/Meta/Translations/Localizable.xcstrings @@ -17320,6 +17320,46 @@ "adminSendingPromotion" : { "extractionState" : "manual", "localizations" : { + "cs" : { + "stringUnit" : { + "state" : "translated", + "value" : "%#@arg1@" + }, + "substitutions" : { + "arg1" : { + "argNum" : 1, + "formatSpecifier" : "lld", + "variations" : { + "plural" : { + "few" : { + "stringUnit" : { + "state" : "translated", + "value" : "Odesílání povýšení na správce" + } + }, + "many" : { + "stringUnit" : { + "state" : "translated", + "value" : "Odesílání povýšení na správce" + } + }, + "one" : { + "stringUnit" : { + "state" : "translated", + "value" : "Odesílání povýšení na správce" + } + }, + "other" : { + "stringUnit" : { + "state" : "translated", + "value" : "Odesílání povýšení na správce" + } + } + } + } + } + } + }, "en" : { "stringUnit" : { "state" : "translated", @@ -17347,6 +17387,34 @@ } } } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "%#@arg1@" + }, + "substitutions" : { + "arg1" : { + "argNum" : 1, + "formatSpecifier" : "lld", + "variations" : { + "plural" : { + "one" : { + "stringUnit" : { + "state" : "translated", + "value" : "Beheerder promotie versturen" + } + }, + "other" : { + "stringUnit" : { + "state" : "translated", + "value" : "Beheerder promoties versturen" + } + } + } + } + } + } } } }, @@ -70992,6 +71060,17 @@ } } }, + "callsPermissionsRequiredDescription1" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "You can enable the \"Voice and Video Calls\" permission in Permissions Settings." + } + } + } + }, "callsReconnecting" : { "extractionState" : "manual", "localizations" : { @@ -134980,6 +135059,46 @@ "deleteMessageConfirm" : { "extractionState" : "manual", "localizations" : { + "cs" : { + "stringUnit" : { + "state" : "translated", + "value" : "%#@arg1@" + }, + "substitutions" : { + "arg1" : { + "argNum" : 1, + "formatSpecifier" : "lld", + "variations" : { + "plural" : { + "few" : { + "stringUnit" : { + "state" : "translated", + "value" : "Opravdu chcete smazat tyto zprávy?" + } + }, + "many" : { + "stringUnit" : { + "state" : "translated", + "value" : "Opravdu chcete smazat tyto zprávy?" + } + }, + "one" : { + "stringUnit" : { + "state" : "translated", + "value" : "Opravdu chcete smazat tuto zprávu?" + } + }, + "other" : { + "stringUnit" : { + "state" : "translated", + "value" : "Opravdu chcete smazat tyto zprávy?" + } + } + } + } + } + } + }, "en" : { "stringUnit" : { "state" : "translated", @@ -135007,6 +135126,102 @@ } } } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "%#@arg1@" + }, + "substitutions" : { + "arg1" : { + "argNum" : 1, + "formatSpecifier" : "lld", + "variations" : { + "plural" : { + "one" : { + "stringUnit" : { + "state" : "translated", + "value" : "Weet u zeker dat u dit bericht wilt verwijderen?" + } + }, + "other" : { + "stringUnit" : { + "state" : "translated", + "value" : "Weet u zeker dat u deze berichten wilt verwijderen?" + } + } + } + } + } + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "%#@arg1@" + }, + "substitutions" : { + "arg1" : { + "argNum" : 1, + "formatSpecifier" : "lld", + "variations" : { + "plural" : { + "few" : { + "stringUnit" : { + "state" : "translated", + "value" : "Вы уверены, что хотите удалить эти сообщения?" + } + }, + "many" : { + "stringUnit" : { + "state" : "translated", + "value" : "Вы уверены, что хотите удалить эти сообщения?" + } + }, + "one" : { + "stringUnit" : { + "state" : "translated", + "value" : "Вы уверены, что хотите удалить это сообщение?" + } + }, + "other" : { + "stringUnit" : { + "state" : "translated", + "value" : "Вы уверены, что хотите удалить эти сообщения?" + } + } + } + } + } + } + }, + "sv-SE" : { + "stringUnit" : { + "state" : "translated", + "value" : "%#@arg1@" + }, + "substitutions" : { + "arg1" : { + "argNum" : 1, + "formatSpecifier" : "lld", + "variations" : { + "plural" : { + "one" : { + "stringUnit" : { + "state" : "translated", + "value" : "Är du säker på att du vill radera detta meddelande?" + } + }, + "other" : { + "stringUnit" : { + "state" : "translated", + "value" : "Är du säker på att du vill radera dessa meddelanden?" + } + } + } + } + } + } } } }, @@ -138236,6 +138451,46 @@ "deleteMessageDescriptionDevice" : { "extractionState" : "manual", "localizations" : { + "cs" : { + "stringUnit" : { + "state" : "translated", + "value" : "%#@arg1@" + }, + "substitutions" : { + "arg1" : { + "argNum" : 1, + "formatSpecifier" : "lld", + "variations" : { + "plural" : { + "few" : { + "stringUnit" : { + "state" : "translated", + "value" : "Opravdu chcete smazat tyto zprávy pouze z tohoto zařízení?" + } + }, + "many" : { + "stringUnit" : { + "state" : "translated", + "value" : "Opravdu chcete smazat tyto zprávy pouze z tohoto zařízení?" + } + }, + "one" : { + "stringUnit" : { + "state" : "translated", + "value" : "Opravdu chcete smazat tuto zprávu pouze z tohoto zařízení?" + } + }, + "other" : { + "stringUnit" : { + "state" : "translated", + "value" : "Opravdu chcete smazat tyto zprávy pouze z tohoto zařízení?" + } + } + } + } + } + } + }, "en" : { "stringUnit" : { "state" : "translated", @@ -138263,6 +138518,74 @@ } } } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "%#@arg1@" + }, + "substitutions" : { + "arg1" : { + "argNum" : 1, + "formatSpecifier" : "lld", + "variations" : { + "plural" : { + "one" : { + "stringUnit" : { + "state" : "translated", + "value" : "Weet u zeker dat u dit bericht enkel van dit apparaat wilt verwijderen?" + } + }, + "other" : { + "stringUnit" : { + "state" : "translated", + "value" : "Weet u zeker dat u deze berichten enkel van dit apparaat wilt verwijderen?" + } + } + } + } + } + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "%#@arg1@" + }, + "substitutions" : { + "arg1" : { + "argNum" : 1, + "formatSpecifier" : "lld", + "variations" : { + "plural" : { + "few" : { + "stringUnit" : { + "state" : "translated", + "value" : "Вы уверены, что хотите удалить эти сообщения только с этого устройства?" + } + }, + "many" : { + "stringUnit" : { + "state" : "translated", + "value" : "Вы уверены, что хотите удалить эти сообщения только с этого устройства?" + } + }, + "one" : { + "stringUnit" : { + "state" : "translated", + "value" : "Вы уверены, что хотите удалить это сообщение только с этого устройства?" + } + }, + "other" : { + "stringUnit" : { + "state" : "translated", + "value" : "Вы уверены, что хотите удалить эти сообщения только с этого устройства?" + } + } + } + } + } + } } } }, @@ -142512,6 +142835,46 @@ "deleteMessageNoteToSelfWarning" : { "extractionState" : "manual", "localizations" : { + "cs" : { + "stringUnit" : { + "state" : "translated", + "value" : "%#@arg1@" + }, + "substitutions" : { + "arg1" : { + "argNum" : 1, + "formatSpecifier" : "lld", + "variations" : { + "plural" : { + "few" : { + "stringUnit" : { + "state" : "translated", + "value" : "Některé z vybraných zpráv nelze smazat ze všech vašich zařízení" + } + }, + "many" : { + "stringUnit" : { + "state" : "translated", + "value" : "Některé z vybraných zpráv nelze smazat ze všech vašich zařízení" + } + }, + "one" : { + "stringUnit" : { + "state" : "translated", + "value" : "Tuto zprávu nelze smazat ze všech vašich zařízení" + } + }, + "other" : { + "stringUnit" : { + "state" : "translated", + "value" : "Některé z vybraných zpráv nelze smazat ze všech vašich zařízení" + } + } + } + } + } + } + }, "en" : { "stringUnit" : { "state" : "translated", @@ -143030,6 +143393,46 @@ "deleteMessageWarning" : { "extractionState" : "manual", "localizations" : { + "cs" : { + "stringUnit" : { + "state" : "translated", + "value" : "%#@arg1@" + }, + "substitutions" : { + "arg1" : { + "argNum" : 1, + "formatSpecifier" : "lld", + "variations" : { + "plural" : { + "few" : { + "stringUnit" : { + "state" : "translated", + "value" : "Některé z vybraných zpráv nelze smazat pro všechny" + } + }, + "many" : { + "stringUnit" : { + "state" : "translated", + "value" : "Některé z vybraných zpráv nelze smazat pro všechny" + } + }, + "one" : { + "stringUnit" : { + "state" : "translated", + "value" : "Tuto zprávu nelze smazat pro všechny" + } + }, + "other" : { + "stringUnit" : { + "state" : "translated", + "value" : "Některé z vybraných zpráv nelze smazat pro všechny" + } + } + } + } + } + } + }, "en" : { "stringUnit" : { "state" : "translated", @@ -143057,6 +143460,34 @@ } } } + }, + "fa" : { + "stringUnit" : { + "state" : "translated", + "value" : "%#@arg1@" + }, + "substitutions" : { + "arg1" : { + "argNum" : 1, + "formatSpecifier" : "lld", + "variations" : { + "plural" : { + "one" : { + "stringUnit" : { + "state" : "translated", + "value" : "این پیام قابل حذف برای همه نیست" + } + }, + "other" : { + "stringUnit" : { + "state" : "translated", + "value" : "برخی از پیام هایی که انتخاب کرده اید را نمیتوان برای همه حذف کرد" + } + } + } + } + } + } } } }, @@ -164660,6 +165091,12 @@ "displayNameVisible" : { "extractionState" : "manual", "localizations" : { + "cs" : { + "stringUnit" : { + "state" : "translated", + "value" : "Vaše zobrazované jméno je viditelné pro uživatele, skupiny a komunity, se kterými jste ve spojení." + } + }, "en" : { "stringUnit" : { "state" : "translated", @@ -187345,6 +187782,17 @@ } } }, + "groupDeletedMemberDescription" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "{group_name} has been deleted by a group admin. You will not be able to send any more messages." + } + } + } + }, "groupDescriptionEnter" : { "extractionState" : "manual", "localizations" : { @@ -193144,6 +193592,12 @@ "groupInviteReinvite" : { "extractionState" : "manual", "localizations" : { + "cs" : { + "stringUnit" : { + "state" : "translated", + "value" : "{name} vás pozval(a), abyste se znovu připojili k {group_name}, kde jste správcem." + } + }, "en" : { "stringUnit" : { "state" : "translated", @@ -193155,6 +193609,12 @@ "groupInviteReinviteYou" : { "extractionState" : "manual", "localizations" : { + "cs" : { + "stringUnit" : { + "state" : "translated", + "value" : "Byli jste pozváni, abyste se znovu připojili k {group_name}, kde jste správcem." + } + }, "en" : { "stringUnit" : { "state" : "translated", @@ -193166,6 +193626,46 @@ "groupInviteSending" : { "extractionState" : "manual", "localizations" : { + "cs" : { + "stringUnit" : { + "state" : "translated", + "value" : "%#@arg1@" + }, + "substitutions" : { + "arg1" : { + "argNum" : 1, + "formatSpecifier" : "lld", + "variations" : { + "plural" : { + "few" : { + "stringUnit" : { + "state" : "translated", + "value" : "Odeslání pozvánek" + } + }, + "many" : { + "stringUnit" : { + "state" : "translated", + "value" : "Odeslání pozvánek" + } + }, + "one" : { + "stringUnit" : { + "state" : "translated", + "value" : "Odesílání pozvánky" + } + }, + "other" : { + "stringUnit" : { + "state" : "translated", + "value" : "Odeslání pozvánek" + } + } + } + } + } + } + }, "en" : { "stringUnit" : { "state" : "translated", @@ -193193,6 +193693,102 @@ } } } + }, + "fa" : { + "stringUnit" : { + "state" : "translated", + "value" : "%#@arg1@" + }, + "substitutions" : { + "arg1" : { + "argNum" : 1, + "formatSpecifier" : "lld", + "variations" : { + "plural" : { + "one" : { + "stringUnit" : { + "state" : "translated", + "value" : "ارسال دعوت نامه" + } + }, + "other" : { + "stringUnit" : { + "state" : "translated", + "value" : "ارسال دعوت نامه ها" + } + } + } + } + } + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "%#@arg1@" + }, + "substitutions" : { + "arg1" : { + "argNum" : 1, + "formatSpecifier" : "lld", + "variations" : { + "plural" : { + "one" : { + "stringUnit" : { + "state" : "translated", + "value" : "Uitnodiging versturen" + } + }, + "other" : { + "stringUnit" : { + "state" : "translated", + "value" : "Uitnodigingen versturen" + } + } + } + } + } + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "%#@arg1@" + }, + "substitutions" : { + "arg1" : { + "argNum" : 1, + "formatSpecifier" : "lld", + "variations" : { + "plural" : { + "few" : { + "stringUnit" : { + "state" : "translated", + "value" : "Отправка приглашений" + } + }, + "many" : { + "stringUnit" : { + "state" : "translated", + "value" : "Отправка приглашений" + } + }, + "one" : { + "stringUnit" : { + "state" : "translated", + "value" : "Отправка приглашения" + } + }, + "other" : { + "stringUnit" : { + "state" : "translated", + "value" : "Отправка приглашений" + } + } + } + } + } + } } } }, @@ -196049,6 +196645,12 @@ "groupInviteYouHistory" : { "extractionState" : "manual", "localizations" : { + "cs" : { + "stringUnit" : { + "state" : "translated", + "value" : "Byli jste pozváni do skupiny. Historie konverzace byla sdílena." + } + }, "en" : { "stringUnit" : { "state" : "translated", @@ -207993,11 +208595,23 @@ "groupNameVisible" : { "extractionState" : "manual", "localizations" : { + "cs" : { + "stringUnit" : { + "state" : "translated", + "value" : "Název skupiny je viditelný pro všechny členy skupiny." + } + }, "en" : { "stringUnit" : { "state" : "translated", "value" : "Group name is visible to all group members." } + }, + "lt" : { + "stringUnit" : { + "state" : "translated", + "value" : "Grupės pavadinimas yra matomas visiems grupės nariams." + } } } }, @@ -213746,11 +214360,23 @@ "groupRemovedYouGeneral" : { "extractionState" : "manual", "localizations" : { + "cs" : { + "stringUnit" : { + "state" : "translated", + "value" : "Byli jste odebráni ze skupiny." + } + }, "en" : { "stringUnit" : { "state" : "translated", "value" : "You were removed from the group." } + }, + "lt" : { + "stringUnit" : { + "state" : "translated", + "value" : "Jūs buvote pašalinti iš grupės." + } } } }, @@ -246927,7 +247553,7 @@ "other" : { "stringUnit" : { "state" : "translated", - "value" : "%lld nariai" + "value" : "%lld narys" } } } @@ -279348,11 +279974,23 @@ "nicknameErrorShorter" : { "extractionState" : "manual", "localizations" : { + "cs" : { + "stringUnit" : { + "state" : "translated", + "value" : "Zadejte prosím kratší přezdívku" + } + }, "en" : { "stringUnit" : { "state" : "translated", "value" : "Please enter a shorter nickname" } + }, + "lt" : { + "stringUnit" : { + "state" : "translated", + "value" : "Įveskite trumpesnį slapyvardį" + } } } }, diff --git a/Session/Meta/Translations/remove_unused_strings.swift b/Session/Meta/Translations/remove_unused_strings.swift index 806a397080f..dcc28fc698c 100755 --- a/Session/Meta/Translations/remove_unused_strings.swift +++ b/Session/Meta/Translations/remove_unused_strings.swift @@ -3,7 +3,9 @@ // Copyright © 2023 Rangeproof Pty Ltd. All rights reserved. // // stringlint:disable -// + +import Foundation + // The way this works is: // • Run the AbandonedStrings executable (see https://www.avanderlee.com/xcode/unused-localized-strings/) // • Paste the list of unused strings below diff --git a/Session/Notifications/NotificationActionHandler.swift b/Session/Notifications/NotificationActionHandler.swift new file mode 100644 index 00000000000..d2c8202a21f --- /dev/null +++ b/Session/Notifications/NotificationActionHandler.swift @@ -0,0 +1,250 @@ +// Copyright © 2023 Rangeproof Pty Ltd. All rights reserved. + +import Foundation +import Combine +import GRDB +import SignalUtilitiesKit +import SessionSnodeKit +import SessionMessagingKit +import SessionUtilitiesKit + +// MARK: - Singleton + +public extension Singleton { + static let notificationActionHandler: SingletonConfig = Dependencies.create( + identifier: "notificationActionHandler", + createInstance: { dependencies in NotificationActionHandler(using: dependencies) } + ) +} + +// MARK: - NotificationActionHandler + +public class NotificationActionHandler { + private let dependencies: Dependencies + + // MARK: - Initialization + + init(using dependencies: Dependencies) { + self.dependencies = dependencies + } + + // MARK: - Handling + + func handleNotificationResponse( + _ response: UNNotificationResponse, + completionHandler: @escaping () -> Void + ) { + Log.assertOnMainThread() + handleNotificationResponse(response) + .subscribe(on: DispatchQueue.global(qos: .userInitiated), using: dependencies) + .receive(on: DispatchQueue.main, using: dependencies) + .sinkUntilComplete( + receiveCompletion: { result in + switch result { + case .finished: break + case .failure(let error): + completionHandler() + Log.error("[NotificationActionHandler] An error occured handling a notification response: \(error)") + } + }, + receiveValue: { _ in completionHandler() } + ) + } + + func handleNotificationResponse(_ response: UNNotificationResponse) -> AnyPublisher { + Log.assertOnMainThread() + assert(dependencies[singleton: .appReadiness].isAppReady) + + let userInfo: [AnyHashable: Any] = response.notification.request.content.userInfo + let applicationState: UIApplication.State = UIApplication.shared.applicationState + + switch response.actionIdentifier { + case UNNotificationDefaultActionIdentifier: + Log.debug("[NotificationActionHandler] Default action") + return showThread(userInfo: userInfo) + .setFailureType(to: Error.self) + .eraseToAnyPublisher() + + case UNNotificationDismissActionIdentifier: + // TODO - mark as read? + Log.debug("[NotificationActionHandler] Dismissed notification") + return Just(()) + .setFailureType(to: Error.self) + .eraseToAnyPublisher() + + default: + // proceed + break + } + + guard let action = UserNotificationConfig.action(identifier: response.actionIdentifier) else { + return Fail(error: NotificationError.failDebug("unable to find action for actionIdentifier: \(response.actionIdentifier)")) + .eraseToAnyPublisher() + } + + switch action { + case .markAsRead: return markAsRead(userInfo: userInfo) + + case .reply: + guard let textInputResponse = response as? UNTextInputNotificationResponse else { + return Fail(error: NotificationError.failDebug("response had unexpected type: \(response)")) + .eraseToAnyPublisher() + } + + return reply( + userInfo: userInfo, + replyText: textInputResponse.userText, + applicationState: applicationState + ) + } + } + + // MARK: - Actions + + func markAsRead(userInfo: [AnyHashable: Any]) -> AnyPublisher { + guard let threadId: String = userInfo[AppNotificationUserInfoKey.threadId] as? String else { + return Fail(error: NotificationError.failDebug("threadId was unexpectedly nil")) + .eraseToAnyPublisher() + } + + guard dependencies[singleton: .storage].read({ db in try SessionThread.exists(db, id: threadId) }) == true else { + return Fail(error: NotificationError.failDebug("unable to find thread with id: \(threadId)")) + .eraseToAnyPublisher() + } + + return markAsRead(threadId: threadId) + } + + func reply( + userInfo: [AnyHashable: Any], + replyText: String, + applicationState: UIApplication.State + ) -> AnyPublisher { + guard let threadId = userInfo[AppNotificationUserInfoKey.threadId] as? String else { + return Fail(error: NotificationError.failDebug("threadId was unexpectedly nil")) + .eraseToAnyPublisher() + } + + guard let thread: SessionThread = dependencies[singleton: .storage].read({ db in try SessionThread.fetchOne(db, id: threadId) }) else { + return Fail(error: NotificationError.failDebug("unable to find thread with id: \(threadId)")) + .eraseToAnyPublisher() + } + + return dependencies[singleton: .storage] + .writePublisher { [dependencies] db -> Network.PreparedRequest in + let interaction: Interaction = try Interaction( + threadId: threadId, + threadVariant: thread.variant, + authorId: dependencies[cache: .general].sessionId.hexString, + variant: .standardOutgoing, + body: replyText, + timestampMs: dependencies[cache: .snodeAPI].currentOffsetTimestampMs(), + hasMention: Interaction.isUserMentioned(db, threadId: threadId, body: replyText, using: dependencies), + using: dependencies + ).inserted(db) + + try Interaction.markAsRead( + db, + interactionId: interaction.id, + threadId: threadId, + threadVariant: thread.variant, + includingOlder: true, + trySendReadReceipt: try SessionThread.canSendReadReceipt( + db, + threadId: threadId, + threadVariant: thread.variant, + using: dependencies + ), + using: dependencies + ) + + return try MessageSender.preparedSend( + db, + interaction: interaction, + fileIds: [], + threadId: threadId, + threadVariant: thread.variant, + using: dependencies + ) + } + .flatMap { [dependencies] request in request.send(using: dependencies) } + .map { _ in () } + .handleEvents( + receiveCompletion: { [dependencies] result in + switch result { + case .finished: break + case .failure: + dependencies[singleton: .storage].read { db in + dependencies[singleton: .notificationsManager].notifyForFailedSend( + db, + in: thread, + applicationState: applicationState + ) + } + } + } + ) + .eraseToAnyPublisher() + } + + func showThread(userInfo: [AnyHashable: Any]) -> AnyPublisher { + guard + let threadId = userInfo[AppNotificationUserInfoKey.threadId] as? String, + let threadVariantRaw = userInfo[AppNotificationUserInfoKey.threadVariantRaw] as? Int, + let threadVariant: SessionThread.Variant = SessionThread.Variant(rawValue: threadVariantRaw) + else { return showHomeVC() } + + // If this happens when the the app is not, visible we skip the animation so the thread + // can be visible to the user immediately upon opening the app, rather than having to watch + // it animate in from the homescreen. + dependencies[singleton: .app].presentConversationCreatingIfNeeded( + for: threadId, + variant: threadVariant, + action: .none, + dismissing: nil, + animated: (UIApplication.shared.applicationState == .active) + ) + + return Just(()).eraseToAnyPublisher() + } + + func showHomeVC() -> AnyPublisher { + dependencies[singleton: .app].showHomeView() + return Just(()).eraseToAnyPublisher() + } + + private func markAsRead(threadId: String) -> AnyPublisher { + return dependencies[singleton: .storage] + .writePublisher { [dependencies] db in + guard + let threadVariant: SessionThread.Variant = try SessionThread + .filter(id: threadId) + .select(.variant) + .asRequest(of: SessionThread.Variant.self) + .fetchOne(db), + let lastInteractionId: Int64 = try Interaction + .select(.id) + .filter(Interaction.Columns.threadId == threadId) + .order(Interaction.Columns.timestampMs.desc) + .asRequest(of: Int64.self) + .fetchOne(db) + else { throw NotificationError.failDebug("unable to required thread info: \(threadId)") } + + try Interaction.markAsRead( + db, + interactionId: lastInteractionId, + threadId: threadId, + threadVariant: threadVariant, + includingOlder: true, + trySendReadReceipt: try SessionThread.canSendReadReceipt( + db, + threadId: threadId, + threadVariant: threadVariant, + using: dependencies + ), + using: dependencies + ) + } + .eraseToAnyPublisher() + } +} diff --git a/Session/Notifications/AppNotifications.swift b/Session/Notifications/NotificationPresenter.swift similarity index 57% rename from Session/Notifications/AppNotifications.swift rename to Session/Notifications/NotificationPresenter.swift index 7d20c552836..a8b88ceb18b 100644 --- a/Session/Notifications/AppNotifications.swift +++ b/Session/Notifications/NotificationPresenter.swift @@ -6,109 +6,81 @@ import GRDB import SessionMessagingKit import SignalUtilitiesKit import SessionUtilitiesKit -import SessionSnodeKit -/// There are two primary components in our system notification integration: -/// -/// 1. The `NotificationPresenter` shows system notifications to the user. -/// 2. The `NotificationActionHandler` handles the users interactions with these -/// notifications. -/// -/// The NotificationPresenter is driven by the adapter pattern to provide a unified interface to -/// presenting notifications on iOS9, which uses UINotifications vs iOS10+ which supports -/// UNUserNotifications. -/// -/// The `NotificationActionHandler`s also need slightly different integrations for UINotifications -/// vs. UNUserNotifications, but because they are integrated at separate system defined callbacks, -/// there is no need for an Adapter, and instead the appropriate NotificationActionHandler is -/// wired directly into the appropriate callback point. +// MARK: - NotificationPresenter -let kAudioNotificationsThrottleCount = 2 -let kAudioNotificationsThrottleInterval: TimeInterval = 5 - -protocol NotificationPresenterAdaptee: AnyObject { - - func registerNotificationSettings() -> AnyPublisher - - func notify( - category: AppNotificationCategory, - title: String?, - body: String, - userInfo: [AnyHashable: Any], - previewType: Preferences.NotificationPreviewType, - sound: Preferences.Sound?, - threadVariant: SessionThread.Variant, - threadName: String, - applicationState: UIApplication.State, - replacingIdentifier: String? - ) - - func cancelNotifications(threadId: String) - func cancelNotifications(identifiers: [String]) - func clearAllNotifications() -} - -extension NotificationPresenterAdaptee { - func notify( - category: AppNotificationCategory, - title: String?, - body: String, - userInfo: [AnyHashable: Any], - previewType: Preferences.NotificationPreviewType, - sound: Preferences.Sound?, - threadVariant: SessionThread.Variant, - threadName: String, - applicationState: UIApplication.State - ) { - notify( - category: category, - title: title, - body: body, - userInfo: userInfo, - previewType: previewType, - sound: sound, - threadVariant: threadVariant, - threadName: threadName, - applicationState: applicationState, - replacingIdentifier: nil - ) +public class NotificationPresenter: NSObject, UNUserNotificationCenterDelegate, NotificationsManagerType { + private static let audioNotificationsThrottleCount = 2 + private static let audioNotificationsThrottleInterval: TimeInterval = 5 + + private let dependencies: Dependencies + private let notificationCenter: UNUserNotificationCenter = UNUserNotificationCenter.current() + private var notifications: Atomic<[String: UNNotificationRequest]> = Atomic([:]) + private var mostRecentNotifications: Atomic> = Atomic(TruncatedList(maxLength: NotificationPresenter.audioNotificationsThrottleCount)) + + // MARK: - Initialization + + required public init(using dependencies: Dependencies) { + self.dependencies = dependencies } -} - -public class NotificationPresenter: NotificationsProtocol { - private let adaptee: NotificationPresenterAdaptee = UserNotificationPresenterAdaptee() - - public init() { - SwiftSingletons.register(self) + + // MARK: - Registration + + public func setDelegate(_ delegate: (any UNUserNotificationCenterDelegate)?) { + notificationCenter.delegate = delegate } - - // MARK: - Presenting Notifications - - func registerNotificationSettings() -> AnyPublisher { - return adaptee.registerNotificationSettings() + + public func registerNotificationSettings() -> AnyPublisher { + return Deferred { [notificationCenter] in + Future { resolver in + notificationCenter.requestAuthorization(options: [.badge, .sound, .alert]) { (granted, error) in + notificationCenter.setNotificationCategories(UserNotificationConfig.allNotificationCategories) + + switch (granted, error) { + case (true, _): break + case (false, .some(let error)): Log.error("[NotificationPresenter] Register settings failed with error: \(error)") + case (false, .none): Log.error("[NotificationPresenter] Register settings failed without error.") + } + + // Note that the promise is fulfilled regardless of if notification permssions were + // granted. This promise only indicates that the user has responded, so we can + // proceed with requesting push tokens and complete registration. + resolver(Result.success(())) + } + } + }.eraseToAnyPublisher() } - + + // MARK: - Presentation + public func notifyUser( _ db: Database, for interaction: Interaction, in thread: SessionThread, applicationState: UIApplication.State ) { - let isMessageRequest: Bool = thread.isMessageRequest(db, includeNonVisible: true) + let isMessageRequest: Bool = SessionThread.isMessageRequest( + db, + threadId: thread.id, + userSessionId: dependencies[cache: .general].sessionId, + includeNonVisible: true + ) // Ensure we should be showing a notification for the thread - guard thread.shouldShowNotification(db, for: interaction, isMessageRequest: isMessageRequest) else { + guard thread.shouldShowNotification(db, for: interaction, isMessageRequest: isMessageRequest, using: dependencies) else { return } // Try to group notifications for interactions from open groups - let identifier: String = interaction.notificationIdentifier( + let identifier: String = Interaction.notificationIdentifier( + for: (interaction.id ?? 0), + threadId: thread.id, shouldGroupMessagesForThread: (thread.variant == .community) ) - + // While batch processing, some of the necessary changes have not been commited. - let rawMessageText = interaction.previewText(db) - + let rawMessageText: String = Interaction.notificationPreviewText(db, interaction: interaction, using: dependencies) + // iOS strips anything that looks like a printf formatting character from // the notification body, so if we want to dispay a literal "%" in a notification // it must be escaped. @@ -118,7 +90,7 @@ public class NotificationPresenter: NotificationsProtocol { let notificationTitle: String? var notificationBody: String? - let senderName = Profile.displayName(db, id: interaction.authorId, threadVariant: thread.variant) + let senderName = Profile.displayName(db, id: interaction.authorId, threadVariant: thread.variant, using: dependencies) let previewType: Preferences.NotificationPreviewType = db[.preferencesNotificationPreviewType] .defaulting(to: .defaultPreviewType) let groupName: String = SessionThread.displayName( @@ -139,11 +111,11 @@ public class NotificationPresenter: NotificationsProtocol { notificationTitle = Constants.app_name case .nameNoPreview, .nameAndPreview: - switch thread.variant { - case .contact: - notificationTitle = (isMessageRequest ? Constants.app_name : senderName) + switch (thread.variant, isMessageRequest) { + case (.contact, true), (.group, true): notificationTitle = Constants.app_name + case (.contact, false): notificationTitle = senderName - case .legacyGroup, .group, .community: + case (.legacyGroup, _), (.group, false), (.community, _): notificationTitle = "notificationsIosGroup" .put(key: "name", value: senderName) .put(key: "conversation_name", value: groupName) @@ -163,37 +135,39 @@ public class NotificationPresenter: NotificationsProtocol { if isMessageRequest { notificationBody = "messageRequestsNew".localized() } - + guard notificationBody != nil || notificationTitle != nil else { SNLog("AppNotifications error: No notification content") return } - + // Don't reply from lockscreen if anyone in this conversation is // "no longer verified". let category = AppNotificationCategory.incomingMessage - + let userInfo: [AnyHashable: Any] = [ AppNotificationUserInfoKey.threadId: thread.id, AppNotificationUserInfoKey.threadVariantRaw: thread.variant.rawValue ] - let userPublicKey: String = getUserHexEncodedPublicKey(db) - let userBlinded15Key: String? = SessionThread.getUserHexEncodedBlindedKey( + let userSessionId: SessionId = dependencies[cache: .general].sessionId + let userBlinded15SessionId: SessionId? = SessionThread.getCurrentUserBlindedSessionId( db, threadId: thread.id, threadVariant: thread.variant, - blindingPrefix: .blinded15 + blindingPrefix: .blinded15, + using: dependencies ) - let userBlinded25Key: String? = SessionThread.getUserHexEncodedBlindedKey( + let userBlinded25SessionId: SessionId? = SessionThread.getCurrentUserBlindedSessionId( db, threadId: thread.id, threadVariant: thread.variant, - blindingPrefix: .blinded25 + blindingPrefix: .blinded25, + using: dependencies ) let fallbackSound: Preferences.Sound = db[.defaultNotificationSound] .defaulting(to: Preferences.Sound.defaultNotificationSound) - + let sound: Preferences.Sound? = requestSound( thread: thread, fallbackSound: fallbackSound, @@ -203,12 +177,13 @@ public class NotificationPresenter: NotificationsProtocol { notificationBody = MentionUtilities.highlightMentionsNoAttributes( in: (notificationBody ?? ""), threadVariant: thread.variant, - currentUserPublicKey: userPublicKey, - currentUserBlinded15PublicKey: userBlinded15Key, - currentUserBlinded25PublicKey: userBlinded25Key + currentUserSessionId: userSessionId.hexString, + currentUserBlinded15SessionId: userBlinded15SessionId?.hexString, + currentUserBlinded25SessionId: userBlinded25SessionId?.hexString, + using: dependencies ) - self.adaptee.notify( + notify( category: category, title: notificationTitle, body: (notificationBody ?? ""), @@ -232,8 +207,8 @@ public class NotificationPresenter: NotificationsProtocol { guard Date().timeIntervalSince1970 > (thread.mutedUntilTimestamp ?? 0) else { return } guard thread.variant != .legacyGroup && - thread.variant != .group && - thread.variant != .community + thread.variant != .group && + thread.variant != .community else { return } guard interaction.variant == .infoCall, @@ -260,7 +235,7 @@ public class NotificationPresenter: NotificationsProtocol { ] let notificationTitle: String = Constants.app_name - let senderName: String = Profile.displayName(db, id: interaction.authorId, threadVariant: thread.variant) + let senderName: String = Profile.displayName(db, id: interaction.authorId, threadVariant: thread.variant, using: dependencies) let notificationBody: String? = { switch messageInfo.state { case .permissionDenied: @@ -284,7 +259,7 @@ public class NotificationPresenter: NotificationsProtocol { applicationState: applicationState ) - self.adaptee.notify( + notify( category: category, title: notificationTitle, body: (notificationBody ?? ""), @@ -304,18 +279,23 @@ public class NotificationPresenter: NotificationsProtocol { in thread: SessionThread, applicationState: UIApplication.State ) { - let isMessageRequest: Bool = thread.isMessageRequest(db, includeNonVisible: true) + let isMessageRequest: Bool = SessionThread.isMessageRequest( + db, + threadId: thread.id, + userSessionId: dependencies[cache: .general].sessionId, + includeNonVisible: true + ) // No reaction notifications for muted, group threads or message requests - guard Date().timeIntervalSince1970 > (thread.mutedUntilTimestamp ?? 0) else { return } + guard dependencies.dateNow.timeIntervalSince1970 > (thread.mutedUntilTimestamp ?? 0) else { return } guard thread.variant != .legacyGroup && - thread.variant != .group && - thread.variant != .community + thread.variant != .group && + thread.variant != .community else { return } guard !isMessageRequest else { return } - let notificationTitle = Profile.displayName(db, id: reaction.authorId, threadVariant: thread.variant) + let notificationTitle = Profile.displayName(db, id: reaction.authorId, threadVariant: thread.variant, using: dependencies) var notificationBody = "emojiReactsNotification" .put(key: "emoji", value: reaction.emoji) .localized() @@ -332,7 +312,7 @@ public class NotificationPresenter: NotificationsProtocol { } let category = AppNotificationCategory.incomingMessage - + let userInfo: [AnyHashable: Any] = [ AppNotificationUserInfoKey.threadId: thread.id, AppNotificationUserInfoKey.threadVariantRaw: thread.variant.rawValue @@ -352,7 +332,7 @@ public class NotificationPresenter: NotificationsProtocol { applicationState: applicationState ) - self.adaptee.notify( + notify( category: category, title: notificationTitle, body: notificationBody, @@ -365,7 +345,7 @@ public class NotificationPresenter: NotificationsProtocol { replacingIdentifier: UUID().uuidString ) } - + public func notifyForFailedSend( _ db: Database, in thread: SessionThread, @@ -385,7 +365,7 @@ public class NotificationPresenter: NotificationsProtocol { .select(.name) .asRequest(of: String.self) .fetchOne(db), - isNoteToSelf: (thread.isNoteToSelf(db) == true), + isNoteToSelf: (thread.isNoteToSelf(db, using: dependencies) == true), profile: try? Profile.fetchOne(db, id: thread.id) ) @@ -395,7 +375,6 @@ public class NotificationPresenter: NotificationsProtocol { } let notificationBody = "messageErrorDelivery".localized() - let userInfo: [AnyHashable: Any] = [ AppNotificationUserInfoKey.threadId: thread.id, AppNotificationUserInfoKey.threadVariantRaw: thread.variant.rawValue @@ -408,7 +387,7 @@ public class NotificationPresenter: NotificationsProtocol { applicationState: applicationState ) - self.adaptee.notify( + notify( category: .errorMessage, title: notificationTitle, body: notificationBody, @@ -421,27 +400,113 @@ public class NotificationPresenter: NotificationsProtocol { ) } - @objc + // MARK: - Clearing + public func cancelNotifications(identifiers: [String]) { - DispatchQueue.main.async { - self.adaptee.cancelNotifications(identifiers: identifiers) + notifications.mutate { notifications in + identifiers.forEach { notifications.removeValue(forKey: $0) } } + notificationCenter.removeDeliveredNotifications(withIdentifiers: identifiers) + notificationCenter.removePendingNotificationRequests(withIdentifiers: identifiers) } - - @objc - public func cancelNotifications(threadId: String) { - self.adaptee.cancelNotifications(threadId: threadId) - } - - @objc + public func clearAllNotifications() { - adaptee.clearAllNotifications() + notificationCenter.removeAllPendingNotificationRequests() + notificationCenter.removeAllDeliveredNotifications() } +} + +// MARK: - Convenience - // MARK: - +private extension NotificationPresenter { + func notify( + category: AppNotificationCategory, + title: String?, + body: String, + userInfo: [AnyHashable: Any], + previewType: Preferences.NotificationPreviewType, + sound: Preferences.Sound?, + threadVariant: SessionThread.Variant, + threadName: String, + applicationState: UIApplication.State, + replacingIdentifier: String? = nil + ) { + let threadIdentifier: String? = (userInfo[AppNotificationUserInfoKey.threadId] as? String) + let content = UNMutableNotificationContent() + content.categoryIdentifier = category.identifier + content.userInfo = userInfo + content.threadIdentifier = (threadIdentifier ?? content.threadIdentifier) + + let shouldGroupNotification: Bool = ( + threadVariant == .community && + replacingIdentifier == threadIdentifier + ) + if let sound = sound, sound != .none { + content.sound = sound.notificationSound(isQuiet: (applicationState == .active)) + } + + let notificationIdentifier: String = (replacingIdentifier ?? UUID().uuidString) + let isReplacingNotification: Bool = (notifications.wrappedValue[notificationIdentifier] != nil) + let shouldPresentNotification: Bool = shouldPresentNotification( + category: category, + applicationState: applicationState, + userInfo: userInfo, + using: dependencies + ) + var trigger: UNNotificationTrigger? - var mostRecentNotifications: Atomic> = Atomic(TruncatedList(maxLength: kAudioNotificationsThrottleCount)) + if shouldPresentNotification { + if let displayableTitle = title?.filteredForDisplay { + content.title = displayableTitle + } + content.body = body.filteredForDisplay + + if shouldGroupNotification { + trigger = UNTimeIntervalNotificationTrigger( + timeInterval: Notifications.delayForGroupedNotifications, + repeats: false + ) + + let numberExistingNotifications: Int? = notifications.wrappedValue[notificationIdentifier]? + .content + .userInfo[AppNotificationUserInfoKey.threadNotificationCounter] + .asType(Int.self) + var numberOfNotifications: Int = (numberExistingNotifications ?? 1) + + if numberExistingNotifications != nil { + numberOfNotifications += 1 // Add one for the current notification + + content.title = (previewType == .noNameNoPreview ? + content.title : + threadName + ) + content.body = "messageNewYouveGot" + .putNumber(numberOfNotifications) + .localized() + } + + content.userInfo[AppNotificationUserInfoKey.threadNotificationCounter] = numberOfNotifications + } + } + else { + // Play sound and vibrate, but without a `body` no banner will show. + Log.debug("supressing notification body") + } + + let request = UNNotificationRequest( + identifier: notificationIdentifier, + content: content, + trigger: trigger + ) + Log.debug("presenting notification with identifier: \(notificationIdentifier)") + + if isReplacingNotification { cancelNotifications(identifiers: [notificationIdentifier]) } + + notificationCenter.add(request) + notifications.mutate { $0[notificationIdentifier] = request } + } + private func requestSound( thread: SessionThread, fallbackSound: Preferences.Sound, @@ -451,178 +516,51 @@ public class NotificationPresenter: NotificationsProtocol { return (thread.notificationSound ?? fallbackSound) } + + private func shouldPresentNotification( + category: AppNotificationCategory, + applicationState: UIApplication.State, + userInfo: [AnyHashable: Any], + using dependencies: Dependencies + ) -> Bool { + guard applicationState == .active else { return true } + guard category == .incomingMessage || category == .errorMessage else { return true } + + guard let notificationThreadId = userInfo[AppNotificationUserInfoKey.threadId] as? String else { + Log.error("[UserNotificationPresenterAdaptee] threadId was unexpectedly nil") + return true + } + + /// Check whether the current `frontMostViewController` is a `ConversationVC` for the conversation this notification + /// would belong to then we don't want to show the notification, so retrieve the `frontMostViewController` (from the main + /// thread) and check + guard + let frontMostViewController: UIViewController = DispatchQueue.main.sync(execute: { + dependencies[singleton: .appContext].frontMostViewController + }), + let conversationViewController: ConversationVC = frontMostViewController as? ConversationVC + else { return true } + + /// Show notifications for any **other** threads + return (conversationViewController.viewModel.threadData.threadId != notificationThreadId) + } private func checkIfShouldPlaySound(applicationState: UIApplication.State) -> Bool { guard applicationState == .active else { return true } - guard Storage.shared[.playNotificationSoundInForeground] else { return false } + guard dependencies[singleton: .storage, key: .playNotificationSoundInForeground] else { return false } let nowMs: UInt64 = UInt64(floor(Date().timeIntervalSince1970 * 1000)) - let recentThreshold = nowMs - UInt64(kAudioNotificationsThrottleInterval * 1000) + let recentThreshold = nowMs - UInt64(NotificationPresenter.audioNotificationsThrottleInterval * 1000) let recentNotifications = mostRecentNotifications.wrappedValue.filter { $0 > recentThreshold } - guard recentNotifications.count < kAudioNotificationsThrottleCount else { return false } + guard recentNotifications.count < NotificationPresenter.audioNotificationsThrottleCount else { return false } mostRecentNotifications.mutate { $0.append(nowMs) } return true } } -class NotificationActionHandler { - - static let shared: NotificationActionHandler = NotificationActionHandler() - - // MARK: - Dependencies - - var notificationPresenter: NotificationPresenter { - return AppEnvironment.shared.notificationPresenter - } - - // MARK: - - - func markAsRead(userInfo: [AnyHashable: Any]) -> AnyPublisher { - guard let threadId: String = userInfo[AppNotificationUserInfoKey.threadId] as? String else { - return Fail(error: NotificationError.failDebug("threadId was unexpectedly nil")) - .eraseToAnyPublisher() - } - - guard Storage.shared.read({ db in try SessionThread.exists(db, id: threadId) }) == true else { - return Fail(error: NotificationError.failDebug("unable to find thread with id: \(threadId)")) - .eraseToAnyPublisher() - } - - return markAsRead(threadId: threadId) - } - - func reply( - userInfo: [AnyHashable: Any], - replyText: String, - applicationState: UIApplication.State, - using dependencies: Dependencies = Dependencies() - ) -> AnyPublisher { - guard let threadId = userInfo[AppNotificationUserInfoKey.threadId] as? String else { - return Fail(error: NotificationError.failDebug("threadId was unexpectedly nil")) - .eraseToAnyPublisher() - } - - guard let thread: SessionThread = Storage.shared.read({ db in try SessionThread.fetchOne(db, id: threadId) }) else { - return Fail(error: NotificationError.failDebug("unable to find thread with id: \(threadId)")) - .eraseToAnyPublisher() - } - - return Storage.shared - .writePublisher { db in - let interaction: Interaction = try Interaction( - threadId: threadId, - threadVariant: thread.variant, - authorId: getUserHexEncodedPublicKey(db), - variant: .standardOutgoing, - body: replyText, - timestampMs: SnodeAPI.currentOffsetTimestampMs(), - hasMention: Interaction.isUserMentioned(db, threadId: threadId, body: replyText) - ).inserted(db) - - try Interaction.markAsRead( - db, - interactionId: interaction.id, - threadId: threadId, - threadVariant: thread.variant, - includingOlder: true, - trySendReadReceipt: try SessionThread.canSendReadReceipt( - db, - threadId: threadId, - threadVariant: thread.variant - ) - ) - - return try MessageSender.preparedSendData( - db, - interaction: interaction, - threadId: threadId, - threadVariant: thread.variant, - using: dependencies - ) - } - .flatMap { MessageSender.sendImmediate(data: $0, using: dependencies) } - .handleEvents( - receiveCompletion: { result in - switch result { - case .finished: break - case .failure: - Storage.shared.read { [weak self] db in - self?.notificationPresenter.notifyForFailedSend( - db, - in: thread, - applicationState: applicationState - ) - } - } - } - ) - .eraseToAnyPublisher() - } - - func showThread(userInfo: [AnyHashable: Any], using dependencies: Dependencies) -> AnyPublisher { - guard - let threadId = userInfo[AppNotificationUserInfoKey.threadId] as? String, - let threadVariantRaw = userInfo[AppNotificationUserInfoKey.threadVariantRaw] as? Int, - let threadVariant: SessionThread.Variant = SessionThread.Variant(rawValue: threadVariantRaw) - else { return showHomeVC(using: dependencies) } - - // If this happens when the the app is not, visible we skip the animation so the thread - // can be visible to the user immediately upon opening the app, rather than having to watch - // it animate in from the homescreen. - SessionApp.presentConversationCreatingIfNeeded( - for: threadId, - variant: threadVariant, - dismissing: nil, - animated: (UIApplication.shared.applicationState == .active) - ) - - return Just(()) - .eraseToAnyPublisher() - } - - func showHomeVC(using dependencies: Dependencies) -> AnyPublisher { - SessionApp.showHomeView(using: dependencies) - return Just(()) - .eraseToAnyPublisher() - } - - private func markAsRead(threadId: String) -> AnyPublisher { - return Storage.shared - .writePublisher { db in - guard - let threadVariant: SessionThread.Variant = try SessionThread - .filter(id: threadId) - .select(.variant) - .asRequest(of: SessionThread.Variant.self) - .fetchOne(db), - let lastInteractionId: Int64 = try Interaction - .select(.id) - .filter(Interaction.Columns.threadId == threadId) - .order(Interaction.Columns.timestampMs.desc) - .asRequest(of: Int64.self) - .fetchOne(db) - else { throw NotificationError.failDebug("unable to required thread info: \(threadId)") } - - try Interaction.markAsRead( - db, - interactionId: lastInteractionId, - threadId: threadId, - threadVariant: threadVariant, - includingOlder: true, - trySendReadReceipt: try SessionThread.canSendReadReceipt( - db, - threadId: threadId, - threadVariant: threadVariant - ) - ) - } - .eraseToAnyPublisher() - } -} - enum NotificationError: Error { case assertionError(description: String) } @@ -634,6 +572,8 @@ extension NotificationError { } } +// MARK: - TruncatedList + struct TruncatedList { let maxLength: Int private var contents: [Element] = [] diff --git a/Session/Notifications/PushRegistrationManager.swift b/Session/Notifications/PushRegistrationManager.swift index 3987ed35c50..b5ce1257cff 100644 --- a/Session/Notifications/PushRegistrationManager.swift +++ b/Session/Notifications/PushRegistrationManager.swift @@ -8,39 +8,20 @@ import SessionMessagingKit import SignalUtilitiesKit import SessionUtilitiesKit -public enum PushRegistrationError: Error { - case assertionError(description: String) - case pushNotSupported(description: String) - case timeout - case publisherNoLongerExists -} - -/** - * Singleton used to integrate with push notification services - registration and routing received remote notifications. - */ -@objc public class PushRegistrationManager: NSObject, PKPushRegistryDelegate { - - // MARK: - Dependencies +// MARK: - Singleton - private var notificationPresenter: NotificationPresenter { - return AppEnvironment.shared.notificationPresenter - } - - // MARK: - Singleton class - - @objc - public static var shared: PushRegistrationManager { - get { - return AppEnvironment.shared.pushRegistrationManager - } - } - - override init() { - super.init() +public extension Singleton { + static let pushRegistrationManager: SingletonConfig = Dependencies.create( + identifier: "pushRegistrationManager", + createInstance: { dependencies in PushRegistrationManager(using: dependencies) } + ) +} - SwiftSingletons.register(self) - } +// MARK: - PushRegistrationManager +public class PushRegistrationManager: NSObject, PKPushRegistryDelegate { + private let dependencies: Dependencies + private var vanillaTokenPublisher: AnyPublisher? private var vanillaTokenResolver: ((Result) -> ())? @@ -48,6 +29,14 @@ public enum PushRegistrationError: Error { private var voipTokenPublisher: AnyPublisher? private var voipTokenResolver: ((Result) -> ())? + // MARK: - Initialization + + fileprivate init(using dependencies: Dependencies) { + self.dependencies = dependencies + + super.init() + } + // MARK: - Public interface public func requestPushTokens() -> AnyPublisher<(pushToken: String, voipToken: String), Error> { @@ -71,8 +60,8 @@ public enum PushRegistrationError: Error { // MARK: Vanilla push token - // Vanilla push token is obtained from the system via AppDelegate - public func didReceiveVanillaPushToken(_ tokenData: Data, using dependencies: Dependencies = Dependencies()) { + /// Vanilla push token is obtained from the system via AppDelegate + public func didReceiveVanillaPushToken(_ tokenData: Data) { guard let vanillaTokenResolver = self.vanillaTokenResolver else { Log.error("[PushRegistrationManager] Publisher completion in \(#function) unexpectedly nil") return @@ -83,8 +72,8 @@ public enum PushRegistrationError: Error { } } - // Vanilla push token is obtained from the system via AppDelegate - public func didFailToReceiveVanillaPushToken(error: Error, using dependencies: Dependencies = Dependencies()) { + /// Vanilla push token is obtained from the system via AppDelegate + public func didFailToReceiveVanillaPushToken(error: Error) { guard let vanillaTokenResolver = self.vanillaTokenResolver else { Log.error("[PushRegistrationManager] Publisher completion in \(#function) unexpectedly nil") return @@ -97,10 +86,9 @@ public enum PushRegistrationError: Error { // MARK: helpers - // User notification settings must be registered *before* AppDelegate will - // return any requested push tokens. + /// User notification settings must be registered *before* AppDelegate will return any requested push tokens. public func registerUserNotificationSettings() -> AnyPublisher { - return notificationPresenter.registerNotificationSettings() + return dependencies[singleton: .notificationsManager].registerNotificationSettings() } /** @@ -277,7 +265,7 @@ public enum PushRegistrationError: Error { // NOTE: This function MUST report an incoming call. public func pushRegistry(_ registry: PKPushRegistry, didReceiveIncomingPushWith payload: PKPushPayload, for type: PKPushType) { Log.info("[PushRegistrationManager] Receive new voip notification.") - Log.assert(Singleton.hasAppContext && Singleton.appContext.isMainApp) + Log.assert(dependencies[singleton: .appContext].isMainApp) Log.assert(type == .voIP) let payload = payload.dictionaryPayload @@ -286,26 +274,23 @@ public enum PushRegistrationError: Error { let caller: String = payload["caller"] as? String, let timestampMs: Int64 = payload["timestamp"] as? Int64 else { - SessionCallManager.reportFakeCall(info: "Missing payload data") // stringlint:ignore + SessionCallManager.reportFakeCall(info: "Missing payload data", using: dependencies) // stringlint:ignore return } - // FIXME: Initialise the `PushRegistrationManager` with a dependencies instance - let dependencies: Dependencies = Dependencies() - - dependencies.storage.resumeDatabaseAccess() - LibSession.resumeNetworkAccess() + dependencies[singleton: .storage].resumeDatabaseAccess() + dependencies.mutate(cache: .libSessionNetwork) { $0.resumeNetworkAccess() } - let maybeCall: SessionCall? = Storage.shared.write { db in + let maybeCall: SessionCall? = dependencies[singleton: .storage].write { [dependencies] db in let messageInfo: CallMessage.MessageInfo = CallMessage.MessageInfo( - state: (caller == getUserHexEncodedPublicKey(db) ? + state: (caller == dependencies[cache: .general].sessionId.hexString ? .outgoing : .incoming ) ) let messageInfoString: String? = { - if let messageInfoData: Data = try? JSONEncoder().encode(messageInfo) { + if let messageInfoData: Data = try? JSONEncoder(using: dependencies).encode(messageInfo) { return String(data: messageInfoData, encoding: .utf8) } else { return "callsIncoming" @@ -314,9 +299,16 @@ public enum PushRegistrationError: Error { } }() - let call: SessionCall = SessionCall(db, for: caller, uuid: uuid, mode: .answer) - let thread: SessionThread = try SessionThread - .fetchOrCreate(db, id: caller, variant: .contact, shouldBeVisible: nil) + let call: SessionCall = SessionCall(db, for: caller, uuid: uuid, mode: .answer, using: dependencies) + let thread: SessionThread = try SessionThread.fetchOrCreate( + db, + id: caller, + variant: .contact, + creationDateTimestamp: TimeInterval(dependencies[cache: .snodeAPI].currentOffsetTimestampMs() / 1000), + shouldBeVisible: nil, + calledFromConfig: nil, + using: dependencies + ) let interaction: Interaction = try Interaction( messageUuid: uuid, @@ -325,7 +317,8 @@ public enum PushRegistrationError: Error { authorId: caller, variant: .infoCall, body: messageInfoString, - timestampMs: timestampMs + timestampMs: timestampMs, + using: dependencies ) .withDisappearingMessagesConfiguration(db, threadVariant: thread.variant) .inserted(db) @@ -336,7 +329,7 @@ public enum PushRegistrationError: Error { } guard let call: SessionCall = maybeCall else { - SessionCallManager.reportFakeCall(info: "Could not retrieve call from database") // stringlint:ignore + SessionCallManager.reportFakeCall(info: "Could not retrieve call from database", using: dependencies) // stringlint:ignore return } @@ -345,15 +338,17 @@ public enum PushRegistrationError: Error { call.reportIncomingCallIfNeeded { error in if let error = error { - SNLog("[Calls] Failed to report incoming call to CallKit due to error: \(error)") + Log.error(.calls, "Failed to report incoming call to CallKit due to error: \(error)") } } } } -// We transmit pushToken data as hex encoded string to the server -fileprivate extension Data { - var hexEncodedString: String { - return map { String(format: "%02hhx", $0) }.joined() // stringlint:ignore - } +// MARK: - PushRegistrationError + +public enum PushRegistrationError: Error { + case assertionError(description: String) + case pushNotSupported(description: String) + case timeout + case publisherNoLongerExists } diff --git a/Session/Notifications/SyncPushTokensJob.swift b/Session/Notifications/SyncPushTokensJob.swift index fdcba678927..f98bf9405e6 100644 --- a/Session/Notifications/SyncPushTokensJob.swift +++ b/Session/Notifications/SyncPushTokensJob.swift @@ -7,6 +7,14 @@ import SessionSnodeKit import SessionMessagingKit import SessionUtilitiesKit +// MARK: - Log.Category + +private extension Log.Category { + static let cat: Log.Category = .create("SyncPushTokensJob", defaultLevel: .info) +} + +// MARK: - SyncPushTokensJob + public enum SyncPushTokensJob: JobExecutor { public static let maxFailureCount: Int = -1 public static let requiresThreadId: Bool = false @@ -17,48 +25,48 @@ public enum SyncPushTokensJob: JobExecutor { public static func run( _ job: Job, queue: DispatchQueue, - success: @escaping (Job, Bool, Dependencies) -> (), - failure: @escaping (Job, Error?, Bool, Dependencies) -> (), - deferred: @escaping (Job, Dependencies) -> (), - using dependencies: Dependencies = Dependencies() + success: @escaping (Job, Bool) -> Void, + failure: @escaping (Job, Error, Bool) -> Void, + deferred: @escaping (Job) -> Void, + using dependencies: Dependencies ) { // Don't run when inactive or not in main app or if the user doesn't exist yet - guard (UserDefaults.sharedLokiProject?[.isMainAppActive]).defaulting(to: false) else { - return deferred(job, dependencies) // Don't need to do anything if it's not the main app + guard dependencies[defaults: .appGroup, key: .isMainAppActive] else { + return deferred(job) // Don't need to do anything if it's not the main app } - guard Identity.userCompletedRequiredOnboarding() else { - Log.info("[SyncPushTokensJob] Deferred due to incomplete registration") - return deferred(job, dependencies) + guard dependencies[cache: .onboarding].state == .completed else { + Log.info(.cat, "Deferred due to incomplete registration") + return deferred(job) } /// Since this job can be dependant on network conditions it's possible for multiple jobs to run at the same time, while this shouldn't cause issues /// it can result in multiple API calls getting made concurrently so to avoid this we defer the job as if the previous one was successful then the - /// `lastPushNotificationSync` value will prevent the subsequent call being made + /// `lastDeviceTokenUpload` value will prevent the subsequent call being made guard - dependencies.jobRunner + dependencies[singleton: .jobRunner] .jobInfoFor(state: .running, variant: .syncPushTokens) .filter({ key, info in key != job.id }) // Exclude this job .isEmpty else { // Defer the job to run 'maxRunFrequency' from when this one ran (if we don't it'll try start // it again immediately which is pointless) - let updatedJob: Job? = dependencies.storage.write { db in + let updatedJob: Job? = dependencies[singleton: .storage].write { db in try job .with(nextRunTimestamp: dependencies.dateNow.timeIntervalSince1970 + maxRunFrequency) .upserted(db) } - Log.info("[SyncPushTokensJob] Deferred due to in progress job") - return deferred(updatedJob ?? job, dependencies) + Log.info(.cat, "Deferred due to in progress job") + return deferred(updatedJob ?? job) } // Determine if the device has 'Fast Mode' (APNS) enabled - let isUsingFullAPNs: Bool = UserDefaults.standard[.isUsingFullAPNs] + let isUsingFullAPNs: Bool = dependencies[defaults: .standard, key: .isUsingFullAPNs] // If the job is running and 'Fast Mode' is disabled then we should try to unregister the existing // token guard isUsingFullAPNs else { - Just(dependencies.storage[.lastRecordedPushToken]) + Just(dependencies[singleton: .storage, key: .lastRecordedPushToken]) .setFailureType(to: Error.self) .flatMap { lastRecordedPushToken -> AnyPublisher in // Tell the device to unregister for remote notifications (essentially try to invalidate @@ -67,19 +75,20 @@ public enum SyncPushTokensJob: JobExecutor { DispatchQueue.main.sync { UIApplication.shared.unregisterForRemoteNotifications() } // Clear the old token - dependencies.storage.write(using: dependencies) { db in + dependencies[singleton: .storage].write { db in db[.lastRecordedPushToken] = nil } // Unregister from our server if let existingToken: String = lastRecordedPushToken { - Log.info("[SyncPushTokensJob] Unregister using last recorded push token: \(redact(existingToken))") - return PushNotificationAPI.unsubscribe(token: Data(hex: existingToken)) + Log.info(.cat, "Unregister using last recorded push token: \(redact(existingToken))") + return PushNotificationAPI + .unsubscribeAll(token: Data(hex: existingToken), using: dependencies) .map { _ in () } .eraseToAnyPublisher() } - Log.info("[SyncPushTokensJob] No previous token stored just triggering device unregister") + Log.info(.cat, "No previous token stored just triggering device unregister") return Just(()) .setFailureType(to: Error.self) .eraseToAnyPublisher() @@ -88,12 +97,12 @@ public enum SyncPushTokensJob: JobExecutor { .sinkUntilComplete( receiveCompletion: { result in switch result { - case .finished: Log.info("[SyncPushTokensJob] Unregister Completed") - case .failure: Log.error("[SyncPushTokensJob] Unregister Failed") + case .finished: Log.info(.cat, "Unregister Completed") + case .failure: Log.error(.cat, "Unregister Failed") } // We want to complete this job regardless of success or failure - success(job, false, dependencies) + success(job, false) } ) return @@ -103,24 +112,21 @@ public enum SyncPushTokensJob: JobExecutor { /// /// **Note:** Apple's documentation states that we should re-register for notifications on every launch: /// https://developer.apple.com/library/archive/documentation/NetworkingInternet/Conceptual/RemoteNotificationsPG/HandlingRemoteNotifications.html#//apple_ref/doc/uid/TP40008194-CH6-SW1 - Log.info("[SyncPushTokensJob] Re-registering for remote notifications") - PushRegistrationManager.shared.requestPushTokens() + Log.info(.cat, "Re-registering for remote notifications") + dependencies[singleton: .pushRegistrationManager].requestPushTokens() .flatMap { (pushToken: String, voipToken: String) -> AnyPublisher<(String, String)?, Error> in - Deferred { - Future<(String, String)?, Error> { resolver in - _ = LibSession.onPathsChanged(skipInitialCallbackIfEmpty: true) { paths, pathsChangedId in - // Only listen for the first callback - LibSession.removePathsChangedCallback(callbackId: pathsChangedId) - - guard !paths.isEmpty else { - Log.info("[SyncPushTokensJob] OS subscription completed, skipping server subscription due to lack of paths") - return resolver(Result.success(nil)) - } - - resolver(Result.success((pushToken, voipToken))) + dependencies[cache: .libSessionNetwork].paths + .first() // Only listen for the first callback + .map { paths in + guard !paths.isEmpty else { + Log.info(.cat, "OS subscription completed, skipping server subscription due to lack of paths") + return nil } + + return (pushToken, voipToken) } - }.eraseToAnyPublisher() + .setFailureType(to: Error.self) + .eraseToAnyPublisher() } .flatMap { (tokenInfo: (String, String)?) -> AnyPublisher in guard let (pushToken, voipToken): (String, String) = tokenInfo else { @@ -130,13 +136,12 @@ public enum SyncPushTokensJob: JobExecutor { } /// For our `subscribe` endpoint we only want to call it if: - /// • It's been longer than `SyncPushTokensJob.maxFrequency` since the last subscription; + /// • It's been longer than `SyncPushTokensJob.maxFrequency` since the last successful subscription; /// • The token has changed; or /// • We want to force an update - let timeSinceLastSubscription: TimeInterval = dependencies.dateNow + let timeSinceLastSuccessfulUpload: TimeInterval = dependencies.dateNow .timeIntervalSince( - dependencies.standardUserDefaults[.lastPushNotificationSync] - .defaulting(to: Date.distantPast) + Date(timeIntervalSince1970: dependencies[defaults: .standard, key: .lastDeviceTokenUpload]) ) let uploadOnlyIfStale: Bool? = { guard @@ -148,18 +153,18 @@ public enum SyncPushTokensJob: JobExecutor { }() guard - timeSinceLastSubscription >= SyncPushTokensJob.maxFrequency || - dependencies.storage[.lastRecordedPushToken] != pushToken || + timeSinceLastSuccessfulUpload >= SyncPushTokensJob.maxFrequency || + dependencies[singleton: .storage, key: .lastRecordedPushToken] != pushToken || uploadOnlyIfStale == false else { - Log.info("[SyncPushTokensJob] OS subscription completed, skipping server subscription due to frequency") + Log.info(.cat, "OS subscription completed, skipping server subscription due to frequency") return Just(()) .setFailureType(to: Error.self) .eraseToAnyPublisher() } return PushNotificationAPI - .subscribe( + .subscribeAll( token: Data(hex: pushToken), isForcedUpdate: true, using: dependencies @@ -169,14 +174,13 @@ public enum SyncPushTokensJob: JobExecutor { receiveCompletion: { result in switch result { case .failure(let error): - Log.error("[SyncPushTokensJob] Failed to register due to error: \(error)") + Log.error(.cat, "Failed to register due to error: \(error)") case .finished: - Log.debug("[SyncPushTokensJob] Recording push tokens locally. pushToken: \(redact(pushToken)), voipToken: \(redact(voipToken))") - Log.info("[SyncPushTokensJob] Completed") - dependencies.standardUserDefaults[.lastPushNotificationSync] = dependencies.dateNow + Log.debug(.cat, "Recording push tokens locally. pushToken: \(redact(pushToken)), voipToken: \(redact(voipToken))") + Log.info(.cat, "Completed") - dependencies.storage.write(using: dependencies) { db in + dependencies[singleton: .storage].write { db in db[.lastRecordedPushToken] = pushToken db[.lastRecordedVoipToken] = voipToken } @@ -189,11 +193,11 @@ public enum SyncPushTokensJob: JobExecutor { .subscribe(on: queue, using: dependencies) .sinkUntilComplete( // We want to complete this job regardless of success or failure - receiveCompletion: { _ in success(job, false, dependencies) } + receiveCompletion: { _ in success(job, false) } ) } - public static func run(uploadOnlyIfStale: Bool) { + public static func run(uploadOnlyIfStale: Bool, using dependencies: Dependencies) { guard let job: Job = Job( variant: .syncPushTokens, behaviour: .runOnce, @@ -206,9 +210,10 @@ public enum SyncPushTokensJob: JobExecutor { SyncPushTokensJob.run( job, queue: DispatchQueue.global(qos: .default), - success: { _, _, _ in }, - failure: { _, _, _, _ in }, - deferred: { _, _ in } + success: { _, _ in }, + failure: { _, _, _ in }, + deferred: { _ in }, + using: dependencies ) } } diff --git a/Session/Notifications/UserNotificationConfig.swift b/Session/Notifications/UserNotificationConfig.swift new file mode 100644 index 00000000000..970e1c007a9 --- /dev/null +++ b/Session/Notifications/UserNotificationConfig.swift @@ -0,0 +1,52 @@ +// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved. + +import Foundation +import Combine +import UserNotifications +import SessionMessagingKit +import SignalUtilitiesKit +import SessionUtilitiesKit + +class UserNotificationConfig { + class var allNotificationCategories: Set { + let categories = AppNotificationCategory.allCases.map { notificationCategory($0) } + return Set(categories) + } + + class func notificationActions(for category: AppNotificationCategory) -> [UNNotificationAction] { + return category.actions.map { notificationAction($0) } + } + + class func notificationCategory(_ category: AppNotificationCategory) -> UNNotificationCategory { + return UNNotificationCategory( + identifier: category.identifier, + actions: notificationActions(for: category), + intentIdentifiers: [], + options: [] + ) + } + + class func notificationAction(_ action: AppNotificationAction) -> UNNotificationAction { + switch action { + case .markAsRead: + return UNNotificationAction( + identifier: action.identifier, + title: "messageMarkRead".localized(), + options: [] + ) + + case .reply: + return UNTextInputNotificationAction( + identifier: action.identifier, + title: "reply".localized(), + options: [], + textInputButtonTitle: "send".localized(), + textInputPlaceholder: "" + ) + } + } + + class func action(identifier: String) -> AppNotificationAction? { + return AppNotificationAction.allCases.first { notificationAction($0).identifier == identifier } + } +} diff --git a/Session/Notifications/UserNotificationsAdaptee.swift b/Session/Notifications/UserNotificationsAdaptee.swift deleted file mode 100644 index b70c33acf10..00000000000 --- a/Session/Notifications/UserNotificationsAdaptee.swift +++ /dev/null @@ -1,308 +0,0 @@ -// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved. - -import Foundation -import Combine -import UserNotifications -import SessionMessagingKit -import SignalUtilitiesKit -import SessionUtilitiesKit - -class UserNotificationConfig { - - class var allNotificationCategories: Set { - let categories = AppNotificationCategory.allCases.map { notificationCategory($0) } - return Set(categories) - } - - class func notificationActions(for category: AppNotificationCategory) -> [UNNotificationAction] { - return category.actions.map { notificationAction($0) } - } - - class func notificationCategory(_ category: AppNotificationCategory) -> UNNotificationCategory { - return UNNotificationCategory( - identifier: category.identifier, - actions: notificationActions(for: category), - intentIdentifiers: [], - options: [] - ) - } - - class func notificationAction(_ action: AppNotificationAction) -> UNNotificationAction { - switch action { - case .markAsRead: - return UNNotificationAction( - identifier: action.identifier, - title: "messageMarkRead".localized(), - options: [] - ) - - case .reply: - return UNTextInputNotificationAction( - identifier: action.identifier, - title: "reply".localized(), - options: [], - textInputButtonTitle: "send".localized(), - textInputPlaceholder: "" - ) - } - } - - class func action(identifier: String) -> AppNotificationAction? { - return AppNotificationAction.allCases.first { notificationAction($0).identifier == identifier } - } -} - -class UserNotificationPresenterAdaptee: NSObject, UNUserNotificationCenterDelegate { - private let notificationCenter: UNUserNotificationCenter - private var notifications: Atomic<[String: UNNotificationRequest]> = Atomic([:]) - - override init() { - self.notificationCenter = UNUserNotificationCenter.current() - - super.init() - - SwiftSingletons.register(self) - } -} - -extension UserNotificationPresenterAdaptee: NotificationPresenterAdaptee { - func registerNotificationSettings() -> AnyPublisher { - return Deferred { - Future { [weak self] resolver in - self?.notificationCenter.requestAuthorization(options: [.badge, .sound, .alert]) { (granted, error) in - self?.notificationCenter.setNotificationCategories(UserNotificationConfig.allNotificationCategories) - - if granted {} - else if let error: Error = error { - Log.error("[UserNotificationPresenterAdaptee] Failed with error: \(error)") - } - else { - Log.error("[UserNotificationPresenterAdaptee] Failed without error.") - } - - // Note that the promise is fulfilled regardless of if notification permssions were - // granted. This promise only indicates that the user has responded, so we can - // proceed with requesting push tokens and complete registration. - resolver(Result.success(())) - } - } - }.eraseToAnyPublisher() - } - - func notify( - category: AppNotificationCategory, - title: String?, - body: String, - userInfo: [AnyHashable: Any], - previewType: Preferences.NotificationPreviewType, - sound: Preferences.Sound?, - threadVariant: SessionThread.Variant, - threadName: String, - applicationState: UIApplication.State, - replacingIdentifier: String? - ) { - let threadIdentifier: String? = (userInfo[AppNotificationUserInfoKey.threadId] as? String) - let content = UNMutableNotificationContent() - content.categoryIdentifier = category.identifier - content.userInfo = userInfo - content.threadIdentifier = (threadIdentifier ?? content.threadIdentifier) - - let shouldGroupNotification: Bool = ( - threadVariant == .community && - replacingIdentifier == threadIdentifier - ) - if let sound = sound, sound != .none { - content.sound = sound.notificationSound(isQuiet: (applicationState == .active)) - } - - let notificationIdentifier: String = (replacingIdentifier ?? UUID().uuidString) - let isReplacingNotification: Bool = (notifications.wrappedValue[notificationIdentifier] != nil) - let shouldPresentNotification: Bool = shouldPresentNotification( - category: category, - applicationState: applicationState, - frontMostViewController: SessionApp.currentlyOpenConversationViewController.wrappedValue, - userInfo: userInfo - ) - var trigger: UNNotificationTrigger? - - if shouldPresentNotification { - if let displayableTitle = title?.filteredForDisplay { - content.title = displayableTitle - } - - content.body = body.filteredForDisplay - - if shouldGroupNotification { - trigger = UNTimeIntervalNotificationTrigger( - timeInterval: Notifications.delayForGroupedNotifications, - repeats: false - ) - - let numberExistingNotifications: Int? = notifications.wrappedValue[notificationIdentifier]? - .content - .userInfo[AppNotificationUserInfoKey.threadNotificationCounter] - .asType(Int.self) - var numberOfNotifications: Int = (numberExistingNotifications ?? 1) - - if numberExistingNotifications != nil { - numberOfNotifications += 1 // Add one for the current notification - - content.title = (previewType == .noNameNoPreview ? - content.title : - threadName - ) - content.body = "messageNewYouveGot" - .putNumber(numberOfNotifications) - .localized() - } - - content.userInfo[AppNotificationUserInfoKey.threadNotificationCounter] = numberOfNotifications - } - } - else { - // Play sound and vibrate, but without a `body` no banner will show. - Log.debug("[UserNotificationPresenterAdaptee] Supressing notification body") - } - - let request = UNNotificationRequest( - identifier: notificationIdentifier, - content: content, - trigger: trigger - ) - - Log.debug("[UserNotificationPresenterAdaptee] Presenting notification with identifier: \(notificationIdentifier)") - - if isReplacingNotification { cancelNotifications(identifiers: [notificationIdentifier]) } - - notificationCenter.add(request) - notifications.mutate { $0[notificationIdentifier] = request } - } - - func cancelNotifications(identifiers: [String]) { - notifications.mutate { notifications in - identifiers.forEach { notifications.removeValue(forKey: $0) } - } - notificationCenter.removeDeliveredNotifications(withIdentifiers: identifiers) - notificationCenter.removePendingNotificationRequests(withIdentifiers: identifiers) - } - - func cancelNotification(_ notification: UNNotificationRequest) { - cancelNotifications(identifiers: [notification.identifier]) - } - - func cancelNotifications(threadId: String) { - let notificationsIdsToCancel: [String] = notifications.wrappedValue - .values - .compactMap { notification in - guard - let notificationThreadId: String = notification.content.userInfo[AppNotificationUserInfoKey.threadId] as? String, - notificationThreadId == threadId - else { return nil } - - return notification.identifier - } - - cancelNotifications(identifiers: notificationsIdsToCancel) - } - - func clearAllNotifications() { - notificationCenter.removeAllPendingNotificationRequests() - notificationCenter.removeAllDeliveredNotifications() - } - - func shouldPresentNotification( - category: AppNotificationCategory, - applicationState: UIApplication.State, - frontMostViewController: UIViewController?, - userInfo: [AnyHashable: Any] - ) -> Bool { - guard applicationState == .active else { return true } - - guard category == .incomingMessage || category == .errorMessage else { - return true - } - - guard let notificationThreadId = userInfo[AppNotificationUserInfoKey.threadId] as? String else { - Log.error("[UserNotificationPresenterAdaptee] threadId was unexpectedly nil") - return true - } - - guard let conversationViewController: ConversationVC = frontMostViewController as? ConversationVC else { - return true - } - - /// Show notifications for any **other** threads - return (conversationViewController.viewModel.threadData.threadId != notificationThreadId) - } -} - -@objc(OWSUserNotificationActionHandler) -public class UserNotificationActionHandler: NSObject { - - var actionHandler: NotificationActionHandler { - return NotificationActionHandler.shared - } - - func handleNotificationResponse( _ response: UNNotificationResponse, completionHandler: @escaping () -> Void, using dependencies: Dependencies) { - Log.assertOnMainThread() - handleNotificationResponse(response, using: dependencies) - .subscribe(on: DispatchQueue.global(qos: .userInitiated)) - .receive(on: DispatchQueue.main) - .sinkUntilComplete( - receiveCompletion: { result in - switch result { - case .finished: break - case .failure(let error): - completionHandler() - Log.error("Failed to handle notification response: \(error)") - } - }, - receiveValue: { _ in completionHandler() } - ) - } - - func handleNotificationResponse( _ response: UNNotificationResponse, using dependencies: Dependencies) -> AnyPublisher { - Log.assertOnMainThread() - assert(Singleton.appReadiness.isAppReady) - - let userInfo: [AnyHashable: Any] = response.notification.request.content.userInfo - let applicationState: UIApplication.State = UIApplication.shared.applicationState - - switch response.actionIdentifier { - case UNNotificationDefaultActionIdentifier: - Log.debug("Notification response: default action") - return actionHandler.showThread(userInfo: userInfo, using: dependencies) - .setFailureType(to: Error.self) - .eraseToAnyPublisher() - - case UNNotificationDismissActionIdentifier: - // TODO - mark as read? - Log.debug("Notification response: dismissed notification") - return Just(()) - .setFailureType(to: Error.self) - .eraseToAnyPublisher() - - default: - // proceed - break - } - - guard let action = UserNotificationConfig.action(identifier: response.actionIdentifier) else { - return Fail(error: NotificationError.failDebug("unable to find action for actionIdentifier: \(response.actionIdentifier)")) - .eraseToAnyPublisher() - } - - switch action { - case .markAsRead: - return actionHandler.markAsRead(userInfo: userInfo) - - case .reply: - guard let textInputResponse = response as? UNTextInputNotificationResponse else { - return Fail(error: NotificationError.failDebug("response had unexpected type: \(response)")) - .eraseToAnyPublisher() - } - - return actionHandler.reply(userInfo: userInfo, replyText: textInputResponse.userText, applicationState: applicationState) - } - } -} diff --git a/Session/Onboarding/DisplayNameScreen.swift b/Session/Onboarding/DisplayNameScreen.swift index 1551f674339..e6252eb8811 100644 --- a/Session/Onboarding/DisplayNameScreen.swift +++ b/Session/Onboarding/DisplayNameScreen.swift @@ -13,11 +13,9 @@ struct DisplayNameScreen: View { @State private var error: String? = nil private let dependencies: Dependencies - private let flow: Onboarding.Flow - public init(flow: Onboarding.Flow, using dependencies: Dependencies) { + public init(using dependencies: Dependencies) { self.dependencies = dependencies - self.flow = flow } var body: some View { @@ -30,7 +28,10 @@ struct DisplayNameScreen: View { ) { Spacer(minLength: 0) - let title: String = (self.flow == .register) ? "displayNamePick".localized() : "displayNameNew".localized() + let title: String = (dependencies[cache: .onboarding].initialFlow == .register ? + "displayNamePick".localized() : + "displayNameNew".localized() + ) Text(title) .bold() .font(.system(size: Values.veryLargeFontSize)) @@ -39,7 +40,10 @@ struct DisplayNameScreen: View { Spacer(minLength: 0) .frame(maxHeight: 2 * Values.mediumSpacing) - let explanation: String = (self.flow == .register) ? "displayNameDescription".localized() : "displayNameErrorNew".localized() + let explanation: String = (dependencies[cache: .onboarding].initialFlow == .register ? + "displayNameDescription".localized() : + "displayNameErrorNew".localized() + ) Text(explanation) .font(.system(size: Values.smallFontSize)) .foregroundColor(themeColor: .textPrimary) @@ -104,40 +108,47 @@ struct DisplayNameScreen: View { error = "displayNameErrorDescription".localized() return } - guard !ProfileManager.isTooLong(profileName: displayName) else { + guard !Profile.isTooLong(profileName: displayName) else { error = "displayNameErrorDescriptionShorter".localized() return } - // Try to save the user name but ignore the result - ProfileManager.updateLocal( - queue: .global(qos: .default), - displayNameUpdate: .currentUserUpdate(displayName), - using: dependencies - ) + // Store the new name in the onboarding cache + dependencies.mutate(cache: .onboarding) { $0.setDisplayName(displayName) } // If we are not in the registration flow then we are finished and should go straight // to the home screen - guard self.flow == .register else { - self.flow.completeRegistration() - - let homeVC: HomeVC = HomeVC(flow: self.flow, using: dependencies) - self.host.controller?.navigationController?.setViewControllers([ homeVC ], animated: true) - - return + guard dependencies[cache: .onboarding].initialFlow == .register else { + return dependencies.mutate(cache: .onboarding) { [dependencies] onboarding in + // If the `initialFlow` is `none` then it means the user is just providing a missing displayName + // and so shouldn't change the APNS setting, otherwise we should base it on the users selection + // during the onboarding process + let shouldSyncPushTokens: Bool = (onboarding.initialFlow != .none && onboarding.useAPNS) + + onboarding.completeRegistration { + // Trigger the 'SyncPushTokensJob' directly as we don't want to wait for paths to build + // before requesting the permission from the user + if shouldSyncPushTokens { SyncPushTokensJob.run(uploadOnlyIfStale: false, using: dependencies) } + + // Go to the home screen + let homeVC: HomeVC = HomeVC(using: dependencies) + dependencies[singleton: .app].setHomeViewController(homeVC) + self.host.controller?.navigationController?.setViewControllers([ homeVC ], animated: true) + } + } } // Need to get the PN mode if registering let viewController: SessionHostingViewController = SessionHostingViewController( - rootView: PNModeScreen(flow: flow, using: dependencies) + rootView: PNModeScreen(using: dependencies) ) - viewController.setUpNavBarSessionIcon() + viewController.setUpNavBarSessionIcon(using: dependencies) self.host.controller?.navigationController?.pushViewController(viewController, animated: true) } } struct DisplayNameView_Previews: PreviewProvider { static var previews: some View { - DisplayNameScreen(flow: .register, using: Dependencies()) + DisplayNameScreen(using: Dependencies.createEmpty()) } } diff --git a/Session/Onboarding/LandingScreen.swift b/Session/Onboarding/LandingScreen.swift index 3590fcb2bd3..ea67bac3fa5 100644 --- a/Session/Onboarding/LandingScreen.swift +++ b/Session/Onboarding/LandingScreen.swift @@ -1,16 +1,59 @@ // Copyright © 2023 Rangeproof Pty Ltd. All rights reserved. import SwiftUI +import Combine import SessionUIKit import SignalUtilitiesKit import SessionUtilitiesKit struct LandingScreen: View { + public class ViewModel { + fileprivate let dependencies: Dependencies + private let onOnboardingComplete: () -> () + private var disposables: Set = Set() + + init(onOnboardingComplete: @escaping () -> Void, using dependencies: Dependencies) { + self.dependencies = dependencies + self.onOnboardingComplete = onOnboardingComplete + } + + fileprivate func register(setupComplete: () -> ()) { + // Reset the Onboarding cache to create a new user (in case the user previously went back) + dependencies.set(cache: .onboarding, to: Onboarding.Cache(flow: .register, using: dependencies)) + + /// Once the onboarding process is complete we need to call `onOnboardingComplete` + dependencies[cache: .onboarding].onboardingCompletePublisher + .subscribe(on: DispatchQueue.main, using: dependencies) + .receive(on: DispatchQueue.main, using: dependencies) + .sink(receiveValue: { [weak self] _ in self?.onOnboardingComplete() }) + .store(in: &disposables) + + setupComplete() + } + + fileprivate func restore(setupComplete: () -> ()) { + // Reset the Onboarding cache to create a new user (in case the user previously went back) + dependencies.set(cache: .onboarding, to: Onboarding.Cache(flow: .restore, using: dependencies)) + + /// Once the onboarding process is complete we need to call `onOnboardingComplete` + dependencies[cache: .onboarding].onboardingCompletePublisher + .subscribe(on: DispatchQueue.main, using: dependencies) + .receive(on: DispatchQueue.main, using: dependencies) + .sink(receiveValue: { [weak self] _ in self?.onOnboardingComplete() }) + .store(in: &disposables) + + setupComplete() + } + } + @EnvironmentObject var host: HostWrapper - private let dependencies: Dependencies + private let viewModel: ViewModel - public init(using dependencies: Dependencies) { - self.dependencies = dependencies + public init(using dependencies: Dependencies, onOnboardingComplete: @escaping () -> ()) { + self.viewModel = ViewModel( + onOnboardingComplete: onOnboardingComplete, + using: dependencies + ) } var body: some View { @@ -31,7 +74,7 @@ struct LandingScreen: View { Spacer(minLength: 0) .frame(maxHeight: 2 * Values.mediumSpacing) - FakeChat() + FakeChat(using: viewModel.dependencies) Spacer(minLength: 0) .frame(maxHeight: Values.massiveSpacing) @@ -116,32 +159,23 @@ struct LandingScreen: View { } private func register() { - let seed: Data! = try! Randomness.generateRandomBytes(numberBytes: 16) - let (ed25519KeyPair, x25519KeyPair): (KeyPair, KeyPair) = try! Identity.generate(from: seed) - Onboarding.Flow.register - .preregister( - with: seed, - ed25519KeyPair: ed25519KeyPair, - x25519KeyPair: x25519KeyPair, - using: dependencies + viewModel.register { + let viewController: SessionHostingViewController = SessionHostingViewController( + rootView: DisplayNameScreen(using: viewModel.dependencies) ) - - let viewController: SessionHostingViewController = SessionHostingViewController( - rootView: DisplayNameScreen(flow: .register, using: dependencies) - ) - viewController.setUpNavBarSessionIcon() - viewController.setUpClearDataBackButton(flow: .register) - self.host.controller?.navigationController?.setViewControllers([viewController], animated: true) + viewController.setUpNavBarSessionIcon(using: viewModel.dependencies) + self.host.controller?.navigationController?.pushViewController(viewController, animated: true) + } } private func restore() { - Onboarding.Flow.register.unregister(using: dependencies) - - let viewController: SessionHostingViewController = SessionHostingViewController( - rootView: LoadAccountScreen(using: dependencies) - ) - viewController.setNavBarTitle("loadAccount".localized()) - self.host.controller?.navigationController?.pushViewController(viewController, animated: true) + viewModel.restore { + let viewController: SessionHostingViewController = SessionHostingViewController( + rootView: LoadAccountScreen(using: viewModel.dependencies) + ) + viewController.setNavBarTitle("loadAccount".localized()) + self.host.controller?.navigationController?.pushViewController(viewController, animated: true) + } } private func openLegalUrl() { @@ -191,6 +225,11 @@ struct ChatBubble: View { struct FakeChat: View { @State var numberOfBubblesShown: Int = 0 + private let dependencies: Dependencies + + public init(using dependencies: Dependencies) { + self.dependencies = dependencies + } let chatBubbles: [ChatBubble] = [ ChatBubble( @@ -255,11 +294,11 @@ struct FakeChat: View { .onAppear { guard numberOfBubblesShown < 4 else { return } - Timer.scheduledTimerOnMainThread(withTimeInterval: 0.2, repeats: false) { _ in + Timer.scheduledTimerOnMainThread(withTimeInterval: 0.2, repeats: false, using: dependencies) { [dependencies] _ in withAnimation(.spring().speed(0.68)) { numberOfBubblesShown = 1 } - Timer.scheduledTimerOnMainThread(withTimeInterval: 1.5, repeats: true) { timer in + Timer.scheduledTimerOnMainThread(withTimeInterval: 1.5, repeats: true, using: dependencies) { timer in withAnimation(.spring().speed(0.68)) { numberOfBubblesShown += 1 if numberOfBubblesShown >= 4 { @@ -274,6 +313,6 @@ struct FakeChat: View { struct LandingView_Previews: PreviewProvider { static var previews: some View { - LandingScreen(using: Dependencies()) + LandingScreen(using: Dependencies.createEmpty(), onOnboardingComplete: {}) } } diff --git a/Session/Onboarding/LoadAccountScreen.swift b/Session/Onboarding/LoadAccountScreen.swift index c1e0a1a8205..41eefd225df 100644 --- a/Session/Onboarding/LoadAccountScreen.swift +++ b/Session/Onboarding/LoadAccountScreen.swift @@ -8,14 +8,13 @@ import AVFoundation struct LoadAccountScreen: View { @EnvironmentObject var host: HostWrapper + private let dependencies: Dependencies @State var tabIndex = 0 @State private var recoveryPassword: String = "" @State private var hexEncodedSeed: String = "" @State private var errorString: String? = nil - private let dependencies: Dependencies - public init(using dependencies: Dependencies) { self.dependencies = dependencies } @@ -46,7 +45,8 @@ struct LoadAccountScreen: View { ScanQRCodeScreen( $hexEncodedSeed, error: $errorString, - continueAction: continueWithhexEncodedSeed + continueAction: continueWithHexEncodedSeed, + using: dependencies ) } } @@ -54,22 +54,18 @@ struct LoadAccountScreen: View { } private func continueWithSeed(seed: Data, from source: Onboarding.SeedSource, onSuccess: (() -> ())?, onError: (() -> ())?) { - if (seed.count != 16) { + do { + guard seed.count == 16 else { throw Mnemonic.DecodingError.generic } + + try dependencies.mutate(cache: .onboarding) { try $0.setSeedData(seed) } + } + catch { errorString = source.genericErrorMessage DispatchQueue.main.asyncAfter(deadline: .now() + 1) { onError?() } return } - let (ed25519KeyPair, x25519KeyPair) = try! Identity.generate(from: seed) - - Onboarding.Flow.recover - .preregister( - with: seed, - ed25519KeyPair: ed25519KeyPair, - x25519KeyPair: x25519KeyPair, - using: dependencies - ) DispatchQueue.main.asyncAfter(deadline: .now() + 1) { onSuccess?() @@ -77,40 +73,29 @@ struct LoadAccountScreen: View { // Otherwise continue on to request push notifications permissions let viewController: SessionHostingViewController = SessionHostingViewController( - rootView: PNModeScreen(flow: .recover, using: dependencies) + rootView: PNModeScreen(using: dependencies) ) - viewController.setUpNavBarSessionIcon() - viewController.setUpClearDataBackButton(flow: .recover) - self.host.controller?.navigationController?.setViewControllers([viewController], animated: true) + viewController.setUpNavBarSessionIcon(using: dependencies) + self.host.controller?.navigationController?.pushViewController(viewController, animated: true) } - func continueWithhexEncodedSeed(onSuccess: (() -> ())?, onError: (() -> ())?) { + func continueWithHexEncodedSeed(onSuccess: (() -> ())?, onError: (() -> ())?) { let seed = Data(hex: hexEncodedSeed) continueWithSeed(seed: seed, from: .qrCode, onSuccess: onSuccess, onError: onError) } func continueWithMnemonic() { - let mnemonic = recoveryPassword.lowercased() - let hexEncodedSeed: String do { - hexEncodedSeed = try Mnemonic.decode(mnemonic: mnemonic) + let hexEncodedSeed: String = try Mnemonic.decode(mnemonic: recoveryPassword.lowercased()) + let seed: Data = Data(hex: hexEncodedSeed) + continueWithSeed(seed: seed, from: .mnemonic, onSuccess: nil, onError: nil) } catch { - if let decodingError = error as? Mnemonic.DecodingError { - switch decodingError { - case .inputTooShort: - errorString = "recoveryPasswordErrorMessageShort".localized() - case .invalidWord: - errorString = "recoveryPasswordErrorMessageIncorrect".localized() - default: - errorString = "recoveryPasswordErrorMessageGeneric".localized() - } - } else { - errorString = "recoveryPasswordErrorMessageGeneric".localized() + switch error as? Mnemonic.DecodingError { + case .some(.inputTooShort): errorString = "recoveryPasswordErrorMessageShort".localized() + case .some(.invalidWord): errorString = "recoveryPasswordErrorMessageIncorrect".localized() + default: errorString = "recoveryPasswordErrorMessageGeneric".localized() } - return } - let seed = Data(hex: hexEncodedSeed) - continueWithSeed(seed: seed, from: .mnemonic, onSuccess: nil, onError: nil) } } @@ -220,6 +205,6 @@ struct EnterRecoveryPasswordScreen: View{ struct LoadAccountView_Previews: PreviewProvider { static var previews: some View { - LoadAccountScreen(using: Dependencies()) + LoadAccountScreen(using: Dependencies.createEmpty()) } } diff --git a/Session/Onboarding/LoadingScreen.swift b/Session/Onboarding/LoadingScreen.swift index 9010591d822..3b4949cd898 100644 --- a/Session/Onboarding/LoadingScreen.swift +++ b/Session/Onboarding/LoadingScreen.swift @@ -1,27 +1,68 @@ // Copyright © 2023 Rangeproof Pty Ltd. All rights reserved. import SwiftUI +import Combine import SessionUIKit +import SessionSnodeKit import SessionMessagingKit import SignalUtilitiesKit import SessionUtilitiesKit struct LoadingScreen: View { + public class ViewModel { + fileprivate let dependencies: Dependencies + fileprivate let preview: Bool + fileprivate var profileRetrievalCancellable: AnyCancellable? + + init(preview: Bool, using dependencies: Dependencies) { + self.preview = preview + self.dependencies = dependencies + } + + deinit { + profileRetrievalCancellable?.cancel() + } + + fileprivate func observeProfileRetrieving(onComplete: @escaping (Bool) -> ()) { + profileRetrievalCancellable = dependencies[cache: .onboarding].displayNamePublisher + .subscribe(on: DispatchQueue.global(qos: .userInitiated)) + .timeout(.seconds(15), scheduler: DispatchQueue.main, customError: { NetworkError.timeout(error: "", rawData: nil) }) + .receive(on: DispatchQueue.main) + .sink( + receiveCompletion: { _ in }, + receiveValue: { displayName in onComplete(displayName?.isEmpty == false) } + ) + } + + fileprivate func completeRegistration(onComplete: @escaping () -> ()) { + dependencies.mutate(cache: .onboarding) { [dependencies] onboarding in + let shouldSyncPushTokens: Bool = onboarding.useAPNS + + onboarding.completeRegistration { + // Trigger the 'SyncPushTokensJob' directly as we don't want to wait for paths to build + // before requesting the permission from the user + if shouldSyncPushTokens { SyncPushTokensJob.run(uploadOnlyIfStale: false, using: dependencies) } + + onComplete() + } + } + } + } + @EnvironmentObject var host: HostWrapper + private let viewModel: ViewModel @State var percentage: Double = 0.0 @State var animationTimer: Timer? - private let dependencies: Dependencies - private let flow: Onboarding.Flow - private let preview: Bool + // MARK: - Initialization - public init(flow: Onboarding.Flow, preview: Bool = false, using dependencies: Dependencies) { - self.dependencies = dependencies - self.flow = flow - self.preview = preview + public init(preview: Bool = false, using dependencies: Dependencies) { + self.viewModel = ViewModel(preview: preview, using: dependencies) } + // MARK: - UI + var body: some View { ZStack(alignment: .center) { ThemeManager.currentTheme.colorSwiftUI(for: .backgroundPrimary).ignoresSafeArea() @@ -43,7 +84,7 @@ struct LoadingScreen: View { .padding(.bottom, Values.mediumSpacing) .onAppear { progress() - observeProfileRetrieving() + viewModel.observeProfileRetrieving { finishLoading(success: $0) } } Text("waitOneMoment".localized()) @@ -65,55 +106,49 @@ struct LoadingScreen: View { private func progress() { animationTimer = Timer.scheduledTimerOnMainThread( withTimeInterval: 0.15, - repeats: true + repeats: true, + using: viewModel.dependencies ) { timer in self.percentage += 0.01 if percentage >= 1 { self.percentage = 1 timer.invalidate() - if !self.preview { finishLoading(success: false) } + if !viewModel.preview { finishLoading(success: false) } } } } - private func observeProfileRetrieving() { - Onboarding.profileNamePublisher - .subscribe(on: DispatchQueue.global(qos: .userInitiated)) - .receive(on: DispatchQueue.main) - .sinkUntilComplete( - receiveValue: { displayName in - if displayName?.isEmpty == false { - finishLoading(success: true) - } - } - ) - } - private func finishLoading(success: Bool) { + viewModel.profileRetrievalCancellable?.cancel() + animationTimer?.invalidate() + animationTimer = nil + guard success else { let viewController: SessionHostingViewController = SessionHostingViewController( - rootView: DisplayNameScreen(flow: flow, using: dependencies) + rootView: DisplayNameScreen(using: viewModel.dependencies) ) - viewController.setUpNavBarSessionIcon() + viewController.setUpNavBarSessionIcon(using: viewModel.dependencies) if let navigationController = self.host.controller?.navigationController { - let index = navigationController.viewControllers.count - 1 - navigationController.pushViewController(viewController, animated: true) - navigationController.viewControllers.remove(at: index) + let updatedViewControllers: [UIViewController] = navigationController.viewControllers + .filter { !$0.isKind(of: SessionHostingViewController.self) } + .appending(viewController) + navigationController.setViewControllers(updatedViewControllers, animated: true) } return } - self.animationTimer?.invalidate() - self.animationTimer = nil + + // Complete the animation and then complete the registration withAnimation(.linear(duration: 0.3)) { self.percentage = 1 } - DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { [dependencies] in - self.flow.completeRegistration() - - let homeVC: HomeVC = HomeVC(flow: self.flow, using: dependencies) - self.host.controller?.navigationController?.setViewControllers([ homeVC ], animated: true) + DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { + viewModel.completeRegistration { + // Go to the home screen + let homeVC: HomeVC = HomeVC(using: viewModel.dependencies) + viewModel.dependencies[singleton: .app].setHomeViewController(homeVC) + self.host.controller?.navigationController?.setViewControllers([ homeVC ], animated: true) + } } - } } @@ -128,7 +163,7 @@ struct AnimatableNumberModifier: AnimatableModifier { func body(content: Content) -> some View { content .overlay( - Text(String(format: "%.0f%%", number)) + Text(String(format: "%.0f%%", number)) // stringlint:ignore .bold() .font(.system(size: Values.superLargeFontSize)) .foregroundColor(themeColor: .textPrimary) @@ -178,6 +213,6 @@ struct CircularProgressView: View { struct LoadingView_Previews: PreviewProvider { static var previews: some View { - LoadingScreen(flow: .recover, preview: true, using: Dependencies()) + LoadingScreen(preview: true, using: Dependencies.createEmpty()) } } diff --git a/Session/Onboarding/Onboarding.swift b/Session/Onboarding/Onboarding.swift index f5a770bb3e2..2b243e952f7 100644 --- a/Session/Onboarding/Onboarding.swift +++ b/Session/Onboarding/Onboarding.swift @@ -7,51 +7,48 @@ import SessionUtilitiesKit import SessionMessagingKit import SessionSnodeKit -enum Onboarding { - private static let profileNameRetrievalIdentifier: Atomic = Atomic(nil) - private static let profileNameRetrievalPublisher: Atomic?> = Atomic(nil) - public static var profileNamePublisher: AnyPublisher { - guard let existingPublisher: AnyPublisher = profileNameRetrievalPublisher.wrappedValue else { - return profileNameRetrievalPublisher.mutate { value in - let requestId: UUID = UUID() - let result: AnyPublisher = createProfileNameRetrievalPublisher(requestId) - - value = result - profileNameRetrievalIdentifier.mutate { $0 = requestId } - return result +// MARK: - Cache + +public extension Cache { + static let onboarding: CacheConfig = Dependencies.create( + identifier: "onboarding", + createInstance: { dependencies in Onboarding.Cache(flow: .none, using: dependencies) }, + mutableInstance: { $0 }, + immutableInstance: { $0 } + ) +} + +// MARK: - Log.Category + +public extension Log.Category { + static let onboarding: Log.Category = .create("Onboarding", defaultLevel: .info) +} + +// MARK: - Onboarding + +public enum Onboarding { + public enum State: CustomStringConvertible { + case noUser + case noUserFailedIdentity + case missingName + case completed + + // stringlint:ignore_contents + public var description: String { + switch self { + case .noUser: return "No User" + case .noUserFailedIdentity: return "No User Failed Identity" + case .missingName: return "Missing Name" + case .completed: return "Completed" } } - - return existingPublisher } - private static func createProfileNameRetrievalPublisher( - _ requestId: UUID, - using dependencies: Dependencies = Dependencies() - ) -> AnyPublisher { - let userPublicKey: String = getUserHexEncodedPublicKey(using: dependencies) - - return CurrentUserPoller() - .poll( - namespaces: [.configUserProfile], - for: userPublicKey, - drainBehaviour: .alwaysRandom, - forceSynchronousProcessing: true, - using: dependencies - ) - .map { _ -> String? in - guard requestId == profileNameRetrievalIdentifier.wrappedValue else { return nil } - - return Storage.shared.read { db in - try Profile - .filter(id: userPublicKey) - .select(.name) - .asRequest(of: String.self) - .fetchOne(db) - } - } - .shareReplay(1) - .eraseToAnyPublisher() + public enum Flow { + case none + case register + case restore + case devSettings } enum SeedSource { @@ -60,157 +57,420 @@ enum Onboarding { var genericErrorMessage: String { switch self { - case .qrCode: - "qrNotRecoveryPassword".localized() - case .mnemonic: - "recoveryPasswordErrorMessageGeneric".localized() + case .qrCode: "qrNotRecoveryPassword".localized() + case .mnemonic: "recoveryPasswordErrorMessageGeneric".localized() } } } - - enum State: CustomStringConvertible { - case newUser - case missingName - case completed +} + +// MARK: - Onboarding.Cache + +extension Onboarding { + class Cache: OnboardingCacheType { + private let dependencies: Dependencies + public let id: UUID = UUID() + public let initialFlow: Onboarding.Flow + public var state: State + private let completionSubject: CurrentValueSubject = CurrentValueSubject(false) - static var current: State { - // If we have no identify information then the user needs to register - guard Identity.userExists() else { return .newUser } - - // If we have no display name then collect one (this can happen if the - // app crashed during onboarding which would leave the user in an invalid - // state with no display name) - guard !Profile.fetchOrCreateCurrentUser().name.isEmpty else { return .missingName } - - // Otherwise we have enough for a full user and can start the app - return .completed + public var seed: Data + public var ed25519KeyPair: KeyPair + public var x25519KeyPair: KeyPair + public var userSessionId: SessionId + public var useAPNS: Bool + + public var displayName: String + public var _displayNamePublisher: AnyPublisher? + private var userProfileConfigMessage: ProcessedMessage? + private var disposables: Set = Set() + + public var displayNamePublisher: AnyPublisher { + _displayNamePublisher ?? Fail(error: NetworkError.notFound).eraseToAnyPublisher() } - // stringlint:ignore_contents - var description: String { - switch self { - case .newUser: return "New User" - case .missingName: return "Missing Name" - case .completed: return "Completed" - } + public var onboardingCompletePublisher: AnyPublisher { + completionSubject + .filter { $0 } + .map { _ in () } + .eraseToAnyPublisher() } - } - - enum Flow { - case register, recover - /// If the user returns to an earlier screen during Onboarding we might need to clear out a partially created - /// account (eg. returning from the PN setting screen to the seed entry screen when linking a device) - func unregister(using dependencies: Dependencies) { - // Clear the in-memory state from LibSession - LibSession.clearMemoryState(using: dependencies) + // MARK: - Initialization + + init(flow: Onboarding.Flow, using dependencies: Dependencies) { + self.dependencies = dependencies + self.initialFlow = flow - // Clear any data which gets set during Onboarding - Storage.shared.write { db in - db[.hasViewedSeed] = false - db[.hideRecoveryPasswordPermanently] = false + /// Determine the current state based on what's in the database + typealias StoredData = ( + state: State, + displayName: String, + ed25519KeyPair: KeyPair, + x25519KeyPair: KeyPair + ) + let storedData: StoredData = dependencies[singleton: .storage].read { db -> StoredData in + // If we have no ed25519KeyPair then the user doesn't have an account + guard + let x25519KeyPair: KeyPair = Identity.fetchUserKeyPair(db), + let ed25519KeyPair: KeyPair = Identity.fetchUserEd25519KeyPair(db) + else { return (.noUser, "", KeyPair.empty, KeyPair.empty) } - try SessionThread.deleteAll(db) - try Profile.deleteAll(db) - try Contact.deleteAll(db) - try Identity.deleteAll(db) - try ConfigDump.deleteAll(db) - try SnodeReceivedMessageInfo.deleteAll(db) - } - - // Clear the profile name retrieve publisher - profileNameRetrievalIdentifier.mutate { $0 = nil } - profileNameRetrievalPublisher.mutate { $0 = nil } - - // Clear the cached 'encodedPublicKey' if needed - dependencies.caches.mutate(cache: .general) { $0.encodedPublicKey = nil } + // If we have no display name then collect one (this can happen if the + // app crashed during onboarding which would leave the user in an invalid + // state with no display name) + let displayName: String = Profile.fetchOrCreateCurrentUser(db, using: dependencies).name + guard !displayName.isEmpty else { return (.missingName, "anonymous".localized(), x25519KeyPair, ed25519KeyPair) } + + // Otherwise we have enough for a full user and can start the app + return (.completed, displayName, x25519KeyPair, ed25519KeyPair) + }.defaulting(to: (.noUser, "", KeyPair.empty, KeyPair.empty)) + + /// Store the initial `displayName` value in case we need it + self.displayName = storedData.displayName - UserDefaults.standard[.hasSyncedInitialConfiguration] = false + /// Update the cached values depending on the `initialState` + switch storedData.state { + case .noUser, .noUserFailedIdentity: + /// Remove the `LibSession.Cache` just in case (to ensure no previous state remains) + dependencies.remove(cache: .libSession) + + /// Try to generate the identity data + guard + let finalSeedData: Data = dependencies[singleton: .crypto].generate(.randomBytes(16)), + let identity: (ed25519KeyPair: KeyPair, x25519KeyPair: KeyPair) = try? Identity.generate( + from: finalSeedData, + using: dependencies + ) + else { + /// Seed or identity generation failed so leave the `Onboarding.Cache` in an invalid state for the UI to + /// recover somehow + self.state = .noUserFailedIdentity + self.seed = Data() + self.ed25519KeyPair = KeyPair(publicKey: [], secretKey: []) + self.x25519KeyPair = KeyPair(publicKey: [], secretKey: []) + self.userSessionId = .invalid + self.useAPNS = false + return + } + + /// The identity data was successfully generated so store it for the onboarding process + self.state = .noUser + self.seed = finalSeedData + self.ed25519KeyPair = identity.ed25519KeyPair + self.x25519KeyPair = identity.x25519KeyPair + self.userSessionId = SessionId(.standard, publicKey: x25519KeyPair.publicKey) + self.useAPNS = false + + case .missingName, .completed: + self.state = storedData.state + self.seed = Data() + self.ed25519KeyPair = storedData.ed25519KeyPair + self.x25519KeyPair = storedData.x25519KeyPair + self.userSessionId = dependencies[cache: .general].sessionId + self.useAPNS = dependencies[defaults: .standard, key: .isUsingFullAPNs] + + /// If we are already in a completed state then updated the completion subject accordingly + if self.state == .completed { + self.completionSubject.send(true) + } + } + } + + /// This initializer should only be used in the `DeveloperSettingsViewModel` when swapping between network service layers + init( + ed25519KeyPair: KeyPair, + x25519KeyPair: KeyPair, + displayName: String, + using dependencies: Dependencies + ) { + self.dependencies = dependencies + self.state = .completed + self.initialFlow = .devSettings + self.seed = Data() + self.ed25519KeyPair = ed25519KeyPair + self.x25519KeyPair = x25519KeyPair + self.userSessionId = SessionId(.standard, publicKey: x25519KeyPair.publicKey) + self.useAPNS = dependencies[defaults: .standard, key: .isUsingFullAPNs] + self.displayName = displayName + self._displayNamePublisher = nil } - func preregister(with seed: Data, ed25519KeyPair: KeyPair, x25519KeyPair: KeyPair, using dependencies: Dependencies) { - let x25519PublicKey = x25519KeyPair.hexEncodedPublicKey + // MARK: - Functions + + public func setSeedData(_ seedData: Data) throws { + /// Reset the disposables in case this was called with different data/ + disposables = Set() - // Create the initial shared util state (won't have been created on - // launch due to lack of ed25519 key) - LibSession.loadState( - userPublicKey: x25519PublicKey, - ed25519SecretKey: ed25519KeyPair.secretKey, + /// Generate the keys and store them + let identity: (ed25519KeyPair: KeyPair, x25519KeyPair: KeyPair) = try Identity.generate( + from: seedData, using: dependencies ) + self.seed = seedData + self.ed25519KeyPair = identity.ed25519KeyPair + self.x25519KeyPair = identity.x25519KeyPair + self.userSessionId = SessionId(.standard, publicKey: identity.x25519KeyPair.publicKey) - // Store the user identity information - Storage.shared.write { db in - try Identity.store( - db, - seed: seed, - ed25519KeyPair: ed25519KeyPair, - x25519KeyPair: x25519KeyPair - ) - - // No need to show the seed again if the user is restoring or linking - db[.hasViewedSeed] = (self == .recover) - - // Create a contact for the current user and set their approval/trusted statuses so - // they don't get weird behaviours - try Contact - .fetchOrCreate(db, id: x25519PublicKey) - .save(db) - try Contact - .filter(id: x25519PublicKey) - .updateAllAndConfig( - db, - Contact.Columns.isTrusted.set(to: true), // Always trust the current user - Contact.Columns.isApproved.set(to: true), - Contact.Columns.didApproveMe.set(to: true) + /// **Note:** We trigger this as a "background poll" as doing so means the received messages will be + /// processed immediately rather than async as part of a Job + let poller: CurrentUserPoller = CurrentUserPoller( + pollerName: "Onboarding Poller", // stringlint:ignore + pollerQueue: Threading.pollerQueue, + pollerDestination: .swarm(self.userSessionId.hexString), + pollerDrainBehaviour: .alwaysRandom, + namespaces: [.configUserProfile], + shouldStoreMessages: false, + logStartAndStopCalls: false, + customAuthMethod: Authentication.standard( + sessionId: userSessionId, + ed25519KeyPair: identity.ed25519KeyPair + ), + using: dependencies + ) + + typealias PollResult = (configMessage: ProcessedMessage, displayName: String) + let publisher: AnyPublisher = poller + .poll(forceSynchronousProcessing: true) + .tryMap { [userSessionId, dependencies] messages, _, _, _ -> PollResult? in + guard + let targetMessage: ProcessedMessage = messages.last, /// Just in case there are multiple + case let .config(_, _, serverHash, serverTimestampMs, data) = targetMessage + else { return nil } + + /// In order to process the config message we need to create and load a `libSession` cache, but we don't want to load this into + /// memory at this stage in case the user cancels the onboarding process part way through + let cache: LibSession.Cache = LibSession.Cache(userSessionId: userSessionId, using: dependencies) + cache.loadDefaultStatesFor( + userConfigVariants: [.userProfile], + groups: [], + userSessionId: userSessionId, + userEd25519KeyPair: identity.ed25519KeyPair ) - - /// Create the 'Note to Self' thread (not visible by default) - /// - /// **Note:** We need to explicitly `updateAllAndConfig` the `shouldBeVisible` value to `false` - /// otherwise it won't actually get synced correctly - try SessionThread - .fetchOrCreate(db, id: x25519PublicKey, variant: .contact, shouldBeVisible: false) - - try SessionThread - .filter(id: x25519PublicKey) - .updateAllAndConfig( - db, - SessionThread.Columns.shouldBeVisible.set(to: false) + try cache.unsafeDirectMergeConfigMessage( + swarmPublicKey: userSessionId.hexString, + messages: [ + ConfigMessageReceiveJob.Details.MessageInfo( + namespace: .configUserProfile, + serverHash: serverHash, + serverTimestampMs: serverTimestampMs, + data: data + ) + ] ) - } - - // Set hasSyncedInitialConfiguration to true so that when we hit the - // home screen a configuration sync is triggered (yes, the logic is a - // bit weird). This is needed so that if the user registers and - // immediately links a device, there'll be a configuration in their swarm. - UserDefaults.standard[.hasSyncedInitialConfiguration] = (self == .register) - - // Only continue if this isn't a new account - guard self != .register else { return } + + return (targetMessage, cache.userProfileDisplayName) + } + .handleEvents( + receiveOutput: { [weak self] result in + guard let result: PollResult = result else { return } + + /// Only store the `displayName` returned from the swarm if the user hasn't provided one in the display + /// name step (otherwise the user could enter a display name and have it immediately overwritten due to the + /// config request running slow) + if self?.displayName.isEmpty == true { + self?.displayName = result.displayName + } + + self?.userProfileConfigMessage = result.configMessage + } + ) + .map { result -> String? in result?.displayName } + .catch { error -> AnyPublisher in + Log.warn(.onboarding, "Failed to retrieve existing profile information due to error: \(error).") + return Just(nil) + .setFailureType(to: Error.self) + .eraseToAnyPublisher() + } + .shareReplay(1) + .eraseToAnyPublisher() - // Fetch any existing profile name - Onboarding.profileNamePublisher - .subscribe(on: DispatchQueue.global(qos: .userInitiated)) - .sinkUntilComplete() + /// Store the publisher and cancelable so we only make one request during onboarding + _displayNamePublisher = publisher + publisher + .subscribe(on: DispatchQueue.global(qos: .userInitiated), using: dependencies) + .sink(receiveCompletion: { _ in }, receiveValue: { _ in }) + .store(in: &disposables) + } + + func setUserAPNS(_ useAPNS: Bool) { + self.useAPNS = useAPNS + } + + func setDisplayName(_ displayName: String) { + self.displayName = displayName } - func completeRegistration() { - // Set the `lastNameUpdate` to the current date, so that we don't overwrite - // what the user set in the display name step with whatever we find in their - // swarm (otherwise the user could enter a display name and have it immediately - // overwritten due to the config request running slow) - Storage.shared.write { db in - try Profile - .filter(id: getUserHexEncodedPublicKey(db)) - .updateAllAndConfig( - db, - Profile.Columns.lastNameUpdate.set(to: Date().timeIntervalSince1970) + func completeRegistration(onComplete: @escaping (() -> Void)) { + DispatchQueue.global(qos: .userInitiated).async(using: dependencies) { [weak self, initialFlow, userSessionId, ed25519KeyPair, x25519KeyPair, useAPNS, displayName, userProfileConfigMessage, dependencies] in + /// Cache the users session id (so we don't need to fetch it from the database every time) + dependencies.mutate(cache: .general) { + $0.setCachedSessionId(sessionId: userSessionId) + } + + /// If we had a proper `initialFlow` then create a new `libSession` cache for the user + if initialFlow != .none { + dependencies.set( + cache: .libSession, + to: LibSession.Cache( + userSessionId: userSessionId, + using: dependencies + ) ) + } + + dependencies[singleton: .storage].write { db in + /// Only update the identity/contact/Note to Self state if we have a proper `initialFlow` + if initialFlow != .none { + /// Store the user identity information + try Identity.store(db, ed25519KeyPair: ed25519KeyPair, x25519KeyPair: x25519KeyPair) + + /// No need to show the seed again if the user is restoring + db[.hasViewedSeed] = (initialFlow == .restore) + + /// Create a contact for the current user and set their approval/trusted statuses so they don't get weird behaviours + try Contact + .fetchOrCreate(db, id: userSessionId.hexString, using: dependencies) + .upsert(db) + try Contact + .filter(id: userSessionId.hexString) + .updateAll( /// Current user `Contact` record not synced so no need to use `updateAllAndConfig` + db, + Contact.Columns.isTrusted.set(to: true), /// Always trust the current user + Contact.Columns.isApproved.set(to: true), + Contact.Columns.didApproveMe.set(to: true) + ) + + /// Create the 'Note to Self' thread (not visible by default) + try SessionThread.fetchOrCreate( + db, + id: userSessionId.hexString, + variant: .contact, + creationDateTimestamp: (dependencies[cache: .snodeAPI].currentOffsetTimestampMs() / 1000), + shouldBeVisible: false, + calledFromConfig: nil, + using: dependencies + ) + + /// Load the initial `libSession` state (won't have been created on launch due to lack of ed25519 key) + dependencies.mutate(cache: .libSession) { + $0.loadState(db) + + /// If we have a `userProfileConfigMessage` then we should try to handle it here as if we don't then + /// we won't even process it (because the hash may be deduped via another process) + if let userProfileConfigMessage: ProcessedMessage = userProfileConfigMessage { + try? $0.handleConfigMessages( + db, + swarmPublicKey: userSessionId.hexString, + messages: ConfigMessageReceiveJob + .Details(messages: [userProfileConfigMessage]) + .messages + ) + } + } + + /// Clear the `lastNameUpdate` timestamp and forcibly set the `displayName` provided during the onboarding + /// step (we do this after handling the config message because we want the value provided during onboarding to + /// superseed any retrieved from the config) + try Profile + .filter(id: userSessionId.hexString) + .updateAll(db, Profile.Columns.lastNameUpdate.set(to: nil)) + try Profile.updateIfNeeded( + db, + publicKey: userSessionId.hexString, + displayNameUpdate: .currentUserUpdate(displayName), + displayPictureUpdate: .none, + sentTimestamp: dependencies.dateNow.timeIntervalSince1970, + calledFromConfig: nil, + using: dependencies + ) + } + + /// Now that everything is saved we should update the `Onboarding.Cache` `state` to be `completed` (we do + /// this within the db write query because then `updateAllAndConfig` below will trigger a config sync which is + /// dependant on this `state` being updated) + self?.state = .completed + + /// We need to explicitly `updateAllAndConfig` the `shouldBeVisible` value to `false` for new accounts otherwise it + /// won't actually get synced correctly and could result in linking a second device and having the 'Note to Self' conversation incorrectly + /// being visible + if initialFlow == .register { + try SessionThread + .filter(id: userSessionId.hexString) + .updateAllAndConfig( + db, + SessionThread.Columns.shouldBeVisible.set(to: false), + SessionThread.Columns.pinnedPriority.set(to: LibSession.hiddenPriority), + calledFromConfig: nil, + using: dependencies + ) + } + } + + /// Store whether the user wants to use APNS + dependencies[defaults: .standard, key: .isUsingFullAPNs] = useAPNS + + /// Set `hasSyncedInitialConfiguration` to true so that when we hit the home screen a configuration sync is + /// triggered (yes, the logic is a bit weird). This is needed so that if the user registers and immediately links a device, + /// there'll be a configuration in their swarm. + dependencies[defaults: .standard, key: .hasSyncedInitialConfiguration] = (initialFlow == .register) + + /// Send an event indicating that registration is complete + self?.completionSubject.send(true) + + DispatchQueue.main.async(using: dependencies) { + onComplete() + } } - - // Notify the app that registration is complete - Identity.didRegister() } } } + +// MARK: - OnboardingCacheType + +/// This is a read-only version of the Cache designed to avoid unintentionally mutating the instance in a non-thread-safe way +public protocol OnboardingImmutableCacheType: ImmutableCacheType { + var id: UUID { get } + var state: Onboarding.State { get } + var initialFlow: Onboarding.Flow { get } + + var seed: Data { get } + var ed25519KeyPair: KeyPair { get } + var x25519KeyPair: KeyPair { get } + var userSessionId: SessionId { get } + var useAPNS: Bool { get } + + var displayName: String { get } + var displayNamePublisher: AnyPublisher { get } + var onboardingCompletePublisher: AnyPublisher { get } +} + +public protocol OnboardingCacheType: OnboardingImmutableCacheType, MutableCacheType { + var id: UUID { get } + var state: Onboarding.State { get } + var initialFlow: Onboarding.Flow { get } + + var seed: Data { get } + var ed25519KeyPair: KeyPair { get } + var x25519KeyPair: KeyPair { get } + var userSessionId: SessionId { get } + var useAPNS: Bool { get } + + var displayName: String { get } + var displayNamePublisher: AnyPublisher { get } + var onboardingCompletePublisher: AnyPublisher { get } + + func setSeedData(_ seedData: Data) throws + func setUserAPNS(_ useAPNS: Bool) + func setDisplayName(_ displayName: String) + + /// Complete the registration process storing the created/updated user state in the database and creating + /// the `libSession` state if needed + /// + /// **Note:** The `onComplete` callback will be run on the main thread + func completeRegistration(onComplete: @escaping (() -> Void)) +} + +public extension OnboardingCacheType { + func completeRegistration() { completeRegistration(onComplete: {}) } +} diff --git a/Session/Onboarding/PNModeScreen.swift b/Session/Onboarding/PNModeScreen.swift index 703499ef52a..969ad57ea6d 100644 --- a/Session/Onboarding/PNModeScreen.swift +++ b/Session/Onboarding/PNModeScreen.swift @@ -18,11 +18,9 @@ struct PNModeScreen: View { @State private var currentSelection: PNMode = .fast private let dependencies: Dependencies - private let flow: Onboarding.Flow - public init(flow: Onboarding.Flow, using dependencies: Dependencies) { + public init(using dependencies: Dependencies) { self.dependencies = dependencies - self.flow = flow } let options: [PNOptionView.Info] = [ @@ -128,43 +126,43 @@ struct PNModeScreen: View { } private func register() { - UserDefaults.standard[.isUsingFullAPNs] = (currentSelection == .fast) + // Store whether we want to use APNS + dependencies.mutate(cache: .onboarding) { $0.setUserAPNS(currentSelection == .fast) } // If we are registering then we can just continue on - guard flow != .register else { - return finishRegister() + guard dependencies[cache: .onboarding].initialFlow != .register else { + return completeRegistration() } // Check if we already have a profile name (ie. profile retrieval completed while waiting on // this screen) - let existingProfileName: String? = Storage.shared - .read { db in - try Profile - .filter(id: getUserHexEncodedPublicKey(db)) - .select(.name) - .asRequest(of: String.self) - .fetchOne(db) - } - - guard existingProfileName?.isEmpty != false else { + guard dependencies[cache: .onboarding].displayName.isEmpty else { // If we have one then we can go straight to the home screen - return finishRegister() + return self.completeRegistration() } // If we don't have one then show a loading indicator and try to retrieve the existing name let viewController: SessionHostingViewController = SessionHostingViewController( - rootView: LoadingScreen(flow: flow, using: dependencies) + rootView: LoadingScreen(using: dependencies) ) - viewController.setUpNavBarSessionIcon() + viewController.setUpNavBarSessionIcon(using: dependencies) self.host.controller?.navigationController?.pushViewController(viewController, animated: true) } - private func finishRegister() { - self.flow.completeRegistration() - - let homeVC: HomeVC = HomeVC(flow: self.flow, using: dependencies) - self.host.controller?.navigationController?.setViewControllers([ homeVC ], animated: true) - return + private func completeRegistration() { + dependencies.mutate(cache: .onboarding) { [dependencies] onboarding in + let shouldSyncPushTokens: Bool = onboarding.useAPNS + + onboarding.completeRegistration { + // Trigger the 'SyncPushTokensJob' directly as we don't want to wait for paths to build + // before requesting the permission from the user + if shouldSyncPushTokens { SyncPushTokensJob.run(uploadOnlyIfStale: false, using: dependencies) } + + let homeVC: HomeVC = HomeVC(using: dependencies) + dependencies[singleton: .app].setHomeViewController(homeVC) + self.host.controller?.navigationController?.setViewControllers([ homeVC ], animated: true) + } + } } } @@ -257,6 +255,6 @@ struct PNOptionView: View { struct PNModeView_Previews: PreviewProvider { static var previews: some View { - PNModeScreen(flow: .register, using: Dependencies()) + PNModeScreen(using: Dependencies.createEmpty()) } } diff --git a/Session/Open Groups/JoinOpenGroupVC.swift b/Session/Open Groups/JoinOpenGroupVC.swift index 09b71135d12..bbd393d8bd6 100644 --- a/Session/Open Groups/JoinOpenGroupVC.swift +++ b/Session/Open Groups/JoinOpenGroupVC.swift @@ -10,6 +10,7 @@ import SessionUtilitiesKit import SignalUtilitiesKit final class JoinOpenGroupVC: BaseVC, UIPageViewControllerDataSource, UIPageViewControllerDelegate, QRScannerDelegate, NavigatableStateHolder { + private let dependencies: Dependencies public let navigatableState: NavigatableState = NavigatableState() private var disposables: Set = Set() @@ -36,14 +37,14 @@ final class JoinOpenGroupVC: BaseVC, UIPageViewControllerDataSource, UIPageViewC }() private lazy var enterURLVC: EnterURLVC = { - let result: EnterURLVC = EnterURLVC() + let result: EnterURLVC = EnterURLVC(using: dependencies) result.joinOpenGroupVC = self return result }() private lazy var scanQRCodePlaceholderVC: ScanQRCodePlaceholderVC = { - let result: ScanQRCodePlaceholderVC = ScanQRCodePlaceholderVC() + let result: ScanQRCodePlaceholderVC = ScanQRCodePlaceholderVC(using: dependencies) result.joinOpenGroupVC = self return result @@ -56,6 +57,18 @@ final class JoinOpenGroupVC: BaseVC, UIPageViewControllerDataSource, UIPageViewC return result }() + // MARK: - Initialization + + init(using dependencies: Dependencies) { + self.dependencies = dependencies + + super.init(nibName: nil, bundle: nil) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + // MARK: - Lifecycle override func viewDidLoad() { @@ -168,10 +181,16 @@ final class JoinOpenGroupVC: BaseVC, UIPageViewControllerDataSource, UIPageViewC joinOpenGroup(roomToken: room, server: server, publicKey: publicKey, shouldOpenCommunity: true, onError: onError) } - fileprivate func joinOpenGroup(roomToken: String, server: String, publicKey: String, shouldOpenCommunity: Bool, onError: (() -> ())?) { + fileprivate func joinOpenGroup( + roomToken: String, + server: String, + publicKey: String, + shouldOpenCommunity: Bool, + onError: (() -> ())? + ) { guard !isJoining, let navigationController: UINavigationController = navigationController else { return } - guard OpenGroupManager.shared.hasExistingOpenGroup( + guard dependencies[singleton: .openGroupManager].hasExistingOpenGroup( roomToken: roomToken, server: server, publicKey: publicKey @@ -185,24 +204,25 @@ final class JoinOpenGroupVC: BaseVC, UIPageViewControllerDataSource, UIPageViewC isJoining = true - ModalActivityIndicatorViewController.present(fromViewController: navigationController, canCancel: false) { [weak self] _ in - Storage.shared + ModalActivityIndicatorViewController.present(fromViewController: navigationController, canCancel: false) { [weak self, dependencies] _ in + dependencies[singleton: .storage] .writePublisher { db in - OpenGroupManager.shared.add( + dependencies[singleton: .openGroupManager].add( db, roomToken: roomToken, server: server, publicKey: publicKey, - calledFromConfigHandling: false + calledFromConfig: nil ) } .flatMap { successfullyAddedGroup in - OpenGroupManager.shared.performInitialRequestsAfterAdd( + dependencies[singleton: .openGroupManager].performInitialRequestsAfterAdd( + queue: DispatchQueue.global(qos: .userInitiated), successfullyAddedGroup: successfullyAddedGroup, roomToken: roomToken, server: server, publicKey: publicKey, - calledFromConfigHandling: false + calledFromConfig: nil ) } .subscribe(on: DispatchQueue.global(qos: .userInitiated)) @@ -214,11 +234,11 @@ final class JoinOpenGroupVC: BaseVC, UIPageViewControllerDataSource, UIPageViewC // If there was a failure then the group will be in invalid state until // the next launch so remove it (the user will be left on the previous // screen so can re-trigger the join) - Storage.shared.writeAsync { db in - OpenGroupManager.shared.delete( + dependencies[singleton: .storage].writeAsync { db in + try dependencies[singleton: .openGroupManager].delete( db, openGroupId: OpenGroup.idFor(roomToken: roomToken, server: server), - calledFromConfigHandling: false + calledFromConfig: nil ) } @@ -236,9 +256,10 @@ final class JoinOpenGroupVC: BaseVC, UIPageViewControllerDataSource, UIPageViewC self?.presentingViewController?.dismiss(animated: true, completion: nil) if shouldOpenCommunity { - SessionApp.presentConversationCreatingIfNeeded( + dependencies[singleton: .app].presentConversationCreatingIfNeeded( for: OpenGroup.idFor(roomToken: roomToken, server: server), variant: .community, + action: .none, dismissing: nil, animated: false ) @@ -270,6 +291,7 @@ final class JoinOpenGroupVC: BaseVC, UIPageViewControllerDataSource, UIPageViewC private final class EnterURLVC: UIViewController, UIGestureRecognizerDelegate, OpenGroupSuggestionGridDelegate { weak var joinOpenGroupVC: JoinOpenGroupVC? + private let dependencies: Dependencies private var isKeyboardShowing = false private var bottomConstraint: NSLayoutConstraint! private let bottomMargin: CGFloat = (UIDevice.current.isIPad ? Values.largeSpacing : 0) @@ -302,7 +324,7 @@ private final class EnterURLVC: UIViewController, UIGestureRecognizerDelegate, O lazy var suggestionGrid: OpenGroupSuggestionGrid = { let maxWidth: CGFloat = (UIScreen.main.bounds.width - Values.largeSpacing * 2) - let result: OpenGroupSuggestionGrid = OpenGroupSuggestionGrid(maxWidth: maxWidth) + let result: OpenGroupSuggestionGrid = OpenGroupSuggestionGrid(maxWidth: maxWidth, using: dependencies) result.delegate = self return result @@ -310,7 +332,23 @@ private final class EnterURLVC: UIViewController, UIGestureRecognizerDelegate, O private var viewWidth: NSLayoutConstraint? private var viewHeight: NSLayoutConstraint? - + + // MARK: - Initialization + + init(using dependencies: Dependencies) { + self.dependencies = dependencies + + super.init(nibName: nil, bundle: nil) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + deinit { + NotificationCenter.default.removeObserver(self) + } + // MARK: - Lifecycle override func viewDidLoad() { @@ -379,9 +417,6 @@ private final class EnterURLVC: UIViewController, UIGestureRecognizerDelegate, O object: nil ) } - deinit { - NotificationCenter.default.removeObserver(self) - } // MARK: - General @@ -507,11 +542,24 @@ private final class EnterURLVC: UIViewController, UIGestureRecognizerDelegate, O } private final class ScanQRCodePlaceholderVC: UIViewController { + private let dependencies: Dependencies weak var joinOpenGroupVC: JoinOpenGroupVC? private var viewWidth: NSLayoutConstraint? private var viewHeight: NSLayoutConstraint? + // MARK: - Initialization + + init(using dependencies: Dependencies) { + self.dependencies = dependencies + + super.init(nibName: nil, bundle: nil) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + // MARK: - Lifecycle override func viewDidLoad() { @@ -566,7 +614,7 @@ private final class ScanQRCodePlaceholderVC: UIViewController { } @objc private func requestCameraAccess() { - Permissions.requestCameraPermissionIfNeeded { [weak self] in + Permissions.requestCameraPermissionIfNeeded(using: dependencies) { [weak self] in self?.joinOpenGroupVC?.handleCameraAccessGranted() } } diff --git a/Session/Open Groups/OpenGroupSuggestionGrid.swift b/Session/Open Groups/OpenGroupSuggestionGrid.swift index 873ee85b914..15f4eb8af32 100644 --- a/Session/Open Groups/OpenGroupSuggestionGrid.swift +++ b/Session/Open Groups/OpenGroupSuggestionGrid.swift @@ -1,15 +1,30 @@ // Copyright © 2022 Rangeproof Pty Ltd. All rights reserved. import Foundation +import GRDB import Combine import NVActivityIndicatorView import SessionMessagingKit import SessionUIKit +import SessionUtilitiesKit final class OpenGroupSuggestionGrid: UIView, UICollectionViewDataSource, UICollectionViewDelegateFlowLayout { + private let dependencies: Dependencies private let itemsPerSection: Int = (UIDevice.current.isIPad ? 4 : 2) private var maxWidth: CGFloat - private var data: [OpenGroupManager.DefaultRoomInfo] = [] { didSet { update() } } + private var data: [OpenGroupManager.DefaultRoomInfo] = [] { + didSet { + // Start an observer for changes + let updatedIds: Set = data.map { $0.openGroup.id }.asSet() + + if oldValue.map({ $0.openGroup.id }).asSet() != updatedIds { + startObservingRoomChanges(for: updatedIds) + } + } + } + private var dataChangeObservable: DatabaseCancellable? { + didSet { oldValue?.cancel() } // Cancel the old observable if there was one + } private var heightConstraint: NSLayoutConstraint! var delegate: OpenGroupSuggestionGridDelegate? @@ -96,7 +111,8 @@ final class OpenGroupSuggestionGrid: UIView, UICollectionViewDataSource, UIColle // MARK: - Initialization - init(maxWidth: CGFloat) { + init(maxWidth: CGFloat, using dependencies: Dependencies) { + self.dependencies = dependencies self.maxWidth = maxWidth super.init(frame: CGRect.zero) @@ -142,7 +158,7 @@ final class OpenGroupSuggestionGrid: UIView, UICollectionViewDataSource, UIColle heightConstraint = set(.height, to: OpenGroupSuggestionGrid.cellHeight) widthAnchor.constraint(greaterThanOrEqualToConstant: OpenGroupSuggestionGrid.cellHeight).isActive = true - OpenGroupManager.getDefaultRoomsIfNeeded() + dependencies[cache: .openGroupManager].defaultRoomsPublisher .subscribe(on: DispatchQueue.global(qos: .default)) .receive(on: DispatchQueue.main) .sinkUntilComplete( @@ -158,6 +174,33 @@ final class OpenGroupSuggestionGrid: UIView, UICollectionViewDataSource, UIColle // MARK: - Updating + private func startObservingRoomChanges(for openGroupIds: Set) { + // We don't actually care about the updated data as the 'update' function has the logic + // to fetch any newly downloaded images + dataChangeObservable = dependencies[singleton: .storage].start( + ValueObservation + .tracking( + regions: [ + OpenGroup.select(.name).filter(ids: openGroupIds), + OpenGroup.select(.roomDescription).filter(ids: openGroupIds), + OpenGroup.select(.displayPictureFilename).filter(ids: openGroupIds) + ], + fetch: { db in try OpenGroup.filter(ids: openGroupIds).fetchAll(db) } + ) + .removeDuplicates(), + onError: { _ in }, + onChange: { [weak self] result in + guard let strongSelf = self else { return } + + let updatedGroupsByToken: [String: OpenGroup] = result + .reduce(into: [:]) { result, next in result[next.roomToken] = next } + strongSelf.data = strongSelf.data + .map { room, oldGroup in (room, (updatedGroupsByToken[room.token] ?? oldGroup)) } + strongSelf.update() + } + ) + } + private func update() { spinner.stopAnimating() spinner.isHidden = true @@ -202,7 +245,7 @@ final class OpenGroupSuggestionGrid: UIView, UICollectionViewDataSource, UIColle func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell { let cell: Cell = collectionView.dequeue(type: Cell.self, for: indexPath) - cell.update(with: data[indexPath.item].room, existingImageData: data[indexPath.item].existingImageData) + cell.update(with: data[indexPath.item].room, openGroup: data[indexPath.item].openGroup, using: dependencies) return cell } @@ -311,53 +354,12 @@ extension OpenGroupSuggestionGrid { snContentView.pin(to: self) } - fileprivate func update(with room: OpenGroupAPI.Room, existingImageData: Data?) { + fileprivate func update(with room: OpenGroupAPI.Room, openGroup: OpenGroup, using dependencies: Dependencies) { label.text = room.name - - // Only continue if we have a room image - guard let imageId: String = room.imageId else { - imageView.isHidden = true - return - } - - imageView.image = nil - - Publishers - .MergeMany( - OpenGroupManager - .roomImage( - fileId: imageId, - for: room.token, - on: OpenGroupAPI.defaultServer, - existingData: existingImageData - ) - .map { ($0, true) } - .eraseToAnyPublisher(), - // If we have already received the room image then the above will emit first and - // we can ignore this 'Just' call which is used to hide the image while loading - Just((Data(), false)) - .setFailureType(to: Error.self) - .delay(for: .milliseconds(10), scheduler: DispatchQueue.main) - .eraseToAnyPublisher() - ) - .subscribe(on: DispatchQueue.global(qos: .userInitiated)) - .receive(on: DispatchQueue.main) - .sinkUntilComplete( - receiveValue: { [weak self] imageData, hasData in - guard hasData else { - // This will emit twice (once with the data and once without it), if we - // have actually received the images then we don't want the second emission - // to hide the imageView anymore - if self?.imageView.image == nil { - self?.imageView.isHidden = true - } - return - } - - self?.imageView.image = UIImage(data: imageData) - self?.imageView.isHidden = (self?.imageView.image == nil) - } - ) + imageView.image = DisplayPictureManager + .displayPicture(owner: .community(openGroup), using: dependencies) + .map { UIImage(data: $0) } + imageView.isHidden = (imageView.image == nil) } } } diff --git a/Session/Path/PathStatusView.swift b/Session/Path/PathStatusView.swift index c2e4325c5d9..a90b9563fa7 100644 --- a/Session/Path/PathStatusView.swift +++ b/Session/Path/PathStatusView.swift @@ -1,6 +1,7 @@ // Copyright © 2022 Rangeproof Pty Ltd. All rights reserved. import UIKit +import Combine import SessionUIKit import SessionSnodeKit import SessionMessagingKit @@ -28,10 +29,12 @@ final class PathStatusView: UIView { // MARK: - Initialization + private let dependencies: Dependencies private let size: Size - private var networkStatusCallbackId: UUID? + private var disposables: Set = Set() - init(size: Size = .small) { + init(size: Size = .small, using dependencies: Dependencies) { + self.dependencies = dependencies self.size = size super.init(frame: .zero) @@ -41,16 +44,7 @@ final class PathStatusView: UIView { } required init?(coder: NSCoder) { - self.size = .small - - super.init(coder: coder) - - setUpViewHierarchy() - registerObservers() - } - - deinit { - LibSession.removeNetworkChangedCallback(callbackId: networkStatusCallbackId) + fatalError("init(coder:) has not been implemented") } // MARK: - Layout @@ -65,15 +59,24 @@ final class PathStatusView: UIView { // MARK: - Functions private func registerObservers() { - // Register for status updates (will be called immediately with current status) - networkStatusCallbackId = LibSession.onNetworkStatusChanged { [weak self] status in - DispatchQueue.main.async { - self?.setStatus(to: status) - } - } + /// Register for status updates (will be called immediately with current status) + dependencies[cache: .libSessionNetwork].networkStatus + .subscribe(on: DispatchQueue.global(qos: .background), using: dependencies) + .receive(on: DispatchQueue.main, using: dependencies) + .sink( + receiveCompletion: { [weak self] _ in + /// If the stream completes it means the network cache was reset in which case we want to + /// re-register for updates in the next run loop (as the new cache should be created by then) + DispatchQueue.global(qos: .background).async { + self?.registerObservers() + } + }, + receiveValue: { [weak self] status in self?.setStatus(to: status) } + ) + .store(in: &disposables) } - private func setStatus(to status: LibSession.NetworkStatus) { + private func setStatus(to status: NetworkStatus) { themeBackgroundColor = status.themeColor layer.themeShadowColor = status.themeColor layer.shadowOffset = CGSize(width: 0, height: 0.8) @@ -91,7 +94,7 @@ final class PathStatusView: UIView { } } -public extension LibSession.NetworkStatus { +public extension NetworkStatus { var themeColor: ThemeValue { switch self { case .unknown: return .path_unknown diff --git a/Session/Path/PathVC.swift b/Session/Path/PathVC.swift index faff67e7813..a64a5fafcac 100644 --- a/Session/Path/PathVC.swift +++ b/Session/Path/PathVC.swift @@ -13,7 +13,7 @@ final class PathVC: BaseVC { public static let expandedDotSize: CGFloat = 16 private static let rowHeight: CGFloat = (isIPhone5OrSmaller ? 52 : 75) - private var pathUpdateId: UUID? + private let dependencies: Dependencies private var lastPath: [LibSession.Snode] = [] private var disposables: Set = Set() @@ -53,12 +53,20 @@ final class PathVC: BaseVC { return result }() - // MARK: - Lifecycle + // MARK: - Initialization - deinit { - LibSession.removeNetworkChangedCallback(callbackId: pathUpdateId) + init(using dependencies: Dependencies) { + self.dependencies = dependencies + + super.init(nibName: nil, bundle: nil) } + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + // MARK: - Lifecycle + override func viewDidLoad() { super.viewDidLoad() @@ -121,16 +129,9 @@ final class PathVC: BaseVC { // Set up spacer constraints topSpacer.heightAnchor.constraint(equalTo: bottomSpacer.heightAnchor).isActive = true - // Register for status updates (will be called immediately with current paths) - pathUpdateId = LibSession.onPathsChanged { [weak self] paths, _ in - DispatchQueue.main.async { - self?.update(paths: paths, force: false) - } - } - // Register for path country updates - IP2Country.cacheLoaded - .receive(on: DispatchQueue.main) + dependencies[cache: .ip2Country].cacheLoaded + .receive(on: DispatchQueue.main, using: dependencies) .sink(receiveValue: { [weak self] _ in switch (self?.lastPath, self?.lastPath.isEmpty == true) { case (.none, _), (_, true): self?.update(paths: [], force: true) @@ -138,10 +139,31 @@ final class PathVC: BaseVC { } }) .store(in: &disposables) + + // Register for network updates + registerNetworkObservables() } // MARK: - Updating + private func registerNetworkObservables() { + /// Register for status updates (will be called immediately with current paths) + dependencies[cache: .libSessionNetwork].paths + .subscribe(on: DispatchQueue.global(qos: .background), using: dependencies) + .receive(on: DispatchQueue.main, using: dependencies) + .sink( + receiveCompletion: { [weak self] _ in + /// If the stream completes it means the network cache was reset in which case we want to + /// re-register for updates in the next run loop (as the new cache should be created by then) + DispatchQueue.global(qos: .background).async { + self?.registerNetworkObservables() + } + }, + receiveValue: { [weak self] paths in self?.update(paths: paths, force: false) } + ) + .store(in: &disposables) + } + private func update(paths: [[LibSession.Snode]], force: Bool) { guard let pathToDisplay: [LibSession.Snode] = paths.first else { pathStackView.arrangedSubviews.forEach { $0.removeFromSuperview() } @@ -200,7 +222,8 @@ final class PathVC: BaseVC { let lineView = LineView( location: location, dotAnimationStartDelay: dotAnimationStartDelay, - dotAnimationRepeatInterval: dotAnimationRepeatInterval + dotAnimationRepeatInterval: dotAnimationRepeatInterval, + using: dependencies ) lineView.set(.width, to: PathVC.expandedDotSize) lineView.set(.height, to: PathVC.rowHeight) @@ -237,7 +260,7 @@ final class PathVC: BaseVC { "onionRoutingPathEntryNode".localized() : "onionRoutingPathServiceNode".localized() ), - subtitle: IP2Country.country(for: snode.ip), + subtitle: dependencies[cache: .ip2Country].country(for: snode.ip), location: location, dotAnimationStartDelay: dotAnimationStartDelay, dotAnimationRepeatInterval: dotAnimationRepeatInterval @@ -262,34 +285,15 @@ private final class LineView: UIView { private var dotViewWidthConstraint: NSLayoutConstraint! private var dotViewHeightConstraint: NSLayoutConstraint! private var dotViewAnimationTimer: Timer! - private var networkStatusCallbackId: UUID? + private var disposables: Set = Set() enum Location { case top, middle, bottom } - - private lazy var dotView: UIView = { - let result = UIView() - result.themeBackgroundColor = .path_connected - result.layer.themeShadowColor = .path_connected - result.layer.shadowOffset = .zero - result.layer.shadowPath = UIBezierPath( - ovalIn: CGRect( - origin: CGPoint.zero, - size: CGSize(width: PathVC.dotSize, height: PathVC.dotSize) - ) - ).cgPath - result.layer.cornerRadius = (PathVC.dotSize / 2) - - ThemeManager.onThemeChange(observer: result) { [weak result] theme, _ in - result?.layer.shadowOpacity = (theme.interfaceStyle == .light ? 0.4 : 1) - result?.layer.shadowRadius = (theme.interfaceStyle == .light ? 1 : 2) - } - - return result - }() - init(location: Location, dotAnimationStartDelay: Double, dotAnimationRepeatInterval: Double) { + // MARK: - Initialization + + init(location: Location, dotAnimationStartDelay: Double, dotAnimationRepeatInterval: Double, using dependencies: Dependencies) { self.location = location self.dotAnimationStartDelay = dotAnimationStartDelay self.dotAnimationRepeatInterval = dotAnimationRepeatInterval @@ -297,7 +301,7 @@ private final class LineView: UIView { super.init(frame: CGRect.zero) setUpViewHierarchy() - registerObservers() + registerObservers(using: dependencies) } override init(frame: CGRect) { @@ -309,10 +313,34 @@ private final class LineView: UIView { } deinit { - LibSession.removeNetworkChangedCallback(callbackId: networkStatusCallbackId) dotViewAnimationTimer?.invalidate() } + // MARK: - Components + + private lazy var dotView: UIView = { + let result = UIView() + result.themeBackgroundColor = .path_connected + result.layer.themeShadowColor = .path_connected + result.layer.shadowOffset = .zero + result.layer.shadowPath = UIBezierPath( + ovalIn: CGRect( + origin: CGPoint.zero, + size: CGSize(width: PathVC.dotSize, height: PathVC.dotSize) + ) + ).cgPath + result.layer.cornerRadius = (PathVC.dotSize / 2) + + ThemeManager.onThemeChange(observer: result) { [weak result] theme, _ in + result?.layer.shadowOpacity = (theme.interfaceStyle == .light ? 0.4 : 1) + result?.layer.shadowRadius = (theme.interfaceStyle == .light ? 1 : 2) + } + + return result + }() + + // MARK: - Layout + private func setUpViewHierarchy() { let lineView = UIView() lineView.set(.width, to: Values.separatorThickness) @@ -347,13 +375,21 @@ private final class LineView: UIView { } } - private func registerObservers() { - // Register for status updates (will be called immediately with current status) - networkStatusCallbackId = LibSession.onNetworkStatusChanged { [weak self] status in - DispatchQueue.main.async { - self?.setStatus(to: status) - } - } + private func registerObservers(using dependencies: Dependencies) { + /// Register for status updates (will be called immediately with current status) + dependencies[cache: .libSessionNetwork].networkStatus + .receive(on: DispatchQueue.main, using: dependencies) + .sink( + receiveCompletion: { [weak self] _ in + /// If the stream completes it means the network cache was reset in which case we want to + /// re-register for updates in the next run loop (as the new cache should be created by then) + DispatchQueue.global(qos: .background).async { + self?.registerObservers(using: dependencies) + } + }, + receiveValue: { [weak self] status in self?.setStatus(to: status) } + ) + .store(in: &disposables) } private func animate() { @@ -379,7 +415,7 @@ private final class LineView: UIView { } } - private func setStatus(to status: LibSession.NetworkStatus) { + private func setStatus(to status: NetworkStatus) { dotView.themeBackgroundColor = status.themeColor dotView.layer.themeShadowColor = status.themeColor } diff --git a/Session/Settings/AppearanceViewController.swift b/Session/Settings/AppearanceViewController.swift index 397bb8f0edc..9e40ec540ef 100644 --- a/Session/Settings/AppearanceViewController.swift +++ b/Session/Settings/AppearanceViewController.swift @@ -6,6 +6,20 @@ import SignalUtilitiesKit import SessionUtilitiesKit final class AppearanceViewController: BaseVC { + // MARK: - Initialization + + private let dependencies: Dependencies + + init(using dependencies: Dependencies) { + self.dependencies = dependencies + + super.init(nibName: nil, bundle: nil) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + // MARK: - Components private let scrollView: UIScrollView = { @@ -48,7 +62,7 @@ final class AppearanceViewController: BaseVC { private lazy var themeSelectionViews: [ThemeSelectionView] = Theme.allCases .map { theme in let result: ThemeSelectionView = ThemeSelectionView(theme: theme) { [weak self] theme in - ThemeManager.currentTheme = theme + ThemeManager.updateThemeState(theme: theme) } result.update(isSelected: (ThemeManager.currentTheme == theme)) @@ -75,8 +89,8 @@ final class AppearanceViewController: BaseVC { return result }() - private let primaryColorPreviewView: ThemePreviewView = { - let result: ThemePreviewView = ThemePreviewView() + private lazy var primaryColorPreviewView: ThemePreviewView = { + let result: ThemePreviewView = ThemePreviewView(using: dependencies) result.translatesAutoresizingMaskIntoConstraints = false return result @@ -94,7 +108,7 @@ final class AppearanceViewController: BaseVC { trailing: Values.largeSpacing ) - if Singleton.hasAppContext && Singleton.appContext.isRTL { + if Dependencies.isRTL { result.transform = CGAffineTransform.identity.scaledBy(x: -1, y: 1) } @@ -114,7 +128,7 @@ final class AppearanceViewController: BaseVC { private lazy var primaryColorSelectionViews: [PrimaryColorSelectionView] = Theme.PrimaryColor.allCases .map { color in let result: PrimaryColorSelectionView = PrimaryColorSelectionView(color: color) { [weak self] color in - ThemeManager.primaryColor = color + ThemeManager.updateThemeState(primaryColor: color) } result.update(isSelected: (ThemeManager.primaryColor == color)) @@ -218,7 +232,7 @@ final class AppearanceViewController: BaseVC { nightModeToggleView.addSubview(nightModeToggleLabel) nightModeToggleView.addSubview(nightModeToggleSwitch) - // Register an observer so when the theme changes the selected theme and primary colour + // Register an observer so when the theme changes the selected theme and primary color // are both updated to match ThemeManager.onThemeChange(observer: self) { [weak self] theme, primaryColor in self?.themeSelectionViews.forEach { view in @@ -281,6 +295,6 @@ final class AppearanceViewController: BaseVC { // MARK: - Actions @objc private func nightModeToggleChanged(sender: UISwitch) { - ThemeManager.matchSystemNightModeSetting = sender.isOn + ThemeManager.updateThemeState(matchSystemNightModeSetting: sender.isOn) } } diff --git a/Session/Settings/BlockedContactsViewModel.swift b/Session/Settings/BlockedContactsViewModel.swift index 764b55cc8fd..57aa14d5287 100644 --- a/Session/Settings/BlockedContactsViewModel.swift +++ b/Session/Settings/BlockedContactsViewModel.swift @@ -6,6 +6,7 @@ import GRDB import DifferenceKit import SessionUIKit import SignalUtilitiesKit +import SessionMessagingKit import SessionUtilitiesKit public class BlockedContactsViewModel: SessionTableViewModel, NavigatableStateHolder, ObservableTableSource, PagedObservationSource { @@ -15,12 +16,12 @@ public class BlockedContactsViewModel: SessionTableViewModel, NavigatableStateHo public let navigatableState: NavigatableState = NavigatableState() public let state: TableDataState = TableDataState() public let observableState: ObservableTableSourceState = ObservableTableSourceState() - private let selectedContactIdsSubject: CurrentValueSubject, Never> = CurrentValueSubject([]) + private let selectedIdsSubject: CurrentValueSubject, Never> = CurrentValueSubject([]) public private(set) var pagedDataObserver: PagedDatabaseObserver? // MARK: - Initialization - init(using dependencies: Dependencies = Dependencies()) { + init(using dependencies: Dependencies) { self.dependencies = dependencies self.pagedDataObserver = nil @@ -68,7 +69,8 @@ public class BlockedContactsViewModel: SessionTableViewModel, NavigatableStateHo else { return } self?.pendingTableDataSubject.send(data) - } + }, + using: dependencies ) // Run the initial query on a background thread so we don't block the push transition @@ -98,7 +100,7 @@ public class BlockedContactsViewModel: SessionTableViewModel, NavigatableStateHo let emptyStateTextPublisher: AnyPublisher = Just("blockBlockedNone".localized()) .eraseToAnyPublisher() - lazy var footerButtonInfo: AnyPublisher = selectedContactIdsSubject + lazy var footerButtonInfo: AnyPublisher = selectedIdsSubject .prepend([]) .map { selectedContactIds in SessionButton.Info( @@ -127,30 +129,24 @@ public class BlockedContactsViewModel: SessionTableViewModel, NavigatableStateHo return (lhsValue < rhsValue) } - .map { [weak self] model -> SessionCell.Info in + .map { [selectedIdsSubject] model -> SessionCell.Info in SessionCell.Info( id: model, - leftAccessory: .profile(id: model.id, profile: model.profile), + leadingAccessory: .profile(id: model.id, profile: model.profile), title: ( model.profile?.displayName() ?? Profile.truncated(id: model.id, truncating: .middle) ), - rightAccessory: .radio( - isSelected: { - self?.selectedContactIdsSubject.value.contains(model.id) == true - } + trailingAccessory: .radio( + liveIsSelected: { selectedIdsSubject.value.contains(model.id) == true } ), onTap: { - var updatedSelectedIds: Set = (self?.selectedContactIdsSubject.value ?? []) - - if !updatedSelectedIds.contains(model.id) { - updatedSelectedIds.insert(model.id) + if !selectedIdsSubject.value.contains(model.id) { + selectedIdsSubject.send(selectedIdsSubject.value.inserting(model.id)) } else { - updatedSelectedIds.remove(model.id) + selectedIdsSubject.send(selectedIdsSubject.value.removing(model.id)) } - - self?.selectedContactIdsSubject.send(updatedSelectedIds) } ) } @@ -164,13 +160,13 @@ public class BlockedContactsViewModel: SessionTableViewModel, NavigatableStateHo } private func unblockTapped() { - guard !selectedContactIdsSubject.value.isEmpty else { return } + guard !selectedIdsSubject.value.isEmpty else { return } - let contactIds: Set = selectedContactIdsSubject.value + let contactIds: Set = selectedIdsSubject.value let contactNames: [String] = contactIds .compactMap { contactId in guard - let section: BlockedContactsViewModel.SectionModel = self.tableData + let section: SectionModel = self.tableData .first(where: { section in section.model == .contacts }), let info: SessionCell.Info = section.elements .first(where: { info in info.id.id == contactId }) @@ -183,19 +179,21 @@ public class BlockedContactsViewModel: SessionTableViewModel, NavigatableStateHo let confirmationBody: NSAttributedString = { let name: String = contactNames.first ?? "" switch contactNames.count { - case 1: - return "blockUnblockName" - .put(key: "name", value: name) - .localizedFormatted(baseFont: .systemFont(ofSize: Values.smallFontSize)) - case 2: - return "blockUnblockNameTwo" - .put(key: "name", value: name) - .localizedFormatted(baseFont: .systemFont(ofSize: Values.smallFontSize)) - default: - return "blockUnblockNameMultiple" - .put(key: "name", value: name) - .put(key: "count", value: contactNames.count - 1) - .localizedFormatted(baseFont: .systemFont(ofSize: Values.smallFontSize)) + case 1: + return "blockUnblockName" + .put(key: "name", value: name) + .localizedFormatted(baseFont: .systemFont(ofSize: Values.smallFontSize)) + + case 2: + return "blockUnblockNameTwo" + .put(key: "name", value: name) + .localizedFormatted(baseFont: .systemFont(ofSize: Values.smallFontSize)) + + default: + return "blockUnblockNameMultiple" + .put(key: "name", value: name) + .put(key: "count", value: contactNames.count - 1) + .localizedFormatted(baseFont: .systemFont(ofSize: Values.smallFontSize)) } }() let confirmationModal: ConfirmationModal = ConfirmationModal( @@ -205,15 +203,20 @@ public class BlockedContactsViewModel: SessionTableViewModel, NavigatableStateHo confirmTitle: "blockUnblock".localized(), confirmStyle: .danger, cancelStyle: .alert_text - ) { [weak self] _ in + ) { [weak self, dependencies] _ in // Unblock the contacts - Storage.shared.write { db in + dependencies[singleton: .storage].write { db in _ = try Contact .filter(ids: contactIds) - .updateAllAndConfig(db, Contact.Columns.isBlocked.set(to: false)) + .updateAllAndConfig( + db, + Contact.Columns.isBlocked.set(to: false), + calledFromConfig: nil, + using: dependencies + ) } - self?.selectedContactIdsSubject.send([]) + self?.selectedIdsSubject.send([]) } ) self.transitionToScreen(confirmationModal, transitionType: .present) diff --git a/Session/Settings/ConversationSettingsViewModel.swift b/Session/Settings/ConversationSettingsViewModel.swift index c190effd9e0..248072d686a 100644 --- a/Session/Settings/ConversationSettingsViewModel.swift +++ b/Session/Settings/ConversationSettingsViewModel.swift @@ -17,7 +17,7 @@ class ConversationSettingsViewModel: SessionTableViewModel, NavigatableStateHold // MARK: - Initialization - init(using dependencies: Dependencies = Dependencies()) { + init(using dependencies: Dependencies) { self.dependencies = dependencies } @@ -69,18 +69,15 @@ class ConversationSettingsViewModel: SessionTableViewModel, NavigatableStateHold id: .messageTrimming, title: "conversationsMessageTrimmingTrimCommunities".localized(), subtitle: "conversationsMessageTrimmingTrimCommunitiesDescription".localized(), - rightAccessory: .toggle( - .boolValue( - key: .trimOpenGroupMessagesOlderThanSixMonths, - value: current.trimOpenGroupMessagesOlderThanSixMonths, - oldValue: (previous ?? current).trimOpenGroupMessagesOlderThanSixMonths - ), + trailingAccessory: .toggle( + current.trimOpenGroupMessagesOlderThanSixMonths, + oldValue: previous?.trimOpenGroupMessagesOlderThanSixMonths, accessibility: Accessibility( identifier: "Trim Communities - Switch" ) ), onTap: { - Storage.shared.write { db in + dependencies[singleton: .storage].write { db in db[.trimOpenGroupMessagesOlderThanSixMonths] = !db[.trimOpenGroupMessagesOlderThanSixMonths] } } @@ -94,18 +91,15 @@ class ConversationSettingsViewModel: SessionTableViewModel, NavigatableStateHold id: .audioMessages, title: "conversationsAutoplayAudioMessage".localized(), subtitle: "conversationsAutoplayAudioMessageDescription".localized(), - rightAccessory: .toggle( - .boolValue( - key: .shouldAutoPlayConsecutiveAudioMessages, - value: current.shouldAutoPlayConsecutiveAudioMessages, - oldValue: (previous ?? current).shouldAutoPlayConsecutiveAudioMessages - ), + trailingAccessory: .toggle( + current.shouldAutoPlayConsecutiveAudioMessages, + oldValue: previous?.shouldAutoPlayConsecutiveAudioMessages, accessibility: Accessibility( identifier: "Autoplay Audio Messages - Switch" ) ), onTap: { - Storage.shared.write { db in + dependencies[singleton: .storage].write { db in db[.shouldAutoPlayConsecutiveAudioMessages] = !db[.shouldAutoPlayConsecutiveAudioMessages] } } @@ -122,9 +116,9 @@ class ConversationSettingsViewModel: SessionTableViewModel, NavigatableStateHold tintColor: .danger, backgroundStyle: .noBackground ), - onTap: { [weak self] in + onTap: { [weak self, dependencies] in self?.transitionToScreen( - SessionTableViewController(viewModel: BlockedContactsViewModel()) + SessionTableViewController(viewModel: BlockedContactsViewModel(using: dependencies)) ) } ) diff --git a/Session/Settings/DeveloperSettingsViewModel.swift b/Session/Settings/DeveloperSettingsViewModel.swift new file mode 100644 index 00000000000..38d51f2636a --- /dev/null +++ b/Session/Settings/DeveloperSettingsViewModel.swift @@ -0,0 +1,1020 @@ +// Copyright © 2023 Rangeproof Pty Ltd. All rights reserved. +// +// stringlint:disable + +import Foundation +import CryptoKit +import GRDB +import DifferenceKit +import SessionUIKit +import SessionSnodeKit +import SessionMessagingKit +import SessionUtilitiesKit + +class DeveloperSettingsViewModel: SessionTableViewModel, NavigatableStateHolder, ObservableTableSource { + public let dependencies: Dependencies + public let navigatableState: NavigatableState = NavigatableState() + public let state: TableDataState = TableDataState() + public let observableState: ObservableTableSourceState = ObservableTableSourceState() + + private var showAdvancedLogging: Bool = false + private var databaseKeyEncryptionPassword: String = "" + + // MARK: - Initialization + + init(using dependencies: Dependencies) { + self.dependencies = dependencies + } + + // MARK: - Section + + public enum Section: SessionTableSection { + case developerMode + case general + case logging + case network + case disappearingMessages + case groups + case database + + var title: String? { + switch self { + case .developerMode: return nil + case .general: return "General" + case .logging: return "Logging" + case .network: return "Network" + case .disappearingMessages: return "Disappearing Messages" + case .groups: return "Groups" + case .database: return "Database" + } + } + + var style: SessionTableSectionStyle { + switch self { + case .developerMode: return .padding + default: return .titleRoundedContent + } + } + } + + public enum TableItem: Hashable, Differentiable, CaseIterable { + case developerMode + + case showStringKeys + + case defaultLogLevel + case advancedLogging + case loggingCategory(String) + + case serviceNetwork + case forceOffline + case resetSnodeCache + + case updatedDisappearingMessages + case debugDisappearingMessageDurations + + case updatedGroups + case updatedGroupsDisableAutoApprove + case updatedGroupsRemoveMessagesOnKick + case updatedGroupsAllowHistoricAccessOnInvite + case updatedGroupsAllowDisplayPicture + case updatedGroupsAllowDescriptionEditing + case updatedGroupsAllowPromotions + case updatedGroupsAllowInviteById + case updatedGroupsDeleteBeforeNow + case updatedGroupsDeleteAttachmentsBeforeNow + + case exportDatabase + + // MARK: - Conformance + + public typealias DifferenceIdentifier = String + + public var differenceIdentifier: String { + switch self { + case .developerMode: return "developerMode" + case .showStringKeys: return "showStringKeys" + + case .defaultLogLevel: return "defaultLogLevel" + case .advancedLogging: return "advancedLogging" + case .loggingCategory(let categoryIdentifier): return "loggingCategory-\(categoryIdentifier)" + + case .serviceNetwork: return "serviceNetwork" + case .forceOffline: return "forceOffline" + case .resetSnodeCache: return "resetSnodeCache" + + case .updatedDisappearingMessages: return "updatedDisappearingMessages" + case .debugDisappearingMessageDurations: return "debugDisappearingMessageDurations" + + case .updatedGroups: return "updatedGroups" + case .updatedGroupsDisableAutoApprove: return "updatedGroupsDisableAutoApprove" + case .updatedGroupsRemoveMessagesOnKick: return "updatedGroupsRemoveMessagesOnKick" + case .updatedGroupsAllowHistoricAccessOnInvite: return "updatedGroupsAllowHistoricAccessOnInvite" + case .updatedGroupsAllowDisplayPicture: return "updatedGroupsAllowDisplayPicture" + case .updatedGroupsAllowDescriptionEditing: return "updatedGroupsAllowDescriptionEditing" + case .updatedGroupsAllowPromotions: return "updatedGroupsAllowPromotions" + case .updatedGroupsAllowInviteById: return "updatedGroupsAllowInviteById" + case .updatedGroupsDeleteBeforeNow: return "updatedGroupsDeleteBeforeNow" + case .updatedGroupsDeleteAttachmentsBeforeNow: return "updatedGroupsDeleteAttachmentsBeforeNow" + + case .exportDatabase: return "exportDatabase" + } + } + + public func isContentEqual(to source: TableItem) -> Bool { + self.differenceIdentifier == source.differenceIdentifier + } + + public static var allCases: [TableItem] { + var result: [TableItem] = [] + switch TableItem.developerMode { + case .developerMode: result.append(.developerMode); fallthrough + case .showStringKeys: result.append(.showStringKeys); fallthrough + + case .defaultLogLevel: result.append(.defaultLogLevel); fallthrough + case .advancedLogging: result.append(.advancedLogging); fallthrough + case .loggingCategory: result.append(.loggingCategory("")); fallthrough + + case .serviceNetwork: result.append(.serviceNetwork); fallthrough + case .forceOffline: result.append(.forceOffline); fallthrough + case .resetSnodeCache: result.append(.resetSnodeCache); fallthrough + + case .updatedDisappearingMessages: result.append(.updatedDisappearingMessages); fallthrough + case .debugDisappearingMessageDurations: result.append(.debugDisappearingMessageDurations); fallthrough + + case .updatedGroups: result.append(.updatedGroups); fallthrough + case .updatedGroupsDisableAutoApprove: result.append(.updatedGroupsDisableAutoApprove); fallthrough + case .updatedGroupsRemoveMessagesOnKick: result.append(.updatedGroupsRemoveMessagesOnKick); fallthrough + case .updatedGroupsAllowHistoricAccessOnInvite: + result.append(.updatedGroupsAllowHistoricAccessOnInvite); fallthrough + case .updatedGroupsAllowDisplayPicture: result.append(.updatedGroupsAllowDisplayPicture); fallthrough + case .updatedGroupsAllowDescriptionEditing: result.append(.updatedGroupsAllowDescriptionEditing); fallthrough + case .updatedGroupsAllowPromotions: result.append(.updatedGroupsAllowPromotions); fallthrough + case .updatedGroupsAllowInviteById: result.append(.updatedGroupsAllowInviteById); fallthrough + case .updatedGroupsDeleteBeforeNow: result.append(.updatedGroupsDeleteBeforeNow); fallthrough + case .updatedGroupsDeleteAttachmentsBeforeNow: result.append(.updatedGroupsDeleteAttachmentsBeforeNow); fallthrough + + case .exportDatabase: result.append(.exportDatabase) + } + + return result + } + } + + // MARK: - Content + + private struct State: Equatable { + let developerMode: Bool + + let showStringKeys: Bool + + let defaultLogLevel: Log.Level + let advancedLogging: Bool + let loggingCategories: [Log.Category: Log.Level] + + let serviceNetwork: ServiceNetwork + let forceOffline: Bool + + let debugDisappearingMessageDurations: Bool + let updatedDisappearingMessages: Bool + + let updatedGroups: Bool + let updatedGroupsDisableAutoApprove: Bool + let updatedGroupsRemoveMessagesOnKick: Bool + let updatedGroupsAllowHistoricAccessOnInvite: Bool + let updatedGroupsAllowDisplayPicture: Bool + let updatedGroupsAllowDescriptionEditing: Bool + let updatedGroupsAllowPromotions: Bool + let updatedGroupsAllowInviteById: Bool + let updatedGroupsDeleteBeforeNow: Bool + let updatedGroupsDeleteAttachmentsBeforeNow: Bool + } + + let title: String = "Developer Settings" + + lazy var observation: TargetObservation = ObservationBuilder + .refreshableData(self) { [weak self, dependencies] () -> State in + State( + developerMode: dependencies[singleton: .storage, key: .developerModeEnabled], + showStringKeys: dependencies[feature: .showStringKeys], + + defaultLogLevel: dependencies[feature: .logLevel(cat: .default)], + advancedLogging: (self?.showAdvancedLogging == true), + loggingCategories: dependencies[feature: .allLogLevels].currentValues(using: dependencies), + + serviceNetwork: dependencies[feature: .serviceNetwork], + forceOffline: dependencies[feature: .forceOffline], + + debugDisappearingMessageDurations: dependencies[feature: .debugDisappearingMessageDurations], + updatedDisappearingMessages: dependencies[feature: .updatedDisappearingMessages], + + updatedGroups: dependencies[feature: .updatedGroups], + updatedGroupsDisableAutoApprove: dependencies[feature: .updatedGroupsDisableAutoApprove], + updatedGroupsRemoveMessagesOnKick: dependencies[feature: .updatedGroupsRemoveMessagesOnKick], + updatedGroupsAllowHistoricAccessOnInvite: dependencies[feature: .updatedGroupsAllowHistoricAccessOnInvite], + updatedGroupsAllowDisplayPicture: dependencies[feature: .updatedGroupsAllowDisplayPicture], + updatedGroupsAllowDescriptionEditing: dependencies[feature: .updatedGroupsAllowDescriptionEditing], + updatedGroupsAllowPromotions: dependencies[feature: .updatedGroupsAllowPromotions], + updatedGroupsAllowInviteById: dependencies[feature: .updatedGroupsAllowInviteById], + updatedGroupsDeleteBeforeNow: dependencies[feature: .updatedGroupsDeleteBeforeNow], + updatedGroupsDeleteAttachmentsBeforeNow: dependencies[feature: .updatedGroupsDeleteAttachmentsBeforeNow] + ) + } + .compactMapWithPrevious { [weak self] prev, current -> [SectionModel]? in self?.content(prev, current) } + + private func content(_ previous: State?, _ current: State) -> [SectionModel] { + return [ + SectionModel( + model: .developerMode, + elements: [ + SessionCell.Info( + id: .developerMode, + title: "Developer Mode", + subtitle: """ + Grants access to this screen. + + Disabling this setting will: + • Reset all the below settings to default (removing data as described below) + • Revoke access to this screen unless Developer Mode is re-enabled + """, + trailingAccessory: .toggle( + current.developerMode, + oldValue: previous?.developerMode + ), + onTap: { [weak self] in + guard current.developerMode else { return } + + self?.disableDeveloperMode() + } + ) + ] + ), + SectionModel( + model: .general, + elements: [ + SessionCell.Info( + id: .showStringKeys, + title: "Show String Keys", + subtitle: """ + Controls whether localised strings should render using their keys rather than the localised value (strings will be rendered as "[{key}]") + + Notes: + • This change will only apply to newly created screens (eg. the Settings screen will need to be closed and reopened before it gets updated + • The "Home" screen won't update as it never gets recreated + """, + trailingAccessory: .toggle( + current.showStringKeys, + oldValue: previous?.showStringKeys + ), + onTap: { [weak self] in + self?.updateFlag( + for: .showStringKeys, + to: !current.showStringKeys + ) + } + ) + ] + ), + SectionModel( + model: .logging, + elements: [ + SessionCell.Info( + id: .defaultLogLevel, + title: "Default Log Level", + subtitle: """ + Sets the default log level + + All logging categories which don't have a custom level set below will use this value + """, + trailingAccessory: .dropDown { current.defaultLogLevel.title }, + onTap: { [weak self, dependencies] in + self?.transitionToScreen( + SessionTableViewController( + viewModel: SessionListViewModel( + title: "Default Log Level", + options: Log.Level.allCases.filter { $0 != .default }, + behaviour: .autoDismiss( + initialSelection: current.defaultLogLevel, + onOptionSelected: self?.updateDefaulLogLevel + ), + using: dependencies + ) + ) + ) + } + ), + SessionCell.Info( + id: .advancedLogging, + title: "Advanced Logging", + subtitle: "Show per-category log levels", + trailingAccessory: .toggle( + current.advancedLogging, + oldValue: previous?.advancedLogging + ), + onTap: { [weak self] in + self?.setAdvancedLoggingVisibility(to: !current.advancedLogging) + } + ) + ].appending( + contentsOf: !current.advancedLogging ? nil : current.loggingCategories + .sorted(by: { lhs, rhs in lhs.key.rawValue < rhs.key.rawValue }) + .map { category, level in + SessionCell.Info( + id: .loggingCategory(category.rawValue), + title: category.rawValue, + subtitle: "Sets the log level for the \(category.rawValue) category", + trailingAccessory: .dropDown { level.title }, + onTap: { [weak self, dependencies] in + self?.transitionToScreen( + SessionTableViewController( + viewModel: SessionListViewModel( + title: "\(category.rawValue) Log Level", + options: [Log.Level.default] // Move 'default' to the top + .appending(contentsOf: Log.Level.allCases.filter { $0 != .default }), + behaviour: .autoDismiss( + initialSelection: level, + onOptionSelected: { updatedLevel in + self?.updateLogLevel(of: category, to: updatedLevel) + } + ), + using: dependencies + ) + ) + ) + } + ) + } + ) + ), + SectionModel( + model: .network, + elements: [ + SessionCell.Info( + id: .serviceNetwork, + title: "Environment", + subtitle: """ + The environment used for sending requests and storing messages. + + Warning: + Changing between some of these options can result in all conversation and snode data being cleared and any pending network requests being cancelled. + """, + trailingAccessory: .dropDown { current.serviceNetwork.title }, + onTap: { [weak self, dependencies] in + self?.transitionToScreen( + SessionTableViewController( + viewModel: SessionListViewModel( + title: "Environment", + options: ServiceNetwork.allCases, + behaviour: .autoDismiss( + initialSelection: current.serviceNetwork, + onOptionSelected: self?.updateServiceNetwork + ), + using: dependencies + ) + ) + ) + } + ), + SessionCell.Info( + id: .forceOffline, + title: "Force Offline", + subtitle: """ + Shut down the current network and cause all future network requests to fail after a 1 second delay with a 'serviceUnavailable' error. + """, + trailingAccessory: .toggle( + current.forceOffline, + oldValue: previous?.forceOffline + ), + onTap: { [weak self] in self?.updateForceOffline(current: current.forceOffline) } + ), + SessionCell.Info( + id: .resetSnodeCache, + title: "Reset Service Node Cache", + subtitle: """ + Reset and rebuild the service node cache and rebuild the paths. + """, + trailingAccessory: .highlightingBackgroundLabel(title: "Reset Cache"), + onTap: { [weak self] in self?.resetServiceNodeCache() } + ) + ] + ), + SectionModel( + model: .disappearingMessages, + elements: [ + SessionCell.Info( + id: .debugDisappearingMessageDurations, + title: "Debug Durations", + subtitle: """ + Adds 10, 30 and 60 second durations for Disappearing Message settings. + + These should only be used for debugging purposes and will likely result in odd behaviours. + """, + trailingAccessory: .toggle( + current.debugDisappearingMessageDurations, + oldValue: previous?.debugDisappearingMessageDurations + ), + onTap: { [weak self] in + self?.updateFlag( + for: .debugDisappearingMessageDurations, + to: !current.debugDisappearingMessageDurations + ) + } + ), + SessionCell.Info( + id: .updatedDisappearingMessages, + title: "Use Updated Disappearing Messages", + subtitle: """ + Controls whether legacy or updated disappearing messages should be used. + """, + trailingAccessory: .toggle( + current.updatedDisappearingMessages, + oldValue: previous?.updatedDisappearingMessages + ), + onTap: { [weak self] in + self?.updateFlag( + for: .updatedDisappearingMessages, + to: !current.updatedDisappearingMessages + ) + } + ) + ] + ), + SectionModel( + model: .groups, + elements: [ + SessionCell.Info( + id: .updatedGroups, + title: "Create Updated Groups", + subtitle: """ + Controls whether newly created groups are updated or legacy groups. + """, + trailingAccessory: .toggle( + current.updatedGroups, + oldValue: previous?.updatedGroups + ), + onTap: { [weak self] in + self?.updateFlag( + for: .updatedGroups, + to: !current.updatedGroups + ) + } + ), + SessionCell.Info( + id: .updatedGroupsDisableAutoApprove, + title: "Disable Auto Approve", + subtitle: """ + Prevents a group from automatically getting approved if the admin is already approved. + + Note: The default behaviour is to automatically approve new groups if the admin that sent the invitation is an approved contact. + """, + trailingAccessory: .toggle( + current.updatedGroupsDisableAutoApprove, + oldValue: previous?.updatedGroupsDisableAutoApprove + ), + onTap: { [weak self] in + self?.updateFlag( + for: .updatedGroupsDisableAutoApprove, + to: !current.updatedGroupsDisableAutoApprove + ) + } + ), + SessionCell.Info( + id: .updatedGroupsRemoveMessagesOnKick, + title: "Remove Messages on Kick", + subtitle: """ + Controls whether a group members messages should be removed when they are kicked from an updated group. + + Note: In a future release we will offer this as an option when removing members but for the initial release it can be controlled via this flag for testing purposes. + """, + trailingAccessory: .toggle( + current.updatedGroupsRemoveMessagesOnKick, + oldValue: previous?.updatedGroupsRemoveMessagesOnKick + ), + onTap: { [weak self] in + self?.updateFlag( + for: .updatedGroupsRemoveMessagesOnKick, + to: !current.updatedGroupsRemoveMessagesOnKick + ) + } + ), + SessionCell.Info( + id: .updatedGroupsAllowHistoricAccessOnInvite, + title: "Allow Historic Message Access", + subtitle: """ + Controls whether members should be granted access to historic messages when invited to an updated group. + + Note: In a future release we will offer this as an option when inviting members but for the initial release it can be controlled via this flag for testing purposes. + """, + trailingAccessory: .toggle( + current.updatedGroupsAllowHistoricAccessOnInvite, + oldValue: previous?.updatedGroupsAllowHistoricAccessOnInvite + ), + onTap: { [weak self] in + self?.updateFlag( + for: .updatedGroupsAllowHistoricAccessOnInvite, + to: !current.updatedGroupsAllowHistoricAccessOnInvite + ) + } + ), + SessionCell.Info( + id: .updatedGroupsAllowDisplayPicture, + title: "Custom Display Pictures", + subtitle: """ + Controls whether the UI allows group admins to set a custom display picture for a group. + + Note: In a future release we will offer this functionality but for the initial release it may not be fully supported across platforms so can be controlled via this flag for testing purposes. + """, + trailingAccessory: .toggle( + current.updatedGroupsAllowDisplayPicture, + oldValue: previous?.updatedGroupsAllowDisplayPicture + ), + onTap: { [weak self] in + self?.updateFlag( + for: .updatedGroupsAllowDisplayPicture, + to: !current.updatedGroupsAllowDisplayPicture + ) + } + ), + SessionCell.Info( + id: .updatedGroupsAllowDescriptionEditing, + title: "Edit Group Descriptions", + subtitle: """ + Controls whether the UI allows group admins to modify the descriptions of updated groups. + + Note: In a future release we will offer this functionality but for the initial release it may not be fully supported across platforms so can be controlled via this flag for testing purposes. + """, + trailingAccessory: .toggle( + current.updatedGroupsAllowDescriptionEditing, + oldValue: previous?.updatedGroupsAllowDescriptionEditing + ), + onTap: { [weak self] in + self?.updateFlag( + for: .updatedGroupsAllowDescriptionEditing, + to: !current.updatedGroupsAllowDescriptionEditing + ) + } + ), + SessionCell.Info( + id: .updatedGroupsAllowPromotions, + title: "Allow Group Promotions", + subtitle: """ + Controls whether the UI allows group admins to promote other group members to admin within an updated group. + + Note: In a future release we will offer this functionality but for the initial release it may not be fully supported across platforms so can be controlled via this flag for testing purposes. + """, + trailingAccessory: .toggle( + current.updatedGroupsAllowPromotions, + oldValue: previous?.updatedGroupsAllowPromotions + ), + onTap: { [weak self] in + self?.updateFlag( + for: .updatedGroupsAllowPromotions, + to: !current.updatedGroupsAllowPromotions + ) + } + ), + SessionCell.Info( + id: .updatedGroupsAllowInviteById, + title: "Allow Invite by ID", + subtitle: """ + Controls whether the UI allows group admins to invite other group members directly by their Account ID. + + Note: In a future release we will offer this functionality but it's not included in the initial release. + """, + trailingAccessory: .toggle( + current.updatedGroupsAllowInviteById, + oldValue: previous?.updatedGroupsAllowInviteById + ), + onTap: { [weak self] in + self?.updateFlag( + for: .updatedGroupsAllowInviteById, + to: !current.updatedGroupsAllowInviteById + ) + } + ), + SessionCell.Info( + id: .updatedGroupsDeleteBeforeNow, + title: "Show button to delete messages before now", + subtitle: """ + Controls whether the UI allows group admins to delete all messages in the group that were sent before the button was pressed. + + Note: In a future release we will offer this functionality but it's not included in the initial release. + """, + trailingAccessory: .toggle( + current.updatedGroupsDeleteBeforeNow, + oldValue: previous?.updatedGroupsDeleteBeforeNow + ), + onTap: { [weak self] in + self?.updateFlag( + for: .updatedGroupsDeleteBeforeNow, + to: !current.updatedGroupsDeleteBeforeNow + ) + } + ), + SessionCell.Info( + id: .updatedGroupsDeleteAttachmentsBeforeNow, + title: "Show button to delete attachments before now", + subtitle: """ + Controls whether the UI allows group admins to delete all attachments (and their associated messages) in the group that were sent before the button was pressed. + + Note: In a future release we will offer this functionality but it's not included in the initial release. + """, + trailingAccessory: .toggle( + current.updatedGroupsDeleteAttachmentsBeforeNow, + oldValue: previous?.updatedGroupsDeleteAttachmentsBeforeNow + ), + onTap: { [weak self] in + self?.updateFlag( + for: .updatedGroupsDeleteAttachmentsBeforeNow, + to: !current.updatedGroupsDeleteAttachmentsBeforeNow + ) + } + ) + ] + ), + SectionModel( + model: .database, + elements: [ + SessionCell.Info( + id: .exportDatabase, + title: "Export Database", + trailingAccessory: .icon( + UIImage(systemName: "square.and.arrow.up.trianglebadge.exclamationmark")? + .withRenderingMode(.alwaysTemplate), + size: .small + ), + styling: SessionCell.StyleInfo( + tintColor: .danger + ), + onTapView: { [weak self] view in self?.exportDatabase(view) } + ) + ] + ) + ] + } + + // MARK: - Functions + + private func disableDeveloperMode() { + /// Loop through all of the sections and reset the features back to default for each one as needed (this way if a new section is added + /// then we will get a compile error if it doesn't get resetting instructions added) + TableItem.allCases.forEach { item in + switch item { + case .developerMode: break // Not a feature + case .showStringKeys: updateFlag(for: .showStringKeys, to: nil) + + case .resetSnodeCache: break // Not a feature + case .exportDatabase: break // Not a feature + case .advancedLogging: break // Not a feature + + case .defaultLogLevel: updateDefaulLogLevel(to: nil) + case .loggingCategory: resetLoggingCategories() + + case .serviceNetwork: updateServiceNetwork(to: nil) + case .forceOffline: updateFlag(for: .forceOffline, to: nil) + + case .debugDisappearingMessageDurations: + updateFlag(for: .debugDisappearingMessageDurations, to: nil) + case .updatedDisappearingMessages: updateFlag(for: .updatedDisappearingMessages, to: nil) + + case .updatedGroups: updateFlag(for: .updatedGroups, to: nil) + case .updatedGroupsDisableAutoApprove: updateFlag(for: .updatedGroupsDisableAutoApprove, to: nil) + case .updatedGroupsRemoveMessagesOnKick: updateFlag(for: .updatedGroupsRemoveMessagesOnKick, to: nil) + case .updatedGroupsAllowHistoricAccessOnInvite: + updateFlag(for: .updatedGroupsAllowHistoricAccessOnInvite, to: nil) + case .updatedGroupsAllowDisplayPicture: updateFlag(for: .updatedGroupsAllowDisplayPicture, to: nil) + case .updatedGroupsAllowDescriptionEditing: + updateFlag(for: .updatedGroupsAllowDescriptionEditing, to: nil) + case .updatedGroupsAllowPromotions: updateFlag(for: .updatedGroupsAllowPromotions, to: nil) + case .updatedGroupsAllowInviteById: updateFlag(for: .updatedGroupsAllowInviteById, to: nil) + case .updatedGroupsDeleteBeforeNow: updateFlag(for: .updatedGroupsDeleteBeforeNow, to: nil) + case .updatedGroupsDeleteAttachmentsBeforeNow: updateFlag(for: .updatedGroupsDeleteAttachmentsBeforeNow, to: nil) + } + } + + /// Disable developer mode + dependencies[singleton: .storage].write { db in + db[.developerModeEnabled] = false + } + + self.dismissScreen(type: .pop) + } + + private func updateDefaulLogLevel(to updatedDefaultLogLevel: Log.Level?) { + dependencies.set(feature: .logLevel(cat: .default), to: updatedDefaultLogLevel) + forceRefresh(type: .databaseQuery) + } + + private func setAdvancedLoggingVisibility(to value: Bool) { + self.showAdvancedLogging = value + forceRefresh(type: .databaseQuery) + } + + private func updateLogLevel(of category: Log.Category, to level: Log.Level) { + switch (level, category.defaultLevel) { + case (.default, category.defaultLevel): dependencies.reset(feature: .logLevel(cat: category)) + default: dependencies.set(feature: .logLevel(cat: category), to: level) + } + forceRefresh(type: .databaseQuery) + } + + private func resetLoggingCategories() { + dependencies[feature: .allLogLevels].currentValues(using: dependencies).forEach { category, _ in + dependencies.reset(feature: .logLevel(cat: category)) + } + forceRefresh(type: .databaseQuery) + } + + private func updateServiceNetwork(to updatedNetwork: ServiceNetwork?) { + struct IdentityData { + let ed25519KeyPair: KeyPair + let x25519KeyPair: KeyPair + } + + /// Make sure we are actually changing the network before clearing all of the data + guard + updatedNetwork != dependencies[feature: .serviceNetwork], + let identityData: IdentityData = dependencies[singleton: .storage].read({ db in + IdentityData( + ed25519KeyPair: KeyPair( + publicKey: Array(try Identity + .filter(Identity.Columns.variant == Identity.Variant.ed25519PublicKey) + .fetchOne(db, orThrow: StorageError.objectNotFound) + .data), + secretKey: Array(try Identity + .filter(Identity.Columns.variant == Identity.Variant.ed25519SecretKey) + .fetchOne(db, orThrow: StorageError.objectNotFound) + .data) + ), + x25519KeyPair: KeyPair( + publicKey: Array(try Identity + .filter(Identity.Columns.variant == Identity.Variant.x25519PublicKey) + .fetchOne(db, orThrow: StorageError.objectNotFound) + .data), + secretKey: Array(try Identity + .filter(Identity.Columns.variant == Identity.Variant.x25519PrivateKey) + .fetchOne(db, orThrow: StorageError.objectNotFound) + .data) + ) + ) + }) + else { return } + + Log.info("[DevSettings] Swapping to \(String(describing: updatedNetwork)), clearing data") + + /// Stop all pollers + dependencies[singleton: .currentUserPoller].stop() + dependencies.remove(cache: .groupPollers) + dependencies.remove(cache: .communityPollers) + + /// Reset the network + dependencies.mutate(cache: .libSessionNetwork) { + $0.setPaths(paths: []) + $0.setNetworkStatus(status: .unknown) + } + dependencies.remove(cache: .libSessionNetwork) + + /// Unsubscribe from push notifications (do this after resetting the network as they are server requests so aren't dependant on a service + /// layer and we don't want these to be cancelled) + if let existingToken: String = dependencies[singleton: .storage, key: .lastRecordedPushToken] { + PushNotificationAPI + .unsubscribeAll(token: Data(hex: existingToken), using: dependencies) + .sinkUntilComplete() + } + + /// Clear the snodeAPI caches + dependencies.remove(cache: .snodeAPI) + + /// Remove the libSession state + dependencies.remove(cache: .libSession) + + /// Remove any network-specific data + dependencies[singleton: .storage].write { [dependencies] db in + let userSessionId: SessionId = dependencies[cache: .general].sessionId + + _ = try SnodeReceivedMessageInfo.deleteAll(db) + _ = try SessionThread.deleteAll(db) + _ = try ControlMessageProcessRecord.deleteAll(db) + _ = try ClosedGroup.deleteAll(db) + _ = try OpenGroup.deleteAll(db) + _ = try Capability.deleteAll(db) + _ = try GroupMember.deleteAll(db) + _ = try Contact + .filter(Contact.Columns.id != userSessionId.hexString) + .deleteAll(db) + _ = try Profile + .filter(Profile.Columns.id != userSessionId.hexString) + .deleteAll(db) + _ = try BlindedIdLookup.deleteAll(db) + _ = try ConfigDump.deleteAll(db) + } + + Log.info("[DevSettings] Reloading state for \(String(describing: updatedNetwork))") + + /// Update to the new `ServiceNetwork` + dependencies.set(feature: .serviceNetwork, to: updatedNetwork) + + /// Start the new network cache + dependencies.warmCache(cache: .libSessionNetwork) + + /// Run the onboarding process as if we are recovering an account (will setup the device in it's proper state) + Onboarding.Cache( + ed25519KeyPair: identityData.ed25519KeyPair, + x25519KeyPair: identityData.x25519KeyPair, + displayName: Profile.fetchOrCreateCurrentUser(using: dependencies) + .name + .nullIfEmpty + .defaulting(to: "Anonymous"), + using: dependencies + ).completeRegistration { [dependencies] in + /// Restart the current user poller (there won't be any other pollers though) + dependencies[singleton: .currentUserPoller].startIfNeeded() + + /// Re-sync the push tokens (if there are any) + SyncPushTokensJob.run(uploadOnlyIfStale: false, using: dependencies) + + Log.info("[DevSettings] Completed swap to \(String(describing: updatedNetwork))") + } + + forceRefresh(type: .databaseQuery) + } + + private func updateFlag(for feature: FeatureConfig, to updatedFlag: Bool?) { + /// Update to the new flag + dependencies.set(feature: feature, to: updatedFlag) + forceRefresh(type: .databaseQuery) + } + + private func updateForceOffline(current: Bool) { + updateFlag(for: .forceOffline, to: !current) + + // Reset the network cache + dependencies.mutate(cache: .libSessionNetwork) { + $0.setPaths(paths: []) + $0.setNetworkStatus(status: current ? .unknown : .disconnected) + } + dependencies.remove(cache: .libSessionNetwork) + } + + private func resetServiceNodeCache() { + self.transitionToScreen( + ConfirmationModal( + info: ConfirmationModal.Info( + title: "Reset Service Node Cache", + body: .text("The device will need to fetch a new cache and rebuild it's paths"), + confirmTitle: "Reset Cache", + confirmStyle: .danger, + cancelStyle: .alert_text, + dismissOnConfirm: true, + onConfirm: { [dependencies] _ in + /// Clear the snodeAPI cache + dependencies.remove(cache: .snodeAPI) + + /// Clear the snode cache + dependencies.mutate(cache: .libSessionNetwork) { $0.clearSnodeCache() } + } + ) + ), + transitionType: .present + ) + } + + private func exportDatabase(_ targetView: UIView?) { + let generatedPassword: String = UUID().uuidString + self.databaseKeyEncryptionPassword = generatedPassword + + self.transitionToScreen( + ConfirmationModal( + info: ConfirmationModal.Info( + title: "Export Database", + body: .input( + explanation: NSAttributedString( + string: """ + Sharing the database and key together is dangerous! + + We've generated a secure password for you but feel free to provide your own (we will show the generated password again after exporting) + + This password will be used to encrypt the database decryption key and will be exported alongside the database + """ + ), + info: ConfirmationModal.Info.Body.InputInfo( + placeholder: "Enter a password", + initialValue: generatedPassword, + clearButton: true + ), + onChange: { [weak self] value in self?.databaseKeyEncryptionPassword = value } + ), + confirmTitle: "Export", + confirmStyle: .danger, + cancelStyle: .alert_text, + dismissOnConfirm: false, + onConfirm: { [weak self, dependencies] modal in + modal.dismiss(animated: true) { + guard let password: String = self?.databaseKeyEncryptionPassword, password.count >= 6 else { + self?.transitionToScreen( + ConfirmationModal( + info: ConfirmationModal.Info( + title: "Error", + body: .text("Password must be at least 6 characters") + ) + ), + transitionType: .present + ) + return + } + + do { + let exportInfo = try dependencies[singleton: .storage].exportInfo(password: password, using: dependencies) + let shareVC = UIActivityViewController( + activityItems: [ + URL(fileURLWithPath: exportInfo.dbPath), + URL(fileURLWithPath: exportInfo.keyPath) + ], + applicationActivities: nil + ) + shareVC.completionWithItemsHandler = { [weak self] _, completed, _, _ in + guard + completed && + generatedPassword == self?.databaseKeyEncryptionPassword + else { return } + + self?.transitionToScreen( + ConfirmationModal( + info: ConfirmationModal.Info( + title: "Password", + body: .text(""" + The generated password was: + \(generatedPassword) + + Avoid sending this via the same means as the database + """), + confirmTitle: "Share", + dismissOnConfirm: false, + onConfirm: { [weak self] modal in + modal.dismiss(animated: true) { + let passwordShareVC = UIActivityViewController( + activityItems: [generatedPassword], + applicationActivities: nil + ) + if UIDevice.current.isIPad { + passwordShareVC.excludedActivityTypes = [] + passwordShareVC.popoverPresentationController?.permittedArrowDirections = (targetView != nil ? [.up] : []) + passwordShareVC.popoverPresentationController?.sourceView = targetView + passwordShareVC.popoverPresentationController?.sourceRect = (targetView?.bounds ?? .zero) + } + + self?.transitionToScreen(passwordShareVC, transitionType: .present) + } + } + ) + ), + transitionType: .present + ) + } + + if UIDevice.current.isIPad { + shareVC.excludedActivityTypes = [] + shareVC.popoverPresentationController?.permittedArrowDirections = (targetView != nil ? [.up] : []) + shareVC.popoverPresentationController?.sourceView = targetView + shareVC.popoverPresentationController?.sourceRect = (targetView?.bounds ?? .zero) + } + + self?.transitionToScreen(shareVC, transitionType: .present) + } + catch { + let message: String = { + switch error { + case CryptoKitError.incorrectKeySize: + return "The password must be between 6 and 32 characters (padded to 32 bytes)" + + default: return "Failed to export database" + } + }() + + self?.transitionToScreen( + ConfirmationModal( + info: ConfirmationModal.Info( + title: "Error", + body: .text(message) + ) + ), + transitionType: .present + ) + } + } + } + ) + ), + transitionType: .present + ) + } +} + +// MARK: - Listable Conformance + +extension ServiceNetwork: @retroactive ContentIdentifiable {} +extension ServiceNetwork: @retroactive ContentEquatable {} +extension ServiceNetwork: @retroactive Listable {} +extension Log.Level: @retroactive ContentIdentifiable {} +extension Log.Level: @retroactive ContentEquatable {} +extension Log.Level: Listable {} diff --git a/Session/Settings/HelpViewModel.swift b/Session/Settings/HelpViewModel.swift index e8fff044134..746f8f1ca8b 100644 --- a/Session/Settings/HelpViewModel.swift +++ b/Session/Settings/HelpViewModel.swift @@ -16,13 +16,9 @@ class HelpViewModel: SessionTableViewModel, NavigatableStateHolder, ObservableTa public let state: TableDataState = TableDataState() public let observableState: ObservableTableSourceState = ObservableTableSourceState() -#if DEBUG - private var databaseKeyEncryptionPassword: String = "" -#endif - // MARK: - Initialization - init(using dependencies: Dependencies = Dependencies()) { + init(using dependencies: Dependencies) { self.dependencies = dependencies } @@ -34,9 +30,6 @@ class HelpViewModel: SessionTableViewModel, NavigatableStateHolder, ObservableTa case feedback case faq case support -#if DEBUG - case exportDatabase -#endif var style: SessionTableSectionStyle { .padding } } @@ -55,10 +48,12 @@ class HelpViewModel: SessionTableViewModel, NavigatableStateHolder, ObservableTa subtitle: "helpReportABugExportLogsDescription" .put(key: "app_name", value: Constants.app_name) .localized(), - rightAccessory: .highlightingBackgroundLabel( + trailingAccessory: .highlightingBackgroundLabel( title: "helpReportABugExportLogs".localized() ), - onTapView: { HelpViewModel.shareLogs(targetView: $0) } + onTapView: { [dependencies] view in + HelpViewModel.shareLogs(targetView: view, using: dependencies) + } ) ] ), @@ -70,7 +65,7 @@ class HelpViewModel: SessionTableViewModel, NavigatableStateHolder, ObservableTa title: "helpHelpUsTranslateSession" .put(key: "app_name", value: Constants.app_name) .localized(), - rightAccessory: .icon( + trailingAccessory: .icon( UIImage(systemName: "arrow.up.forward.app")? .withRenderingMode(.alwaysTemplate), size: .small @@ -91,7 +86,7 @@ class HelpViewModel: SessionTableViewModel, NavigatableStateHolder, ObservableTa SessionCell.Info( id: .feedback, title: "helpWedLoveYourFeedback".localized(), - rightAccessory: .icon( + trailingAccessory: .icon( UIImage(systemName: "arrow.up.forward.app")? .withRenderingMode(.alwaysTemplate), size: .small @@ -112,7 +107,7 @@ class HelpViewModel: SessionTableViewModel, NavigatableStateHolder, ObservableTa SessionCell.Info( id: .faq, title: "helpFAQ".localized(), - rightAccessory: .icon( + trailingAccessory: .icon( UIImage(systemName: "arrow.up.forward.app")? .withRenderingMode(.alwaysTemplate), size: .small @@ -133,7 +128,7 @@ class HelpViewModel: SessionTableViewModel, NavigatableStateHolder, ObservableTa SessionCell.Info( id: .support, title: "helpSupport".localized(), - rightAccessory: .icon( + trailingAccessory: .icon( UIImage(systemName: "arrow.up.forward.app")? .withRenderingMode(.alwaysTemplate), size: .small @@ -147,45 +142,21 @@ class HelpViewModel: SessionTableViewModel, NavigatableStateHolder, ObservableTa } ) ] - ), - maybeExportDbSection + ) ] -#if DEBUG - private lazy var maybeExportDbSection: SectionModel? = SectionModel( - model: .exportDatabase, - elements: [ - SessionCell.Info( - id: .support, - title: "Export Database", // stringlint:ignore - rightAccessory: .icon( - UIImage(systemName: "square.and.arrow.up.trianglebadge.exclamationmark")? - .withRenderingMode(.alwaysTemplate), - size: .small - ), - styling: SessionCell.StyleInfo( - tintColor: .danger - ), - onTapView: { [weak self] view in self?.exportDatabase(view) } - ) - ] - ) -#else - private let maybeExportDbSection: SectionModel? = nil -#endif - // MARK: - Functions public static func shareLogs( viewControllerToDismiss: UIViewController? = nil, targetView: UIView? = nil, animated: Bool = true, + using dependencies: Dependencies, onShareComplete: (() -> ())? = nil ) { guard - let latestLogFilePath: String = Log.logFilePath(), - Singleton.hasAppContext, - let viewController: UIViewController = Singleton.appContext.frontmostViewController + let latestLogFilePath: String = Log.logFilePath(using: dependencies), + let viewController: UIViewController = dependencies[singleton: .appContext].frontMostViewController else { return } #if targetEnvironment(simulator) @@ -205,6 +176,7 @@ class HelpViewModel: SessionTableViewModel, NavigatableStateHolder, ObservableTa viewControllerToDismiss: viewControllerToDismiss, targetView: targetView, animated: animated, + using: dependencies, onShareComplete: onShareComplete ) } @@ -217,6 +189,7 @@ class HelpViewModel: SessionTableViewModel, NavigatableStateHolder, ObservableTa viewControllerToDismiss: viewControllerToDismiss, targetView: targetView, animated: animated, + using: dependencies, onShareComplete: onShareComplete ) #endif @@ -226,15 +199,15 @@ class HelpViewModel: SessionTableViewModel, NavigatableStateHolder, ObservableTa viewControllerToDismiss: UIViewController? = nil, targetView: UIView? = nil, animated: Bool = true, + using dependencies: Dependencies, onShareComplete: (() -> ())? = nil ) { - Log.info("[Version] \(SessionApp.versionInfo)") + Log.info("[Version] \(dependencies[cache: .appVersion].versionInfo)") Log.flush() guard - let latestLogFilePath: String = Log.logFilePath(), - Singleton.hasAppContext, - let viewController: UIViewController = Singleton.appContext.frontmostViewController + let latestLogFilePath: String = Log.logFilePath(using: dependencies), + let viewController: UIViewController = dependencies[singleton: .appContext].frontMostViewController else { return } let showShareSheet: () -> () = { @@ -262,133 +235,4 @@ class HelpViewModel: SessionTableViewModel, NavigatableStateHolder, ObservableTa showShareSheet() } } - -#if DEBUG - // stringlint:ignore_contents - private func exportDatabase(_ targetView: UIView?) { - let generatedPassword: String = UUID().uuidString - self.databaseKeyEncryptionPassword = generatedPassword - - self.transitionToScreen( - ConfirmationModal( - info: ConfirmationModal.Info( - title: "Export Database", - body: .input( - explanation: NSAttributedString( - string: """ - Sharing the database and key together is dangerous! - - We've generated a secure password for you but feel free to provide your own (we will show the generated password again after exporting) - - This password will be used to encrypt the database decryption key and will be exported alongside the database - """ - ), - placeholder: "Enter a password", - initialValue: generatedPassword, - clearButton: true, - onChange: { [weak self] value in self?.databaseKeyEncryptionPassword = value } - ), - confirmTitle: "Export", - dismissOnConfirm: false, - onConfirm: { [weak self] modal in - modal.dismiss(animated: true) { - guard let password: String = self?.databaseKeyEncryptionPassword, password.count >= 6 else { - self?.transitionToScreen( - ConfirmationModal( - info: ConfirmationModal.Info( - title: "Error", - body: .text("Password must be at least 6 characters") - ) - ), - transitionType: .present - ) - return - } - - do { - let exportInfo = try Storage.shared.exportInfo(password: password) - let shareVC = UIActivityViewController( - activityItems: [ - URL(fileURLWithPath: exportInfo.dbPath), - URL(fileURLWithPath: exportInfo.keyPath) - ], - applicationActivities: nil - ) - shareVC.completionWithItemsHandler = { [weak self] _, completed, _, _ in - guard - completed && - generatedPassword == self?.databaseKeyEncryptionPassword - else { return } - - self?.transitionToScreen( - ConfirmationModal( - info: ConfirmationModal.Info( - title: "Password", - body: .text(""" - The generated password was: - \(generatedPassword) - - Avoid sending this via the same means as the database - """), - confirmTitle: "Share", - dismissOnConfirm: false, - onConfirm: { [weak self] modal in - modal.dismiss(animated: true) { - let passwordShareVC = UIActivityViewController( - activityItems: [generatedPassword], - applicationActivities: nil - ) - if UIDevice.current.isIPad { - passwordShareVC.excludedActivityTypes = [] - passwordShareVC.popoverPresentationController?.permittedArrowDirections = (targetView != nil ? [.up] : []) - passwordShareVC.popoverPresentationController?.sourceView = targetView - passwordShareVC.popoverPresentationController?.sourceRect = (targetView?.bounds ?? .zero) - } - - self?.transitionToScreen(passwordShareVC, transitionType: .present) - } - } - ) - ), - transitionType: .present - ) - } - - if UIDevice.current.isIPad { - shareVC.excludedActivityTypes = [] - shareVC.popoverPresentationController?.permittedArrowDirections = (targetView != nil ? [.up] : []) - shareVC.popoverPresentationController?.sourceView = targetView - shareVC.popoverPresentationController?.sourceRect = (targetView?.bounds ?? .zero) - } - - self?.transitionToScreen(shareVC, transitionType: .present) - } - catch { - let message: String = { - switch error { - case CryptoKitError.incorrectKeySize: - return "The password must be between 6 and 32 characters (padded to 32 bytes)" - - default: return "Failed to export database" - } - }() - - self?.transitionToScreen( - ConfirmationModal( - info: ConfirmationModal.Info( - title: "Error", - body: .text(message) - ) - ), - transitionType: .present - ) - } - } - } - ) - ), - transitionType: .present - ) - } -#endif } diff --git a/Session/Settings/NotificationContentViewModel.swift b/Session/Settings/NotificationContentViewModel.swift index c78f2a6d709..8780665426f 100644 --- a/Session/Settings/NotificationContentViewModel.swift +++ b/Session/Settings/NotificationContentViewModel.swift @@ -17,7 +17,7 @@ class NotificationContentViewModel: SessionTableViewModel, NavigatableStateHolde // MARK: - Initialization - init(using dependencies: Dependencies = Dependencies()) { + init(using dependencies: Dependencies) { self.dependencies = dependencies } @@ -44,11 +44,11 @@ class NotificationContentViewModel: SessionTableViewModel, NavigatableStateHolde SessionCell.Info( id: previewType, title: previewType.name, - rightAccessory: .radio( - isSelected: { (currentSelection == previewType) } + trailingAccessory: .radio( + isSelected: (currentSelection == previewType) ), onTap: { - dependencies.storage.writeAsync { db in + dependencies[singleton: .storage].writeAsync { db in db[.preferencesNotificationPreviewType] = previewType } diff --git a/Session/Settings/NotificationSettingsViewModel.swift b/Session/Settings/NotificationSettingsViewModel.swift index 09ed92b318d..f4400664dc2 100644 --- a/Session/Settings/NotificationSettingsViewModel.swift +++ b/Session/Settings/NotificationSettingsViewModel.swift @@ -15,7 +15,7 @@ class NotificationSettingsViewModel: SessionTableViewModel, NavigatableStateHold // MARK: - Initialization - init(using dependencies: Dependencies = Dependencies()) { + init(using dependencies: Dependencies) { self.dependencies = dependencies } @@ -72,9 +72,9 @@ class NotificationSettingsViewModel: SessionTableViewModel, NavigatableStateHold .defaulting(to: Preferences.NotificationPreviewType.defaultPreviewType) ) } - .map { dbState -> State in + .map { [dependencies] dbState -> State in State( - isUsingFullAPNs: UserDefaults.standard[.isUsingFullAPNs], + isUsingFullAPNs: dependencies[defaults: .standard, key: .isUsingFullAPNs], notificationSound: dbState.notificationSound, playNotificationSoundInForeground: dbState.playNotificationSoundInForeground, previewType: dbState.previewType @@ -89,11 +89,9 @@ class NotificationSettingsViewModel: SessionTableViewModel, NavigatableStateHold id: .strategyUseFastMode, title: "useFastMode".localized(), subtitle: "notificationsFastModeDescriptionIos".localized(), - rightAccessory: .toggle( - .boolValue( - current.isUsingFullAPNs, - oldValue: (previous ?? current).isUsingFullAPNs - ), + trailingAccessory: .toggle( + current.isUsingFullAPNs, + oldValue: previous?.isUsingFullAPNs, accessibility: Accessibility( identifier: "Use Fast Mode - Switch" ) @@ -104,14 +102,11 @@ class NotificationSettingsViewModel: SessionTableViewModel, NavigatableStateHold ), // stringlint:ignore_contents onTap: { [weak self] in - UserDefaults.standard.set( - !UserDefaults.standard.bool(forKey: "isUsingFullAPNs"), - forKey: "isUsingFullAPNs" - ) + dependencies[defaults: .standard, key: .isUsingFullAPNs] = !dependencies[defaults: .standard, key: .isUsingFullAPNs] // Force sync the push tokens on change - SyncPushTokensJob.run(uploadOnlyIfStale: false) - self?.forceRefresh() + SyncPushTokensJob.run(uploadOnlyIfStale: false, using: dependencies) + self?.forceRefresh(type: .postDatabaseQuery) } ), SessionCell.Info( @@ -135,30 +130,27 @@ class NotificationSettingsViewModel: SessionTableViewModel, NavigatableStateHold SessionCell.Info( id: .styleSound, title: "notificationsSound".localized(), - rightAccessory: .dropDown( - .dynamicString { current.notificationSound.displayName } - ), + trailingAccessory: .dropDown { current.notificationSound.displayName }, onTap: { [weak self] in self?.transitionToScreen( - SessionTableViewController(viewModel: NotificationSoundViewModel()) + SessionTableViewController( + viewModel: NotificationSoundViewModel(using: dependencies) + ) ) } ), SessionCell.Info( id: .styleSoundWhenAppIsOpen, title: "notificationsSoundDescription".localized(), - rightAccessory: .toggle( - .boolValue( - key: .playNotificationSoundInForeground, - value: current.playNotificationSoundInForeground, - oldValue: (previous ?? current).playNotificationSoundInForeground - ), + trailingAccessory: .toggle( + current.playNotificationSoundInForeground, + oldValue: previous?.playNotificationSoundInForeground, accessibility: Accessibility( identifier: "Sound when App is open - Switch" ) ), onTap: { - Storage.shared.write { db in + dependencies[singleton: .storage].write { db in db[.playNotificationSoundInForeground] = !db[.playNotificationSoundInForeground] } } @@ -172,12 +164,12 @@ class NotificationSettingsViewModel: SessionTableViewModel, NavigatableStateHold id: .content, title: "notificationsContent".localized(), subtitle: "notificationsContentDescription".localized(), - rightAccessory: .dropDown( - .dynamicString { current.previewType.name } - ), + trailingAccessory: .dropDown { current.previewType.name }, onTap: { [weak self] in self?.transitionToScreen( - SessionTableViewController(viewModel: NotificationContentViewModel()) + SessionTableViewController( + viewModel: NotificationContentViewModel(using: dependencies) + ) ) } ) diff --git a/Session/Settings/NotificationSoundViewModel.swift b/Session/Settings/NotificationSoundViewModel.swift index 0479c994050..3f6c4e060bf 100644 --- a/Session/Settings/NotificationSoundViewModel.swift +++ b/Session/Settings/NotificationSoundViewModel.swift @@ -16,17 +16,19 @@ class NotificationSoundViewModel: SessionTableViewModel, NavigationItemSource, N public let state: TableDataState = TableDataState() public let observableState: ObservableTableSourceState = ObservableTableSourceState() - // FIXME: Remove `threadId` once we ditch the per-thread notification sound - private let threadId: String? + private let originalSelection: Preferences.Sound private var audioPlayer: OWSAudioPlayer? - private var storedSelection: Preferences.Sound? - private var currentSelection: CurrentValueSubject = CurrentValueSubject(nil) + private var currentSelection: CurrentValueSubject // MARK: - Initialization - init(threadId: String? = nil, using dependencies: Dependencies = Dependencies()) { + init(using dependencies: Dependencies) { self.dependencies = dependencies - self.threadId = threadId + + let originalSelection: Preferences.Sound = dependencies[singleton: .storage, key: .defaultNotificationSound] + .defaulting(to: .defaultNotificationSound) + self.originalSelection = originalSelection + self.currentSelection = CurrentValueSubject(originalSelection) } deinit { @@ -57,7 +59,7 @@ class NotificationSoundViewModel: SessionTableViewModel, NavigationItemSource, N lazy var rightNavItems: AnyPublisher<[SessionNavItem], Never> = currentSelection .removeDuplicates() - .map { [weak self] currentSelection in (self?.storedSelection != currentSelection) } + .map { [originalSelection] currentSelection in (originalSelection != currentSelection) } .map { isChanged in guard isChanged else { return [] } @@ -79,26 +81,8 @@ class NotificationSoundViewModel: SessionTableViewModel, NavigationItemSource, N let title: String = "notificationsSound".localized() lazy var observation: TargetObservation = ObservationBuilder - .databaseObservation(self) { [threadId] db -> Preferences.Sound in - guard let threadId: String = threadId else { - return db[.defaultNotificationSound] - .defaulting(to: .defaultNotificationSound) - } - - return try SessionThread - .filter(id: threadId) - .select(.notificationSound) - .asRequest(of: Preferences.Sound.self) - .fetchOne(db) - .defaulting( - to: db[.defaultNotificationSound] - .defaulting(to: .defaultNotificationSound) - ) - } - .map { [weak self] storedSelection in - self?.storedSelection = storedSelection - self?.currentSelection.send(self?.currentSelection.value ?? storedSelection) - + .subject(currentSelection) + .map { [weak self] selectedSound in return [ SectionModel( model: .content, @@ -113,8 +97,8 @@ class NotificationSoundViewModel: SessionTableViewModel, NavigationItemSource, N return sound.displayName }(), - rightAccessory: .radio( - isSelected: { (self?.currentSelection.value == sound) } + trailingAccessory: .radio( + isSelected: (selectedSound == sound) ), onTap: { self?.currentSelection.send(sound) @@ -139,22 +123,8 @@ class NotificationSoundViewModel: SessionTableViewModel, NavigationItemSource, N // MARK: - Functions private func saveChanges() { - guard let currentSelection: Preferences.Sound = self.currentSelection.value else { return } - - let threadId: String? = self.threadId - - Storage.shared.writeAsync { db in - guard let threadId: String = threadId else { - db[.defaultNotificationSound] = currentSelection - return - } - - try SessionThread - .filter(id: threadId) - .updateAll( - db, - SessionThread.Columns.notificationSound.set(to: currentSelection) - ) + dependencies[singleton: .storage].writeAsync { [currentSelection] db in + db[.defaultNotificationSound] = currentSelection.value } } } diff --git a/Session/Settings/NukeDataModal.swift b/Session/Settings/NukeDataModal.swift index a17a973853b..c7b538fbf46 100644 --- a/Session/Settings/NukeDataModal.swift +++ b/Session/Settings/NukeDataModal.swift @@ -10,9 +10,13 @@ import SignalUtilitiesKit import SessionUtilitiesKit final class NukeDataModal: Modal { + private let dependencies: Dependencies + // MARK: - Initialization - override init(targetView: UIView? = nil, dismissType: DismissType = .recursive, afterClosed: (() -> ())? = nil) { + init(targetView: UIView? = nil, dismissType: DismissType = .recursive, using dependencies: Dependencies, afterClosed: (() -> ())? = nil) { + self.dependencies = dependencies + super.init(targetView: targetView, dismissType: dismissType, afterClosed: afterClosed) self.modalPresentationStyle = .overFullScreen @@ -151,8 +155,8 @@ final class NukeDataModal: Modal { } private func clearDeviceOnly() { - ModalActivityIndicatorViewController.present(fromViewController: self, canCancel: false) { [weak self] _ in - ConfigurationSyncJob.run() + ModalActivityIndicatorViewController.present(fromViewController: self, canCancel: false) { [weak self, dependencies] _ in + ConfigurationSyncJob.run(swarmPublicKey: dependencies[cache: .general].sessionId.hexString, using: dependencies) .subscribe(on: DispatchQueue.global(qos: .userInitiated)) .receive(on: DispatchQueue.main) .sinkUntilComplete( @@ -165,52 +169,60 @@ final class NukeDataModal: Modal { } private func clearEntireAccount(presentedViewController: UIViewController) { - let dependencies: Dependencies = Dependencies() + typealias PreparedClearRequests = ( + deleteAll: Network.PreparedRequest<[String: Bool]>, + inboxRequestInfo: [Network.PreparedRequest] + ) ModalActivityIndicatorViewController - .present(fromViewController: presentedViewController, canCancel: false) { [weak self] _ in - Publishers - .MergeMany( - Storage.shared - .read { db -> [(String, Network.PreparedRequest)] in - return try OpenGroup - .filter(OpenGroup.Columns.isActive == true) - .select(.server) - .distinct() - .asRequest(of: String.self) - .fetchSet(db) - .map { server in - ( - server, - try OpenGroupAPI.preparedClearInbox( - db, - on: server, - requestAndPathBuildTimeout: Network.defaultTimeout, - using: dependencies - ) - ) - } - } - .defaulting(to: []) - .compactMap { server, preparedRequest in - preparedRequest - .send(using: dependencies) - .map { _ in [server: true] } - .eraseToAnyPublisher() - } - ) - .collect() - .subscribe(on: DispatchQueue.global(qos: .userInitiated)) - .flatMap { results in - SnodeAPI - .deleteAllMessages( + .present(fromViewController: presentedViewController, canCancel: false) { [weak self, dependencies] _ in + dependencies[singleton: .storage] + .readPublisher { db -> PreparedClearRequests in + ( + try SnodeAPI.preparedDeleteAllMessages( namespace: .all, - requestAndPathBuildTimeout: Network.defaultTimeout - ) - .map { results.reduce($0) { result, next in result.updated(with: next) } } + requestAndPathBuildTimeout: Network.defaultTimeout, + authMethod: try Authentication.with( + db, + swarmPublicKey: dependencies[cache: .general].sessionId.hexString, + using: dependencies + ), + using: dependencies + ), + try OpenGroup + .filter(OpenGroup.Columns.isActive == true) + .select(.server) + .distinct() + .asRequest(of: String.self) + .fetchSet(db) + .map { server in + try OpenGroupAPI + .preparedClearInbox( + db, + on: server, + requestAndPathBuildTimeout: Network.defaultTimeout, + using: dependencies + ) + .map { _, _ in server } + } + ) + } + .subscribe(on: DispatchQueue.global(qos: .userInitiated), using: dependencies) + .flatMap { preparedRequests -> AnyPublisher<(Network.PreparedRequest<[String: Bool]>, [String]), Error> in + Publishers + .MergeMany(preparedRequests.inboxRequestInfo.map { $0.send(using: dependencies) }) + .collect() + .map { response in (preparedRequests.deleteAll, response.map { $0.1 }) } .eraseToAnyPublisher() } - .receive(on: DispatchQueue.main) + .flatMap { preparedDeleteAllRequest, clearedServers in + preparedDeleteAllRequest + .send(using: dependencies) + .map { _, data in + clearedServers.reduce(into: data) { result, next in result[next] = true } + } + } + .receive(on: DispatchQueue.main, using: dependencies) .sinkUntilComplete( receiveCompletion: { result in switch result { @@ -280,14 +292,14 @@ final class NukeDataModal: Modal { } } - private func deleteAllLocalData(using dependencies: Dependencies = Dependencies()) { + private func deleteAllLocalData() { // Unregister push notifications if needed - let isUsingFullAPNs: Bool = UserDefaults.standard[.isUsingFullAPNs] - let maybeDeviceToken: String? = UserDefaults.standard[.deviceToken] + let isUsingFullAPNs: Bool = dependencies[defaults: .standard, key: .isUsingFullAPNs] + let maybeDeviceToken: String? = dependencies[defaults: .standard, key: .deviceToken] if isUsingFullAPNs, let deviceToken: String = maybeDeviceToken { PushNotificationAPI - .unsubscribe(token: Data(hex: deviceToken)) + .unsubscribeAll(token: Data(hex: deviceToken), using: dependencies) .sinkUntilComplete() } @@ -295,31 +307,33 @@ final class NukeDataModal: Modal { /// /// **Note:** This is file as long as this process kills the app, if it doesn't then we need an alternate mechanism to flag that /// the `JobRunner` is allowed to start it's queues again - JobRunner.stopAndClearPendingJobs(using: dependencies) + dependencies[singleton: .jobRunner].stopAndClearPendingJobs() // Clear the app badge and notifications - AppEnvironment.shared.notificationPresenter.clearAllNotifications() + dependencies[singleton: .notificationsManager].clearAllNotifications() UIApplication.shared.applicationIconBadgeNumber = 0 // Clear out the user defaults - UserDefaults.removeAll() + UserDefaults.removeAll(using: dependencies) - // Remove the cached key so it gets re-cached on next access - dependencies.caches.mutate(cache: .general) { - $0.encodedPublicKey = nil - $0.recentReactionTimestamps = [] - } + // Remove the general cache + dependencies.remove(cache: .general) // Stop any pollers (UIApplication.shared.delegate as? AppDelegate)?.stopPollers() // Call through to the SessionApp's "resetAppData" which will wipe out logs, database and // profile storage - let wasUnlinked: Bool = UserDefaults.standard[.wasUnlinked] + let wasUnlinked: Bool = dependencies[defaults: .standard, key: .wasUnlinked] + let serviceNetwork: ServiceNetwork = dependencies[feature: .serviceNetwork] - SessionApp.resetAppData(using: dependencies) { + dependencies[singleton: .app].resetData { [dependencies] in // Resetting the data clears the old user defaults. We need to restore the unlink default. - UserDefaults.standard[.wasUnlinked] = wasUnlinked + dependencies[defaults: .standard, key: .wasUnlinked] = wasUnlinked + + // We also want to keep the `ServiceNetwork` setting (so someone testing can delete and restore + // accounts on Testnet without issue + dependencies.set(feature: .serviceNetwork, to: serviceNetwork) } } } diff --git a/Session/Settings/PrivacySettingsViewModel.swift b/Session/Settings/PrivacySettingsViewModel.swift index 181622fb78c..5fa1188b10f 100644 --- a/Session/Settings/PrivacySettingsViewModel.swift +++ b/Session/Settings/PrivacySettingsViewModel.swift @@ -19,7 +19,7 @@ class PrivacySettingsViewModel: SessionTableViewModel, NavigationItemSource, Nav // MARK: - Initialization - init(shouldShowCloseButton: Bool = false, using dependencies: Dependencies = Dependencies()) { + init(shouldShowCloseButton: Bool = false, using dependencies: Dependencies) { self.dependencies = dependencies self.shouldShowCloseButton = shouldShowCloseButton } @@ -111,12 +111,9 @@ class PrivacySettingsViewModel: SessionTableViewModel, NavigationItemSource, Nav subtitle: "lockAppDescriptionIos" .put(key: "app_name", value: Constants.app_name) .localized(), - rightAccessory: .toggle( - .boolValue( - key: .isScreenLockEnabled, - value: current.isScreenLockEnabled, - oldValue: (previous ?? current).isScreenLockEnabled - ), + trailingAccessory: .toggle( + current.isScreenLockEnabled, + oldValue: previous?.isScreenLockEnabled, accessibility: Accessibility( identifier: "Lock App - Switch" ) @@ -138,8 +135,12 @@ class PrivacySettingsViewModel: SessionTableViewModel, NavigationItemSource, Nav return } - Storage.shared.write { db in - try db.setAndUpdateConfig(.isScreenLockEnabled, to: !db[.isScreenLockEnabled]) + dependencies[singleton: .storage].write { db in + try db.setAndUpdateConfig( + .isScreenLockEnabled, + to: !db[.isScreenLockEnabled], + using: dependencies + ) } } ) @@ -152,21 +153,19 @@ class PrivacySettingsViewModel: SessionTableViewModel, NavigationItemSource, Nav id: .communityMessageRequests, title: "messageRequestsCommunities".localized(), subtitle: "messageRequestsCommunitiesDescription".localized(), - rightAccessory: .toggle( - .boolValue( - key: .checkForCommunityMessageRequests, - value: current.checkForCommunityMessageRequests, - oldValue: (previous ?? current).checkForCommunityMessageRequests - ), + trailingAccessory: .toggle( + current.checkForCommunityMessageRequests, + oldValue: previous?.checkForCommunityMessageRequests, accessibility: Accessibility( identifier: "Community Message Requests - Switch" ) ), onTap: { [weak self] in - Storage.shared.write { db in + dependencies[singleton: .storage].write { db in try db.setAndUpdateConfig( .checkForCommunityMessageRequests, - to: !db[.checkForCommunityMessageRequests] + to: !db[.checkForCommunityMessageRequests], + using: dependencies ) } } @@ -180,19 +179,20 @@ class PrivacySettingsViewModel: SessionTableViewModel, NavigationItemSource, Nav id: .readReceipts, title: "readReceipts".localized(), subtitle: "readReceiptsDescription".localized(), - rightAccessory: .toggle( - .boolValue( - key: .areReadReceiptsEnabled, - value: current.areReadReceiptsEnabled, - oldValue: (previous ?? current).areReadReceiptsEnabled - ), + trailingAccessory: .toggle( + current.areReadReceiptsEnabled, + oldValue: previous?.areReadReceiptsEnabled, accessibility: Accessibility( identifier: "Read Receipts - Switch" ) ), onTap: { - Storage.shared.write { db in - try db.setAndUpdateConfig(.areReadReceiptsEnabled, to: !db[.areReadReceiptsEnabled]) + dependencies[singleton: .storage].write { db in + try db.setAndUpdateConfig( + .areReadReceiptsEnabled, + to: !db[.areReadReceiptsEnabled], + using: dependencies + ) } } ) @@ -240,19 +240,20 @@ class PrivacySettingsViewModel: SessionTableViewModel, NavigationItemSource, Nav return result } ), - rightAccessory: .toggle( - .boolValue( - key: .typingIndicatorsEnabled, - value: current.typingIndicatorsEnabled, - oldValue: (previous ?? current).typingIndicatorsEnabled - ), + trailingAccessory: .toggle( + current.typingIndicatorsEnabled, + oldValue: previous?.typingIndicatorsEnabled, accessibility: Accessibility( identifier: "Typing Indicators - Switch" ) ), onTap: { - Storage.shared.write { db in - try db.setAndUpdateConfig(.typingIndicatorsEnabled, to: !db[.typingIndicatorsEnabled]) + dependencies[singleton: .storage].write { db in + try db.setAndUpdateConfig( + .typingIndicatorsEnabled, + to: !db[.typingIndicatorsEnabled], + using: dependencies + ) } } ) @@ -265,19 +266,20 @@ class PrivacySettingsViewModel: SessionTableViewModel, NavigationItemSource, Nav id: .linkPreviews, title: "linkPreviewsSend".localized(), subtitle: "linkPreviewsDescription".localized(), - rightAccessory: .toggle( - .boolValue( - key: .areLinkPreviewsEnabled, - value: current.areLinkPreviewsEnabled, - oldValue: (previous ?? current).areLinkPreviewsEnabled - ), + trailingAccessory: .toggle( + current.areLinkPreviewsEnabled, + oldValue: previous?.areLinkPreviewsEnabled, accessibility: Accessibility( identifier: "Send Link Previews - Switch" ) ), onTap: { - Storage.shared.write { db in - try db.setAndUpdateConfig(.areLinkPreviewsEnabled, to: !db[.areLinkPreviewsEnabled]) + dependencies[singleton: .storage].write { db in + try db.setAndUpdateConfig( + .areLinkPreviewsEnabled, + to: !db[.areLinkPreviewsEnabled], + using: dependencies + ) } } ) @@ -290,12 +292,9 @@ class PrivacySettingsViewModel: SessionTableViewModel, NavigationItemSource, Nav id: .calls, title: "callsVoiceAndVideo".localized(), subtitle: "callsVoiceAndVideoToggleDescription".localized(), - rightAccessory: .toggle( - .boolValue( - key: .areCallsEnabled, - value: current.areCallsEnabled, - oldValue: (previous ?? current).areCallsEnabled - ), + trailingAccessory: .toggle( + current.areCallsEnabled, + oldValue: previous?.areCallsEnabled, accessibility: Accessibility( identifier: "Voice and Video Calls - Switch" ) @@ -310,11 +309,15 @@ class PrivacySettingsViewModel: SessionTableViewModel, NavigationItemSource, Nav confirmTitle: "theContinue".localized(), confirmStyle: .danger, cancelStyle: .alert_text, - onConfirm: { _ in Permissions.requestMicrophonePermissionIfNeeded() } + onConfirm: { _ in Permissions.requestMicrophonePermissionIfNeeded(using: dependencies) } ), onTap: { - Storage.shared.write { db in - try db.setAndUpdateConfig(.areCallsEnabled, to: !db[.areCallsEnabled]) + dependencies[singleton: .storage].write { db in + try db.setAndUpdateConfig( + .areCallsEnabled, + to: !db[.areCallsEnabled], + using: dependencies + ) } } ) diff --git a/Session/Settings/QRCodeScreen.swift b/Session/Settings/QRCodeScreen.swift index c7d59fe4efc..1a9666bfecd 100644 --- a/Session/Settings/QRCodeScreen.swift +++ b/Session/Settings/QRCodeScreen.swift @@ -8,11 +8,16 @@ import AVFoundation struct QRCodeScreen: View { @EnvironmentObject var host: HostWrapper + let dependencies: Dependencies @State var tabIndex = 0 @State private var accountId: String = "" @State private var errorString: String? = nil + init(using dependencies: Dependencies) { + self.dependencies = dependencies + } + var body: some View { ZStack(alignment: .topLeading) { VStack( @@ -27,13 +32,14 @@ struct QRCodeScreen: View { ).frame(maxWidth: .infinity) if tabIndex == 0 { - MyQRCodeScreen() + MyQRCodeScreen(using: dependencies) } else { ScanQRCodeScreen( $accountId, error: $errorString, - continueAction: continueWithAccountId + continueAction: continueWithAccountId, + using: dependencies ) } } @@ -46,7 +52,7 @@ struct QRCodeScreen: View { errorString = "qrNotAccountId".localized() } else { - SessionApp.presentConversationCreatingIfNeeded( + dependencies[singleton: .app].presentConversationCreatingIfNeeded( for: hexEncodedPublicKey, variant: .contact, action: .compose, @@ -63,12 +69,18 @@ struct QRCodeScreen: View { } struct MyQRCodeScreen: View { + let dependencies: Dependencies + + init(using dependencies: Dependencies) { + self.dependencies = dependencies + } + var body: some View{ VStack( spacing: Values.mediumSpacing ) { QRCodeView( - string: getUserHexEncodedPublicKey(), + string: dependencies[cache: .general].sessionId.hexString, hasBackground: false, logo: "SessionWhite40", // stringlint:ignore themeStyle: ThemeManager.currentTheme.interfaceStyle @@ -94,5 +106,5 @@ struct MyQRCodeScreen: View { } #Preview { - QRCodeScreen() + QRCodeScreen(using: Dependencies.createEmpty()) } diff --git a/Session/Settings/RecoveryPasswordScreen.swift b/Session/Settings/RecoveryPasswordScreen.swift index 17f0667d99a..680cf2feb95 100644 --- a/Session/Settings/RecoveryPasswordScreen.swift +++ b/Session/Settings/RecoveryPasswordScreen.swift @@ -7,6 +7,7 @@ import SessionUtilitiesKit struct RecoveryPasswordScreen: View { @EnvironmentObject var host: HostWrapper + private let dependencies: Dependencies @State private var copied: Bool = false @State private var showQRCode: Bool = false @@ -17,12 +18,14 @@ struct RecoveryPasswordScreen: View { static private let backgroundCornerRadius: CGFloat = 17 static private let buttonWidth: CGFloat = UIDevice.current.isIPad ? Values.iPadButtonWidth : 130 - public init() throws { - self.mnemonic = try Identity.mnemonic() - self.hexEncodedSeed = Identity.fetchHexEncodedSeed() + public init(using dependencies: Dependencies) throws { + self.dependencies = dependencies + self.mnemonic = try Identity.mnemonic(using: dependencies) + self.hexEncodedSeed = try Mnemonic.decode(mnemonic: self.mnemonic) } - public init(hardcode: String) { + public init(hardcode: String, using dependencies: Dependencies) { + self.dependencies = dependencies self.mnemonic = hardcode self.hexEncodedSeed = try? Mnemonic.decode(mnemonic: hardcode) } @@ -239,7 +242,7 @@ struct RecoveryPasswordScreen: View { } .backgroundColor(themeColor: .backgroundPrimary) .onAppear { - Storage.shared.writeAsync { db in db[.hasViewedSeed] = true } + dependencies[singleton: .storage].writeAsync { db in db[.hasViewedSeed] = true } } } @@ -271,7 +274,9 @@ struct RecoveryPasswordScreen: View { cancelStyle: .danger, onCancel: { modal in modal.dismiss(animated: true) { - Storage.shared.writeAsync { db in db[.hideRecoveryPasswordPermanently] = true } + dependencies[singleton: .storage].writeAsync { db in + db[.hideRecoveryPasswordPermanently] = true + } self.host.controller?.navigationController?.popViewController(animated: true) } } @@ -288,6 +293,9 @@ struct RecoveryPasswordScreen: View { struct RecoveryPasswordView_Previews: PreviewProvider { static var previews: some View { - RecoveryPasswordScreen(hardcode: "Voyage urban toyed maverick peculiar tuxedo penguin tree grass building listen speak withdraw terminal plane") + RecoveryPasswordScreen( + hardcode: "Voyage urban toyed maverick peculiar tuxedo penguin tree grass building listen speak withdraw terminal plane", + using: Dependencies.createEmpty() + ) } } diff --git a/Session/Settings/SeedModal.swift b/Session/Settings/SeedModal.swift index 56f1474fdf5..3417b128a65 100644 --- a/Session/Settings/SeedModal.swift +++ b/Session/Settings/SeedModal.swift @@ -5,12 +5,14 @@ import SessionUIKit import SessionUtilitiesKit final class SeedModal: Modal { + private let dependencies: Dependencies private let mnemonic: String // MARK: - Initialization - init() throws { - self.mnemonic = try Identity.mnemonic() + init(using dependencies: Dependencies) throws { + self.dependencies = dependencies + self.mnemonic = try Identity.mnemonic(using: dependencies) super.init(targetView: nil, dismissType: .recursive, afterClosed: nil) @@ -122,7 +124,7 @@ final class SeedModal: Modal { mnemonicLabel.pin(to: mnemonicLabelContainer, withInset: isIPhone6OrSmaller ? 4 : Values.smallSpacing) // Mark seed as viewed - Storage.shared.writeAsync { db in db[.hasViewedSeed] = true } + dependencies[singleton: .storage].writeAsync { db in db[.hasViewedSeed] = true } } // MARK: - Interaction diff --git a/Session/Settings/SettingsViewModel.swift b/Session/Settings/SettingsViewModel.swift index 003900c7ed0..07419412484 100644 --- a/Session/Settings/SettingsViewModel.swift +++ b/Session/Settings/SettingsViewModel.swift @@ -9,47 +9,34 @@ import SessionMessagingKit import SessionUtilitiesKit import SignalUtilitiesKit -class SettingsViewModel: SessionTableViewModel, NavigationItemSource, NavigatableStateHolder, EditableStateHolder, ObservableTableSource { +class SettingsViewModel: SessionTableViewModel, NavigationItemSource, NavigatableStateHolder, ObservableTableSource { public let dependencies: Dependencies public let navigatableState: NavigatableState = NavigatableState() - public let editableState: EditableState = EditableState() public let state: TableDataState = TableDataState() public let observableState: ObservableTableSourceState = ObservableTableSourceState() - private let userSessionId: String + private let userSessionId: SessionId + private var updatedName: String? + private var onDisplayPictureSelected: ((ConfirmationModal.ValueUpdate) -> Void)? private lazy var imagePickerHandler: ImagePickerHandler = ImagePickerHandler( onTransition: { [weak self] in self?.transitionToScreen($0, transitionType: $1) }, onImageDataPicked: { [weak self] resultImageData in - self?.updatedProfilePictureSelected( - displayPictureUpdate: .currentUserUploadImageData(resultImageData) - ) + self?.onDisplayPictureSelected?(.image(resultImageData)) } ) - fileprivate var oldDisplayName: String - private var editedDisplayName: String? - private var editProfilePictureModal: ConfirmationModal? - private var editProfilePictureModalInfo: ConfirmationModal.Info? // MARK: - Initialization - init(using dependencies: Dependencies = Dependencies()) { + init(using dependencies: Dependencies) { self.dependencies = dependencies - self.userSessionId = getUserHexEncodedPublicKey(using: dependencies) - self.oldDisplayName = Profile.fetchOrCreateCurrentUser(using: dependencies).name + self.userSessionId = dependencies[cache: .general].sessionId } // MARK: - Config - enum NavState { - case standard - case editing - } - enum NavItem: Equatable { case close case qrCode - case cancel - case done } public enum Section: SessionTableSection { @@ -90,128 +77,44 @@ class SettingsViewModel: SessionTableViewModel, NavigationItemSource, Navigatabl case inviteAFriend case recoveryPhrase case help + case developerSettings case clearData } - // MARK: - Navigation + // MARK: - NavigationItemSource - lazy var navState: AnyPublisher = { - Publishers - .CombineLatest( - 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 - .shareReplay(1) - .eraseToAnyPublisher() - }() - - lazy var leftNavItems: AnyPublisher<[SessionNavItem], Never> = navState - .map { navState -> [SessionNavItem] in - switch navState { - case .standard: - return [ - SessionNavItem( - id: .close, - image: UIImage(named: "X")? - .withRenderingMode(.alwaysTemplate), - style: .plain, - accessibilityIdentifier: "Close button" - ) { [weak self] in self?.dismissScreen() } - ] - - case .editing: - return [ - SessionNavItem( - id: .cancel, - systemItem: .cancel, - accessibilityIdentifier: "Cancel button" - ) { [weak self] in - self?.setIsEditing(false) - self?.editedDisplayName = self?.oldDisplayName - } - ] - } - } - .eraseToAnyPublisher() + lazy var leftNavItems: AnyPublisher<[SessionNavItem], Never> = [ + SessionNavItem( + id: .close, + image: UIImage(named: "X")? + .withRenderingMode(.alwaysTemplate), + style: .plain, + accessibilityIdentifier: "Close button" + ) { [weak self] in self?.dismissScreen() } + ] - lazy var rightNavItems: AnyPublisher<[SessionNavItem], Never> = navState - .map { [weak self] navState -> [SessionNavItem] in - switch navState { - case .standard: - return [ - SessionNavItem( - id: .qrCode, - image: UIImage(named: "QRCode")? - .withRenderingMode(.alwaysTemplate), - style: .plain, - accessibilityIdentifier: "View QR code", - action: { [weak self] in - let viewController: SessionHostingViewController = SessionHostingViewController(rootView: QRCodeScreen()) - viewController.setNavBarTitle("qrCode".localized()) - self?.transitionToScreen(viewController) - } - ) - ] - - case .editing: - return [ - SessionNavItem( - id: .done, - systemItem: .done, - accessibilityIdentifier: "Done" - ) { [weak self] in - let updatedNickname: String = (self?.editedDisplayName ?? "") - .trimmingCharacters(in: .whitespacesAndNewlines) - - guard !updatedNickname.isEmpty else { - self?.transitionToScreen( - ConfirmationModal( - info: ConfirmationModal.Info( - title: "displayNameErrorDescription".localized(), - cancelTitle: "okay".localized(), - cancelStyle: .alert_text - ) - ), - transitionType: .present - ) - return - } - guard !ProfileManager.isTooLong(profileName: updatedNickname) else { - self?.transitionToScreen( - ConfirmationModal( - info: ConfirmationModal.Info( - title: "displayNameErrorDescriptionShorter".localized(), - cancelTitle: "okay".localized(), - cancelStyle: .alert_text - ) - ), - transitionType: .present - ) - return - } - - self?.setIsEditing(false) - self?.oldDisplayName = updatedNickname - self?.updateProfile(displayNameUpdate: .currentUserUpdate(updatedNickname)) - } - ] - } + lazy var rightNavItems: AnyPublisher<[SessionNavItem], Never> = [ + SessionNavItem( + id: .qrCode, + image: UIImage(named: "QRCode")? + .withRenderingMode(.alwaysTemplate), + style: .plain, + accessibilityIdentifier: "View QR code", + action: { [weak self, dependencies] in + let viewController: SessionHostingViewController = SessionHostingViewController( + rootView: QRCodeScreen(using: dependencies) + ) + viewController.setNavBarTitle("qrCode".localized()) + self?.transitionToScreen(viewController) } - .eraseToAnyPublisher() + ) + ] // MARK: - Content + private struct State: Equatable { let profile: Profile + let developerModeEnabled: Bool let hideRecoveryPasswordPermanently: Bool } @@ -221,221 +124,243 @@ class SettingsViewModel: SessionTableViewModel, NavigationItemSource, Navigatabl .databaseObservation(self) { [weak self, dependencies] db -> State in State( profile: Profile.fetchOrCreateCurrentUser(db, using: dependencies), + developerModeEnabled: db[.developerModeEnabled], hideRecoveryPasswordPermanently: db[.hideRecoveryPasswordPermanently] ) } - .map { [weak self] state -> [SectionModel] in - return [ - SectionModel( - model: .profileInfo, - elements: [ - SessionCell.Info( - id: .avatar, - accessory: .profile( - id: state.profile.id, - size: .hero, - profile: state.profile - ), - styling: SessionCell.StyleInfo( - alignment: .centerHugging, - customPadding: SessionCell.Padding(bottom: Values.smallSpacing), - backgroundStyle: .noBackground + .compactMap { [weak self] state -> [SectionModel]? in self?.content(state) } + + private func content(_ state: State) -> [SectionModel] { + let editIcon: UIImage? = UIImage(systemName: "pencil") + + return [ + SectionModel( + model: .profileInfo, + elements: [ + SessionCell.Info( + id: .avatar, + accessory: .profile( + id: state.profile.id, + size: .hero, + profile: state.profile, + profileIcon: { + switch (dependencies[feature: .serviceNetwork], dependencies[feature: .forceOffline]) { + case (.testnet, false): return .letter("T", false) // stringlint:ignore + case (.testnet, true): return .letter("T", true) // stringlint:ignore + default: return .none + } + }() + ), + styling: SessionCell.StyleInfo( + alignment: .centerHugging, + customPadding: SessionCell.Padding(bottom: Values.smallSpacing), + backgroundStyle: .noBackground + ), + accessibility: Accessibility( + identifier: "User settings", + label: "Profile picture" + ), + onTap: { [weak self] in + self?.updateProfilePicture(currentFileName: state.profile.profilePictureFileName) + } + ), + SessionCell.Info( + id: .profileName, + leadingAccessory: .icon( + editIcon?.withRenderingMode(.alwaysTemplate), + size: .mediumAspectFill, + customTint: .textSecondary, + shouldFill: true + ), + title: SessionCell.TextInfo( + state.profile.displayName(), + font: .titleLarge, + alignment: .center, + interaction: .editable + ), + styling: SessionCell.StyleInfo( + alignment: .centerHugging, + customPadding: SessionCell.Padding( + top: Values.smallSpacing, + leading: -((IconSize.medium.size + (Values.smallSpacing * 2)) / 2), + bottom: Values.mediumSpacing ), + backgroundStyle: .noBackground + ), + accessibility: Accessibility( + identifier: "Username", + label: state.profile.displayName() + ), + onTap: { [weak self] in self?.updateDisplayName(current: state.profile.displayName()) } + ) + ] + ), + SectionModel( + model: .sessionId, + elements: [ + SessionCell.Info( + id: .sessionId, + title: SessionCell.TextInfo( + state.profile.id, + font: .monoLarge, + alignment: .center, + interaction: .copy + ), + styling: SessionCell.StyleInfo( + customPadding: SessionCell.Padding(bottom: Values.smallSpacing), + backgroundStyle: .noBackground + ), + accessibility: Accessibility( + identifier: "Account ID", + label: state.profile.id + ) + ), + SessionCell.Info( + id: .idActions, + leadingAccessory: .button( + style: .bordered, + title: "share".localized(), accessibility: Accessibility( - identifier: "User settings", - label: "Profile picture" + identifier: "Share button", + label: "Share button" ), - onTap: { - self?.updateProfilePicture(currentFileName: state.profile.profilePictureFileName) + run: { [weak self] _ in + self?.shareSessionId(state.profile.id) } ), - SessionCell.Info( - id: .profileName, - title: SessionCell.TextInfo( - state.profile.displayName(), - font: .titleLarge, - alignment: .center, - interaction: .editable - ), - styling: SessionCell.StyleInfo( - customPadding: SessionCell.Padding(top: Values.smallSpacing), - backgroundStyle: .noBackground - ), + trailingAccessory: .button( + style: .bordered, + title: "copy".localized(), accessibility: Accessibility( - identifier: "Username", - label: state.profile.displayName() + identifier: "Copy button", + label: "Copy button" ), - onTap: { self?.setIsEditing(true) } - ) - ] - ), - SectionModel( - model: .sessionId, - elements: [ - SessionCell.Info( - id: .sessionId, - title: SessionCell.TextInfo( - state.profile.id, - font: .monoLarge, - alignment: .center, - interaction: .copy - ), - styling: SessionCell.StyleInfo( - customPadding: SessionCell.Padding(bottom: Values.smallSpacing), - backgroundStyle: .noBackground - ), - accessibility: Accessibility( - identifier: "Account ID", - label: state.profile.id - ) + run: { [weak self] button in + self?.copySessionId(state.profile.id, button: button) + } ), - SessionCell.Info( - id: .idActions, - leftAccessory: .button( - style: .bordered, - title: "share".localized(), - accessibility: Accessibility( - identifier: "Share button", - label: "Share button" - ), - run: { _ in - self?.shareSessionId(state.profile.id) - } - ), - rightAccessory: .button( - style: .bordered, - title: "copy".localized(), - accessibility: Accessibility( - identifier: "Copy button", - label: "Copy button" - ), - run: { button in - self?.copySessionId(state.profile.id, button: button) - } + styling: SessionCell.StyleInfo( + customPadding: SessionCell.Padding( + top: Values.smallSpacing, + leading: 0, + trailing: 0 ), - styling: SessionCell.StyleInfo( - customPadding: SessionCell.Padding( - top: Values.smallSpacing, - leading: 0, - trailing: 0 - ), - backgroundStyle: .noBackground - ) + backgroundStyle: .noBackground ) - ] - ), - SectionModel( - model: .menus, - elements: [ - SessionCell.Info( - id: .path, - leftAccessory: .customView(hashValue: "PathStatusView") { // stringlint:ignore - // Need to ensure this view is the same size as the icons so - // wrap it in a larger view - let result: UIView = UIView() - let pathView: PathStatusView = PathStatusView(size: .large) - result.addSubview(pathView) - - result.set(.width, to: IconSize.medium.size) - result.set(.height, to: IconSize.medium.size) - pathView.center(in: result) - - return result - }, - title: "onionRoutingPath".localized(), - onTap: { self?.transitionToScreen(PathVC()) } + ) + ] + ), + SectionModel( + model: .menus, + elements: [ + SessionCell.Info( + id: .path, + leadingAccessory: .customView(uniqueId: "PathStatusView") { [dependencies] in // stringlint:ignore + // Need to ensure this view is the same size as the icons so + // wrap it in a larger view + let result: UIView = UIView() + let pathView: PathStatusView = PathStatusView(size: .large, using: dependencies) + result.addSubview(pathView) + + result.set(.width, to: IconSize.medium.size) + result.set(.height, to: IconSize.medium.size) + pathView.center(in: result) + + return result + }, + title: "onionRoutingPath".localized(), + onTap: { [weak self, dependencies] in self?.transitionToScreen(PathVC(using: dependencies)) } + ), + SessionCell.Info( + id: .privacy, + leadingAccessory: .icon( + UIImage(named: "icon_privacy")? + .withRenderingMode(.alwaysTemplate) ), - SessionCell.Info( - id: .privacy, - leftAccessory: .icon( - UIImage(named: "icon_privacy")? - .withRenderingMode(.alwaysTemplate) - ), - title: "sessionPrivacy".localized(), - onTap: { - self?.transitionToScreen( - SessionTableViewController(viewModel: PrivacySettingsViewModel()) - ) - } + title: "sessionPrivacy".localized(), + onTap: { [weak self, dependencies] in + self?.transitionToScreen( + SessionTableViewController(viewModel: PrivacySettingsViewModel(using: dependencies)) + ) + } + ), + SessionCell.Info( + id: .notifications, + leadingAccessory: .icon( + UIImage(named: "icon_speaker")? + .withRenderingMode(.alwaysTemplate) ), - SessionCell.Info( - id: .notifications, - leftAccessory: .icon( - UIImage(named: "icon_speaker")? - .withRenderingMode(.alwaysTemplate) - ), - title: "sessionNotifications".localized(), - onTap: { - self?.transitionToScreen( - SessionTableViewController(viewModel: NotificationSettingsViewModel()) - ) - } + title: "sessionNotifications".localized(), + onTap: { [weak self, dependencies] in + self?.transitionToScreen( + SessionTableViewController(viewModel: NotificationSettingsViewModel(using: dependencies)) + ) + } + ), + SessionCell.Info( + id: .conversations, + leadingAccessory: .icon( + UIImage(named: "icon_msg")? + .withRenderingMode(.alwaysTemplate) ), - SessionCell.Info( - id: .conversations, - leftAccessory: .icon( - UIImage(named: "icon_msg")? - .withRenderingMode(.alwaysTemplate) - ), - title: "sessionConversations".localized(), - onTap: { - self?.transitionToScreen( - SessionTableViewController(viewModel: ConversationSettingsViewModel()) - ) - } + title: "sessionConversations".localized(), + onTap: { [weak self, dependencies] in + self?.transitionToScreen( + SessionTableViewController(viewModel: ConversationSettingsViewModel(using: dependencies)) + ) + } + ), + SessionCell.Info( + id: .messageRequests, + leadingAccessory: .icon( + UIImage(named: "icon_msg_req")? + .withRenderingMode(.alwaysTemplate) ), - SessionCell.Info( - id: .messageRequests, - leftAccessory: .icon( - UIImage(named: "icon_msg_req")? - .withRenderingMode(.alwaysTemplate) - ), - title: "sessionMessageRequests".localized(), - onTap: { - self?.transitionToScreen( - SessionTableViewController(viewModel: MessageRequestsViewModel()) - ) - } + title: "sessionMessageRequests".localized(), + onTap: { [weak self, dependencies] in + self?.transitionToScreen( + SessionTableViewController(viewModel: MessageRequestsViewModel(using: dependencies)) + ) + } + ), + SessionCell.Info( + id: .appearance, + leadingAccessory: .icon( + UIImage(named: "icon_apperance")? + .withRenderingMode(.alwaysTemplate) ), - SessionCell.Info( - id: .appearance, - leftAccessory: .icon( - UIImage(named: "icon_apperance")? - .withRenderingMode(.alwaysTemplate) - ), - title: "sessionAppearance".localized(), - onTap: { - self?.transitionToScreen(AppearanceViewController()) - } + title: "sessionAppearance".localized(), + onTap: { [weak self, dependencies] in + self?.transitionToScreen(AppearanceViewController(using: dependencies)) + } + ), + SessionCell.Info( + id: .inviteAFriend, + leadingAccessory: .icon( + UIImage(named: "icon_invite")? + .withRenderingMode(.alwaysTemplate) ), - SessionCell.Info( - id: .inviteAFriend, - leftAccessory: .icon( - UIImage(named: "icon_invite")? - .withRenderingMode(.alwaysTemplate) - ), - title: "sessionInviteAFriend".localized(), - onTap: { - let invitation: String = "accountIdShare" - .put(key: "app_name", value: Constants.app_name) - .put(key: "account_id", value: state.profile.id) - .put(key: "session_download_url", value: Constants.session_download_url) - .localized() - - self?.transitionToScreen( - UIActivityViewController( - activityItems: [ invitation ], - applicationActivities: nil - ), - transitionType: .present - ) - } - ) - ].appending( - state.hideRecoveryPasswordPermanently ? nil : + title: "sessionInviteAFriend".localized(), + onTap: { [weak self] in + let invitation: String = "accountIdShare" + .put(key: "app_name", value: Constants.app_name) + .put(key: "account_id", value: state.profile.id) + .put(key: "session_download_url", value: Constants.session_download_url) + .localized() + + self?.transitionToScreen( + UIActivityViewController( + activityItems: [ invitation ], + applicationActivities: nil + ), + transitionType: .present + ) + } + ), + (state.hideRecoveryPasswordPermanently ? nil : SessionCell.Info( id: .recoveryPhrase, - leftAccessory: .icon( + leadingAccessory: .icon( UIImage(named: "SessionShield")? .withRenderingMode(.alwaysTemplate) ), @@ -444,12 +369,8 @@ class SettingsViewModel: SessionTableViewModel, NavigationItemSource, Navigatabl identifier: "Recovery password menu item", label: "Recovery password menu item" ), - onTap: { - if let recoveryPasswordView: RecoveryPasswordScreen = try? RecoveryPasswordScreen() { - let viewController: SessionHostingViewController = SessionHostingViewController(rootView: recoveryPasswordView) - viewController.setNavBarTitle("sessionRecoveryPassword".localized()) - self?.transitionToScreen(viewController) - } else { + onTap: { [weak self, dependencies] in + guard let recoveryPasswordView: RecoveryPasswordScreen = try? RecoveryPasswordScreen(using: dependencies) else { let targetViewController: UIViewController = ConfirmationModal( info: ConfirmationModal.Info( title: "theError".localized(), @@ -459,120 +380,188 @@ class SettingsViewModel: SessionTableViewModel, NavigationItemSource, Navigatabl ) ) self?.transitionToScreen(targetViewController, transitionType: .present) + return } + + let viewController: SessionHostingViewController = SessionHostingViewController(rootView: recoveryPasswordView) + viewController.setNavBarTitle("sessionRecoveryPassword".localized()) + self?.transitionToScreen(viewController) } ) - ).appending(contentsOf: [ + ), + SessionCell.Info( + id: .help, + leadingAccessory: .icon( + UIImage(named: "icon_help")? + .withRenderingMode(.alwaysTemplate) + ), + title: "sessionHelp".localized(), + onTap: { [weak self, dependencies] in + self?.transitionToScreen( + SessionTableViewController(viewModel: HelpViewModel(using: dependencies)) + ) + } + ), + (!state.developerModeEnabled ? nil : SessionCell.Info( - id: .help, - leftAccessory: .icon( - UIImage(named: "icon_help")? + id: .developerSettings, + leadingAccessory: .icon( + UIImage(systemName: "wrench.and.screwdriver")? .withRenderingMode(.alwaysTemplate) ), - title: "sessionHelp".localized(), - onTap: { + title: "Developer Settings", // stringlint:ignore + styling: SessionCell.StyleInfo(tintColor: .warning), + onTap: { [weak self, dependencies] in self?.transitionToScreen( - SessionTableViewController(viewModel: HelpViewModel()) + SessionTableViewController(viewModel: DeveloperSettingsViewModel(using: dependencies)) ) } - ), - SessionCell.Info( - id: .clearData, - leftAccessory: .icon( - UIImage(named: "icon_bin")? - .withRenderingMode(.alwaysTemplate) - ), - title: "sessionClearData".localized(), - styling: SessionCell.StyleInfo(tintColor: .danger), - onTap: { - self?.transitionToScreen(NukeDataModal(), transitionType: .present) - } ) - ]) - ) - ] - } + ), + SessionCell.Info( + id: .clearData, + leadingAccessory: .icon( + UIImage(named: "icon_bin")? + .withRenderingMode(.alwaysTemplate) + ), + title: "sessionClearData".localized(), + styling: SessionCell.StyleInfo(tintColor: .danger), + onTap: { [weak self, dependencies] in + self?.transitionToScreen(NukeDataModal(using: dependencies), transitionType: .present) + } + ) + ].compactMap { $0 } + ) + ] + } - public let footerView: AnyPublisher = Just(VersionFooterView()).eraseToAnyPublisher() + public lazy var footerView: AnyPublisher = Just(VersionFooterView(numTaps: 9) { [weak self, dependencies] in + /// Do nothing if developer mode is already enabled + guard !dependencies[singleton: .storage, key: .developerModeEnabled] else { return } + + dependencies[singleton: .storage].write { db in + db[.developerModeEnabled] = true + } + }).eraseToAnyPublisher() // MARK: - Functions - private func updateProfilePicture(currentFileName: String?) { - let existingDisplayName: String = self.oldDisplayName - let existingImageData: Data? = ProfileManager - .profileAvatar(id: self.userSessionId) - let editProfilePictureModalInfo: ConfirmationModal.Info = ConfirmationModal.Info( - title: "profileDisplayPictureSet".localized(), - body: .image( - placeholderData: UIImage(named: "profile_placeholder")?.pngData(), - valueData: existingImageData, - icon: .rightPlus, - style: .circular, - accessibility: Accessibility( - identifier: "Image picker", - label: "Image picker" - ), - onClick: { [weak self] in self?.showPhotoLibraryForAvatar() } - ), - confirmTitle: "save".localized(), - confirmEnabled: false, - cancelTitle: "remove".localized(), - cancelEnabled: (existingImageData != nil), - hasCloseButton: true, - dismissOnConfirm: false, - onConfirm: { modal in modal.close() }, - onCancel: { [weak self] modal in - self?.updateProfile( - displayPictureUpdate: .currentUserRemove, - onComplete: { [weak modal] in modal?.close() } + private func updateDisplayName(current: String) { + /// Set `updatedName` to `current` so we can disable the "save" button when there are no changes and don't need to worry + /// about retrieving them in the confirmation closure + self.updatedName = current + self.transitionToScreen( + ConfirmationModal( + info: ConfirmationModal.Info( + title: "displayNameSet".localized(), + body: .input( + explanation: NSAttributedString(string: "displayNameVisible".localized()), + info: ConfirmationModal.Info.Body.InputInfo( + placeholder: "displayNameEnter".localized(), + initialValue: current + ), + onChange: { [weak self] updatedName in self?.updatedName = updatedName } + ), + confirmTitle: "save".localized(), + confirmEnabled: .afterChange { [weak self] _ in + self?.updatedName?.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty == false && + self?.updatedName != current + }, + cancelStyle: .alert_text, + dismissOnConfirm: false, + onConfirm: { [weak self] modal in + guard + let finalDisplayName: String = (self?.updatedName ?? "") + .trimmingCharacters(in: .whitespacesAndNewlines) + .nullIfEmpty + else { return } + + /// Check if the data violates the size constraints + guard !Profile.isTooLong(profileName: finalDisplayName) else { + self?.transitionToScreen( + ConfirmationModal( + info: ConfirmationModal.Info( + title: "theError".localized(), + body: .text("displayNameErrorDescriptionShorter".localized()), + cancelTitle: "okay".localized(), + cancelStyle: .alert_text, + dismissType: .single + ) + ), + transitionType: .present + ) + return + } + + /// Update the nickname + self?.updateProfile(displayNameUpdate: .currentUserUpdate(finalDisplayName)) { + modal.dismiss(animated: true) + } + } ) - }, - afterClosed: { [weak self] in - self?.editProfilePictureModal = nil - self?.editProfilePictureModalInfo = nil - } + ), + transitionType: .present ) - let modal: ConfirmationModal = ConfirmationModal(info: editProfilePictureModalInfo) - - self.editProfilePictureModalInfo = editProfilePictureModalInfo - self.editProfilePictureModal = modal - self.transitionToScreen(modal, transitionType: .present) } - - fileprivate func updatedProfilePictureSelected(displayPictureUpdate: ProfileManager.DisplayPictureUpdate) { - guard let info: ConfirmationModal.Info = self.editProfilePictureModalInfo else { return } - - self.editProfilePictureModal?.updateContent( - with: info.with( - body: .image( - placeholderData: UIImage(named: "profile_placeholder")?.pngData(), - valueData: { - switch displayPictureUpdate { - case .currentUserUploadImageData(let imageData): return imageData - default: return nil + + private func updateProfilePicture(currentFileName: String?) { + let existingImageData: Data? = dependencies[singleton: .storage].read { [userSessionId, dependencies] db in + DisplayPictureManager.displayPicture(db, id: .user(userSessionId.hexString), using: dependencies) + } + self.transitionToScreen( + ConfirmationModal( + info: ConfirmationModal.Info( + title: "profileDisplayPictureSet".localized(), + body: .image( + placeholderData: UIImage(named: "profile_placeholder")?.pngData(), + valueData: existingImageData, + icon: .rightPlus, + style: .circular, + accessibility: Accessibility( + identifier: "Upload", + label: "Upload" + ), + onClick: { [weak self] onDisplayPictureSelected in + self?.onDisplayPictureSelected = onDisplayPictureSelected + self?.showPhotoLibraryForAvatar() } - }(), - icon: .rightPlus, - style: .circular, - accessibility: Accessibility( - identifier: "Image picker", - label: "Image picker" ), - onClick: { [weak self] in self?.showPhotoLibraryForAvatar() } - ), - confirmEnabled: true, - onConfirm: { [weak self] modal in - self?.updateProfile( - displayPictureUpdate: displayPictureUpdate, - onComplete: { [weak modal] in modal?.close() } - ) - } - ) + confirmTitle: "save".localized(), + confirmEnabled: .afterChange { info in + switch info.body { + case .image(_, let valueData, _, _, _, _): return (valueData != nil) + default: return false + } + }, + cancelTitle: "remove".localized(), + cancelEnabled: .bool(existingImageData != nil), + hasCloseButton: true, + dismissOnConfirm: false, + onConfirm: { [weak self] modal in + switch modal.info.body { + case .image(_, .some(let valueData), _, _, _, _): + self?.updateProfile( + displayPictureUpdate: .currentUserUploadImageData(valueData), + onComplete: { [weak modal] in modal?.close() } + ) + + default: modal.close() + } + }, + onCancel: { [weak self] modal in + self?.updateProfile( + displayPictureUpdate: .currentUserRemove, + onComplete: { [weak modal] in modal?.close() } + ) + } + ) + ), + transitionType: .present ) } private func showPhotoLibraryForAvatar() { - Permissions.requestLibraryPermissionIfNeeded(isSavingMedia: false) { [weak self] in + Permissions.requestLibraryPermissionIfNeeded(isSavingMedia: false, using: dependencies) { [weak self] in DispatchQueue.main.async { let picker: UIImagePickerController = UIImagePickerController() picker.sourceType = .photoLibrary @@ -585,21 +574,21 @@ class SettingsViewModel: SessionTableViewModel, NavigationItemSource, Navigatabl } fileprivate func updateProfile( - displayNameUpdate: ProfileManager.DisplayNameUpdate = .none, - displayPictureUpdate: ProfileManager.DisplayPictureUpdate = .none, - onComplete: (() -> ())? = nil + displayNameUpdate: Profile.DisplayNameUpdate = .none, + displayPictureUpdate: DisplayPictureManager.Update = .none, + onComplete: @escaping () -> () ) { - let viewController = ModalActivityIndicatorViewController(canCancel: false) { [weak self] modalActivityIndicator in - ProfileManager.updateLocal( + let viewController = ModalActivityIndicatorViewController(canCancel: false) { [weak self, dependencies] modalActivityIndicator in + Profile.updateLocal( queue: .global(qos: .default), displayNameUpdate: displayNameUpdate, displayPictureUpdate: displayPictureUpdate, success: { db in // Wait for the database transaction to complete before updating the UI - db.afterNextTransactionNested { _ in + db.afterNextTransactionNested(using: dependencies) { _ in DispatchQueue.main.async { modalActivityIndicator.dismiss(completion: { - onComplete?() + onComplete() }) } } @@ -610,7 +599,7 @@ class SettingsViewModel: SessionTableViewModel, NavigationItemSource, Navigatabl let message: String = { switch (displayPictureUpdate, error) { case (.currentUserRemove, _): return "profileDisplayPictureRemoveError".localized() - case (_, .avatarUploadMaxFileSizeExceeded): + case (_, .uploadMaxFileSizeExceeded): return "profileDisplayPictureSizeError".localized() default: return "errorConnection".localized() @@ -631,7 +620,8 @@ class SettingsViewModel: SessionTableViewModel, NavigationItemSource, Navigatabl ) } } - } + }, + using: dependencies ) } diff --git a/Session/Settings/Views/PrimaryColorSelectionView.swift b/Session/Settings/Views/PrimaryColorSelectionView.swift index 3bbba11ef94..4423f8887c8 100644 --- a/Session/Settings/Views/PrimaryColorSelectionView.swift +++ b/Session/Settings/Views/PrimaryColorSelectionView.swift @@ -59,7 +59,7 @@ class PrimaryColorSelectionView: UIView { // MARK: - Layout private func setupUI(color: Theme.PrimaryColor) { - // Set the appropriate colours + // Set the appropriate colors selectionView.themeBackgroundColorForced = .primary(color) // Add the UI diff --git a/Session/Settings/Views/ThemePreviewView.swift b/Session/Settings/Views/ThemePreviewView.swift index 9ad812c9b13..6375224b6de 100644 --- a/Session/Settings/Views/ThemePreviewView.swift +++ b/Session/Settings/Views/ThemePreviewView.swift @@ -3,8 +3,11 @@ import UIKit import SessionUIKit import SessionMessagingKit +import SessionUtilitiesKit public class ThemePreviewView: UIView { + private let dependencies: Dependencies + // MARK: - Components private lazy var incomingMessagePreview: UIView = { @@ -26,7 +29,8 @@ public class ThemePreviewView: UIView { mediaCache: NSCache(), playbackInfo: nil, showExpandedReactions: false, - lastSearchText: nil + lastSearchText: nil, + using: dependencies ) return result @@ -45,7 +49,8 @@ public class ThemePreviewView: UIView { mediaCache: NSCache(), playbackInfo: nil, showExpandedReactions: false, - lastSearchText: nil + lastSearchText: nil, + using: dependencies ) return result @@ -53,7 +58,9 @@ public class ThemePreviewView: UIView { // MARK: - Initializtion - init() { + init(using dependencies: Dependencies) { + self.dependencies = dependencies + super.init(frame: .zero) setupUI() diff --git a/Session/Settings/Views/ThemeSelectionView.swift b/Session/Settings/Views/ThemeSelectionView.swift index 865fb21f0cb..229aca4d364 100644 --- a/Session/Settings/Views/ThemeSelectionView.swift +++ b/Session/Settings/Views/ThemeSelectionView.swift @@ -79,7 +79,7 @@ class ThemeSelectionView: UIView { private func setupUI(theme: Theme) { self.themeBackgroundColor = .appearance_sectionBackground - // Set the appropriate colours + // Set the appropriate colors previewView.themeBackgroundColorForced = .theme(theme, color: .backgroundPrimary) previewView.themeBorderColorForced = .theme(theme, color: .borderSeparator) previewIncomingMessageView.themeBackgroundColorForced = .theme(theme, color: .messageBubble_incomingBackground) diff --git a/Session/Settings/Views/VersionFooterView.swift b/Session/Settings/Views/VersionFooterView.swift index fa0eb801ce1..96e112c63a6 100644 --- a/Session/Settings/Views/VersionFooterView.swift +++ b/Session/Settings/Views/VersionFooterView.swift @@ -7,6 +7,8 @@ class VersionFooterView: UIView { private static let footerHeight: CGFloat = 75 private static let logoHeight: CGFloat = 24 + private let multiTapCallback: (() -> Void)? + // MARK: - UI private lazy var logoImageView: UIImageView = { @@ -43,18 +45,18 @@ class VersionFooterView: UIView { .joined(separator: " - ") // stringlint:ignore_stop result.text = [ - "Version \(version)", - (!buildInfo.isEmpty ? " (" : ""), - buildInfo, - (!buildInfo.isEmpty ? ")" : ""), - ].joined() + "Version \(version)", // stringlint:ignore + buildInfo + ].compactMap { $0 }.joined(separator: " ") // stringlint:ignore return result }() // MARK: - Initialization - init() { + init(numTaps: Int = 0, multiTapCallback: (() -> Void)? = nil) { + self.multiTapCallback = multiTapCallback + // Note: Need to explicitly set the height for a table footer view // or it will have no height super.init( @@ -66,7 +68,7 @@ class VersionFooterView: UIView { ) ) - setupViewHierarchy() + setupViewHierarchy(numTaps: numTaps) } required init?(coder: NSCoder) { @@ -75,7 +77,7 @@ class VersionFooterView: UIView { // MARK: - Content - private func setupViewHierarchy() { + private func setupViewHierarchy(numTaps: Int) { addSubview(logoImageView) addSubview(versionLabel) @@ -84,5 +86,18 @@ class VersionFooterView: UIView { versionLabel.pin(.top, to: .bottom, of: logoImageView, withInset: Values.mediumSpacing) versionLabel.pin(.left, to: .left, of: self) versionLabel.pin(.right, to: .right, of: self) + + if numTaps > 0 { + let tapGestureRecognizer: UITapGestureRecognizer = UITapGestureRecognizer( + target: self, + action: #selector(onMultiTap) + ) + tapGestureRecognizer.numberOfTapsRequired = numTaps + addGestureRecognizer(tapGestureRecognizer) + } + } + + @objc private func onMultiTap() { + self.multiTapCallback?() } } diff --git a/Session/Shared/BaseVC.swift b/Session/Shared/BaseVC.swift index 4e2dc7ec180..c6bff73df94 100644 --- a/Session/Shared/BaseVC.swift +++ b/Session/Shared/BaseVC.swift @@ -69,14 +69,4 @@ public class BaseVC: UIViewController { navigationItem.titleView = headingImageView } - - internal func setUpNavBarSessionIcon() { - let logoImageView = UIImageView() - logoImageView.image = #imageLiteral(resourceName: "SessionGreen32") - logoImageView.contentMode = .scaleAspectFit - logoImageView.set(.width, to: 32) - logoImageView.set(.height, to: 32) - - navigationItem.titleView = logoImageView - } } diff --git a/Session/Shared/FullConversationCell.swift b/Session/Shared/FullConversationCell.swift index acffa39937d..8cfb2edbc41 100644 --- a/Session/Shared/FullConversationCell.swift +++ b/Session/Shared/FullConversationCell.swift @@ -267,13 +267,14 @@ public final class FullConversationCell: UITableViewCell, SwipeActionOptimisticC // MARK: - Content // MARK: --Search Results - public func updateForDefaultContacts(with cellViewModel: SessionThreadViewModel) { + public func updateForDefaultContacts(with cellViewModel: SessionThreadViewModel, using dependencies: Dependencies) { profilePictureView.update( publicKey: cellViewModel.threadId, threadVariant: cellViewModel.threadVariant, - customImageData: cellViewModel.openGroupProfilePictureData, + displayPictureFilename: cellViewModel.displayPictureFilename, profile: cellViewModel.profile, - additionalProfile: cellViewModel.additionalProfile + additionalProfile: cellViewModel.additionalProfile, + using: dependencies ) isPinnedIcon.isHidden = true @@ -294,13 +295,18 @@ public final class FullConversationCell: UITableViewCell, SwipeActionOptimisticC } } - public func updateForMessageSearchResult(with cellViewModel: SessionThreadViewModel, searchText: String) { + public func updateForMessageSearchResult( + with cellViewModel: SessionThreadViewModel, + searchText: String, + using dependencies: Dependencies + ) { profilePictureView.update( publicKey: cellViewModel.threadId, threadVariant: cellViewModel.threadVariant, - customImageData: cellViewModel.openGroupProfilePictureData, + displayPictureFilename: cellViewModel.displayPictureFilename, profile: cellViewModel.profile, - additionalProfile: cellViewModel.additionalProfile + additionalProfile: cellViewModel.additionalProfile, + using: dependencies ) isPinnedIcon.isHidden = true @@ -330,29 +336,36 @@ public final class FullConversationCell: UITableViewCell, SwipeActionOptimisticC authorDisplayName: cellViewModel.authorName(for: .contact), attachmentDescriptionInfo: cellViewModel.interactionAttachmentDescriptionInfo, attachmentCount: cellViewModel.interactionAttachmentCount, - isOpenGroupInvitation: (cellViewModel.interactionIsOpenGroupInvitation == true) + isOpenGroupInvitation: (cellViewModel.interactionIsOpenGroupInvitation == true), + using: dependencies ), - authorName: (cellViewModel.authorId != cellViewModel.currentUserPublicKey ? + authorName: (cellViewModel.authorId != cellViewModel.currentUserSessionId ? cellViewModel.authorName(for: .contact) : nil ), - currentUserPublicKey: cellViewModel.currentUserPublicKey, - currentUserBlinded15PublicKey: cellViewModel.currentUserBlinded15PublicKey, - currentUserBlinded25PublicKey: cellViewModel.currentUserBlinded25PublicKey, + currentUserSessionId: cellViewModel.currentUserSessionId, + currentUserBlinded15SessionId: cellViewModel.currentUserBlinded15SessionId, + currentUserBlinded25SessionId: cellViewModel.currentUserBlinded25SessionId, searchText: searchText.lowercased(), fontSize: Values.smallFontSize, - textColor: textColor + textColor: textColor, + using: dependencies ) } } - public func updateForContactAndGroupSearchResult(with cellViewModel: SessionThreadViewModel, searchText: String) { + public func updateForContactAndGroupSearchResult( + with cellViewModel: SessionThreadViewModel, + searchText: String, + using dependencies: Dependencies + ) { profilePictureView.update( publicKey: cellViewModel.threadId, threadVariant: cellViewModel.threadVariant, - customImageData: cellViewModel.openGroupProfilePictureData, + displayPictureFilename: cellViewModel.displayPictureFilename, profile: cellViewModel.profile, - additionalProfile: cellViewModel.additionalProfile + additionalProfile: cellViewModel.additionalProfile, + using: dependencies ) isPinnedIcon.isHidden = true @@ -366,12 +379,13 @@ public final class FullConversationCell: UITableViewCell, SwipeActionOptimisticC displayNameLabel?.attributedText = self?.getHighlightedSnippet( content: cellViewModel.displayName, - currentUserPublicKey: cellViewModel.currentUserPublicKey, - currentUserBlinded15PublicKey: cellViewModel.currentUserBlinded15PublicKey, - currentUserBlinded25PublicKey: cellViewModel.currentUserBlinded25PublicKey, + currentUserSessionId: cellViewModel.currentUserSessionId, + currentUserBlinded15SessionId: cellViewModel.currentUserBlinded15SessionId, + currentUserBlinded25SessionId: cellViewModel.currentUserBlinded25SessionId, searchText: searchText.lowercased(), fontSize: Values.mediumFontSize, - textColor: textColor + textColor: textColor, + using: dependencies ) } @@ -386,12 +400,13 @@ public final class FullConversationCell: UITableViewCell, SwipeActionOptimisticC if cellViewModel.threadVariant == .legacyGroup || cellViewModel.threadVariant == .group { snippetLabel?.attributedText = self?.getHighlightedSnippet( content: (cellViewModel.threadMemberNames ?? ""), - currentUserPublicKey: cellViewModel.currentUserPublicKey, - currentUserBlinded15PublicKey: cellViewModel.currentUserBlinded15PublicKey, - currentUserBlinded25PublicKey: cellViewModel.currentUserBlinded25PublicKey, + currentUserSessionId: cellViewModel.currentUserSessionId, + currentUserBlinded15SessionId: cellViewModel.currentUserBlinded15SessionId, + currentUserBlinded25SessionId: cellViewModel.currentUserBlinded25SessionId, searchText: searchText.lowercased(), fontSize: Values.smallFontSize, - textColor: textColor + textColor: textColor, + using: dependencies ) } } @@ -400,7 +415,7 @@ public final class FullConversationCell: UITableViewCell, SwipeActionOptimisticC // MARK: --Standard - public func update(with cellViewModel: SessionThreadViewModel) { + public func update(with cellViewModel: SessionThreadViewModel, using dependencies: Dependencies) { let unreadCount: UInt = (cellViewModel.threadUnreadCount ?? 0) let threadIsUnread: Bool = ( unreadCount > 0 || @@ -442,9 +457,10 @@ public final class FullConversationCell: UITableViewCell, SwipeActionOptimisticC profilePictureView.update( publicKey: cellViewModel.threadId, threadVariant: cellViewModel.threadVariant, - customImageData: cellViewModel.openGroupProfilePictureData, + displayPictureFilename: cellViewModel.displayPictureFilename, profile: cellViewModel.profile, - additionalProfile: cellViewModel.additionalProfile + additionalProfile: cellViewModel.additionalProfile, + using: dependencies ) displayNameLabel.text = cellViewModel.displayName timestampLabel.text = cellViewModel.lastInteractionDate.formattedForDisplay @@ -456,7 +472,7 @@ public final class FullConversationCell: UITableViewCell, SwipeActionOptimisticC } else { displayNameLabel.themeTextColor = { - guard cellViewModel.interactionVariant != .infoClosedGroupCurrentUserLeaving else { + guard cellViewModel.interactionVariant != .infoGroupCurrentUserLeaving else { return .textSecondary } @@ -466,26 +482,29 @@ public final class FullConversationCell: UITableViewCell, SwipeActionOptimisticC typingIndicatorView.stopAnimation() ThemeManager.onThemeChange(observer: snippetLabel) { [weak self, weak snippetLabel] theme, _ in - if cellViewModel.interactionVariant == .infoClosedGroupCurrentUserLeaving { + if cellViewModel.interactionVariant == .infoGroupCurrentUserLeaving { guard let textColor: UIColor = theme.color(for: .textSecondary) else { return } snippetLabel?.attributedText = self?.getSnippet( cellViewModel: cellViewModel, - textColor: textColor + textColor: textColor, + using: dependencies ) - } else if cellViewModel.interactionVariant == .infoClosedGroupCurrentUserErrorLeaving { + } else if cellViewModel.interactionVariant == .infoGroupCurrentUserErrorLeaving { guard let textColor: UIColor = theme.color(for: .danger) else { return } snippetLabel?.attributedText = self?.getSnippet( cellViewModel: cellViewModel, - textColor: textColor + textColor: textColor, + using: dependencies ) } else { guard let textColor: UIColor = theme.color(for: .textPrimary) else { return } snippetLabel?.attributedText = self?.getSnippet( cellViewModel: cellViewModel, - textColor: textColor + textColor: textColor, + using: dependencies ) } } @@ -568,10 +587,26 @@ public final class FullConversationCell: UITableViewCell, SwipeActionOptimisticC private func getSnippet( cellViewModel: SessionThreadViewModel, - textColor: UIColor - ) -> NSMutableAttributedString { + textColor: UIColor, + using dependencies: Dependencies + ) -> NSAttributedString { + guard cellViewModel.groupIsDestroyed != true else { + return NSAttributedString( + string: "groupDeletedMemberDescription" + .put(key: "group_name", value: cellViewModel.displayName) + .localizedDeformatted() + ) + } + guard cellViewModel.wasKickedFromGroup != true else { + return NSAttributedString( + string: "groupRemovedYou" + .put(key: "group_name", value: cellViewModel.displayName) + .localizedDeformatted() + ) + } + // If we don't have an interaction then do nothing - guard cellViewModel.interactionId != nil else { return NSMutableAttributedString() } + guard cellViewModel.interactionId != nil else { return NSAttributedString() } let result = NSMutableAttributedString() @@ -616,29 +651,34 @@ public final class FullConversationCell: UITableViewCell, SwipeActionOptimisticC } let previewText: String = { - if cellViewModel.interactionVariant == .infoClosedGroupCurrentUserErrorLeaving { - return "groupLeaveErrorFailed" - .put(key: "group_name", value: cellViewModel.displayName) - .localized() + switch cellViewModel.interactionVariant { + case .infoGroupCurrentUserErrorLeaving: + return "groupLeaveErrorFailed" + .put(key: "group_name", value: cellViewModel.displayName) + .localized() + + default: + return Interaction.previewText( + variant: (cellViewModel.interactionVariant ?? .standardIncoming), + body: cellViewModel.interactionBody, + threadContactDisplayName: cellViewModel.threadContactName(), + authorDisplayName: cellViewModel.authorName(for: cellViewModel.threadVariant), + attachmentDescriptionInfo: cellViewModel.interactionAttachmentDescriptionInfo, + attachmentCount: cellViewModel.interactionAttachmentCount, + isOpenGroupInvitation: (cellViewModel.interactionIsOpenGroupInvitation == true), + using: dependencies + ) } - return Interaction.previewText( - variant: (cellViewModel.interactionVariant ?? .standardIncoming), - body: cellViewModel.interactionBody, - threadContactDisplayName: cellViewModel.threadContactName(), - authorDisplayName: cellViewModel.authorName(for: cellViewModel.threadVariant), - attachmentDescriptionInfo: cellViewModel.interactionAttachmentDescriptionInfo, - attachmentCount: cellViewModel.interactionAttachmentCount, - isOpenGroupInvitation: (cellViewModel.interactionIsOpenGroupInvitation == true) - ) }() result.append(NSAttributedString( string: MentionUtilities.highlightMentionsNoAttributes( in: previewText, threadVariant: cellViewModel.threadVariant, - currentUserPublicKey: cellViewModel.currentUserPublicKey, - currentUserBlinded15PublicKey: cellViewModel.currentUserBlinded15PublicKey, - currentUserBlinded25PublicKey: cellViewModel.currentUserBlinded25PublicKey + currentUserSessionId: cellViewModel.currentUserSessionId, + currentUserBlinded15SessionId: cellViewModel.currentUserBlinded15SessionId, + currentUserBlinded25SessionId: cellViewModel.currentUserBlinded25SessionId, + using: dependencies ), attributes: [ .foregroundColor: textColor ] )) @@ -649,12 +689,13 @@ public final class FullConversationCell: UITableViewCell, SwipeActionOptimisticC private func getHighlightedSnippet( content: String, authorName: String? = nil, - currentUserPublicKey: String, - currentUserBlinded15PublicKey: String?, - currentUserBlinded25PublicKey: String?, + currentUserSessionId: String, + currentUserBlinded15SessionId: String?, + currentUserBlinded25SessionId: String?, searchText: String, fontSize: CGFloat, - textColor: UIColor + textColor: UIColor, + using dependencies: Dependencies ) -> NSAttributedString { guard !content.isEmpty, content != "noteToSelf".localized() else { if let authorName: String = authorName, !authorName.isEmpty { @@ -680,9 +721,10 @@ public final class FullConversationCell: UITableViewCell, SwipeActionOptimisticC let mentionReplacedContent: String = MentionUtilities.highlightMentionsNoAttributes( in: content, threadVariant: .contact, - currentUserPublicKey: currentUserPublicKey, - currentUserBlinded15PublicKey: currentUserBlinded15PublicKey, - currentUserBlinded25PublicKey: currentUserBlinded25PublicKey + currentUserSessionId: currentUserSessionId, + currentUserBlinded15SessionId: currentUserBlinded15SessionId, + currentUserBlinded25SessionId: currentUserBlinded25SessionId, + using: dependencies ) let result: NSMutableAttributedString = NSMutableAttributedString( string: mentionReplacedContent, @@ -698,16 +740,16 @@ public final class FullConversationCell: UITableViewCell, SwipeActionOptimisticC SessionThreadViewModel.searchTermParts(searchText) .map { part -> String in - guard part.hasPrefix("\"") && part.hasSuffix("\"") else { return part } + guard part.hasPrefix("\"") && part.hasSuffix("\"") else { return part } // stringlint:ignore - return part.trimmingCharacters(in: CharacterSet(charactersIn: "\"")) + return part.trimmingCharacters(in: CharacterSet(charactersIn: "\"")) // stringlint:ignore } .forEach { part in // Highlight all ranges of the text (Note: The search logic only finds results that start // with the term so we use the regex below to ensure we only highlight those cases) normalizedSnippet .ranges( - of: (Singleton.hasAppContext && Singleton.appContext.isRTL ? + of: (Dependencies.isRTL ? "(\(part.lowercased()))(^|[^a-zA-Z0-9])" : // stringlint:ignore "(^|[^a-zA-Z0-9])(\(part.lowercased()))" // stringlint:ignore ), diff --git a/Session/Shared/OWSBezierPathView.h b/Session/Shared/OWSBezierPathView.h deleted file mode 100644 index c3aea740062..00000000000 --- a/Session/Shared/OWSBezierPathView.h +++ /dev/null @@ -1,28 +0,0 @@ -// -// Copyright (c) 2017 Open Whisper Systems. All rights reserved. -// - -#import - -typedef void (^ConfigureShapeLayerBlock)(CAShapeLayer *_Nonnull layer, CGRect bounds); - -NS_ASSUME_NONNULL_BEGIN - -@interface OWSBezierPathView : UIView - -// Configure the view with this method if it uses a single Bezier path. -@property (nonatomic) ConfigureShapeLayerBlock configureShapeLayerBlock; - -// Configure the view with this method if it uses multiple Bezier paths. -// -// Paths will be rendered in back-to-front order. -@property (nonatomic) NSArray *configureShapeLayerBlocks; - -// This method forces the view to reconstruct its layer content. It shouldn't -// be necessary to call this unless the ConfigureShapeLayerBlocks depend on external -// state which has changed. -- (void)updateLayers; - -@end - -NS_ASSUME_NONNULL_END diff --git a/Session/Shared/OWSBezierPathView.m b/Session/Shared/OWSBezierPathView.m deleted file mode 100644 index a568c7d3e28..00000000000 --- a/Session/Shared/OWSBezierPathView.m +++ /dev/null @@ -1,100 +0,0 @@ -// -// Copyright (c) 2018 Open Whisper Systems. All rights reserved. -// - -#import -#import "OWSBezierPathView.h" - -NS_ASSUME_NONNULL_BEGIN - -#pragma mark - - -@implementation OWSBezierPathView - -- (id)init -{ - self = [super init]; - if (self) { - [self initCommon]; - } - - return self; -} - -- (id)initWithFrame:(CGRect)frame -{ - self = [super initWithFrame:frame]; - if (self) { - [self initCommon]; - } - return self; -} - -- (void)initCommon -{ - self.opaque = NO; - self.userInteractionEnabled = NO; -} - -- (void)setFrame:(CGRect)frame -{ - BOOL didChangeSize = !CGSizeEqualToSize(frame.size, self.frame.size); - - [super setFrame:frame]; - - if (didChangeSize) { - [self updateLayers]; - } -} - -- (void)setBounds:(CGRect)bounds -{ - BOOL didChangeSize = !CGSizeEqualToSize(bounds.size, self.bounds.size); - - [super setBounds:bounds]; - - if (didChangeSize) { - [self updateLayers]; - } -} - -- (void)setConfigureShapeLayerBlock:(ConfigureShapeLayerBlock)configureShapeLayerBlock -{ - [self setConfigureShapeLayerBlocks:@[ configureShapeLayerBlock ]]; -} - -- (void)setConfigureShapeLayerBlocks:(NSArray *)configureShapeLayerBlocks -{ - _configureShapeLayerBlocks = configureShapeLayerBlocks; - - [self updateLayers]; -} - -- (void)updateLayers -{ - if (self.bounds.size.width <= 0.f || self.bounds.size.height <= 0.f) { - return; - } - - for (CALayer *layer in self.layer.sublayers) { - [layer removeFromSuperlayer]; - } - - // Prevent the shape layer from animating changes. - [CATransaction begin]; - [CATransaction setDisableActions:YES]; - - for (ConfigureShapeLayerBlock configureShapeLayerBlock in self.configureShapeLayerBlocks) { - CAShapeLayer *shapeLayer = [CAShapeLayer new]; - configureShapeLayerBlock(shapeLayer, self.bounds); - [self.layer addSublayer:shapeLayer]; - } - - [CATransaction commit]; - - [self setNeedsDisplay]; -} - -@end - -NS_ASSUME_NONNULL_END diff --git a/Session/Shared/ScanQRCodeScreen.swift b/Session/Shared/ScanQRCodeScreen.swift index 767e13399ef..52d53360085 100644 --- a/Session/Shared/ScanQRCodeScreen.swift +++ b/Session/Shared/ScanQRCodeScreen.swift @@ -6,6 +6,8 @@ import AVFoundation import SessionUtilitiesKit struct ScanQRCodeScreen: View { + private let dependencies: Dependencies + @Binding var result: String @Binding var error: String? @State var hasCameraAccess: Bool = (AVCaptureDevice.authorizationStatus(for: .video) == .authorized) @@ -15,8 +17,10 @@ struct ScanQRCodeScreen: View { init( _ result: Binding, error: Binding, - continueAction: (((() -> ())?, (() -> ())?) -> Void)? + continueAction: (((() -> ())?, (() -> ())?) -> Void)?, + using dependencies: Dependencies ) { + self.dependencies = dependencies self._result = result self._error = error self.continueAction = continueAction @@ -70,7 +74,7 @@ struct ScanQRCodeScreen: View { } private func requestCameraAccess() { - Permissions.requestCameraPermissionIfNeeded { + Permissions.requestCameraPermissionIfNeeded(using: dependencies) { hasCameraAccess.toggle() } } diff --git a/Session/Shared/ScreenLockUI.swift b/Session/Shared/ScreenLockUI.swift index 27003c28916..635b16f7af7 100644 --- a/Session/Shared/ScreenLockUI.swift +++ b/Session/Shared/ScreenLockUI.swift @@ -9,10 +9,12 @@ import SignalUtilitiesKit class ScreenLockUI { public static let shared: ScreenLockUI = ScreenLockUI() + private var dependencies: Dependencies? + public lazy var screenBlockingWindow: UIWindow = { let result: UIWindow = UIWindow() result.isHidden = false - result.windowLevel = ._Background + result.windowLevel = .background result.isOpaque = true result.themeBackgroundColorForced = .theme(.classicDark, color: .backgroundPrimary) result.rootViewController = self.screenBlockingViewController @@ -40,11 +42,11 @@ class ScreenLockUI { /// Unlike UIApplication.applicationState, this state reflects the notifications, i.e. "did become active", "will resign active", /// "will enter foreground", "did enter background". /// - ///We want to update our state to reflect these transitions and have the "update" logic be consistent with "last reported" - ///state. i.e. when you're responding to "will resign active", we need to behave as though we're already inactive. + /// We want to update our state to reflect these transitions and have the "update" logic be consistent with "last reported" + /// state. i.e. when you're responding to "will resign active", we need to behave as though we're already inactive. /// - ///Secondly, we need to show the screen protection _before_ we become inactive in order for it to be reflected in the - ///app switcher. + /// Secondly, we need to show the screen protection _before_ we become inactive in order for it to be reflected in the + /// app switcher. private var appIsInactiveOrBackground: Bool = false { didSet { if self.appIsInactiveOrBackground { @@ -81,11 +83,11 @@ class ScreenLockUI { /// /// * The user is locked out by default on app launch. /// * The user is also locked out if the app is sent to the background - private var isScreenLockLocked: Bool = false + private var isScreenLockLocked: Atomic = Atomic(false) // Determines what the state of the app should be. private var desiredUIState: ScreenLockViewController.State { - if isScreenLockLocked { + if isScreenLockLocked.wrappedValue { if appIsInactiveOrBackground { Log.verbose("desiredUIState: screen protection 1.") return .protection @@ -148,7 +150,8 @@ class ScreenLockUI { ) } - public func setupWithRootWindow(rootWindow: UIWindow) { + public func setupWithRootWindow(rootWindow: UIWindow, using dependencies: Dependencies) { + self.dependencies = dependencies self.screenBlockingWindow.frame = rootWindow.bounds } @@ -165,16 +168,21 @@ class ScreenLockUI { // // It's not safe to access OWSScreenLock.isScreenLockEnabled // until the app is ready. - Singleton.appReadiness.runNowOrWhenAppWillBecomeReady { [weak self] in - self?.isScreenLockLocked = Storage.shared[.isScreenLockEnabled] - self?.ensureUI() + dependencies?[singleton: .appReadiness].runNowOrWhenAppWillBecomeReady { [weak self, dependencies] in + DispatchQueue.global(qos: .background).async { + self?.isScreenLockLocked.mutate { $0 = (dependencies?[singleton: .storage, key: .isScreenLockEnabled] == true) } + + DispatchQueue.main.async { + self?.ensureUI() + } + } } } // MARK: - Functions private func tryToActivateScreenLockBasedOnCountdown() { - guard Singleton.appReadiness.isAppReady else { + guard dependencies?[singleton: .appReadiness].isAppReady == true else { // It's not safe to access OWSScreenLock.isScreenLockEnabled // until the app is ready. // @@ -183,18 +191,18 @@ class ScreenLockUI { Log.verbose("tryToActivateScreenLockUponBecomingActive NO 0") return } - guard Storage.shared[.isScreenLockEnabled] else { + guard dependencies?[singleton: .storage, key: .isScreenLockEnabled] == true else { // Screen lock is not enabled. Log.verbose("tryToActivateScreenLockUponBecomingActive NO 1") return; } - guard !isScreenLockLocked else { + guard !isScreenLockLocked.wrappedValue else { // Screen lock is already activated. Log.verbose("tryToActivateScreenLockUponBecomingActive NO 2") return; } - self.isScreenLockLocked = true + self.isScreenLockLocked.mutate { $0 = true } } /// Ensure that: @@ -202,8 +210,8 @@ class ScreenLockUI { /// * The blocking window has the correct state. /// * That we show the "iOS auth UI to unlock" if necessary. private func ensureUI() { - guard Singleton.appReadiness.isAppReady else { - Singleton.appReadiness.runNowOrWhenAppWillBecomeReady { [weak self] in + guard dependencies?[singleton: .appReadiness].isAppReady == true else { + dependencies?[singleton: .appReadiness].runNowOrWhenAppWillBecomeReady { [weak self] in self?.ensureUI() } return @@ -233,7 +241,7 @@ class ScreenLockUI { success: { [weak self] in Log.info("unlock screen lock succeeded.") self?.isShowingScreenLockUI = false - self?.isScreenLockLocked = false + self?.isScreenLockLocked.mutate { $0 = false } self?.didUnlockJustSucceed = true self?.ensureUI() }, @@ -288,7 +296,7 @@ class ScreenLockUI { private func createScreenBlockingWindow(rootWindow: UIWindow) { let window: UIWindow = UIWindow(frame: rootWindow.bounds) window.isHidden = false - window.windowLevel = ._Background + window.windowLevel = .background window.isOpaque = true window.themeBackgroundColorForced = .theme(.classicDark, color: .backgroundPrimary) @@ -361,7 +369,7 @@ class ScreenLockUI { @objc private func clockDidChange() { Log.info("clock did change") - guard Singleton.appReadiness.isAppReady else { + guard dependencies?[singleton: .appReadiness].isAppReady == true else { // It's not safe to access OWSScreenLock.isScreenLockEnabled // until the app is ready. // @@ -371,11 +379,15 @@ class ScreenLockUI { return; } - self.isScreenLockLocked = Storage.shared[.isScreenLockEnabled] - - // NOTE: this notifications fires _before_ applicationDidBecomeActive, - // which is desirable. Don't assume that though; call ensureUI - // just in case it's necessary. - self.ensureUI() + DispatchQueue.global(qos: .background).async { [dependencies] in + self.isScreenLockLocked.mutate { $0 = (dependencies?[singleton: .storage, key: .isScreenLockEnabled] == true) } + + DispatchQueue.main.async { + // NOTE: this notifications fires _before_ applicationDidBecomeActive, + // which is desirable. Don't assume that though; call ensureUI + // just in case it's necessary. + self.ensureUI() + } + } } } diff --git a/Session/Shared/SessionCarouselView+SwiftUI.swift b/Session/Shared/SessionCarouselView+SwiftUI.swift index 2d7df10eefe..030010410c2 100644 --- a/Session/Shared/SessionCarouselView+SwiftUI.swift +++ b/Session/Shared/SessionCarouselView+SwiftUI.swift @@ -1,15 +1,20 @@ // Copyright © 2023 Rangeproof Pty Ltd. All rights reserved. import SwiftUI +import SessionMessagingKit +import SessionUtilitiesKit public struct SessionCarouselView_SwiftUI: View { @Binding var index: Int + + private let dependencies: Dependencies let isOutgoing: Bool var contentInfos: [Attachment] let numberOfPages: Int - public init(index: Binding, isOutgoing: Bool, contentInfos: [Attachment]) { + public init(index: Binding, isOutgoing: Bool, contentInfos: [Attachment], using dependencies: Dependencies) { self._index = index + self.dependencies = dependencies self.isOutgoing = isOutgoing self.contentInfos = contentInfos self.numberOfPages = contentInfos.count @@ -31,7 +36,8 @@ public struct SessionCarouselView_SwiftUI: View { attachment: attachment, isOutgoing: self.isOutgoing, shouldSupressControls: true, - cornerRadius: 0 + cornerRadius: 0, + using: dependencies ) } } @@ -218,7 +224,8 @@ struct SessionCarouselView_SwiftUI_Previews: PreviewProvider { contentType: "jpeg", byteCount: 100 ) - ] + ], + using: Dependencies.createEmpty() ) } } diff --git a/Session/Shared/SessionHostingViewController.swift b/Session/Shared/SessionHostingViewController.swift index 4d1dd36a1b7..bb9fde27f73 100644 --- a/Session/Shared/SessionHostingViewController.swift +++ b/Session/Shared/SessionHostingViewController.swift @@ -111,123 +111,70 @@ public class SessionHostingViewController: UIHostingController + +class SessionListViewModel: SessionTableViewModel, NavigationItemSource, NavigatableStateHolder, ObservableTableSource { + typealias TableItem = T + + public let dependencies: Dependencies + public let navigatableState: NavigatableState = NavigatableState() + public let state: TableDataState = TableDataState() + public let observableState: ObservableTableSourceState = ObservableTableSourceState() + private let selectedOptionsSubject: CurrentValueSubject, Never> + + let title: String + private let options: [T] + private let behaviour: Behaviour + + // MARK: - Initialization + + init( + title: String, + options: [T], + behaviour: Behaviour, + using dependencies: Dependencies + ) { + self.title = title + self.selectedOptionsSubject = { + switch behaviour { + case .autoDismiss(let initial, _), .singleSelect(let initial, _, _): return CurrentValueSubject([initial]) + case .multiSelect(let initial, _, _): return CurrentValueSubject(initial) + } + }() + self.options = options + self.behaviour = behaviour + self.dependencies = dependencies + } + + // MARK: - Config + + public enum Behaviour { + case autoDismiss(initialSelection: T, onOptionSelected: ((T) -> Void)?) + case singleSelect(initialSelection: T, onOptionSelected: ((T) -> Void)?, onSaved: ((T) -> Void)?) + case multiSelect(initialSelection: Set, onOptionSelected: ((Set) -> Void)?, onSaved: ((Set) -> Void)?) + + static func singleSelect(initialSelection: T, onSaved: ((T) -> Void)?) -> Behaviour { + return .singleSelect(initialSelection: initialSelection, onOptionSelected: nil, onSaved: onSaved) + } + + static func multiSelect(initialSelection: Set, onSaved: ((Set) -> Void)?) -> Behaviour { + return .multiSelect(initialSelection: initialSelection, onOptionSelected: nil, onSaved: onSaved) + } + } + + enum NavItem: Equatable { + case cancel + case save + } + + public enum Section: SessionTableSection { + case content + } + + // MARK: - Navigation + + lazy var leftNavItems: AnyPublisher<[SessionNavItem], Never> = { + switch behaviour { + case .autoDismiss: return Just([]).eraseToAnyPublisher() + case .singleSelect, .multiSelect: + return Just([ + SessionNavItem( + id: .cancel, + systemItem: .cancel, + accessibilityIdentifier: "Cancel button" + ) { [weak self] in self?.dismissScreen() } + ]).eraseToAnyPublisher() + } + }() + + lazy var rightNavItems: AnyPublisher<[SessionNavItem], Never> = { + switch behaviour { + case .autoDismiss: return Just([]).eraseToAnyPublisher() + case .singleSelect, .multiSelect: + return selectedOptionsSubject + .removeDuplicates() + .map { [behaviour] currentSelection -> (isChanged: Bool, currentSelection: Set) in + switch behaviour { + case .autoDismiss(let initialSelection, _), .singleSelect(let initialSelection, _, _): + return (([initialSelection] != currentSelection), currentSelection) + + case .multiSelect(let initialSelection, _, _): + return ((initialSelection != currentSelection), currentSelection) + } + } + .map { [behaviour] isChanged, currentSelection in + guard isChanged, let firstSelection: T = currentSelection.first else { return [] } + + return [ + SessionNavItem( + id: .save, + systemItem: .save, + accessibilityIdentifier: "Save button" + ) { [weak self] in + switch behaviour { + case .autoDismiss: return + case .singleSelect(_, _, let onSaved): onSaved?(firstSelection) + case .multiSelect(_, _, let onSaved): onSaved?(currentSelection) + } + + self?.dismissScreen() + } + ] + } + .eraseToAnyPublisher() + } + }() + + // MARK: - Content + + lazy var observation: TargetObservation = ObservationBuilder + .subject(selectedOptionsSubject) + .map { [weak self, options, behaviour] currentSelections -> [SectionModel] in + return [ + SectionModel( + model: .content, + elements: options + .map { option in + SessionCell.Info( + id: option, + title: option.title, + subtitle: option.subtitle, + trailingAccessory: .radio( + isSelected: currentSelections.contains(option) + ), + onTap: { + switch (behaviour, currentSelections.contains(option)) { + case (.autoDismiss(_, let onOptionSelected), _): + onOptionSelected?(option) + self?.dismissScreen() + + case (.singleSelect(_, let onOptionSelected, _), _): + self?.selectedOptionsSubject.send([option]) + onOptionSelected?(option) + + case (.multiSelect(_, let onOptionSelected, _), true): + let updatedSelection: Set = currentSelections.removing(option) + self?.selectedOptionsSubject.send(updatedSelection) + onOptionSelected?(updatedSelection) + + case (.multiSelect(_, let onOptionSelected, _), false): + let updatedSelection: Set = currentSelections.inserting(option) + self?.selectedOptionsSubject.send(updatedSelection) + onOptionSelected?(updatedSelection) + } + } + ) + } + ) + ] + } +} diff --git a/Session/Shared/SessionTableViewController.swift b/Session/Shared/SessionTableViewController.swift index b6ec2fea8cf..f2d13e69a7c 100644 --- a/Session/Shared/SessionTableViewController.swift +++ b/Session/Shared/SessionTableViewController.swift @@ -6,6 +6,7 @@ import GRDB import DifferenceKit import SessionUIKit import SessionUtilitiesKit +import SessionMessagingKit import SignalUtilitiesKit protocol SessionViewModelAccessible { @@ -33,6 +34,25 @@ class SessionTableViewController: BaseVC, UITableViewDataSource, UITa private lazy var titleView: SessionTableViewTitleView = SessionTableViewTitleView() + private lazy var contentStackView: UIStackView = { + let result: UIStackView = UIStackView(arrangedSubviews: [ + infoBanner, + tableView + ]) + result.axis = .vertical + result.alignment = .fill + result.distribution = .fill + + return result + }() + + private lazy var infoBanner: InfoBanner = { + let result: InfoBanner = InfoBanner(info: .empty) + result.isHidden = true + + return result + }() + private lazy var tableView: UITableView = { let result: UITableView = UITableView() result.translatesAutoresizingMaskIntoConstraints = false @@ -129,7 +149,7 @@ class SessionTableViewController: BaseVC, UITableViewDataSource, UITa titleView.update(title: self.viewModel.title, subtitle: self.viewModel.subtitle) view.themeBackgroundColor = .backgroundPrimary - view.addSubview(tableView) + view.addSubview(contentStackView) view.addSubview(initialLoadLabel) view.addSubview(emptyStateLabel) view.addSubview(fadeView) @@ -183,7 +203,7 @@ class SessionTableViewController: BaseVC, UITableViewDataSource, UITa } private func setupLayout() { - tableView.pin(to: view) + contentStackView.pin(to: view) initialLoadLabel.pin(.top, to: .top, of: self.view, withInset: Values.massiveSpacing) initialLoadLabel.pin(.leading, to: .leading, of: self.view, withInset: Values.mediumSpacing) @@ -211,7 +231,7 @@ class SessionTableViewController: BaseVC, UITableViewDataSource, UITa receiveCompletion: { [weak self] result in switch result { case .failure(let error): - let title: String = (self?.viewModel.title ?? "unknown".localized()) + let title: String = (self?.viewModel.title ?? "unknown".localized()) // If we got an error then try to restart the stream once, otherwise log the error guard self?.dataStreamJustFailed == false else { @@ -341,6 +361,19 @@ class SessionTableViewController: BaseVC, UITableViewDataSource, UITa } .store(in: &disposables) + viewModel.bannerInfo + .receive(on: DispatchQueue.main) + .sink { [weak self] info in + switch info { + case .some(let info): + self?.infoBanner.update(with: info) + self?.infoBanner.isHidden = false + + case .none: self?.infoBanner.isHidden = true + } + } + .store(in: &disposables) + viewModel.emptyStateTextPublisher .receive(on: DispatchQueue.main) .sink { [weak self] text in @@ -360,7 +393,7 @@ class SessionTableViewController: BaseVC, UITableViewDataSource, UITa .sink { [weak self] buttonInfo in if let buttonInfo: SessionButton.Info = buttonInfo { self?.footerButton.setTitle(buttonInfo.title, for: .normal) - self?.footerButton.setStyle(buttonInfo.style) + self?.footerButton.style = buttonInfo.style self?.footerButton.isEnabled = buttonInfo.isEnabled self?.footerButton.set(.width, greaterThanOrEqualTo: buttonInfo.minWidth) self?.footerButton.accessibilityIdentifier = buttonInfo.accessibility?.identifier @@ -407,7 +440,7 @@ class SessionTableViewController: BaseVC, UITableViewDataSource, UITa switch (cell, info) { case (let cell as SessionCell, _): - cell.update(with: info) + cell.update(with: info, using: viewModel.dependencies) cell.update( isEditing: (self.isEditing || (info.title?.interaction == .alwaysEditing)), becomeFirstResponder: false, @@ -427,7 +460,7 @@ class SessionTableViewController: BaseVC, UITableViewDataSource, UITa case (let cell as FullConversationCell, let threadInfo as SessionCell.Info): cell.accessibilityIdentifier = info.accessibility?.identifier cell.isAccessibilityElement = (info.accessibility != nil) - cell.update(with: threadInfo.id) + cell.update(with: threadInfo.id, using: viewModel.dependencies) default: SNLog("[SessionTableViewController] Got invalid combination of cellType: \(viewModel.cellType) and tableData: \(SessionCell.Info.self)") @@ -532,23 +565,56 @@ class SessionTableViewController: BaseVC, UITableViewDataSource, UITa return nil } - switch (info.leftAccessory, info.rightAccessory) { - case (_, .highlightingBackgroundLabel(_, _)): - return (!cell.rightAccessoryView.isHidden ? cell.rightAccessoryView : cell) + // Retrieve the last touch location from the cell + let touchLocation: UITouch? = cell.lastTouchLocation + cell.lastTouchLocation = nil + + switch (info.leadingAccessory, info.trailingAccessory) { + case (_, is SessionCell.AccessoryConfig.HighlightingBackgroundLabel): + return (!cell.trailingAccessoryView.isHidden ? cell.trailingAccessoryView : cell) + + case (is SessionCell.AccessoryConfig.HighlightingBackgroundLabel, _): + return (!cell.leadingAccessoryView.isHidden ? cell.leadingAccessoryView : cell) + + case (_, is SessionCell.AccessoryConfig.HighlightingBackgroundLabelAndRadio): + guard let touchLocation: UITouch = touchLocation else { return cell } + + let localPoint: CGPoint = touchLocation.location(in: cell.trailingAccessoryView.highlightingBackgroundLabel) - case (.highlightingBackgroundLabel(_, _), _): - return (!cell.leftAccessoryView.isHidden ? cell.leftAccessoryView : cell) + guard + !cell.trailingAccessoryView.isHidden && + cell.trailingAccessoryView.highlightingBackgroundLabel.bounds.contains(localPoint) + else { return (!cell.trailingAccessoryView.isHidden ? cell.trailingAccessoryView : cell) } + + return cell.trailingAccessoryView.highlightingBackgroundLabel + + case (is SessionCell.AccessoryConfig.HighlightingBackgroundLabelAndRadio, _): + guard let touchLocation: UITouch = touchLocation else { return cell } + + let localPoint: CGPoint = touchLocation.location(in: cell.trailingAccessoryView.highlightingBackgroundLabel) + + guard + !cell.leadingAccessoryView.isHidden && + cell.leadingAccessoryView.highlightingBackgroundLabel.bounds.contains(localPoint) + else { return (!cell.leadingAccessoryView.isHidden ? cell.leadingAccessoryView : cell) } + + return cell.leadingAccessoryView.highlightingBackgroundLabel default: return cell } }() + 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() + switch (info.leadingAccessory, info.trailingAccessory) { + case (_, let accessory as SessionCell.AccessoryConfig.Radio): return accessory.liveIsSelected() + case (let accessory as SessionCell.AccessoryConfig.Radio, _): return accessory.liveIsSelected() + case (_, let accessory as SessionCell.AccessoryConfig.HighlightingBackgroundLabelAndRadio): + return accessory.liveIsSelected() + case (let accessory as SessionCell.AccessoryConfig.HighlightingBackgroundLabelAndRadio, _): + return accessory.liveIsSelected() default: return false } }) @@ -595,7 +661,7 @@ class SessionTableViewController: BaseVC, UITableViewDataSource, UITa ) { // 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, isManualReload: true) + existingCell.update(with: info, isManualReload: true, using: viewModel.dependencies) } else { tableView.reloadRows(at: [indexPath], with: .none) diff --git a/Session/Shared/SessionTableViewModel.swift b/Session/Shared/SessionTableViewModel.swift index 4273d58191f..8a411a336e1 100644 --- a/Session/Shared/SessionTableViewModel.swift +++ b/Session/Shared/SessionTableViewModel.swift @@ -15,6 +15,7 @@ protocol SessionTableViewModel: AnyObject, SectionedTableData { var subtitle: String? { get } var initialLoadMessage: String? { get } var cellType: SessionTableViewCellType { get } + var bannerInfo: AnyPublisher { get } var emptyStateTextPublisher: AnyPublisher { get } var state: TableDataState { get } var footerView: AnyPublisher { get } @@ -31,6 +32,7 @@ extension SessionTableViewModel { var subtitle: String? { nil } var initialLoadMessage: String? { nil } var cellType: SessionTableViewCellType { .general } + var bannerInfo: AnyPublisher { Just(nil).eraseToAnyPublisher() } var emptyStateTextPublisher: AnyPublisher { Just(nil).eraseToAnyPublisher() } var tableData: [SectionModel] { state.tableData } var footerView: AnyPublisher { Just(nil).eraseToAnyPublisher() } diff --git a/Session/Shared/Types/Navigatable.swift b/Session/Shared/Types/Navigatable.swift index 201b12126b9..daa0a3ee23e 100644 --- a/Session/Shared/Types/Navigatable.swift +++ b/Session/Shared/Types/Navigatable.swift @@ -1,6 +1,6 @@ // Copyright © 2023 Rangeproof Pty Ltd. All rights reserved. -import Foundation +import UIKit import Combine import SessionUIKit import SessionUtilitiesKit diff --git a/Session/Shared/Types/NavigatableState.swift b/Session/Shared/Types/NavigatableState.swift index 96fc97bc90a..6d7be06413d 100644 --- a/Session/Shared/Types/NavigatableState.swift +++ b/Session/Shared/Types/NavigatableState.swift @@ -13,8 +13,12 @@ public protocol NavigatableStateHolder { } public extension NavigatableStateHolder { - func showToast(text: String, backgroundColor: ThemeValue = .backgroundPrimary, insect: CGFloat = Values.largeSpacing) { - navigatableState._showToast.send((text, backgroundColor, insect)) + func showToast(text: String, backgroundColor: ThemeValue = .backgroundPrimary, inset: CGFloat = Values.largeSpacing) { + navigatableState._showToast.send((NSAttributedString(string: text), backgroundColor, inset)) + } + + func showToast(text: NSAttributedString, backgroundColor: ThemeValue = .backgroundPrimary, inset: CGFloat = Values.largeSpacing) { + navigatableState._showToast.send((text, backgroundColor, inset)) } func dismissScreen(type: DismissType = .auto) { @@ -29,13 +33,13 @@ public extension NavigatableStateHolder { // MARK: - NavigatableState public struct NavigatableState { - let showToast: AnyPublisher<(String, ThemeValue, CGFloat), Never> + let showToast: AnyPublisher<(NSAttributedString, ThemeValue, CGFloat), Never> let transitionToScreen: AnyPublisher<(UIViewController, TransitionType), Never> let dismissScreen: AnyPublisher // MARK: - Internal Variables - fileprivate let _showToast: PassthroughSubject<(String, ThemeValue, CGFloat), Never> = PassthroughSubject() + fileprivate let _showToast: PassthroughSubject<(NSAttributedString, ThemeValue, CGFloat), Never> = PassthroughSubject() fileprivate let _transitionToScreen: PassthroughSubject<(UIViewController, TransitionType), Never> = PassthroughSubject() fileprivate let _dismissScreen: PassthroughSubject = PassthroughSubject() @@ -55,11 +59,11 @@ public struct NavigatableState { ) { self.showToast .receive(on: DispatchQueue.main) - .sink { [weak viewController] text, color, insect in + .sink { [weak viewController] text, color, inset in guard let view: UIView = viewController?.view else { return } let toastController: ToastController = ToastController(text: text, background: color) - toastController.presentToastView(fromBottomOfView: view, inset: insect) + toastController.presentToastView(fromBottomOfView: view, inset: inset) } .store(in: &disposables) diff --git a/Session/Shared/Types/ObservableTableSource.swift b/Session/Shared/Types/ObservableTableSource.swift index 006a9dd5d7e..9c6b82df20c 100644 --- a/Session/Shared/Types/ObservableTableSource.swift +++ b/Session/Shared/Types/ObservableTableSource.swift @@ -22,6 +22,11 @@ public protocol ObservableTableSource: AnyObject, SectionedTableData { func didReturnFromBackground() } +public enum ObservableTableSourceRefreshType { + case databaseQuery + case postDatabaseQuery +} + extension ObservableTableSource { public var pendingTableDataSubject: CurrentValueSubject<[SectionModel], Never> { self.observableState.pendingTableDataSubject @@ -33,25 +38,33 @@ extension ObservableTableSource { public var tableDataPublisher: TargetPublisher { self.observation.finalPublisher(self, using: dependencies) } public func didReturnFromBackground() {} - public func forceRefresh() { self.observableState._forcedRefresh.send(()) } + public func forceRefresh(type: ObservableTableSourceRefreshType = .databaseQuery) { + switch type { + case .databaseQuery: self.observableState._forcedRequery.send(()) + case .postDatabaseQuery: self.observableState._forcedPostQueryRefresh.send(()) + } + } } // MARK: - State Manager (ObservableTableSource) public class ObservableTableSourceState: SectionedTableData { - public let forcedRefresh: AnyPublisher + fileprivate let forcedRequery: AnyPublisher + fileprivate let forcedPostQueryRefresh: AnyPublisher public let pendingTableDataSubject: CurrentValueSubject<[SectionModel], Never> // MARK: - Internal Variables fileprivate var hasEmittedInitialData: Bool - fileprivate let _forcedRefresh: PassthroughSubject = PassthroughSubject() + fileprivate let _forcedRequery: PassthroughSubject = PassthroughSubject() + fileprivate let _forcedPostQueryRefresh: PassthroughSubject = PassthroughSubject() // MARK: - Initialization init() { self.hasEmittedInitialData = false - self.forcedRefresh = _forcedRefresh.shareReplay(0) + self.forcedRequery = _forcedRequery.shareReplay(0) + self.forcedPostQueryRefresh = _forcedPostQueryRefresh.shareReplay(0) self.pendingTableDataSubject = CurrentValueSubject([]) } } @@ -152,12 +165,57 @@ public enum ObservationBuilder { /// fetch (after the ones in `ValueConcurrentObserver.asyncStart`/`ValueConcurrentObserver.syncStart`) /// just in case the database has changed between the two reads - unfortunately it doesn't look like there is a way to prevent this return TableObservation { viewModel, dependencies in - return ValueObservation - .trackingConstantRegion(fetch) - .removeDuplicates() - .handleEvents(didFail: { SNLog("[\(type(of: viewModel))] Observation failed with error: \($0)") }) - .publisher(in: dependencies.storage, scheduling: dependencies.scheduler) - .manualRefreshFrom(source.observableState.forcedRefresh) + let subject: CurrentValueSubject = CurrentValueSubject(nil) + var forcedRefreshCancellable: AnyCancellable? + var observationCancellable: DatabaseCancellable? + + /// In order to force a `ValueObservation` to requery we need to resubscribe to it, as a result we create a + /// `CurrentValueSubject` and in the `receiveSubscription` call we start the `ValueObservation` sending + /// it's output into the subject + /// + /// **Note:** We need to use a `CurrentValueSubject` here because the `ValueObservation` could send it's + /// first value _before_ the subscription is properly setup, by using a `CurrentValueSubject` the value will be stored + /// and emitted once the subscription becomes valid + return subject + .compactMap { $0 } + .handleEvents( + receiveSubscription: { subscription in + forcedRefreshCancellable = source.observableState.forcedRequery + .prepend(()) + .sink( + receiveCompletion: { _ in }, + receiveValue: { _ in + /// Cancel any previous observation and create a brand new observation for this refresh + /// + /// **Note:** The `ValueObservation` **MUST** be started from the main thread + observationCancellable?.cancel() + observationCancellable = dependencies[singleton: .storage].start( + ValueObservation + .trackingConstantRegion(fetch) + .removeDuplicates(), + scheduling: dependencies[singleton: .scheduler], + onError: { error in + let log: String = [ + "[\(type(of: viewModel))]", // stringlint:ignore + "Observation failed with error:", // stringlint:ignore + "\(error)" // stringlint:ignore + ].joined(separator: " ") + SNLog(log) + subject.send(completion: Subscribers.Completion.failure(error)) + }, + onChange: { subject.send($0) } + ) + } + ) + }, + receiveCancel: { + forcedRefreshCancellable?.cancel() + observationCancellable?.cancel() + } + ) + .manualRefreshFrom(source.observableState.forcedPostQueryRefresh) + .shareReplay(1) // Share to prevent multiple subscribers resulting in multiple ValueObservations + .eraseToAnyPublisher() } } @@ -169,12 +227,67 @@ public enum ObservationBuilder { /// fetch (after the ones in `ValueConcurrentObserver.asyncStart`/`ValueConcurrentObserver.syncStart`) /// just in case the database has changed between the two reads - unfortunately it doesn't look like there is a way to prevent this return TableObservation { viewModel, dependencies in - return ValueObservation - .trackingConstantRegion(fetch) - .removeDuplicates() - .handleEvents(didFail: { SNLog("[\(type(of: viewModel))] Observation failed with error: \($0)") }) - .publisher(in: dependencies.storage, scheduling: dependencies.scheduler) - .manualRefreshFrom(source.observableState.forcedRefresh) + let subject: CurrentValueSubject<[T]?, Error> = CurrentValueSubject(nil) + var forcedRefreshCancellable: AnyCancellable? + var observationCancellable: DatabaseCancellable? + + /// In order to force a `ValueObservation` to requery we need to resubscribe to it, as a result we create a + /// `CurrentValueSubject` and in the `receiveSubscription` call we start the `ValueObservation` sending + /// it's output into the subject + /// + /// **Note:** We need to use a `CurrentValueSubject` here because the `ValueObservation` could send it's + /// first value _before_ the subscription is properly setup, by using a `CurrentValueSubject` the value will be stored + /// and emitted once the subscription becomes valid + return subject + .compactMap { $0 } + .handleEvents( + receiveSubscription: { subscription in + forcedRefreshCancellable = source.observableState.forcedRequery + .prepend(()) + .sink( + receiveCompletion: { _ in }, + receiveValue: { _ in + /// Cancel any previous observation and create a brand new observation for this refresh + /// + /// **Note:** The `ValueObservation` **MUST** be started from the main thread + observationCancellable?.cancel() + observationCancellable = dependencies[singleton: .storage].start( + ValueObservation + .trackingConstantRegion(fetch) + .removeDuplicates(), + scheduling: dependencies[singleton: .scheduler], + onError: { error in + let log: String = [ + "[\(type(of: viewModel))]", // stringlint:ignore + "Observation failed with error:", // stringlint:ignore + "\(error)" // stringlint:ignore + ].joined(separator: " ") + SNLog(log) + subject.send(completion: Subscribers.Completion.failure(error)) + }, + onChange: { subject.send($0) } + ) + } + ) + }, + receiveCancel: { + forcedRefreshCancellable?.cancel() + observationCancellable?.cancel() + } + ) + .manualRefreshFrom(source.observableState.forcedPostQueryRefresh) + .shareReplay(1) // Share to prevent multiple subscribers resulting in multiple ValueObservations + .eraseToAnyPublisher() + } + } + + static func refreshableData(_ source: S, fetch: @escaping () -> T) -> TableObservation { + return TableObservation { viewModel, dependencies in + source.observableState.forcedRequery + .prepend(()) + .setFailureType(to: Error.self) + .map { _ in fetch() } + .manualRefreshFrom(source.observableState.forcedPostQueryRefresh) } } } @@ -188,6 +301,12 @@ public extension TableObservation { } } + func compactMap(transform: @escaping (T) -> R?) -> TableObservation { + return TableObservation { viewModel, dependencies in + self.generatePublisher(viewModel, dependencies).compactMap(transform).eraseToAnyPublisher() + } + } + func mapWithPrevious(transform: @escaping (T?, T) -> R) -> TableObservation { return TableObservation { viewModel, dependencies in self.generatePublisher(viewModel, dependencies) diff --git a/Session/Shared/Types/PagedObservationSource.swift b/Session/Shared/Types/PagedObservationSource.swift index a7bf8c795dd..33c0da78e7a 100644 --- a/Session/Shared/Types/PagedObservationSource.swift +++ b/Session/Shared/Types/PagedObservationSource.swift @@ -17,7 +17,7 @@ protocol PagedObservationSource { extension PagedObservationSource { public func didInit(using dependencies: Dependencies) { - dependencies.storage.addObserver(pagedDataObserver) + dependencies[singleton: .storage].addObserver(pagedDataObserver) } } diff --git a/Session/Shared/Types/SessionCell+Accessory.swift b/Session/Shared/Types/SessionCell+Accessory.swift index fcf9f6d63ba..c170a11a0c6 100644 --- a/Session/Shared/Types/SessionCell+Accessory.swift +++ b/Session/Shared/Types/SessionCell+Accessory.swift @@ -6,497 +6,689 @@ import SessionUIKit import SessionMessagingKit import SessionUtilitiesKit -extension SessionCell { - public enum Accessory: Hashable, Equatable { - case icon( - UIImage?, - size: IconSize, - customTint: ThemeValue?, - shouldFill: Bool, - accessibility: Accessibility? - ) - case iconAsync( - size: IconSize, - customTint: ThemeValue?, - shouldFill: Bool, - accessibility: Accessibility?, - setter: (UIImageView) -> Void - ) - case toggle( - DataSource, - accessibility: Accessibility? - ) - case dropDown( - DataSource, - accessibility: Accessibility? - ) - case radio( - size: RadioSize, - isSelected: () -> Bool, - storedSelection: Bool, - accessibility: Accessibility? - ) - - case highlightingBackgroundLabel( - title: String, - accessibility: Accessibility? - ) - case profile( - id: String, - size: ProfilePictureView.Size, - threadVariant: SessionThread.Variant, - customImageData: Data?, - profile: Profile?, - profileIcon: ProfilePictureView.ProfileIcon, - additionalProfile: Profile?, - additionalProfileIcon: ProfilePictureView.ProfileIcon, - accessibility: Accessibility? - ) - - case search( - placeholder: String, - accessibility: Accessibility?, - searchTermChanged: (String?) -> Void - ) - case button( - style: SessionButton.Style, - title: String, - accessibility: Accessibility?, - run: (SessionButton?) -> Void - ) - case customView( - hashValue: AnyHashable, - viewGenerator: () -> UIView - ) - - // MARK: - Convenience Vatiables +public extension SessionCell { + enum AccessoryConfig {} + + class Accessory: Hashable, Equatable { + public let accessibility: Accessibility? + public var shouldFitToEdge: Bool { false } + public var currentBoolValue: Bool { false } - var shouldFitToEdge: Bool { - switch self { - case .icon(_, _, _, let shouldFill, _), .iconAsync(_, _, let shouldFill, _, _): - return shouldFill - default: return false - } + fileprivate init(accessibility: Accessibility?) { + self.accessibility = accessibility } - var currentBoolValue: Bool { - switch self { - case .toggle(let dataSource, _), .dropDown(let dataSource, _): return dataSource.currentBoolValue - case .radio(_, let isSelected, _, _): return isSelected() - default: return false - } - } + public func hash(into hasher: inout Hasher) {} + fileprivate func isEqual(to other: SessionCell.Accessory) -> Bool { return false } - // MARK: - Conformance - - public func hash(into hasher: inout Hasher) { - switch self { - 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, let accessibility, _): - size.hash(into: &hasher) - customTint.hash(into: &hasher) - shouldFill.hash(into: &hasher) - accessibility.hash(into: &hasher) - - case .toggle(let dataSource, let accessibility): - dataSource.hash(into: &hasher) - accessibility.hash(into: &hasher) - - case .dropDown(let dataSource, let accessibility): - dataSource.hash(into: &hasher) - accessibility.hash(into: &hasher) - - 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, let accessibility): - title.hash(into: &hasher) - accessibility.hash(into: &hasher) - - case .profile( - let profileId, - let size, - let threadVariant, - let customImageData, - let profile, - let profileIcon, - let additionalProfile, - let additionalProfileIcon, - let accessibility - ): - profileId.hash(into: &hasher) - size.hash(into: &hasher) - threadVariant.hash(into: &hasher) - customImageData.hash(into: &hasher) - profile.hash(into: &hasher) - profileIcon.hash(into: &hasher) - additionalProfile.hash(into: &hasher) - additionalProfileIcon.hash(into: &hasher) - accessibility.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, let lhsAccessibility), .icon(let rhsImage, let rhsSize, let rhsCustomTint, let rhsShouldFill, let rhsAccessibility)): - return ( - lhsImage == rhsImage && - lhsSize == rhsSize && - lhsCustomTint == rhsCustomTint && - lhsShouldFill == rhsShouldFill && - lhsAccessibility == rhsAccessibility - ) - - 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 && - lhsAccessibility == rhsAccessibility - ) - - case (.toggle(let lhsDataSource, let lhsAccessibility), .toggle(let rhsDataSource, let rhsAccessibility)): - return ( - lhsDataSource == rhsDataSource && - lhsAccessibility == rhsAccessibility - ) - - case (.dropDown(let lhsDataSource, let lhsAccessibility), .dropDown(let rhsDataSource, let rhsAccessibility)): - return ( - lhsDataSource == rhsDataSource && - lhsAccessibility == rhsAccessibility - ) - - 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 && - lhsAccessibility == rhsAccessibility - ) - - case (.highlightingBackgroundLabel(let lhsTitle, let lhsAccessibility), .highlightingBackgroundLabel(let rhsTitle, let rhsAccessibility)): - return ( - lhsTitle == rhsTitle && - lhsAccessibility == rhsAccessibility - ) - - case ( - .profile( - let lhsProfileId, - let lhsSize, - let lhsThreadVariant, - let lhsCustomImageData, - let lhsProfile, - let lhsProfileIcon, - let lhsAdditionalProfile, - let lhsAdditionalProfileIcon, - let lhsAccessibility - ), - .profile( - let rhsProfileId, - let rhsSize, - let rhsThreadVariant, - let rhsCustomImageData, - let rhsProfile, - let rhsProfileIcon, - let rhsAdditionalProfile, - let rhsAdditionalProfileIcon, - let rhsAccessibility - ) - ): - return ( - lhsProfileId == rhsProfileId && - lhsSize == rhsSize && - lhsThreadVariant == rhsThreadVariant && - lhsCustomImageData == rhsCustomImageData && - lhsProfile == rhsProfile && - lhsProfileIcon == rhsProfileIcon && - lhsAdditionalProfile == rhsAdditionalProfile && - lhsAdditionalProfileIcon == rhsAdditionalProfileIcon && - lhsAccessibility == rhsAccessibility - ) - - case (.search(let lhsPlaceholder, let lhsAccessibility, _), .search(let rhsPlaceholder, let rhsAccessibility, _)): - return ( - 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 - } + public static func == (lhs: SessionCell.Accessory, rhs: SessionCell.Accessory) -> Bool { + return lhs.isEqual(to: rhs) } } } -// MARK: - Convenience Types +// MARK: - DSL -/// These are here because XCode doesn't realy like default values within enums so auto-complete and syntax -/// highlighting don't work properly -extension SessionCell.Accessory { - // MARK: - .icon Variants - - public static func icon(_ image: UIImage?) -> SessionCell.Accessory { - 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, accessibility: nil) +public extension SessionCell.Accessory { + static func icon( + _ image: UIImage?, + size: IconSize = .medium, + customTint: ThemeValue? = nil, + shouldFill: Bool = false, + accessibility: Accessibility? = nil + ) -> SessionCell.Accessory { + return SessionCell.AccessoryConfig.Icon( + image: image, + iconSize: size, + customTint: customTint, + shouldFill: shouldFill, + accessibility: accessibility + ) } - public static func icon(_ image: UIImage?, size: IconSize) -> SessionCell.Accessory { - return .icon(image, size: size, customTint: nil, shouldFill: false, accessibility: nil) + static func iconAsync( + size: IconSize = .medium, + customTint: ThemeValue? = nil, + shouldFill: Bool = false, + accessibility: Accessibility? = nil, + setter: @escaping (UIImageView) -> Void + ) -> SessionCell.Accessory { + return SessionCell.AccessoryConfig.IconAsync( + iconSize: size, + customTint: customTint, + shouldFill: shouldFill, + setter: setter, + accessibility: accessibility + ) } - public static func icon(_ image: UIImage?, size: IconSize, customTint: ThemeValue) -> SessionCell.Accessory { - return .icon(image, size: size, customTint: customTint, shouldFill: false, accessibility: nil) + static func toggle( + _ value: Bool, + oldValue: Bool?, + accessibility: Accessibility = Accessibility(identifier: "Switch") + ) -> SessionCell.Accessory { + return SessionCell.AccessoryConfig.Toggle( + value: value, + oldValue: (oldValue ?? value), + accessibility: accessibility + ) } - public static func icon(_ image: UIImage?, shouldFill: Bool) -> SessionCell.Accessory { - return .icon(image, size: .medium, customTint: nil, shouldFill: shouldFill, accessibility: nil) + static func dropDown( + _ dynamicString: @escaping () -> String?, + accessibility: Accessibility? = nil + ) -> SessionCell.Accessory { + return SessionCell.AccessoryConfig.DropDown( + dynamicString: dynamicString, + accessibility: accessibility + ) } - public static func icon(_ image: UIImage?, accessibility: Accessibility) -> SessionCell.Accessory { - return .icon(image, size: .medium, customTint: nil, shouldFill: false, accessibility: accessibility) + static func radio( + _ size: SessionCell.AccessoryConfig.Radio.Size = .medium, + isSelected: Bool? = nil, + liveIsSelected: (() -> Bool)? = nil, + wasSavedSelection: Bool = false, + accessibility: Accessibility = Accessibility(identifier: "Radio") + ) -> SessionCell.Accessory { + return SessionCell.AccessoryConfig.Radio( + size: size, + initialIsSelected: ((isSelected ?? liveIsSelected?()) ?? false), + liveIsSelected: (liveIsSelected ?? { (isSelected ?? false) }), + wasSavedSelection: wasSavedSelection, + accessibility: accessibility + ) } - // MARK: - .iconAsync Variants - - public static func iconAsync(_ setter: @escaping (UIImageView) -> Void) -> SessionCell.Accessory { - return .iconAsync(size: .medium, customTint: nil, shouldFill: false, accessibility: nil, setter: setter) + static func highlightingBackgroundLabel( + title: String, + accessibility: Accessibility? = nil + ) -> SessionCell.Accessory { + return SessionCell.AccessoryConfig.HighlightingBackgroundLabel( + title: title, + accessibility: accessibility + ) } - public static func iconAsync(customTint: ThemeValue, _ setter: @escaping (UIImageView) -> Void) -> SessionCell.Accessory { - return .iconAsync(size: .medium, customTint: customTint, shouldFill: false, accessibility: nil, setter: setter) + static func highlightingBackgroundLabelAndRadio( + title: String, + radioSize: SessionCell.AccessoryConfig.HighlightingBackgroundLabelAndRadio.Size = .medium, + isSelected: Bool? = nil, + liveIsSelected: (() -> Bool)? = nil, + wasSavedSelection: Bool = false, + labelAccessibility: Accessibility? = nil, + radioAccessibility: Accessibility? = nil + ) -> SessionCell.Accessory { + return SessionCell.AccessoryConfig.HighlightingBackgroundLabelAndRadio( + title: title, + radioSize: radioSize, + initialIsSelected: ((isSelected ?? liveIsSelected?()) ?? false), + liveIsSelected: (liveIsSelected ?? { (isSelected ?? false) }), + wasSavedSelection: wasSavedSelection, + labelAccessibility: labelAccessibility, + radioAccessibility: radioAccessibility + ) } - public static func iconAsync(size: IconSize, setter: @escaping (UIImageView) -> Void) -> SessionCell.Accessory { - return .iconAsync(size: size, customTint: nil, shouldFill: false, accessibility: nil, setter: setter) + static func profile( + id: String, + size: ProfilePictureView.Size = .list, + threadVariant: SessionThread.Variant = .contact, + displayPictureFilename: String? = nil, + profile: Profile? = nil, + profileIcon: ProfilePictureView.ProfileIcon = .none, + additionalProfile: Profile? = nil, + additionalProfileIcon: ProfilePictureView.ProfileIcon = .none, + accessibility: Accessibility? = nil + ) -> SessionCell.Accessory { + return SessionCell.AccessoryConfig.DisplayPicture( + id: id, + size: size, + threadVariant: threadVariant, + displayPictureFilename: displayPictureFilename, + profile: profile, + profileIcon: profileIcon, + additionalProfile: additionalProfile, + additionalProfileIcon: additionalProfileIcon, + accessibility: accessibility + ) } - public static func iconAsync(shouldFill: Bool, setter: @escaping (UIImageView) -> Void) -> SessionCell.Accessory { - return .iconAsync(size: .medium, customTint: nil, shouldFill: shouldFill, accessibility: nil, setter: setter) + static func search( + placeholder: String, + accessibility: Accessibility? = nil, + searchTermChanged: @escaping (String?) -> Void + ) -> SessionCell.Accessory { + return SessionCell.AccessoryConfig.Search( + placeholder: placeholder, + searchTermChanged: searchTermChanged, + accessibility: accessibility + ) } - public static func iconAsync(size: IconSize, customTint: ThemeValue, setter: @escaping (UIImageView) -> Void) -> SessionCell.Accessory { - return .iconAsync(size: size, customTint: customTint, shouldFill: false, accessibility: nil, setter: setter) + static func button( + style: SessionButton.Style, + title: String, + accessibility: Accessibility? = nil, + run: @escaping (SessionButton?) -> Void + ) -> SessionCell.Accessory { + return SessionCell.AccessoryConfig.Button( + style: style, + title: title, + run: run, + accessibility: accessibility + ) } - public static func iconAsync(size: IconSize, shouldFill: Bool, setter: @escaping (UIImageView) -> Void) -> SessionCell.Accessory { - return .iconAsync(size: size, customTint: nil, shouldFill: shouldFill, accessibility: nil, setter: setter) + static func customView( + uniqueId: AnyHashable, + accessibility: Accessibility? = nil, + viewGenerator: @escaping () -> UIView + ) -> SessionCell.Accessory { + return SessionCell.AccessoryConfig.CustomView( + uniqueId: uniqueId, + viewGenerator: viewGenerator, + accessibility: accessibility + ) } +} + +// MARK: Structs + +public extension SessionCell.AccessoryConfig { + // MARK: - Icon - // MARK: - .toggle Variants - - public static func toggle(_ dataSource: DataSource) -> SessionCell.Accessory { - return .toggle(dataSource, accessibility: Accessibility(identifier: "Switch")) + class Icon: SessionCell.Accessory { + public let image: UIImage? + public let iconSize: IconSize + public let customTint: ThemeValue? + public let shouldFill: Bool + + fileprivate init( + image: UIImage?, + iconSize: IconSize, + customTint: ThemeValue?, + shouldFill: Bool, + accessibility: Accessibility? + ) { + self.image = image + self.iconSize = iconSize + self.customTint = customTint + self.shouldFill = shouldFill + + super.init(accessibility: accessibility) + } + + // MARK: - Conformance + + override public func hash(into hasher: inout Hasher) { + image.hash(into: &hasher) + iconSize.hash(into: &hasher) + customTint.hash(into: &hasher) + shouldFill.hash(into: &hasher) + accessibility.hash(into: &hasher) + } + + override fileprivate func isEqual(to other: SessionCell.Accessory) -> Bool { + guard let rhs: Icon = other as? Icon else { return false } + + return ( + image == rhs.image && + iconSize == rhs.iconSize && + customTint == rhs.customTint && + shouldFill == rhs.shouldFill && + accessibility == rhs.accessibility + ) + } } - // MARK: - .dropDown Variants + // MARK: - IconAsync - public static func dropDown(_ dataSource: DataSource) -> SessionCell.Accessory { - return .dropDown(dataSource, accessibility: nil) + class IconAsync: SessionCell.Accessory { + public let iconSize: IconSize + public let customTint: ThemeValue? + public let shouldFill: Bool + public let setter: (UIImageView) -> Void + + fileprivate init( + iconSize: IconSize, + customTint: ThemeValue?, + shouldFill: Bool, + setter: @escaping (UIImageView) -> Void, + accessibility: Accessibility? + ) { + self.iconSize = iconSize + self.customTint = customTint + self.shouldFill = shouldFill + self.setter = setter + + super.init(accessibility: accessibility) + } + + // MARK: - Conformance + + override public func hash(into hasher: inout Hasher) { + iconSize.hash(into: &hasher) + customTint.hash(into: &hasher) + shouldFill.hash(into: &hasher) + accessibility.hash(into: &hasher) + } + + override fileprivate func isEqual(to other: SessionCell.Accessory) -> Bool { + guard let rhs: IconAsync = other as? IconAsync else { return false } + + return ( + iconSize == rhs.iconSize && + customTint == rhs.customTint && + shouldFill == rhs.shouldFill && + accessibility == rhs.accessibility + ) + } } - // MARK: - .radio Variants + // MARK: - Toggle - public static func radio(isSelected: @escaping () -> Bool) -> SessionCell.Accessory { - return .radio( - size: .medium, - isSelected: isSelected, - storedSelection: false, - accessibility: Accessibility(identifier: "Radio") - ) + class Toggle: SessionCell.Accessory { + public let value: Bool + public let oldValue: Bool + + override public var currentBoolValue: Bool { value } + + fileprivate init( + value: Bool, + oldValue: Bool, + accessibility: Accessibility? + ) { + self.value = value + self.oldValue = oldValue + + super.init(accessibility: accessibility) + } + + // MARK: - Conformance + + override public func hash(into hasher: inout Hasher) { + value.hash(into: &hasher) + oldValue.hash(into: &hasher) + accessibility.hash(into: &hasher) + } + + override fileprivate func isEqual(to other: SessionCell.Accessory) -> Bool { + guard let rhs: Toggle = other as? Toggle else { return false } + + return ( + value == rhs.value && + oldValue == rhs.oldValue && + accessibility == rhs.accessibility + ) + } } - public static func radio(isSelected: @escaping () -> Bool, accessibility: Accessibility) -> SessionCell.Accessory { - return .radio( - size: .medium, - isSelected: isSelected, - storedSelection: false, - accessibility: accessibility - ) - } + // MARK: - DropDown - public static func radio(isSelected: @escaping () -> Bool, storedSelection: Bool) -> SessionCell.Accessory { - return .radio( - size: .medium, - isSelected: isSelected, - storedSelection: storedSelection, - accessibility: Accessibility(identifier: "Radio") - ) + class DropDown: SessionCell.Accessory { + public let dynamicString: () -> String? + + fileprivate init( + dynamicString: @escaping () -> String?, + accessibility: Accessibility? + ) { + self.dynamicString = dynamicString + + super.init(accessibility: accessibility) + } + + // MARK: - Conformance + + override public func hash(into hasher: inout Hasher) { + dynamicString().hash(into: &hasher) + accessibility.hash(into: &hasher) + } + + override fileprivate func isEqual(to other: SessionCell.Accessory) -> Bool { + guard let rhs: DropDown = other as? DropDown else { return false } + + return ( + dynamicString() == rhs.dynamicString() && + accessibility == rhs.accessibility + ) + } } - // MARK: - .highlightingBackgroundLabel Variants + // MARK: - Radio - public static func highlightingBackgroundLabel(title: String) -> SessionCell.Accessory { - return .highlightingBackgroundLabel(title: title, accessibility: nil) + class Radio: SessionCell.Accessory { + public enum Size: Hashable, Equatable { + case small + case medium + + var borderSize: CGFloat { + switch self { + case .small: return 20 + case .medium: return 26 + } + } + + var selectionSize: CGFloat { + switch self { + case .small: return 15 + case .medium: return 20 + } + } + } + + public let size: Size + public let initialIsSelected: Bool + public let liveIsSelected: () -> Bool + public let wasSavedSelection: Bool + + override public var currentBoolValue: Bool { liveIsSelected() } + + fileprivate init( + size: Size, + initialIsSelected: Bool, + liveIsSelected: @escaping () -> Bool, + wasSavedSelection: Bool, + accessibility: Accessibility? + ) { + self.size = size + self.initialIsSelected = initialIsSelected + self.liveIsSelected = liveIsSelected + self.wasSavedSelection = wasSavedSelection + + super.init(accessibility: accessibility) + } + + // MARK: - Conformance + + override public func hash(into hasher: inout Hasher) { + size.hash(into: &hasher) + initialIsSelected.hash(into: &hasher) + wasSavedSelection.hash(into: &hasher) + accessibility.hash(into: &hasher) + } + + override fileprivate func isEqual(to other: SessionCell.Accessory) -> Bool { + guard let rhs: Radio = other as? Radio else { return false } + + return ( + size == rhs.size && + initialIsSelected == rhs.initialIsSelected && + wasSavedSelection == rhs.wasSavedSelection && + accessibility == rhs.accessibility + ) + } } - // MARK: - .profile Variants - - public static func profile(id: String, profile: Profile?) -> SessionCell.Accessory { - return .profile( - id: id, - size: .list, - threadVariant: .contact, - customImageData: nil, - profile: profile, - profileIcon: .none, - additionalProfile: nil, - additionalProfileIcon: .none, - accessibility: nil - ) - } + // MARK: - HighlightingBackgroundLabel - public static func profile(id: String, size: ProfilePictureView.Size, profile: Profile?) -> SessionCell.Accessory { - return .profile( - id: id, - size: size, - threadVariant: .contact, - customImageData: nil, - profile: profile, - profileIcon: .none, - additionalProfile: nil, - additionalProfileIcon: .none, - accessibility: nil - ) + class HighlightingBackgroundLabel: SessionCell.Accessory { + public let title: String + + init( + title: String, + accessibility: Accessibility? + ) { + self.title = title + + super.init(accessibility: accessibility) + } + + // MARK: - Conformance + + override public func hash(into hasher: inout Hasher) { + title.hash(into: &hasher) + accessibility.hash(into: &hasher) + } + + override fileprivate func isEqual(to other: SessionCell.Accessory) -> Bool { + guard let rhs: HighlightingBackgroundLabel = other as? HighlightingBackgroundLabel else { + return false + } + + return ( + title == rhs.title && + accessibility == rhs.accessibility + ) + } } - // MARK: - .search Variants + // MARK: - HighlightingBackgroundLabelAndRadio - public static func search(placeholder: String, searchTermChanged: @escaping (String?) -> Void) -> SessionCell.Accessory { - return .search(placeholder: placeholder, accessibility: nil, searchTermChanged: searchTermChanged) + class HighlightingBackgroundLabelAndRadio: SessionCell.Accessory { + public enum Size: Hashable, Equatable { + case small + case medium + + var borderSize: CGFloat { + switch self { + case .small: return 20 + case .medium: return 26 + } + } + + var selectionSize: CGFloat { + switch self { + case .small: return 15 + case .medium: return 20 + } + } + } + + public let title: String + public let size: Size + public let initialIsSelected: Bool + public let liveIsSelected: () -> Bool + public let wasSavedSelection: Bool + public let labelAccessibility: Accessibility? + + override public var currentBoolValue: Bool { liveIsSelected() } + + fileprivate init( + title: String, + radioSize: Size, + initialIsSelected: Bool, + liveIsSelected: @escaping () -> Bool, + wasSavedSelection: Bool, + labelAccessibility: Accessibility?, + radioAccessibility: Accessibility? + ) { + self.title = title + self.size = radioSize + self.initialIsSelected = initialIsSelected + self.liveIsSelected = liveIsSelected + self.wasSavedSelection = wasSavedSelection + self.labelAccessibility = labelAccessibility + + super.init(accessibility: radioAccessibility) + } + + // MARK: - Conformance + + override public func hash(into hasher: inout Hasher) { + title.hash(into: &hasher) + size.hash(into: &hasher) + initialIsSelected.hash(into: &hasher) + wasSavedSelection.hash(into: &hasher) + accessibility.hash(into: &hasher) + labelAccessibility.hash(into: &hasher) + } + + override fileprivate func isEqual(to other: SessionCell.Accessory) -> Bool { + guard let rhs: HighlightingBackgroundLabelAndRadio = other as? HighlightingBackgroundLabelAndRadio else { return false } + + return ( + title == rhs.title && + size == rhs.size && + initialIsSelected == rhs.initialIsSelected && + wasSavedSelection == rhs.wasSavedSelection && + accessibility == rhs.accessibility && + labelAccessibility == rhs.labelAccessibility + ) + } } - // MARK: - .button Variants + // MARK: - DisplayPicture - 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) - } -} - -// MARK: - SessionCell.Accessory.DataSource - -extension SessionCell.Accessory { - public enum DataSource: Hashable, Equatable { - case boolValue(key: String, value: Bool, oldValue: Bool) - case dynamicString(() -> String?) + class DisplayPicture: SessionCell.Accessory { + public let id: String + public let size: ProfilePictureView.Size + public let threadVariant: SessionThread.Variant + public let displayPictureFilename: String? + public let profile: Profile? + public let profileIcon: ProfilePictureView.ProfileIcon + public let additionalProfile: Profile? + public let additionalProfileIcon: ProfilePictureView.ProfileIcon - static func boolValue(_ value: Bool, oldValue: Bool) -> DataSource { - return .boolValue(key: "", value: value, oldValue: oldValue) + fileprivate init( + id: String, + size: ProfilePictureView.Size, + threadVariant: SessionThread.Variant, + displayPictureFilename: String?, + profile: Profile?, + profileIcon: ProfilePictureView.ProfileIcon, + additionalProfile: Profile?, + additionalProfileIcon: ProfilePictureView.ProfileIcon, + accessibility: Accessibility? + ) { + self.id = id + self.size = size + self.threadVariant = threadVariant + self.displayPictureFilename = displayPictureFilename + self.profile = profile + self.profileIcon = profileIcon + self.additionalProfile = additionalProfile + self.additionalProfileIcon = additionalProfileIcon + + super.init(accessibility: accessibility) } - static func boolValue(key: Setting.BoolKey, value: Bool, oldValue: Bool) -> DataSource { - return .boolValue(key: key.rawValue, value: value, oldValue: oldValue) + // MARK: - Conformance + + override public func hash(into hasher: inout Hasher) { + id.hash(into: &hasher) + size.hash(into: &hasher) + threadVariant.hash(into: &hasher) + displayPictureFilename.hash(into: &hasher) + profile.hash(into: &hasher) + profileIcon.hash(into: &hasher) + additionalProfile.hash(into: &hasher) + additionalProfileIcon.hash(into: &hasher) + accessibility.hash(into: &hasher) } - // MARK: - Convenience + override fileprivate func isEqual(to other: SessionCell.Accessory) -> Bool { + guard let rhs: DisplayPicture = other as? DisplayPicture else { return false } + + return ( + id == rhs.id && + size == rhs.size && + threadVariant == rhs.threadVariant && + displayPictureFilename == rhs.displayPictureFilename && + profile == rhs.profile && + profileIcon == rhs.profileIcon && + additionalProfile == rhs.additionalProfile && + additionalProfileIcon == rhs.additionalProfileIcon && + accessibility == rhs.accessibility + ) + } + } + + class Search: SessionCell.Accessory { + public let placeholder: String + public let searchTermChanged: (String?) -> Void - public var currentBoolValue: Bool { - switch self { - case .boolValue(_, let value, _): return value - case .dynamicString: return false - } + fileprivate init( + placeholder: String, + searchTermChanged: @escaping (String?) -> Void, + accessibility: Accessibility? + ) { + self.placeholder = placeholder + self.searchTermChanged = searchTermChanged + + super.init(accessibility: accessibility) } - public var oldBoolValue: Bool { - switch self { - case .boolValue(_, _, let oldValue): return oldValue - default: return false - } + // MARK: - Conformance + + override public func hash(into hasher: inout Hasher) { + placeholder.hash(into: &hasher) + accessibility.hash(into: &hasher) } - public var currentStringValue: String? { - switch self { - case .dynamicString(let value): return value() - default: return nil - } + override fileprivate func isEqual(to other: SessionCell.Accessory) -> Bool { + return ( + other is Search && + placeholder == (other as? Search)?.placeholder && + accessibility == (other as? Search)?.accessibility + ) + } + } + + class Button: SessionCell.Accessory { + public let style: SessionButton.Style + public let title: String + public let run: (SessionButton?) -> Void + + fileprivate init( + style: SessionButton.Style, + title: String, + run: @escaping (SessionButton?) -> Void, + accessibility: Accessibility? + ) { + self.style = style + self.title = title + self.run = run + + super.init(accessibility: accessibility) } // MARK: - Conformance - public func hash(into hasher: inout Hasher) { - switch self { - case .boolValue(let key, let value, let oldValue): - key.hash(into: &hasher) - value.hash(into: &hasher) - oldValue.hash(into: &hasher) - - case .dynamicString(let generator): generator().hash(into: &hasher) - } + override public func hash(into hasher: inout Hasher) { + style.hash(into: &hasher) + title.hash(into: &hasher) + accessibility.hash(into: &hasher) } - public static func == (lhs: DataSource, rhs: DataSource) -> Bool { - switch (lhs, rhs) { - case (.boolValue(let lhsKey, let lhsValue, let lhsOldValue), .boolValue(let rhsKey, let rhsValue, let rhsOldValue)): - return ( - lhsKey == rhsKey && - lhsValue == rhsValue && - lhsOldValue == rhsOldValue - ) - - case (.dynamicString(let lhsGenerator), .dynamicString(let rhsGenerator)): - return (lhsGenerator() == rhsGenerator()) - - default: return false - } + override fileprivate func isEqual(to other: SessionCell.Accessory) -> Bool { + return ( + other is Button && + style == (other as? Button)?.style && + title == (other as? Button)?.title && + accessibility == (other as? Button)?.accessibility + ) } } -} - -// MARK: - SessionCell.Accessory.RadioSize - -extension SessionCell.Accessory { - public enum RadioSize { - case small - case medium - - var borderSize: CGFloat { - switch self { - case .small: return 20 - case .medium: return 26 - } + + class CustomView: SessionCell.Accessory { + public let uniqueId: AnyHashable + public let viewGenerator: () -> UIView + + fileprivate init( + uniqueId: AnyHashable, + viewGenerator: @escaping () -> UIView, + accessibility: Accessibility? + ) { + self.uniqueId = uniqueId + self.viewGenerator = viewGenerator + + super.init(accessibility: accessibility) } - var selectionSize: CGFloat { - switch self { - case .small: return 15 - case .medium: return 20 - } + // MARK: - Conformance + + override public func hash(into hasher: inout Hasher) { + uniqueId.hash(into: &hasher) + accessibility.hash(into: &hasher) + } + + override fileprivate func isEqual(to other: SessionCell.Accessory) -> Bool { + return ( + other is CustomView && + uniqueId == (other as? CustomView)?.uniqueId && + accessibility == (other as? CustomView)?.accessibility + ) } } } diff --git a/Session/Shared/Types/SessionCell+Info.swift b/Session/Shared/Types/SessionCell+Info.swift index 35d63cb03ab..f07eea0a01a 100644 --- a/Session/Shared/Types/SessionCell+Info.swift +++ b/Session/Shared/Types/SessionCell+Info.swift @@ -3,15 +3,16 @@ import UIKit import DifferenceKit import SessionUIKit +import SessionMessagingKit extension SessionCell { public struct Info: Equatable, Hashable, Differentiable { let id: ID let position: Position - let leftAccessory: SessionCell.Accessory? + let leadingAccessory: SessionCell.Accessory? let title: TextInfo? let subtitle: TextInfo? - let rightAccessory: SessionCell.Accessory? + let trailingAccessory: SessionCell.Accessory? let styling: StyleInfo let isEnabled: Bool let accessibility: Accessibility? @@ -21,8 +22,8 @@ extension SessionCell { var currentBoolValue: Bool { return ( - (leftAccessory?.currentBoolValue ?? false) || - (rightAccessory?.currentBoolValue ?? false) + (leadingAccessory?.currentBoolValue ?? false) || + (trailingAccessory?.currentBoolValue ?? false) ) } @@ -31,10 +32,10 @@ extension SessionCell { init( id: ID, position: Position = .individual, - leftAccessory: SessionCell.Accessory? = nil, + leadingAccessory: SessionCell.Accessory? = nil, title: SessionCell.TextInfo? = nil, subtitle: SessionCell.TextInfo? = nil, - rightAccessory: SessionCell.Accessory? = nil, + trailingAccessory: SessionCell.Accessory? = nil, styling: StyleInfo = StyleInfo(), isEnabled: Bool = true, accessibility: Accessibility? = nil, @@ -44,10 +45,10 @@ extension SessionCell { ) { self.id = id self.position = position - self.leftAccessory = leftAccessory + self.leadingAccessory = leadingAccessory self.title = title self.subtitle = subtitle - self.rightAccessory = rightAccessory + self.trailingAccessory = trailingAccessory self.styling = styling self.isEnabled = isEnabled self.accessibility = accessibility @@ -63,10 +64,10 @@ extension SessionCell { public func hash(into hasher: inout Hasher) { id.hash(into: &hasher) position.hash(into: &hasher) - leftAccessory.hash(into: &hasher) + leadingAccessory.hash(into: &hasher) title.hash(into: &hasher) subtitle.hash(into: &hasher) - rightAccessory.hash(into: &hasher) + trailingAccessory.hash(into: &hasher) styling.hash(into: &hasher) isEnabled.hash(into: &hasher) accessibility.hash(into: &hasher) @@ -77,10 +78,10 @@ extension SessionCell { return ( lhs.id == rhs.id && lhs.position == rhs.position && - lhs.leftAccessory == rhs.leftAccessory && + lhs.leadingAccessory == rhs.leadingAccessory && lhs.title == rhs.title && lhs.subtitle == rhs.subtitle && - lhs.rightAccessory == rhs.rightAccessory && + lhs.trailingAccessory == rhs.trailingAccessory && lhs.styling == rhs.styling && lhs.isEnabled == rhs.isEnabled && lhs.accessibility == rhs.accessibility @@ -93,10 +94,10 @@ extension SessionCell { return Info( id: id, position: Position.with(index, count: count), - leftAccessory: leftAccessory, + leadingAccessory: leadingAccessory, title: title, subtitle: subtitle, - rightAccessory: rightAccessory, + trailingAccessory: trailingAccessory, styling: styling, isEnabled: isEnabled, accessibility: accessibility, @@ -125,10 +126,10 @@ public extension SessionCell.Info { ) { self.id = id self.position = position - self.leftAccessory = accessory + self.leadingAccessory = accessory self.title = nil self.subtitle = nil - self.rightAccessory = nil + self.trailingAccessory = nil self.styling = styling self.isEnabled = isEnabled self.accessibility = accessibility @@ -137,13 +138,13 @@ public extension SessionCell.Info { self.onTapView = nil } - // leftAccessory, rightAccessory + // leadingAccessory, trailingAccessory init( id: ID, position: Position = .individual, - leftAccessory: SessionCell.Accessory, - rightAccessory: SessionCell.Accessory, + leadingAccessory: SessionCell.Accessory, + trailingAccessory: SessionCell.Accessory, styling: SessionCell.StyleInfo = SessionCell.StyleInfo(), isEnabled: Bool = true, accessibility: Accessibility? = nil, @@ -151,10 +152,10 @@ public extension SessionCell.Info { ) { self.id = id self.position = position - self.leftAccessory = leftAccessory + self.leadingAccessory = leadingAccessory self.title = nil self.subtitle = nil - self.rightAccessory = rightAccessory + self.trailingAccessory = trailingAccessory self.styling = styling self.isEnabled = isEnabled self.accessibility = accessibility @@ -168,9 +169,9 @@ public extension SessionCell.Info { init( id: ID, position: Position = .individual, - leftAccessory: SessionCell.Accessory? = nil, + leadingAccessory: SessionCell.Accessory? = nil, title: String, - rightAccessory: SessionCell.Accessory? = nil, + trailingAccessory: SessionCell.Accessory? = nil, styling: SessionCell.StyleInfo = SessionCell.StyleInfo(), isEnabled: Bool = true, accessibility: Accessibility? = nil, @@ -179,10 +180,10 @@ public extension SessionCell.Info { ) { self.id = id self.position = position - self.leftAccessory = leftAccessory + self.leadingAccessory = leadingAccessory self.title = SessionCell.TextInfo(title, font: .title) self.subtitle = nil - self.rightAccessory = rightAccessory + self.trailingAccessory = trailingAccessory self.styling = styling self.isEnabled = isEnabled self.accessibility = accessibility @@ -196,9 +197,9 @@ public extension SessionCell.Info { init( id: ID, position: Position = .individual, - leftAccessory: SessionCell.Accessory? = nil, + leadingAccessory: SessionCell.Accessory? = nil, title: SessionCell.TextInfo, - rightAccessory: SessionCell.Accessory? = nil, + trailingAccessory: SessionCell.Accessory? = nil, styling: SessionCell.StyleInfo = SessionCell.StyleInfo(), isEnabled: Bool = true, accessibility: Accessibility? = nil, @@ -207,10 +208,10 @@ public extension SessionCell.Info { ) { self.id = id self.position = position - self.leftAccessory = leftAccessory + self.leadingAccessory = leadingAccessory self.title = title self.subtitle = nil - self.rightAccessory = rightAccessory + self.trailingAccessory = trailingAccessory self.styling = styling self.isEnabled = isEnabled self.accessibility = accessibility @@ -224,10 +225,10 @@ public extension SessionCell.Info { init( id: ID, position: Position = .individual, - leftAccessory: SessionCell.Accessory? = nil, + leadingAccessory: SessionCell.Accessory? = nil, title: String, subtitle: String?, - rightAccessory: SessionCell.Accessory? = nil, + trailingAccessory: SessionCell.Accessory? = nil, styling: SessionCell.StyleInfo = SessionCell.StyleInfo(), isEnabled: Bool = true, accessibility: Accessibility? = nil, @@ -237,10 +238,10 @@ public extension SessionCell.Info { ) { self.id = id self.position = position - self.leftAccessory = leftAccessory + self.leadingAccessory = leadingAccessory self.title = SessionCell.TextInfo(title, font: .title) self.subtitle = SessionCell.TextInfo(subtitle, font: .subtitle) - self.rightAccessory = rightAccessory + self.trailingAccessory = trailingAccessory self.styling = styling self.isEnabled = isEnabled self.accessibility = accessibility diff --git a/Session/Shared/Types/SessionCell+Styling.swift b/Session/Shared/Types/SessionCell+Styling.swift index e7a4543884f..30f5e050197 100644 --- a/Session/Shared/Types/SessionCell+Styling.swift +++ b/Session/Shared/Types/SessionCell+Styling.swift @@ -18,6 +18,7 @@ public extension SessionCell { let textAlignment: NSTextAlignment let editingPlaceholder: String? let interaction: Interaction + let accessibility: Accessibility? let extraViewGenerator: (() -> UIView)? private let fontStyle: FontStyle @@ -29,6 +30,7 @@ public extension SessionCell { alignment: NSTextAlignment = .left, editingPlaceholder: String? = nil, interaction: Interaction = .none, + accessibility: Accessibility? = nil, extraViewGenerator: (() -> UIView)? = nil ) { self.text = text @@ -36,6 +38,7 @@ public extension SessionCell { self.textAlignment = alignment self.editingPlaceholder = editingPlaceholder self.interaction = interaction + self.accessibility = accessibility self.extraViewGenerator = extraViewGenerator } @@ -47,6 +50,7 @@ public extension SessionCell { textAlignment.hash(into: &hasher) interaction.hash(into: &hasher) editingPlaceholder.hash(into: &hasher) + accessibility.hash(into: &hasher) } public static func == (lhs: TextInfo, rhs: TextInfo) -> Bool { @@ -55,13 +59,15 @@ public extension SessionCell { lhs.fontStyle == rhs.fontStyle && lhs.textAlignment == rhs.textAlignment && lhs.interaction == rhs.interaction && - lhs.editingPlaceholder == rhs.editingPlaceholder + lhs.editingPlaceholder == rhs.editingPlaceholder && + lhs.accessibility == rhs.accessibility ) } } struct StyleInfo: Equatable, Hashable { let tintColor: ThemeValue + let subtitleTintColor: ThemeValue let alignment: SessionCell.Alignment let allowedSeparators: Separators let customPadding: Padding? @@ -69,12 +75,14 @@ public extension SessionCell { public init( tintColor: ThemeValue = .textPrimary, + subtitleTintColor: ThemeValue? = nil, alignment: SessionCell.Alignment = .leading, allowedSeparators: Separators = [.top, .bottom], customPadding: Padding? = nil, backgroundStyle: SessionCell.BackgroundStyle = .rounded ) { self.tintColor = tintColor + self.subtitleTintColor = (subtitleTintColor ?? tintColor) self.alignment = alignment self.allowedSeparators = allowedSeparators self.customPadding = customPadding @@ -121,6 +129,7 @@ public extension SessionCell { case rounded case edgeToEdge case noBackground + case noBackgroundEdgeToEdge } struct Separators: OptionSet, Equatable, Hashable { diff --git a/Session/Shared/Types/SessionTableSection.swift b/Session/Shared/Types/SessionTableSection.swift index a56118a3123..571f01f950f 100644 --- a/Session/Shared/Types/SessionTableSection.swift +++ b/Session/Shared/Types/SessionTableSection.swift @@ -1,6 +1,6 @@ // Copyright © 2022 Rangeproof Pty Ltd. All rights reserved. -import Foundation +import UIKit import DifferenceKit import SessionUIKit diff --git a/Session/Shared/UserListViewModel.swift b/Session/Shared/UserListViewModel.swift new file mode 100644 index 00000000000..3fbe3ada8f0 --- /dev/null +++ b/Session/Shared/UserListViewModel.swift @@ -0,0 +1,282 @@ +// Copyright © 2023 Rangeproof Pty Ltd. All rights reserved. + +import Foundation +import Combine +import GRDB +import YYImage +import DifferenceKit +import SessionUIKit +import SessionMessagingKit +import SessionUtilitiesKit +import SignalUtilitiesKit + +class UserListViewModel: SessionTableViewModel, NavigatableStateHolder, ObservableTableSource { + public let dependencies: Dependencies + public let navigatableState: NavigatableState = NavigatableState() + public let state: TableDataState = TableDataState() + public let observableState: ObservableTableSourceState = ObservableTableSourceState() + private let selectedUsersSubject: CurrentValueSubject>, Never> = CurrentValueSubject([]) + + public let title: String + public let infoBanner: InfoBanner.Info? + public let emptyState: String? + private let showProfileIcons: Bool + private let request: (any FetchRequest) + private let footerTitle: String? + private let footerAccessibility: Accessibility? + private let onTapAction: OnTapAction + private let onSubmitAction: OnSubmitAction + + // MARK: - Initialization + + init( + title: String, + infoBanner: InfoBanner.Info? = nil, + emptyState: String? = nil, + showProfileIcons: Bool, + request: (any FetchRequest), + footerTitle: String? = nil, + footerAccessibility: Accessibility? = nil, + onTap: OnTapAction = .radio, + onSubmit: OnSubmitAction = .none, + using dependencies: Dependencies + ) { + self.dependencies = dependencies + self.title = title + self.infoBanner = infoBanner + self.emptyState = emptyState + self.showProfileIcons = showProfileIcons + self.request = request + self.footerTitle = footerTitle + self.footerAccessibility = footerAccessibility + self.onTapAction = onTap + self.onSubmitAction = onSubmit + } + + // MARK: - Config + + public enum Section: SessionTableSection { + case users + } + + public enum TableItem: Equatable, Hashable, Differentiable { + case user(String) + } + + // MARK: - Content + + public indirect enum OnTapAction { + case none + case callback((UserListViewModel?, WithProfile) -> Void) + case radio + case conditionalAction(action: (WithProfile) -> OnTapAction) + case custom(trailingAccessory: (WithProfile) -> SessionCell.Accessory, onTap: (UserListViewModel?, WithProfile) -> Void) + } + + public enum OnSubmitAction { + case none + case callback((UserListViewModel?, Set>) throws -> Void) + case publisher((UserListViewModel?, Set>) -> AnyPublisher) + + var hasAction: Bool { + switch self { + case .none: return false + default: return true + } + } + } + + var bannerInfo: AnyPublisher { Just(infoBanner).eraseToAnyPublisher() } + var emptyStateTextPublisher: AnyPublisher { Just(emptyState).eraseToAnyPublisher() } + + lazy var observation: TargetObservation = ObservationBuilder + .databaseObservation(self) { [request, dependencies] db -> [WithProfile] in + try request.fetchAllWithProfiles(db, using: dependencies) + } + .map { [weak self, dependencies, showProfileIcons, onTapAction, selectedUsersSubject] (users: [WithProfile]) -> [SectionModel] in + let userSessionId: SessionId = dependencies[cache: .general].sessionId + + return [ + SectionModel( + model: .users, + elements: users + .sorted() + .map { userInfo -> SessionCell.Info in + func finalAction(for action: OnTapAction) -> OnTapAction { + switch action { + case .conditionalAction(let targetAction): + return finalAction(for: targetAction(userInfo)) + + default: return action + } + } + func generateAccessory(_ action: OnTapAction) -> SessionCell.Accessory? { + switch action { + case .none, .callback: return nil + case .custom(let accessoryGenerator, _): return accessoryGenerator(userInfo) + case .conditionalAction(let targetAction): + return generateAccessory(targetAction(userInfo)) + + case .radio: + return .radio( + isSelected: selectedUsersSubject.value.contains(where: { selectedUserInfo in + selectedUserInfo.profileId == userInfo.profileId + }) + ) + } + } + + let finalAction: OnTapAction = finalAction(for: onTapAction) + let trailingAccessory: SessionCell.Accessory? = generateAccessory(finalAction) + let title: String = { + guard userInfo.profileId != userSessionId.hexString else { return "you".localized() } + + return ( + userInfo.profile?.displayName() ?? + Profile.truncated(id: userInfo.profileId, truncating: .middle) + ) + }() + + return SessionCell.Info( + id: .user(userInfo.profileId), + leadingAccessory: .profile( + id: userInfo.profileId, + profile: userInfo.profile, + profileIcon: (showProfileIcons ? userInfo.value.profileIcon : .none) + ), + title: title, + subtitle: userInfo.itemDescription(using: dependencies), + trailingAccessory: trailingAccessory, + styling: SessionCell.StyleInfo( + subtitleTintColor: userInfo.itemDescriptionColor(using: dependencies), + allowedSeparators: [], + customPadding: SessionCell.Padding( + top: Values.smallSpacing, + bottom: Values.smallSpacing + ), + backgroundStyle: .noBackgroundEdgeToEdge + ), + accessibility: Accessibility( + identifier: "Contact", + label: title + ), + onTap: { + // Trigger any 'onTap' actions + switch finalAction { + case .none: return + case .callback(let callback): callback(self, userInfo) + case .custom(_, let callback): callback(self, userInfo) + case .radio: break + case .conditionalAction(_): return // Shouldn't hit this case + } + + // Only update the selection if the accessory is a 'radio' + guard trailingAccessory is SessionCell.AccessoryConfig.Radio else { return } + + // Toggle the selection + if !selectedUsersSubject.value.contains(userInfo) { + selectedUsersSubject.send(selectedUsersSubject.value.inserting(userInfo)) + } + else { + selectedUsersSubject.send(selectedUsersSubject.value.removing(userInfo)) + } + + // Force the table data to be refreshed (the database wouldn't have been changed) + self?.forceRefresh(type: .postDatabaseQuery) + } + ) + } + ) + ] + } + + lazy var footerButtonInfo: AnyPublisher = selectedUsersSubject + .prepend([]) + .map { [weak self, dependencies, footerTitle, footerAccessibility] selectedUsers -> SessionButton.Info? in + guard self?.onSubmitAction.hasAction == true, let title: String = footerTitle else { return nil } + + return SessionButton.Info( + style: .bordered, + title: title, + isEnabled: !selectedUsers.isEmpty, + accessibility: footerAccessibility, + onTap: { self?.submit(with: selectedUsers) } + ) + } + .eraseToAnyPublisher() + + // MARK: - Functions + + private func submit(with selectedUsers: Set>) { + switch onSubmitAction { + case .none: return + + case .callback(let submission): + do { + try submission(self, selectedUsers) + selectedUsersSubject.send([]) + forceRefresh() // Just in case the filter was impacted + } + catch { + transitionToScreen( + ConfirmationModal( + info: ConfirmationModal.Info( + title: "theError".localized(), + body: .text(error.localizedDescription), + cancelTitle: "okay".localized(), + cancelStyle: .alert_text + ) + ), + transitionType: .present + ) + } + + case .publisher(let submission): + transitionToScreen( + ModalActivityIndicatorViewController(canCancel: false) { [weak self, dependencies] modalActivityIndicator in + submission(self, selectedUsers) + .subscribe(on: DispatchQueue.global(qos: .userInitiated), using: dependencies) + .receive(on: DispatchQueue.main, using: dependencies) + .sinkUntilComplete( + receiveCompletion: { result in + switch result { + case .finished: + self?.selectedUsersSubject.send([]) + self?.forceRefresh() // Just in case the filter was impacted + modalActivityIndicator.dismiss(completion: {}) + + case .failure(let error): + modalActivityIndicator.dismiss(completion: { + self?.transitionToScreen( + ConfirmationModal( + info: ConfirmationModal.Info( + title: "theError".localized(), + body: .text(error.localizedDescription), + cancelTitle: "okay".localized(), + cancelStyle: .alert_text + ) + ), + transitionType: .present + ) + }) + } + } + ) + }, + transitionType: .present + ) + } + } +} + +// MARK: - UserListError + +public enum UserListError: LocalizedError { + case error(String) + + public var errorDescription: String? { + switch self { + case .error(let content): return content + } + } +} diff --git a/Session/Shared/UserSelectionVC.swift b/Session/Shared/UserSelectionVC.swift deleted file mode 100644 index 8e823ffbe1a..00000000000 --- a/Session/Shared/UserSelectionVC.swift +++ /dev/null @@ -1,103 +0,0 @@ -// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved. - -import UIKit -import SessionUIKit -import SessionMessagingKit - -final class UserSelectionVC: BaseVC, UITableViewDataSource, UITableViewDelegate { - private let navBarTitle: String - private let usersToExclude: Set - private let completion: (Set) -> Void - private var selectedUsers: Set = [] - - private lazy var users: [Profile] = { - return Profile - .fetchAllContactProfiles(excluding: usersToExclude) - }() - - // MARK: - Components - - @objc private lazy var tableView: UITableView = { - let result: UITableView = UITableView() - result.dataSource = self - result.delegate = self - result.separatorStyle = .none - result.themeBackgroundColor = .clear - result.showsVerticalScrollIndicator = false - result.alwaysBounceVertical = false - result.register(view: SessionCell.self) - - return result - }() - - // MARK: - Lifecycle - - init(with title: String, excluding usersToExclude: Set, completion: @escaping (Set) -> Void) { - self.navBarTitle = title - self.usersToExclude = usersToExclude - self.completion = completion - - super.init(nibName: nil, bundle: nil) - } - - required init?(coder: NSCoder) { preconditionFailure("Use init(excluding:) instead.") } - override init(nibName: String?, bundle: Bundle?) { preconditionFailure("Use init(excluding:) instead.") } - - override func viewDidLoad() { - super.viewDidLoad() - - setNavBarTitle(navBarTitle) - - let doneButton = UIBarButtonItem(barButtonSystemItem: .done, target: self, action: #selector(handleDoneButtonTapped)) - doneButton.accessibilityLabel = "Done" - navigationItem.rightBarButtonItem = doneButton - - view.addSubview(tableView) - tableView.pin(to: view) - } - - // MARK: - UITableViewDataSource - - func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { - return users.count - } - - func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { - let cell: SessionCell = tableView.dequeue(type: SessionCell.self, for: indexPath) - let profile: Profile = users[indexPath.row] - cell.update( - with: SessionCell.Info( - 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 - }), - styling: SessionCell.StyleInfo(backgroundStyle: .edgeToEdge), - accessibility: Accessibility(identifier: "Contact") - ) - ) - - return cell - } - - // MARK: - Interaction - - func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { - if !selectedUsers.contains(users[indexPath.row].id) { - selectedUsers.insert(users[indexPath.row].id) - } - else { - selectedUsers.remove(users[indexPath.row].id) - } - - tableView.deselectRow(at: indexPath, animated: true) - tableView.reloadRows(at: [indexPath], with: .none) - } - - @objc private func handleDoneButtonTapped() { - completion(selectedUsers) - navigationController!.popViewController(animated: true) - } -} diff --git a/Session/Dependencies/SRCopyableLabel.swift b/Session/Shared/Views/SRCopyableLabel.swift similarity index 100% rename from Session/Dependencies/SRCopyableLabel.swift rename to Session/Shared/Views/SRCopyableLabel.swift diff --git a/Session/Shared/Views/SessionCell+AccessoryView.swift b/Session/Shared/Views/SessionCell+AccessoryView.swift index 7f2a81d5aa0..ae67c78d7e9 100644 --- a/Session/Shared/Views/SessionCell+AccessoryView.swift +++ b/Session/Shared/Views/SessionCell+AccessoryView.swift @@ -46,7 +46,8 @@ 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.center(.horizontal, in: self), + radioBorderView.pin(.leading, to: .leading, of: self, withInset: Values.smallSpacing), + radioBorderView.pin(.trailing, to: .trailing, of: self, withInset: -Values.smallSpacing), radioBorderView.pin(.bottom, to: .bottom, of: self) ] private lazy var highlightingBackgroundLabelConstraints: [NSLayoutConstraint] = [ @@ -55,6 +56,14 @@ extension SessionCell { highlightingBackgroundLabel.pin(.trailing, to: .trailing, of: self, withInset: -Values.smallSpacing), highlightingBackgroundLabel.pin(.bottom, to: .bottom, of: self) ] + private lazy var highlightingBackgroundLabelAndRadioConstraints: [NSLayoutConstraint] = [ + highlightingBackgroundLabel.pin(.top, to: .top, of: self), + highlightingBackgroundLabel.pin(.leading, to: .leading, of: self, withInset: Values.smallSpacing), + highlightingBackgroundLabel.pin(.trailing, to: .leading, of: radioBorderView, withInset: -Values.smallSpacing), + highlightingBackgroundLabel.pin(.bottom, to: .bottom, of: self), + radioBorderView.center(.vertical, in: self), + radioBorderView.pin(.trailing, to: .trailing, of: self, withInset: -Values.smallSpacing), + ] private lazy var profilePictureViewConstraints: [NSLayoutConstraint] = [ profilePictureView.pin(.top, to: .top, of: self), profilePictureView.pin(.leading, to: .leading, of: self), @@ -269,6 +278,7 @@ extension SessionCell { radioBorderViewHeightConstraint.isActive = false radioBorderViewConstraints.forEach { $0.isActive = false } highlightingBackgroundLabelConstraints.forEach { $0.isActive = false } + highlightingBackgroundLabelAndRadioConstraints.forEach { $0.isActive = false } profilePictureViewConstraints.forEach { $0.isActive = false } searchBarConstraints.forEach { $0.isActive = false } buttonConstraints.forEach { $0.isActive = false } @@ -278,7 +288,8 @@ extension SessionCell { with accessory: Accessory?, tintColor: ThemeValue, isEnabled: Bool, - isManualReload: Bool + isManualReload: Bool, + using dependencies: Dependencies ) { guard let accessory: Accessory = accessory else { return } @@ -286,71 +297,88 @@ extension SessionCell { self.isHidden = false switch accessory { - 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) + // MARK: -- Icon + case let accessory as SessionCell.AccessoryConfig.Icon: + imageView.accessibilityIdentifier = accessory.accessibility?.identifier + imageView.accessibilityLabel = accessory.accessibility?.label + imageView.image = accessory.image + imageView.themeTintColor = (accessory.customTint ?? tintColor) + imageView.contentMode = (accessory.shouldFill ? .scaleAspectFill : .scaleAspectFit) imageView.isHidden = false - switch iconSize { + switch accessory.iconSize { case .fit: imageView.sizeToFit() - fixedWidthConstraint.constant = (imageView.bounds.width + (shouldFill ? 0 : (Values.smallSpacing * 2))) + fixedWidthConstraint.constant = (imageView.bounds.width + (accessory.shouldFill ? 0 : (Values.smallSpacing * 2))) fixedWidthConstraint.isActive = true imageViewWidthConstraint.constant = imageView.bounds.width imageViewHeightConstraint.constant = imageView.bounds.height + case .mediumAspectFill: + imageView.sizeToFit() + + imageViewWidthConstraint.constant = (imageView.bounds.width > imageView.bounds.height ? + (accessory.iconSize.size * (imageView.bounds.width / imageView.bounds.height)) : + accessory.iconSize.size + ) + imageViewHeightConstraint.constant = (imageView.bounds.width > imageView.bounds.height ? + accessory.iconSize.size : + (accessory.iconSize.size * (imageView.bounds.height / imageView.bounds.width)) + ) + fixedWidthConstraint.constant = imageViewWidthConstraint.constant + fixedWidthConstraint.isActive = true + default: - fixedWidthConstraint.isActive = (iconSize.size <= fixedWidthConstraint.constant) - imageViewWidthConstraint.constant = iconSize.size - imageViewHeightConstraint.constant = iconSize.size + fixedWidthConstraint.isActive = (accessory.iconSize.size <= fixedWidthConstraint.constant) + imageViewWidthConstraint.constant = accessory.iconSize.size + imageViewHeightConstraint.constant = accessory.iconSize.size } minWidthConstraint.isActive = !fixedWidthConstraint.isActive - imageViewLeadingConstraint.constant = (shouldFill ? 0 : Values.smallSpacing) - imageViewTrailingConstraint.constant = (shouldFill ? 0 : -Values.smallSpacing) + imageViewLeadingConstraint.constant = (accessory.shouldFill ? 0 : Values.smallSpacing) + imageViewTrailingConstraint.constant = (accessory.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 accessibility, let setter): - setter(imageView) - imageView.accessibilityIdentifier = accessibility?.identifier - imageView.accessibilityLabel = accessibility?.label - imageView.themeTintColor = (customTint ?? tintColor) - imageView.contentMode = (shouldFill ? .scaleAspectFill : .scaleAspectFit) + // MARK: -- IconAsync + case let accessory as SessionCell.AccessoryConfig.IconAsync: + accessory.setter(imageView) + imageView.accessibilityIdentifier = accessory.accessibility?.identifier + imageView.accessibilityLabel = accessory.accessibility?.label + imageView.themeTintColor = (accessory.customTint ?? tintColor) + imageView.contentMode = (accessory.shouldFill ? .scaleAspectFill : .scaleAspectFit) imageView.isHidden = false - switch iconSize { + switch accessory.iconSize { case .fit: imageView.sizeToFit() - fixedWidthConstraint.constant = (imageView.bounds.width + (shouldFill ? 0 : (Values.smallSpacing * 2))) + fixedWidthConstraint.constant = (imageView.bounds.width + (accessory.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 + fixedWidthConstraint.isActive = (accessory.iconSize.size <= fixedWidthConstraint.constant) + imageViewWidthConstraint.constant = accessory.iconSize.size + imageViewHeightConstraint.constant = accessory.iconSize.size } minWidthConstraint.isActive = !fixedWidthConstraint.isActive - imageViewLeadingConstraint.constant = (shouldFill ? 0 : Values.smallSpacing) - imageViewTrailingConstraint.constant = (shouldFill ? 0 : -Values.smallSpacing) + imageViewLeadingConstraint.constant = (accessory.shouldFill ? 0 : Values.smallSpacing) + imageViewTrailingConstraint.constant = (accessory.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, let accessibility): - toggleSwitch.accessibilityIdentifier = accessibility?.identifier - toggleSwitch.accessibilityLabel = accessibility?.label + // MARK: -- Toggle + case let accessory as SessionCell.AccessoryConfig.Toggle: + toggleSwitch.accessibilityIdentifier = accessory.accessibility?.identifier + toggleSwitch.accessibilityLabel = accessory.accessibility?.label toggleSwitch.isHidden = false toggleSwitch.isEnabled = isEnabled @@ -358,31 +386,33 @@ extension SessionCell { toggleSwitchConstraints.forEach { $0.isActive = true } if !isManualReload { - toggleSwitch.setOn(dataSource.oldBoolValue, animated: false) + toggleSwitch.setOn(accessory.oldValue, animated: false) // Dispatch so the cell reload doesn't conflict with the setting change animation - if dataSource.oldBoolValue != dataSource.currentBoolValue { + if accessory.oldValue != accessory.value { DispatchQueue.main.asyncAfter(deadline: .now() + .milliseconds(10)) { [weak toggleSwitch] in - toggleSwitch?.setOn(dataSource.currentBoolValue, animated: true) + toggleSwitch?.setOn(accessory.value, animated: true) } } } - case .dropDown(let dataSource, let accessibility): - dropDownLabel.accessibilityIdentifier = accessibility?.identifier - dropDownLabel.accessibilityLabel = accessibility?.label - dropDownLabel.text = dataSource.currentStringValue + // MARK: -- DropDown + case let accessory as SessionCell.AccessoryConfig.DropDown: + dropDownLabel.accessibilityIdentifier = accessory.accessibility?.identifier + dropDownLabel.accessibilityLabel = accessory.accessibility?.label + dropDownLabel.text = accessory.dynamicString() dropDownStackView.isHidden = false dropDownStackViewConstraints.forEach { $0.isActive = true } minWidthConstraint.isActive = true - case .radio(let size, let isSelectedRetriever, let storedSelection, let accessibility): - let isSelected: Bool = isSelectedRetriever() - let wasOldSelection: Bool = (!isSelected && storedSelection) + // MARK: -- Radio + case let accessory as SessionCell.AccessoryConfig.Radio: + let isSelected: Bool = accessory.liveIsSelected() + let wasOldSelection: Bool = (!isSelected && accessory.wasSavedSelection) radioBorderView.isAccessibilityElement = true - radioBorderView.accessibilityIdentifier = accessibility?.identifier - radioBorderView.accessibilityLabel = accessibility?.label + radioBorderView.accessibilityIdentifier = accessory.accessibility?.identifier + radioBorderView.accessibilityLabel = accessory.accessibility?.label if isSelected || wasOldSelection { radioBorderView.accessibilityTraits.insert(.selected) @@ -402,10 +432,10 @@ extension SessionCell { ) }() - radioBorderView.layer.cornerRadius = (size.borderSize / 2) + radioBorderView.layer.cornerRadius = (accessory.size.borderSize / 2) radioView.alpha = (wasOldSelection ? 0.3 : 1) - radioView.isHidden = (!isSelected && !storedSelection) + radioView.isHidden = (!isSelected && !accessory.wasSavedSelection) radioView.themeBackgroundColor = { guard isEnabled else { return (isSelected || wasOldSelection ? @@ -419,81 +449,143 @@ extension SessionCell { .radioButton_unselectedBackground ) }() - radioView.layer.cornerRadius = (size.selectionSize / 2) + radioView.layer.cornerRadius = (accessory.size.selectionSize / 2) - radioViewWidthConstraint.constant = size.selectionSize - radioViewHeightConstraint.constant = size.selectionSize - radioBorderViewWidthConstraint.constant = size.borderSize - radioBorderViewHeightConstraint.constant = size.borderSize + radioViewWidthConstraint.constant = accessory.size.selectionSize + radioViewHeightConstraint.constant = accessory.size.selectionSize + radioBorderViewWidthConstraint.constant = accessory.size.borderSize + radioBorderViewHeightConstraint.constant = accessory.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, let accessibility): - highlightingBackgroundLabel.accessibilityIdentifier = accessibility?.identifier - highlightingBackgroundLabel.accessibilityLabel = accessibility?.label - highlightingBackgroundLabel.text = title + // MARK: -- HighlightingBackgroundLabel + case let accessory as SessionCell.AccessoryConfig.HighlightingBackgroundLabel: + highlightingBackgroundLabel.isAccessibilityElement = (accessory.accessibility != nil) + highlightingBackgroundLabel.accessibilityIdentifier = accessory.accessibility?.identifier + highlightingBackgroundLabel.accessibilityLabel = accessory.accessibility?.label + highlightingBackgroundLabel.text = accessory.title highlightingBackgroundLabel.themeTextColor = tintColor highlightingBackgroundLabel.isHidden = false highlightingBackgroundLabelConstraints.forEach { $0.isActive = true } minWidthConstraint.isActive = true - case .profile( - let profileId, - let profileSize, - let threadVariant, - let customImageData, - let profile, - let profileIcon, - let additionalProfile, - let additionalProfileIcon, - let accessibility - ): + // MARK: -- HighlightingBackgroundLabelAndRadio + case let accessory as SessionCell.AccessoryConfig.HighlightingBackgroundLabelAndRadio: + let isSelected: Bool = accessory.liveIsSelected() + let wasOldSelection: Bool = (!isSelected && accessory.wasSavedSelection) + highlightingBackgroundLabel.isAccessibilityElement = (accessory.labelAccessibility != nil) + highlightingBackgroundLabel.accessibilityIdentifier = accessory.labelAccessibility?.identifier + highlightingBackgroundLabel.accessibilityLabel = accessory.labelAccessibility?.label + + radioBorderView.isAccessibilityElement = true + radioBorderView.accessibilityIdentifier = accessory.accessibility?.identifier + radioBorderView.accessibilityLabel = accessory.accessibility?.label + + if isSelected || wasOldSelection { + radioView.accessibilityTraits.insert(.selected) + radioView.accessibilityValue = "selected" + } else { + radioView.accessibilityTraits.remove(.selected) + radioView.accessibilityValue = nil + } + + highlightingBackgroundLabel.text = accessory.title + highlightingBackgroundLabel.themeTextColor = tintColor + highlightingBackgroundLabel.isHidden = false + radioBorderView.isHidden = false + radioBorderView.themeBorderColor = { + guard isEnabled else { return .radioButton_disabledBorder } + + return (isSelected ? + .radioButton_selectedBorder : + .radioButton_unselectedBorder + ) + }() + + radioBorderView.layer.cornerRadius = (accessory.size.borderSize / 2) + + radioView.alpha = (wasOldSelection ? 0.3 : 1) + radioView.isHidden = (!isSelected && !accessory.wasSavedSelection) + radioView.themeBackgroundColor = { + guard isEnabled else { + return (isSelected || wasOldSelection ? + .radioButton_disabledSelectedBackground : + .radioButton_disabledUnselectedBackground + ) + } + + return (isSelected || wasOldSelection ? + .radioButton_selectedBackground : + .radioButton_unselectedBackground + ) + }() + radioView.layer.cornerRadius = (accessory.size.selectionSize / 2) + + radioViewWidthConstraint.constant = accessory.size.selectionSize + radioViewHeightConstraint.constant = accessory.size.selectionSize + radioBorderViewWidthConstraint.constant = accessory.size.borderSize + radioBorderViewHeightConstraint.constant = accessory.size.borderSize + + radioViewWidthConstraint.isActive = true + radioViewHeightConstraint.isActive = true + radioBorderViewWidthConstraint.isActive = true + radioBorderViewHeightConstraint.isActive = true + highlightingBackgroundLabelAndRadioConstraints.forEach { $0.isActive = true } + minWidthConstraint.isActive = true + + // MARK: -- DisplayPicture + case let accessory as SessionCell.AccessoryConfig.DisplayPicture: // Note: We MUST set the 'size' property before triggering the 'update' // function or the profile picture won't layout correctly - profilePictureView.accessibilityIdentifier = accessibility?.identifier - profilePictureView.accessibilityLabel = accessibility?.label - profilePictureView.isAccessibilityElement = (accessibility != nil) - profilePictureView.size = profileSize + profilePictureView.accessibilityIdentifier = accessory.accessibility?.identifier + profilePictureView.accessibilityLabel = accessory.accessibility?.label + profilePictureView.isAccessibilityElement = (accessory.accessibility != nil) + profilePictureView.size = accessory.size profilePictureView.update( - publicKey: profileId, - threadVariant: threadVariant, - customImageData: customImageData, - profile: profile, - profileIcon: profileIcon, - additionalProfile: additionalProfile, - additionalProfileIcon: additionalProfileIcon + publicKey: accessory.id, + threadVariant: accessory.threadVariant, + displayPictureFilename: accessory.displayPictureFilename, + profile: accessory.profile, + profileIcon: accessory.profileIcon, + additionalProfile: accessory.additionalProfile, + additionalProfileIcon: accessory.additionalProfileIcon, + using: dependencies ) profilePictureView.isHidden = false - fixedWidthConstraint.constant = profileSize.viewSize + fixedWidthConstraint.constant = accessory.size.viewSize fixedWidthConstraint.isActive = true profilePictureViewConstraints.forEach { $0.isActive = true } - case .search(let placeholder, let accessibility, let searchTermChanged): - self.searchTermChanged = searchTermChanged - searchBar.accessibilityIdentifier = accessibility?.identifier - searchBar.accessibilityLabel = accessibility?.label - searchBar.placeholder = placeholder + // MARK: -- Search + case let accessory as SessionCell.AccessoryConfig.Search: + self.searchTermChanged = accessory.searchTermChanged + searchBar.accessibilityIdentifier = accessory.accessibility?.identifier + searchBar.accessibilityLabel = accessory.accessibility?.label + searchBar.placeholder = accessory.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) + // MARK: -- Button + case let accessory as SessionCell.AccessoryConfig.Button: + self.onTap = accessory.run + button.accessibilityIdentifier = accessory.accessibility?.identifier + button.accessibilityLabel = accessory.accessibility?.label + button.setTitle(accessory.title, for: .normal) + button.style = accessory.style button.isHidden = false minWidthConstraint.isActive = true buttonConstraints.forEach { $0.isActive = true } - case .customView(_, let viewGenerator): - let generatedView: UIView = viewGenerator() + // MARK: -- CustomView + case let accessory as SessionCell.AccessoryConfig.CustomView: + let generatedView: UIView = accessory.viewGenerator() + generatedView.accessibilityIdentifier = accessory.accessibility?.identifier + generatedView.accessibilityLabel = accessory.accessibility?.label addSubview(generatedView) generatedView.pin(.top, to: .top, of: self) @@ -504,6 +596,9 @@ extension SessionCell { customView?.removeFromSuperview() // Just in case customView = generatedView minWidthConstraint.isActive = true + + // If we get an unknown case then just hide again + default: self.isHidden = true } } diff --git a/Session/Shared/Views/SessionCell.swift b/Session/Shared/Views/SessionCell.swift index aa70661f84e..142f809a6c1 100644 --- a/Session/Shared/Views/SessionCell.swift +++ b/Session/Shared/Views/SessionCell.swift @@ -12,6 +12,7 @@ public class SessionCell: UITableViewCell { private var isEditingTitle = false public private(set) var interactionMode: SessionCell.TextInfo.Interaction = .none + public var lastTouchLocation: UITouch? private var shouldHighlightTitle: Bool = true private var originalInputValue: String? private var titleExtraView: UIView? @@ -22,22 +23,23 @@ public class SessionCell: UITableViewCell { private var backgroundLeftConstraint: NSLayoutConstraint = NSLayoutConstraint() private var backgroundRightConstraint: NSLayoutConstraint = NSLayoutConstraint() - private var topSeparatorLeftConstraint: NSLayoutConstraint = NSLayoutConstraint() - private var topSeparatorRightConstraint: NSLayoutConstraint = NSLayoutConstraint() - private var botSeparatorLeftConstraint: NSLayoutConstraint = NSLayoutConstraint() - private var botSeparatorRightConstraint: NSLayoutConstraint = NSLayoutConstraint() + private var topSeparatorLeadingConstraint: NSLayoutConstraint = NSLayoutConstraint() + private var topSeparatorTrailingConstraint: NSLayoutConstraint = NSLayoutConstraint() + private var botSeparatorLeadingConstraint: NSLayoutConstraint = NSLayoutConstraint() + private var botSeparatorTrailingConstraint: 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 contentStackViewWidthConstraint: NSLayoutConstraint = contentStackView.set(.width, lessThanOrEqualTo: .width, of: cellBackgroundView) + private lazy var leadingAccessoryFillConstraint: NSLayoutConstraint = contentStackView.set(.height, to: .height, of: leadingAccessoryView) 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 lazy var trailingAccessoryFillConstraint: NSLayoutConstraint = contentStackView.set(.height, to: .height, of: trailingAccessoryView) + private lazy var accessoryWidthMatchConstraint: NSLayoutConstraint = leadingAccessoryView.set(.width, to: .width, of: trailingAccessoryView) private let cellBackgroundView: UIView = { let result: UIView = UIView() @@ -76,7 +78,7 @@ public class SessionCell: UITableViewCell { return result }() - public let leftAccessoryView: AccessoryView = { + public let leadingAccessoryView: AccessoryView = { let result: AccessoryView = AccessoryView() result.isHidden = true @@ -132,7 +134,7 @@ public class SessionCell: UITableViewCell { return result }() - public let rightAccessoryView: AccessoryView = { + public let trailingAccessoryView: AccessoryView = { let result: AccessoryView = AccessoryView() result.isHidden = true @@ -171,9 +173,9 @@ public class SessionCell: UITableViewCell { cellBackgroundView.addSubview(contentStackView) cellBackgroundView.addSubview(botSeparator) - contentStackView.addArrangedSubview(leftAccessoryView) + contentStackView.addArrangedSubview(leadingAccessoryView) contentStackView.addArrangedSubview(titleStackView) - contentStackView.addArrangedSubview(rightAccessoryView) + contentStackView.addArrangedSubview(trailingAccessoryView) titleStackView.addArrangedSubview(titleLabel) titleStackView.addArrangedSubview(subtitleLabel) @@ -192,16 +194,16 @@ public class SessionCell: UITableViewCell { cellSelectedBackgroundView.pin(to: cellBackgroundView) topSeparator.pin(.top, to: .top, of: cellBackgroundView) - topSeparatorLeftConstraint = topSeparator.pin(.left, to: .left, of: cellBackgroundView) - topSeparatorRightConstraint = topSeparator.pin(.right, to: .right, of: cellBackgroundView) + topSeparatorLeadingConstraint = topSeparator.pin(.leading, to: .leading, of: cellBackgroundView) + topSeparatorTrailingConstraint = topSeparator.pin(.trailing, to: .trailing, of: 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) + botSeparatorLeadingConstraint = botSeparator.pin(.leading, to: .leading, of: cellBackgroundView) + botSeparatorTrailingConstraint = botSeparator.pin(.trailing, to: .trailing, of: cellBackgroundView) botSeparator.pin(.bottom, to: .bottom, of: cellBackgroundView) // Explicitly call this to ensure we have initialised the constraints before we initially @@ -293,10 +295,11 @@ public class SessionCell: UITableViewCell { contentStackViewLeadingConstraint.isActive = false contentStackViewTrailingConstraint.isActive = false contentStackViewHorizontalCenterConstraint.isActive = false + contentStackViewWidthConstraint.isActive = false titleMinHeightConstraint.isActive = false - leftAccessoryView.prepareForReuse() - leftAccessoryView.alpha = 1 - leftAccessoryFillConstraint.isActive = false + leadingAccessoryView.prepareForReuse() + leadingAccessoryView.alpha = 1 + leadingAccessoryFillConstraint.isActive = false titleLabel.text = "" titleLabel.themeTextColor = .textPrimary titleLabel.alpha = 1 @@ -306,11 +309,11 @@ public class SessionCell: UITableViewCell { titleTextField.isHidden = true titleTextField.alpha = 0 subtitleLabel.isUserInteractionEnabled = false - subtitleLabel.text = "" + subtitleLabel.attributedText = nil subtitleLabel.themeTextColor = .textPrimary - rightAccessoryView.prepareForReuse() - rightAccessoryView.alpha = 1 - rightAccessoryFillConstraint.isActive = false + trailingAccessoryView.prepareForReuse() + trailingAccessoryView.alpha = 1 + trailingAccessoryFillConstraint.isActive = false accessoryWidthMatchConstraint.isActive = false topSeparator.isHidden = true @@ -318,27 +321,32 @@ public class SessionCell: UITableViewCell { botSeparator.isHidden = true } - public func update(with info: Info, isManualReload: Bool = false) { + public func update( + with info: Info, + isManualReload: Bool = false, + using dependencies: Dependencies + ) { 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 - isAccessibilityElement = true + isAccessibilityElement = (info.accessibility != nil) originalInputValue = info.title?.text // Convenience Flags - let leftFitToEdge: Bool = (info.leftAccessory?.shouldFitToEdge == true) - let rightFitToEdge: Bool = (!leftFitToEdge && info.rightAccessory?.shouldFitToEdge == true) + let leadingFitToEdge: Bool = (info.leadingAccessory?.shouldFitToEdge == true) + let trailingFitToEdge: Bool = (!leadingFitToEdge && info.trailingAccessory?.shouldFitToEdge == true) // Content contentStackView.spacing = (info.styling.customPadding?.interItem ?? Values.mediumSpacing) - leftAccessoryView.update( - with: info.leftAccessory, + leadingAccessoryView.update( + with: info.leadingAccessory, tintColor: info.styling.tintColor, isEnabled: info.isEnabled, - isManualReload: isManualReload + isManualReload: isManualReload, + using: dependencies ) titleStackView.isHidden = (info.title == nil && info.subtitle == nil) titleLabel.isUserInteractionEnabled = (info.title?.interaction == .copy) @@ -346,44 +354,53 @@ public class SessionCell: UITableViewCell { titleLabel.text = info.title?.text titleLabel.themeTextColor = info.styling.tintColor titleLabel.textAlignment = (info.title?.textAlignment ?? .left) + titleLabel.accessibilityIdentifier = info.title?.accessibility?.identifier + titleLabel.accessibilityLabel = info.title?.accessibility?.label 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) - titleTextField.accessibilityIdentifier = info.accessibility?.identifier - titleTextField.accessibilityLabel = info.accessibility?.label + titleTextField.accessibilityIdentifier = info.title?.accessibility?.identifier + titleTextField.accessibilityLabel = info.title?.accessibility?.label subtitleLabel.isUserInteractionEnabled = (info.subtitle?.interaction == .copy) subtitleLabel.font = info.subtitle?.font - subtitleLabel.text = info.subtitle?.text - subtitleLabel.themeTextColor = info.styling.tintColor + subtitleLabel.attributedText = info.subtitle.map { subtitle -> NSAttributedString? in + NSAttributedString(stringWithHTMLTags: subtitle.text, font: subtitle.font) + } + subtitleLabel.themeTextColor = info.styling.subtitleTintColor subtitleLabel.textAlignment = (info.subtitle?.textAlignment ?? .left) + subtitleLabel.accessibilityIdentifier = info.subtitle?.accessibility?.identifier + subtitleLabel.accessibilityLabel = info.subtitle?.accessibility?.label subtitleLabel.isHidden = (info.subtitle == nil) - rightAccessoryView.update( - with: info.rightAccessory, + trailingAccessoryView.update( + with: info.trailingAccessory, tintColor: info.styling.tintColor, isEnabled: info.isEnabled, - isManualReload: isManualReload + isManualReload: isManualReload, + using: dependencies ) 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 + contentStackViewWidthConstraint.constant = -(abs((info.styling.customPadding?.leading ?? 0) + (info.styling.customPadding?.trailing ?? 0)) * 2) // Double the center offset to keep within bounds + contentStackViewWidthConstraint.isActive = (info.styling.alignment == .centerHugging) + leadingAccessoryFillConstraint.isActive = leadingFitToEdge + trailingAccessoryFillConstraint.isActive = trailingFitToEdge accessoryWidthMatchConstraint.isActive = { - switch (info.leftAccessory, info.rightAccessory) { - case (.button, .button): return true + switch (info.leadingAccessory, info.trailingAccessory) { + case is (SessionCell.AccessoryConfig.Button, SessionCell.AccessoryConfig.Button): return true default: return false } }() titleLabel.setContentHuggingPriority( - (info.rightAccessory != nil ? .defaultLow : .required), + (info.trailingAccessory != nil ? .defaultLow : .required), for: .horizontal ) titleLabel.setContentCompressionResistancePriority( - (info.rightAccessory != nil ? .defaultLow : .required), + (info.trailingAccessory != nil ? .defaultLow : .required), for: .horizontal ) contentStackViewTopConstraint.constant = { @@ -391,38 +408,38 @@ public class SessionCell: UITableViewCell { return customPadding } - return (leftFitToEdge || rightFitToEdge ? 0 : Values.mediumSpacing) + return (leadingFitToEdge || trailingFitToEdge ? 0 : Values.mediumSpacing) }() contentStackViewLeadingConstraint.constant = { if let customPadding: CGFloat = info.styling.customPadding?.leading { return customPadding } - return (leftFitToEdge ? 0 : Values.mediumSpacing) + return (leadingFitToEdge ? 0 : Values.mediumSpacing) }() contentStackViewTrailingConstraint.constant = { if let customPadding: CGFloat = info.styling.customPadding?.trailing { return -customPadding } - return -(rightFitToEdge ? 0 : Values.mediumSpacing) + return -(trailingFitToEdge ? 0 : Values.mediumSpacing) }() contentStackViewBottomConstraint.constant = { if let customPadding: CGFloat = info.styling.customPadding?.bottom { return -customPadding } - return -(leftFitToEdge || rightFitToEdge ? 0 : Values.mediumSpacing) + return -(leadingFitToEdge || trailingFitToEdge ? 0 : Values.mediumSpacing) }() titleTextFieldLeadingConstraint.constant = { guard info.styling.backgroundStyle != .noBackground else { return 0 } - return (leftFitToEdge ? 0 : Values.mediumSpacing) + return (leadingFitToEdge ? 0 : Values.mediumSpacing) }() titleTextFieldTrailingConstraint.constant = { guard info.styling.backgroundStyle != .noBackground else { return 0 } - return -(rightFitToEdge ? 0 : Values.mediumSpacing) + return -(trailingFitToEdge ? 0 : Values.mediumSpacing) }() // Styling and positioning @@ -454,57 +471,72 @@ public class SessionCell: UITableViewCell { cellBackgroundView.themeBackgroundColor = nil cellBackgroundView.layer.cornerRadius = 0 cellSelectedBackgroundView.isHidden = true + + case .noBackgroundEdgeToEdge: + defaultEdgePadding = 0 + backgroundLeftConstraint.constant = 0 + backgroundRightConstraint.constant = 0 + 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, _, _, _, _): - return iconSize.size - + case let accessory as SessionCell.AccessoryConfig.Icon: return accessory.iconSize.size + case let accessory as SessionCell.AccessoryConfig.IconAsync: return accessory.iconSize.size default: return defaultEdgePadding } } - guard leftFitToEdge else { - guard rightFitToEdge else { return defaultEdgePadding } + guard leadingFitToEdge else { + guard trailingFitToEdge else { return defaultEdgePadding } - return targetSize(accessory: info.rightAccessory) + return targetSize(accessory: info.trailingAccessory) } - return targetSize(accessory: info.leftAccessory) + return targetSize(accessory: info.leadingAccessory) }() - topSeparatorLeftConstraint.constant = (leftFitToEdge ? fittedEdgePadding : defaultEdgePadding) - topSeparatorRightConstraint.constant = (rightFitToEdge ? -fittedEdgePadding : -defaultEdgePadding) - botSeparatorLeftConstraint.constant = (leftFitToEdge ? fittedEdgePadding : defaultEdgePadding) - botSeparatorRightConstraint.constant = (rightFitToEdge ? -fittedEdgePadding : -defaultEdgePadding) + topSeparatorLeadingConstraint.constant = (leadingFitToEdge ? fittedEdgePadding : defaultEdgePadding) + topSeparatorTrailingConstraint.constant = (trailingFitToEdge ? -fittedEdgePadding : -defaultEdgePadding) + botSeparatorLeadingConstraint.constant = (leadingFitToEdge ? fittedEdgePadding : defaultEdgePadding) + botSeparatorTrailingConstraint.constant = (trailingFitToEdge ? -fittedEdgePadding : -defaultEdgePadding) switch info.position { case .top: cellBackgroundView.layer.maskedCorners = [.layerMinXMinYCorner, .layerMaxXMinYCorner] topSeparator.isHidden = ( - !info.styling.allowedSeparators.contains(.top) || - info.styling.backgroundStyle != .edgeToEdge + !info.styling.allowedSeparators.contains(.top) || ( + info.styling.backgroundStyle != .edgeToEdge && + info.styling.backgroundStyle != .noBackgroundEdgeToEdge + ) ) botSeparator.isHidden = ( - !info.styling.allowedSeparators.contains(.bottom) || - info.styling.backgroundStyle == .noBackground + !info.styling.allowedSeparators.contains(.bottom) || ( + info.styling.backgroundStyle == .noBackground && + info.styling.backgroundStyle != .noBackgroundEdgeToEdge + ) ) case .middle: cellBackgroundView.layer.maskedCorners = [] topSeparator.isHidden = true botSeparator.isHidden = ( - !info.styling.allowedSeparators.contains(.bottom) || - info.styling.backgroundStyle == .noBackground + !info.styling.allowedSeparators.contains(.bottom) || ( + info.styling.backgroundStyle == .noBackground && + info.styling.backgroundStyle != .noBackgroundEdgeToEdge + ) ) case .bottom: cellBackgroundView.layer.maskedCorners = [.layerMinXMaxYCorner, .layerMaxXMaxYCorner] topSeparator.isHidden = true botSeparator.isHidden = ( - !info.styling.allowedSeparators.contains(.bottom) || - info.styling.backgroundStyle != .edgeToEdge + !info.styling.allowedSeparators.contains(.bottom) || ( + info.styling.backgroundStyle != .edgeToEdge && + info.styling.backgroundStyle != .noBackgroundEdgeToEdge + ) ) case .individual: @@ -513,12 +545,16 @@ public class SessionCell: UITableViewCell { .layerMinXMaxYCorner, .layerMaxXMaxYCorner ] topSeparator.isHidden = ( - !info.styling.allowedSeparators.contains(.top) || - info.styling.backgroundStyle != .edgeToEdge + !info.styling.allowedSeparators.contains(.top) || ( + info.styling.backgroundStyle != .edgeToEdge && + info.styling.backgroundStyle != .noBackgroundEdgeToEdge + ) ) botSeparator.isHidden = ( - !info.styling.allowedSeparators.contains(.bottom) || - info.styling.backgroundStyle != .edgeToEdge + !info.styling.allowedSeparators.contains(.bottom) || ( + info.styling.backgroundStyle != .edgeToEdge && + info.styling.backgroundStyle != .noBackgroundEdgeToEdge + ) ) } } @@ -533,8 +569,8 @@ public class SessionCell: UITableViewCell { 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?.leadingAccessoryView.alpha = (isEditing ? 0 : 1) + self?.trailingAccessoryView.alpha = (isEditing ? 0 : 1) self?.titleMinHeightConstraint.isActive = isEditing } let completion: (Bool) -> Void = { [weak self] complete in @@ -579,15 +615,21 @@ public class SessionCell: UITableViewCell { } cellSelectedBackgroundView.alpha = (highlighted ? 1 : 0) - leftAccessoryView.setHighlighted(highlighted, animated: animated) - rightAccessoryView.setHighlighted(highlighted, animated: animated) + leadingAccessoryView.setHighlighted(highlighted, animated: animated) + trailingAccessoryView.setHighlighted(highlighted, animated: animated) } public override func setSelected(_ selected: Bool, animated: Bool) { super.setSelected(selected, animated: animated) - leftAccessoryView.setSelected(selected, animated: animated) - rightAccessoryView.setSelected(selected, animated: animated) + leadingAccessoryView.setSelected(selected, animated: animated) + trailingAccessoryView.setSelected(selected, animated: animated) + } + + public override func touchesEnded(_ touches: Set, with event: UIEvent?) { + super.touchesEnded(touches, with: event) + + lastTouchLocation = touches.first } } diff --git a/Session/Shared/Views/SessionFooterView.swift b/Session/Shared/Views/SessionFooterView.swift index b1f2304055e..ce4c09f9e77 100644 --- a/Session/Shared/Views/SessionFooterView.swift +++ b/Session/Shared/Views/SessionFooterView.swift @@ -69,7 +69,7 @@ class SessionFooterView: UITableViewHeaderFooterView { // Align to the start of the text in the cell return (Values.largeSpacing + Values.mediumSpacing) - case .edgeToEdge, .noBackground: return Values.largeSpacing + case .edgeToEdge, .noBackground, .noBackgroundEdgeToEdge: return Values.largeSpacing } }() diff --git a/Session/Utilities/BackgroundPoller.swift b/Session/Utilities/BackgroundPoller.swift index c38be763e6f..9def82740e1 100644 --- a/Session/Utilities/BackgroundPoller.swift +++ b/Session/Utilities/BackgroundPoller.swift @@ -1,4 +1,6 @@ // Copyright © 2022 Rangeproof Pty Ltd. All rights reserved. +// +// stringlint:disable import Foundation import Combine @@ -7,22 +9,26 @@ import SessionSnodeKit import SessionMessagingKit import SessionUtilitiesKit +// MARK: - Log.Category + +public extension Log.Category { + static let backgroundPoller: Log.Category = .create("BackgroundPoller", defaultLevel: .info) +} + +// MARK: - BackgroundPoller + public final class BackgroundPoller { - let currentUserPoller: CurrentUserPoller = CurrentUserPoller() - var groupPollers: [Poller] = [] - var communityPollers: [OpenGroupAPI.Poller] = [] - public func poll(using dependencies: Dependencies) -> AnyPublisher { let pollStart: TimeInterval = dependencies.dateNow.timeIntervalSince1970 - return dependencies.storage - .readPublisher(using: dependencies) { db -> (Set, Set) in + return dependencies[singleton: .storage] + .readPublisher { db -> (Set, Set) in ( try ClosedGroup .select(.threadId) .joining( required: ClosedGroup.members - .filter(GroupMember.Columns.profileId == getUserHexEncodedPublicKey(db)) + .filter(GroupMember.Columns.profileId == dependencies[cache: .general].sessionId.hexString) ) .asRequest(of: String.self) .fetchSet(db), @@ -36,7 +42,7 @@ public final class BackgroundPoller { .filter( OpenGroup.Columns.roomToken != "" && OpenGroup.Columns.isActive && - OpenGroup.Columns.pollFailureCount < OpenGroupAPI.Poller.maxRoomFailureCountForBackgroundPoll + OpenGroup.Columns.pollFailureCount < CommunityPoller.maxRoomFailureCountForBackgroundPoll ) .distinct() .asRequest(of: String.self) @@ -46,22 +52,41 @@ public final class BackgroundPoller { .catch { _ in Just(([], [])).eraseToAnyPublisher() } .handleEvents( receiveOutput: { groupIds, servers in - Log.info("[BackgroundPoller] Fetching Users: 1, Groups: \(groupIds.count), Communities: \(servers.count).") + Log.info(.backgroundPoller, "Fetching Users: 1, Groups: \(groupIds.count), Communities: \(servers.count).") } ) - .map { [weak self] groupIds, servers -> ([(Poller, String)], [(OpenGroupAPI.Poller, String)]) in - let groupPollerInfo: [(Poller, String)] = groupIds.map { (ClosedGroupPoller(), $0) } - let communityPollerInfo: [(OpenGroupAPI.Poller, String)] = servers.map { (OpenGroupAPI.Poller(for: $0), $0) } - self?.groupPollers = groupPollerInfo.map { poller, _ in poller } - self?.communityPollers = communityPollerInfo.map { poller, _ in poller } + .map { groupIds, servers -> ([GroupPoller], [CommunityPoller]) in + let groupPollers: [GroupPoller] = groupIds.map { groupId in + GroupPoller( + pollerName: "Background Group poller for: \(groupId)", // stringlint:ignore + pollerQueue: DispatchQueue.main, + pollerDestination: .swarm(groupId), + pollerDrainBehaviour: .alwaysRandom, + namespaces: GroupPoller.namespaces(swarmPublicKey: groupId), + shouldStoreMessages: true, + logStartAndStopCalls: false, + using: dependencies + ) + } + let communityPollers: [CommunityPoller] = servers.map { server in + CommunityPoller( + pollerName: "Background Community poller for: \(server)", // stringlint:ignore + pollerQueue: DispatchQueue.main, + pollerDestination: .server(server), + failureCount: 0, + shouldStoreMessages: true, + logStartAndStopCalls: false, + using: dependencies + ) + } - return (groupPollerInfo, communityPollerInfo) + return (groupPollers, communityPollers) } - .flatMap { groupPollerInfo, communityPollerInfo in + .flatMap { groupPollers, communityPollers in Publishers.MergeMany( [BackgroundPoller.pollUserMessages(using: dependencies)] - .appending(contentsOf: BackgroundPoller.poll(pollerInfo: groupPollerInfo, using: dependencies)) - .appending(contentsOf: BackgroundPoller.poll(pollerInfo: communityPollerInfo, using: dependencies)) + .appending(contentsOf: BackgroundPoller.poll(pollers: groupPollers, using: dependencies)) + .appending(contentsOf: BackgroundPoller.poll(pollerInfo: communityPollers, using: dependencies)) ) } .collect() @@ -70,7 +95,7 @@ public final class BackgroundPoller { receiveOutput: { _ in let endTime: TimeInterval = dependencies.dateNow.timeIntervalSince1970 let duration: TimeUnit = .seconds(endTime - pollStart) - Log.info("[BackgroundPoller] Finished polling after \(duration, unit: .s).") + Log.info(.backgroundPoller, "Finished polling after \(duration, unit: .s).") } ) .eraseToAnyPublisher() @@ -79,100 +104,96 @@ public final class BackgroundPoller { private static func pollUserMessages( using dependencies: Dependencies ) -> AnyPublisher { - let userPublicKey: String = getUserHexEncodedPublicKey(using: dependencies) - - let poller: Poller = CurrentUserPoller() - let pollerName: String = poller.pollerName(for: userPublicKey) - let pollStart: TimeInterval = dependencies.dateNow.timeIntervalSince1970 - - return poller.pollFromBackground( + let poller: CurrentUserPoller = CurrentUserPoller( + pollerName: "Background Main Poller", + pollerQueue: DispatchQueue.main, + pollerDestination: .swarm(dependencies[cache: .general].sessionId.hexString), + pollerDrainBehaviour: .limitedReuse(count: 6), namespaces: CurrentUserPoller.namespaces, - for: userPublicKey, - drainBehaviour: .alwaysRandom, + shouldStoreMessages: true, + logStartAndStopCalls: false, using: dependencies ) - .handleEvents( - receiveOutput: { _, _, validMessageCount, _ in - let endTime: TimeInterval = dependencies.dateNow.timeIntervalSince1970 - let duration: TimeUnit = .seconds(endTime - pollStart) - Log.info("[BackgroundPoller] \(pollerName) received \(validMessageCount) valid message(s) after \(duration, unit: .s).") - }, - receiveCompletion: { result in - switch result { - case .finished: break - case .failure(let error): - let endTime: TimeInterval = dependencies.dateNow.timeIntervalSince1970 - let duration: TimeUnit = .seconds(endTime - pollStart) - Log.error("[BackgroundPoller] \(pollerName) failed after \(duration, unit: .s) due to error: \(error).") - } - } - ) - .map { _ in () } - .catch { _ in Just(()).eraseToAnyPublisher() } - .eraseToAnyPublisher() - } - - private static func poll( - pollerInfo: [(poller: Poller, groupPublicKey: String)], - using dependencies: Dependencies - ) -> [AnyPublisher] { - // Fetch all closed groups (excluding any don't contain the current user as a - // GroupMemeber as the user is no longer a member of those) - return pollerInfo.map { poller, groupPublicKey in - let pollerName: String = poller.pollerName(for: groupPublicKey) - let pollStart: TimeInterval = dependencies.dateNow.timeIntervalSince1970 - - return poller.pollFromBackground( - namespaces: ClosedGroupPoller.namespaces, - for: groupPublicKey, - drainBehaviour: .alwaysRandom, - using: dependencies - ) + let pollStart: TimeInterval = dependencies.dateNow.timeIntervalSince1970 + + return poller + .pollFromBackground() .handleEvents( - receiveOutput: { _, _, validMessageCount, _ in + receiveOutput: { [pollerName = poller.pollerName] _, _, validMessageCount, _ in let endTime: TimeInterval = dependencies.dateNow.timeIntervalSince1970 let duration: TimeUnit = .seconds(endTime - pollStart) - Log.info("[BackgroundPoller] \(pollerName) received \(validMessageCount) valid message(s) after \(duration, unit: .s).") + Log.info(.backgroundPoller, "\(pollerName) received \(validMessageCount) valid message(s) after \(duration, unit: .s).") }, - receiveCompletion: { result in + receiveCompletion: { [pollerName = poller.pollerName] result in switch result { case .finished: break case .failure(let error): let endTime: TimeInterval = dependencies.dateNow.timeIntervalSince1970 let duration: TimeUnit = .seconds(endTime - pollStart) - Log.error("[BackgroundPoller] \(pollerName) failed after \(duration, unit: .s) due to error: \(error).") + Log.error(.backgroundPoller, "\(pollerName) failed after \(duration, unit: .s) due to error: \(error).") } } ) .map { _ in () } .catch { _ in Just(()).eraseToAnyPublisher() } .eraseToAnyPublisher() + } + + private static func poll( + pollers: [GroupPoller], + using dependencies: Dependencies + ) -> [AnyPublisher] { + // Fetch all closed groups (excluding any don't contain the current user as a + // GroupMemeber as the user is no longer a member of those) + return pollers.map { poller in + let pollStart: TimeInterval = dependencies.dateNow.timeIntervalSince1970 + + return poller + .pollFromBackground() + .handleEvents( + receiveOutput: { [pollerName = poller.pollerName] _, _, validMessageCount, _ in + let endTime: TimeInterval = dependencies.dateNow.timeIntervalSince1970 + let duration: TimeUnit = .seconds(endTime - pollStart) + Log.info(.backgroundPoller, "\(pollerName) received \(validMessageCount) valid message(s) after \(duration, unit: .s).") + }, + receiveCompletion: { [pollerName = poller.pollerName] result in + switch result { + case .finished: break + case .failure(let error): + let endTime: TimeInterval = dependencies.dateNow.timeIntervalSince1970 + let duration: TimeUnit = .seconds(endTime - pollStart) + Log.error(.backgroundPoller, "\(pollerName) failed after \(duration, unit: .s) due to error: \(error).") + } + } + ) + .map { _ in () } + .catch { _ in Just(()).eraseToAnyPublisher() } + .eraseToAnyPublisher() } } private static func poll( - pollerInfo: [(poller: OpenGroupAPI.Poller, server: String)], + pollerInfo: [CommunityPoller], using dependencies: Dependencies ) -> [AnyPublisher] { - return pollerInfo.map { poller, server -> AnyPublisher in - let pollerName: String = "Community poller for server: \(server)" // stringlint:ignore + return pollerInfo.map { poller -> AnyPublisher in let pollStart: TimeInterval = dependencies.dateNow.timeIntervalSince1970 return poller - .pollFromBackground(using: dependencies) + .pollFromBackground() .handleEvents( - receiveOutput: { _ in + receiveOutput: { [pollerName = poller.pollerName] _ in let endTime: TimeInterval = dependencies.dateNow.timeIntervalSince1970 let duration: TimeUnit = .seconds(endTime - pollStart) - Log.info("[BackgroundPoller] \(pollerName) succeeded after \(duration, unit: .s).") + Log.info(.backgroundPoller, "\(pollerName) succeeded after \(duration, unit: .s).") }, - receiveCompletion: { result in + receiveCompletion: { [pollerName = poller.pollerName] result in switch result { case .finished: break case .failure(let error): let endTime: TimeInterval = dependencies.dateNow.timeIntervalSince1970 let duration: TimeUnit = .seconds(endTime - pollStart) - Log.error("[BackgroundPoller] \(pollerName) failed after \(duration, unit: .s) due to error: \(error).") + Log.error(.backgroundPoller, "\(pollerName) failed after \(duration, unit: .s) due to error: \(error).") } } ) diff --git a/Session/Utilities/Date+Utilities.swift b/Session/Utilities/Date+Utilities.swift index 731825fc72b..54222fb04a7 100644 --- a/Session/Utilities/Date+Utilities.swift +++ b/Session/Utilities/Date+Utilities.swift @@ -37,6 +37,10 @@ public extension Date { return formatter.string(from: self) } + + var formattedForBanner: String { + return Date.dateOnlyFormatter.string(from: self) + } } // MARK: - Formatters @@ -82,6 +86,16 @@ fileprivate extension Date { return result }() + static let dateOnlyFormatter: DateFormatter = { + let result: DateFormatter = DateFormatter() + result.locale = Locale.current + + // 6 Jun 2023 + result.dateFormat = "d MMM YYYY" + + return result + }() + static var hourFormat: String { guard let format: String = DateFormatter.dateFormat(fromTemplate: "j", options: 0, locale: Locale.current), diff --git a/Session/Utilities/HapticFeedback.swift b/Session/Utilities/HapticFeedback.swift index 39461379f77..3f78984f864 100644 --- a/Session/Utilities/HapticFeedback.swift +++ b/Session/Utilities/HapticFeedback.swift @@ -2,7 +2,7 @@ // Copyright (c) 2019 Open Whisper Systems. All rights reserved. // -import Foundation +import UIKit protocol SelectionHapticFeedbackAdapter { func selectionChanged() diff --git a/Session/Utilities/IP2Country.swift b/Session/Utilities/IP2Country.swift index 3fa8f382d54..8ef98ce3228 100644 --- a/Session/Utilities/IP2Country.swift +++ b/Session/Utilities/IP2Country.swift @@ -8,7 +8,18 @@ import GRDB import SessionSnodeKit import SessionUtilitiesKit -// MARK: - Log.Level +// MARK: - Cache + +public extension Cache { + static let ip2Country: CacheConfig = Dependencies.create( + identifier: "ip2Country", + createInstance: { dependencies in IP2Country(using: dependencies) }, + mutableInstance: { $0 }, + immutableInstance: { $0 } + ) +} + +// MARK: - Log.Category public extension Log.Category { static let ip2Country: Log.Category = .create("IP2Country", defaultLevel: .info) @@ -16,15 +27,11 @@ public extension Log.Category { // MARK: - IP2Country -public enum IP2Country { - public static var isInitialized: Atomic = Atomic(false) - private static var countryNamesCache: Atomic<[String: String]> = Atomic([:]) - private static var pathsChangedCallbackId: Atomic = Atomic(nil) - private static let _cacheLoaded: CurrentValueSubject = CurrentValueSubject(false) - public static var cacheLoaded: AnyPublisher { - _cacheLoaded.filter { $0 }.eraseToAnyPublisher() - } - private static var currentLocale: String { +fileprivate class IP2Country: IP2CountryCacheType { + private var countryNamesCache: [String: String] = [:] + private let _cacheLoaded: CurrentValueSubject = CurrentValueSubject(false) + private var disposables: Set = Set() + private var currentLocale: String { let result: String? = Locale.current.identifier .components(separatedBy: "_") .first @@ -37,6 +44,9 @@ public enum IP2Country { return (result ?? "en") // Fallback to English } + public var cacheLoaded: AnyPublisher { + _cacheLoaded.filter { $0 }.eraseToAnyPublisher() + } // MARK: - Tables @@ -62,7 +72,7 @@ public enum IP2Country { var countryLocationsCountryName: [String] = [] } - private static var cache: IP2CountryCache = { + private var cache: IP2CountryCache = { guard let url: URL = Bundle.main.url(forResource: "GeoLite2-Country-Blocks-IPv4", withExtension: nil), let data: Data = try? Data(contentsOf: url) @@ -152,39 +162,57 @@ public enum IP2Country { ) }() - // MARK: - Implementation - - static func populateCacheIfNeededAsync() { - DispatchQueue.global(qos: .utility).async { - /// Ensure the lookup tables get loaded in the background - _ = cache + + // MARK: - Initialization + + init(using dependencies: Dependencies) { + /// Ensure the lookup tables get loaded in the background + DispatchQueue.global(qos: .utility).async { [weak self] in + _ = self?.cache - pathsChangedCallbackId.mutate { pathsChangedCallbackId in - guard pathsChangedCallbackId == nil else { return } - - pathsChangedCallbackId = LibSession.onPathsChanged(callback: { paths, _ in - self.populateCacheIfNeeded(paths: paths) - }) - } + /// Then register for path change callbacks which will be used to update the country name cache + self?.registerNetworkObservables(using: dependencies) } } + + // MARK: - Functions + + private func registerNetworkObservables(using dependencies: Dependencies) { + /// Register for path change callbacks which will be used to update the country name cache + dependencies[cache: .libSessionNetwork].paths + .subscribe(on: DispatchQueue.global(qos: .utility), using: dependencies) + .receive(on: DispatchQueue.global(qos: .utility), using: dependencies) + .sink( + receiveCompletion: { [weak self] _ in + /// If the stream completes it means the network cache was reset in which case we want to + /// re-register for updates in the next run loop (as the new cache should be created by then) + DispatchQueue.global(qos: .background).async { + self?.registerNetworkObservables(using: dependencies) + } + }, + receiveValue: { [weak self] paths in + dependencies.mutate(cache: .ip2Country) { _ in + self?.populateCacheIfNeeded(paths: paths) + } + } + ) + .store(in: &disposables) + } - private static func populateCacheIfNeeded(paths: [[LibSession.Snode]]) { + private func populateCacheIfNeeded(paths: [[LibSession.Snode]]) { guard !paths.isEmpty else { return } - countryNamesCache.mutate { cache in - paths.forEach { path in - path.forEach { snode in - self.cacheCountry(for: snode.ip, inCache: &cache) - } + paths.forEach { path in + path.forEach { snode in + self.cacheCountry(for: snode.ip, inCache: &countryNamesCache) } } self._cacheLoaded.send(true) - Log.info("Updated onion request path countries.") + Log.info(.ip2Country, "Update onion request path countries.") } - private static func cacheCountry(for ip: String, inCache nameCache: inout [String: String]) { + private func cacheCountry(for ip: String, inCache nameCache: inout [String: String]) { let currentLocale: String = self.currentLocale // Store local copy for efficiency guard nameCache["\(ip)-\(currentLocale)"] == nil else { return } @@ -205,11 +233,24 @@ public enum IP2Country { // MARK: - Functions - public static func country(for ip: String) -> String { - let fallback: String = "Resolving..." - - guard _cacheLoaded.value else { return fallback } + public func country(for ip: String) -> String { + guard _cacheLoaded.value else { return "resolving".localized() } - return (countryNamesCache.wrappedValue["\(ip)-\(currentLocale)"] ?? fallback) + return (countryNamesCache["\(ip)-\(currentLocale)"] ?? "onionRoutingPathUnknownCountry".localized()) } } + +// MARK: - IP2CountryCacheType + +/// This is a read-only version of the Cache designed to avoid unintentionally mutating the instance in a non-thread-safe way +public protocol IP2CountryImmutableCacheType: ImmutableCacheType { + var cacheLoaded: AnyPublisher { get } + + func country(for ip: String) -> String +} + +public protocol IP2CountryCacheType: IP2CountryImmutableCacheType, MutableCacheType { + var cacheLoaded: AnyPublisher { get } + + func country(for ip: String) -> String +} diff --git a/Session/Utilities/MentionUtilities.swift b/Session/Utilities/MentionUtilities.swift index e7beb820c7e..26d1fc05f46 100644 --- a/Session/Utilities/MentionUtilities.swift +++ b/Session/Utilities/MentionUtilities.swift @@ -4,6 +4,7 @@ import Foundation import GRDB import SessionUIKit import SessionMessagingKit +import SessionUtilitiesKit public enum MentionUtilities { public enum MentionLocation { @@ -18,22 +19,24 @@ public enum MentionUtilities { public static func highlightMentionsNoAttributes( in string: String, threadVariant: SessionThread.Variant, - currentUserPublicKey: String, - currentUserBlinded15PublicKey: String?, - currentUserBlinded25PublicKey: String? + currentUserSessionId: String, + currentUserBlinded15SessionId: String?, + currentUserBlinded25SessionId: String?, + using dependencies: Dependencies ) -> String { /// **Note:** We are returning the string here so the 'textColor' and 'primaryColor' values are irrelevant return highlightMentions( in: string, threadVariant: threadVariant, - currentUserPublicKey: currentUserPublicKey, - currentUserBlinded15PublicKey: currentUserBlinded15PublicKey, - currentUserBlinded25PublicKey: currentUserBlinded25PublicKey, + currentUserSessionId: currentUserSessionId, + currentUserBlinded15SessionId: currentUserBlinded15SessionId, + currentUserBlinded25SessionId: currentUserBlinded25SessionId, location: .styleFree, textColor: .black, theme: .classicDark, primaryColor: Theme.PrimaryColor.green, - attributes: [:] + attributes: [:], + using: dependencies ) .string .deformatted() @@ -42,14 +45,15 @@ public enum MentionUtilities { public static func highlightMentions( in string: String, threadVariant: SessionThread.Variant, - currentUserPublicKey: String?, - currentUserBlinded15PublicKey: String?, - currentUserBlinded25PublicKey: String?, + currentUserSessionId: String?, + currentUserBlinded15SessionId: String?, + currentUserBlinded25SessionId: String?, location: MentionLocation, textColor: UIColor, theme: Theme, primaryColor: Theme.PrimaryColor, - attributes: [NSAttributedString.Key: Any] + attributes: [NSAttributedString.Key: Any], + using dependencies: Dependencies ) -> NSAttributedString { guard let regex: NSRegularExpression = try? NSRegularExpression(pattern: "@[0-9a-fA-F]{66}", options: []) @@ -60,10 +64,10 @@ public enum MentionUtilities { var string = string var lastMatchEnd: Int = 0 var mentions: [(range: NSRange, isCurrentUser: Bool)] = [] - let currentUserPublicKeys: Set = [ - currentUserPublicKey, - currentUserBlinded15PublicKey, - currentUserBlinded25PublicKey + let currentUserSessionIds: Set = [ + currentUserSessionId, + currentUserBlinded15SessionId, + currentUserBlinded25SessionId ] .compactMap { $0 } .asSet() @@ -75,12 +79,13 @@ public enum MentionUtilities { ) { guard let range: Range = Range(match.range, in: string) else { break } - let publicKey: String = String(string[range].dropFirst()) // Drop the @ - let isCurrentUser: Bool = currentUserPublicKeys.contains(publicKey) + let sessionId: String = String(string[range].dropFirst()) // Drop the @ + let isCurrentUser: Bool = currentUserSessionIds.contains(sessionId) guard let targetString: String = { guard !isCurrentUser else { return "you".localized() } - guard let displayName: String = Profile.displayNameNoFallback(id: publicKey, threadVariant: threadVariant) else { + // FIXME: This does a database query and is happening when populating UI - should try to refactor it somehow (ideally resolve a set of mentioned profiles as part of the database query) + guard let displayName: String = Profile.displayNameNoFallback(id: sessionId, threadVariant: threadVariant, using: dependencies) else { lastMatchEnd = (match.range.location + match.range.length) return nil } diff --git a/Session/Utilities/MockDataGenerator.swift b/Session/Utilities/MockDataGenerator.swift index c490df3d67b..85cce509f0c 100644 --- a/Session/Utilities/MockDataGenerator.swift +++ b/Session/Utilities/MockDataGenerator.swift @@ -36,7 +36,7 @@ enum MockDataGenerator { let stringContent: [String] = "abcdefghijklmnopqrstuvwxyz ABCDEFGHIJKLMNOPQRSTUVWXYZ 0123456789 ".map { String($0) } let wordContent: [String] = ["alias", "consequatur", "aut", "perferendis", "sit", "voluptatem", "accusantium", "doloremque", "aperiam", "eaque", "ipsa", "quae", "ab", "illo", "inventore", "veritatis", "et", "quasi", "architecto", "beatae", "vitae", "dicta", "sunt", "explicabo", "aspernatur", "aut", "odit", "aut", "fugit", "sed", "quia", "consequuntur", "magni", "dolores", "eos", "qui", "ratione", "voluptatem", "sequi", "nesciunt", "neque", "dolorem", "ipsum", "quia", "dolor", "sit", "amet", "consectetur", "adipisci", "velit", "sed", "quia", "non", "numquam", "eius", "modi", "tempora", "incidunt", "ut", "labore", "et", "dolore", "magnam", "aliquam", "quaerat", "voluptatem", "ut", "enim", "ad", "minima", "veniam", "quis", "nostrum", "exercitationem", "ullam", "corporis", "nemo", "enim", "ipsam", "voluptatem", "quia", "voluptas", "sit", "suscipit", "laboriosam", "nisi", "ut", "aliquid", "ex", "ea", "commodi", "consequatur", "quis", "autem", "vel", "eum", "iure", "reprehenderit", "qui", "in", "ea", "voluptate", "velit", "esse", "quam", "nihil", "molestiae", "et", "iusto", "odio", "dignissimos", "ducimus", "qui", "blanditiis", "praesentium", "laudantium", "totam", "rem", "voluptatum", "deleniti", "atque", "corrupti", "quos", "dolores", "et", "quas", "molestias", "excepturi", "sint", "occaecati", "cupiditate", "non", "provident", "sed", "ut", "perspiciatis", "unde", "omnis", "iste", "natus", "error", "similique", "sunt", "in", "culpa", "qui", "officia", "deserunt", "mollitia", "animi", "id", "est", "laborum", "et", "dolorum", "fuga", "et", "harum", "quidem", "rerum", "facilis", "est", "et", "expedita", "distinctio", "nam", "libero", "tempore", "cum", "soluta", "nobis", "est", "eligendi", "optio", "cumque", "nihil", "impedit", "quo", "porro", "quisquam", "est", "qui", "minus", "id", "quod", "maxime", "placeat", "facere", "possimus", "omnis", "voluptas", "assumenda", "est", "omnis", "dolor", "repellendus", "temporibus", "autem", "quibusdam", "et", "aut", "consequatur", "vel", "illum", "qui", "dolorem", "eum", "fugiat", "quo", "voluptas", "nulla", "pariatur", "at", "vero", "eos", "et", "accusamus", "officiis", "debitis", "aut", "rerum", "necessitatibus", "saepe", "eveniet", "ut", "et", "voluptates", "repudiandae", "sint", "et", "molestiae", "non", "recusandae", "itaque", "earum", "rerum", "hic", "tenetur", "a", "sapiente", "delectus", "ut", "aut", "reiciendis", "voluptatibus", "maiores", "doloribus", "asperiores", "repellat"] let timestampNow: TimeInterval = Date().timeIntervalSince1970 - let userSessionId: String = getUserHexEncodedPublicKey(db) + let userSessionId: SessionId = dependencies[cache: .general].sessionId let logProgress: (String, String) -> () = { title, event in guard printProgress else { return } @@ -49,8 +49,15 @@ enum MockDataGenerator { logProgress("", "Start") // First create the thread used to indicate that the mock data has been generated - _ = try? SessionThread - .fetchOrCreate(db, id: "MockDatabaseThread", variant: .contact, shouldBeVisible: false) + _ = try? SessionThread.fetchOrCreate( + db, + id: "MockDatabaseThread", + variant: .contact, + creationDateTimestamp: timestampNow, + shouldBeVisible: false, + calledFromConfig: nil, + using: dependencies + ) // MARK: - -- DM Thread @@ -67,7 +74,7 @@ enum MockDataGenerator { logProgress("DM Thread \(threadIndex)", "Start") let data: Data = Data(dmThreadRandomGenerator.nextBytes(count: 16)) - let randomSessionId: String = try! Identity.generate(from: data).x25519KeyPair.hexEncodedPublicKey + let randomSessionId: String = SessionId(.standard, publicKey: try! Identity.generate(from: data, using: dependencies).x25519KeyPair.publicKey).hexString let isMessageRequest: Bool = Bool.random(using: &dmThreadRandomGenerator) let contactNameLength: Int = ((5..<20).randomElement(using: &dmThreadRandomGenerator) ?? 0) let numMessages: Int = (messageRangePerThread[threadIndex % messageRangePerThread.count] @@ -79,7 +86,10 @@ enum MockDataGenerator { db, id: randomSessionId, variant: .contact, - shouldBeVisible: true + creationDateTimestamp: TimeInterval(floor(timestampNow - Double(index * 5))), + shouldBeVisible: true, + calledFromConfig: nil, + using: dependencies ) // Generate the contact @@ -92,16 +102,17 @@ enum MockDataGenerator { !isMessageRequest && (((0..<10).randomElement(using: &dmThreadRandomGenerator) ?? 0) < 8) // 80% approved the current user ), - hasBeenBlocked: false + hasBeenBlocked: false, + using: dependencies ) - .saved(db) - _ = try! Profile( + .upserted(db) + try! Profile( id: randomSessionId, name: (0.. Void)? = nil ) -> Bool { switch AVCaptureDevice.authorizationStatus(for: .video) { @@ -20,8 +21,7 @@ public enum Permissions { case .denied, .restricted: guard - Singleton.hasAppContext, - let presentingViewController: UIViewController = (presentingViewController ?? Singleton.appContext.frontmostViewController) + let presentingViewController: UIViewController = (presentingViewController ?? dependencies[singleton: .appContext].frontMostViewController) else { return false } let confirmationModal: ConfirmationModal = ConfirmationModal( @@ -55,14 +55,14 @@ public enum Permissions { public static func requestMicrophonePermissionIfNeeded( presentingViewController: UIViewController? = nil, + using dependencies: Dependencies, onNotGranted: (() -> Void)? = nil ) { switch AVAudioSession.sharedInstance().recordPermission { case .granted: break case .denied: guard - Singleton.hasAppContext, - let presentingViewController: UIViewController = (presentingViewController ?? Singleton.appContext.frontmostViewController) + let presentingViewController: UIViewController = (presentingViewController ?? dependencies[singleton: .appContext].frontMostViewController) else { return } onNotGranted?() @@ -97,6 +97,7 @@ public enum Permissions { public static func requestLibraryPermissionIfNeeded( isSavingMedia: Bool, presentingViewController: UIViewController? = nil, + using dependencies: Dependencies, onAuthorized: @escaping () -> Void ) { let targetPermission: PHAccessLevel = (isSavingMedia ? .addOnly : .readWrite) @@ -124,8 +125,7 @@ public enum Permissions { case .authorized, .limited: onAuthorized() case .denied, .restricted: guard - Singleton.hasAppContext, - let presentingViewController: UIViewController = (presentingViewController ?? Singleton.appContext.frontmostViewController) + let presentingViewController: UIViewController = (presentingViewController ?? dependencies[singleton: .appContext].frontMostViewController) else { return } let confirmationModal: ConfirmationModal = ConfirmationModal( diff --git a/Session/Utilities/UIApplication+OWS.swift b/Session/Utilities/UIApplication+OWS.swift index f67a2ded005..e4de776e9a1 100644 --- a/Session/Utilities/UIApplication+OWS.swift +++ b/Session/Utilities/UIApplication+OWS.swift @@ -1,29 +1,21 @@ // Copyright © 2022 Rangeproof Pty Ltd. All rights reserved. import UIKit +import SignalUtilitiesKit import SessionUtilitiesKit -@objc public extension UIApplication { - - var frontmostViewControllerIgnoringAlerts: UIViewController? { - return findFrontmostViewController(ignoringAlerts: true) - } - - var frontmostViewController: UIViewController? { - return findFrontmostViewController(ignoringAlerts: false) - } - - internal func findFrontmostViewController(ignoringAlerts: Bool) -> UIViewController? { - guard - Singleton.hasAppContext, - let window: UIWindow = Singleton.appContext.mainWindow - else { return nil } +public extension UIApplication { + func frontMostViewController( + ignoringAlerts: Bool = false, + using dependencies: Dependencies + ) -> UIViewController? { + guard let window: UIWindow = dependencies[singleton: .appContext].mainWindow else { return nil } guard let viewController: UIViewController = window.rootViewController else { Log.error("[UIApplication] Missing root view controller.") return nil } - return viewController.findFrontmostViewController(ignoringAlerts: ignoringAlerts) + return viewController.findFrontMostViewController(ignoringAlerts: ignoringAlerts) } func openSystemSettings() { diff --git a/Session/Utilities/UIContextualAction+Utilities.swift b/Session/Utilities/UIContextualAction+Utilities.swift index eb46ad1d3ed..1d52c562296 100644 --- a/Session/Utilities/UIContextualAction+Utilities.swift +++ b/Session/Utilities/UIContextualAction+Utilities.swift @@ -1,6 +1,7 @@ // Copyright © 2023 Rangeproof Pty Ltd. All rights reserved. import UIKit +import GRDB import SessionMessagingKit import SessionUIKit import SessionUtilitiesKit @@ -50,7 +51,8 @@ public extension UIContextualAction { tableView: UITableView, threadViewModel: SessionThreadViewModel, viewController: UIViewController?, - navigatableStateHolder: NavigatableStateHolder? + navigatableStateHolder: NavigatableStateHolder?, + using dependencies: Dependencies ) -> [UIContextualAction]? { guard !actions.isEmpty else { return nil } @@ -67,7 +69,7 @@ public extension UIContextualAction { return targetActions .enumerated() - .map { index, action -> UIContextualAction in + .compactMap { index, action -> UIContextualAction? in // Even though we have to reverse the actions above, the indexes in the view hierarchy // are in the expected order let targetIndex: Int = (side == .trailing ? (targetActions.count - index) : index) @@ -95,6 +97,7 @@ public extension UIContextualAction { ), themeTintColor: .white, themeBackgroundColor: .conversationButton_swipeRead, // Always Custom + accessibility: Accessibility(identifier: (isUnread ? "Mark Read button" : "Mark Unread button")), side: side, actionIndex: targetIndex, indexPath: indexPath, @@ -106,10 +109,11 @@ public extension UIContextualAction { case true: threadViewModel.markAsRead( target: .threadAndInteractions( interactionsBeforeInclusive: threadViewModel.interactionId - ) + ), + using: dependencies ) - case false: threadViewModel.markAsUnread() + case false: threadViewModel.markAsUnread(using: dependencies) } } completionHandler(true) @@ -137,12 +141,14 @@ public extension UIContextualAction { cancelStyle: .alert_text, dismissOnConfirm: true, onConfirm: { _ in - Storage.shared.writeAsync { db in + dependencies[singleton: .storage].writeAsync { db in try SessionThread.deleteOrLeave( db, type: .hideContactConversationAndDeleteContent, threadId: threadViewModel.threadId, - calledFromConfigHandling: false + threadVariant: threadViewModel.threadVariant, + calledFromConfig: nil, + using: dependencies ) } @@ -163,6 +169,7 @@ public extension UIContextualAction { icon: UIImage(systemName: "eye.slash"), themeTintColor: .white, themeBackgroundColor: themeBackgroundColor, + accessibility: Accessibility(identifier: "Hide button"), side: side, actionIndex: targetIndex, indexPath: indexPath, @@ -170,7 +177,9 @@ public extension UIContextualAction { ) { _, _, completionHandler in switch threadViewModel.threadId { case SessionThreadViewModel.messageRequestsSectionId: - Storage.shared.write { db in db[.hasHiddenMessageRequests] = true } + dependencies[singleton: .storage].write { db in + db[.hasHiddenMessageRequests] = true + } completionHandler(true) default: @@ -183,12 +192,14 @@ public extension UIContextualAction { cancelStyle: .alert_text, dismissOnConfirm: true, onConfirm: { _ in - Storage.shared.writeAsync { db in + dependencies[singleton: .storage].writeAsync { db in try SessionThread.deleteOrLeave( db, type: .hideContactConversationAndDeleteContent, threadId: threadViewModel.threadId, - calledFromConfigHandling: false + threadVariant: threadViewModel.threadVariant, + calledFromConfig: nil, + using: dependencies ) } @@ -216,6 +227,9 @@ public extension UIContextualAction { ), themeTintColor: .white, themeBackgroundColor: .conversationButton_swipeTertiary, // Always Tertiary + accessibility: Accessibility( + identifier: (threadViewModel.threadPinnedPriority > 0 ? "Pin button" : "Unpin button") + ), side: side, actionIndex: targetIndex, indexPath: indexPath, @@ -229,13 +243,15 @@ public extension UIContextualAction { // Delay the change to give the cell "unswipe" animation some time to complete DispatchQueue.global(qos: .default).asyncAfter(deadline: .now() + unswipeAnimationDelay) { - Storage.shared.writeAsync { db in + dependencies[singleton: .storage].writeAsync { db in try SessionThread .filter(id: threadViewModel.threadId) .updateAllAndConfig( db, SessionThread.Columns.pinnedPriority - .set(to: (threadViewModel.threadPinnedPriority == 0 ? 1 : 0)) + .set(to: (threadViewModel.threadPinnedPriority == 0 ? 1 : 0)), + calledFromConfig: nil, + using: dependencies ) } } @@ -256,6 +272,9 @@ public extension UIContextualAction { iconHeight: Values.mediumFontSize, themeTintColor: .white, themeBackgroundColor: themeBackgroundColor, + accessibility: Accessibility( + identifier: (threadViewModel.threadMutedUntilTimestamp == nil ? "Mute button" : "Unmute button") + ), side: side, actionIndex: targetIndex, indexPath: indexPath, @@ -269,7 +288,7 @@ public extension UIContextualAction { // Delay the change to give the cell "unswipe" animation some time to complete DispatchQueue.global(qos: .default).asyncAfter(deadline: .now() + unswipeAnimationDelay) { - Storage.shared.writeAsync { db in + dependencies[singleton: .storage].writeAsync { db in let currentValue: TimeInterval? = try SessionThread .filter(id: threadViewModel.threadId) .select(.mutedUntilTimestamp) @@ -294,6 +313,36 @@ public extension UIContextualAction { // MARK: -- block case .block: + /// If we don't have the `profileInfo` then we can't actually block so don't offer the block option in that case + guard + let profileInfo: (id: String, profile: Profile?) = dependencies[singleton: .storage] + .read({ db in + switch threadViewModel.threadVariant { + case .contact: + return ( + threadViewModel.threadId, + try Profile.fetchOne(db, id: threadViewModel.threadId) + ) + + case .group: + let firstAdmin: GroupMember? = try GroupMember + .filter(GroupMember.Columns.groupId == threadViewModel.threadId) + .filter(GroupMember.Columns.role == GroupMember.Role.admin) + .fetchOne(db) + + return try firstAdmin + .map { admin in + ( + admin.profileId, + try Profile.fetchOne(db, id: admin.profileId) + ) + } + + default: return nil + } + }) + else { return nil } + return UIContextualAction( title: (threadViewModel.threadIsBlocked == true ? "blockUnblock".localized() : @@ -303,6 +352,7 @@ public extension UIContextualAction { iconHeight: Values.mediumFontSize, themeTintColor: .white, themeBackgroundColor: themeBackgroundColor, + accessibility: Accessibility(identifier: "Block button"), side: side, actionIndex: targetIndex, indexPath: indexPath, @@ -329,15 +379,38 @@ public extension UIContextualAction { // Delay the change to give the cell "unswipe" animation some time to complete DispatchQueue.global(qos: .default).asyncAfter(deadline: .now() + unswipeAnimationDelay) { - Storage.shared + dependencies[singleton: .storage] .writePublisher { db in // Create the contact if it doesn't exist - try Contact - .fetchOrCreate(db, id: threadViewModel.threadId) - .save(db) - try Contact - .filter(id: threadViewModel.threadId) - .updateAllAndConfig(db, contactChanges) + switch threadViewModel.threadVariant { + case .contact: + try Contact + .fetchOrCreate(db, id: threadViewModel.threadId, using: dependencies) + .upsert(db) + try Contact + .filter(id: threadViewModel.threadId) + .updateAllAndConfig( + db, + contactChanges, + calledFromConfig: nil, + using: dependencies + ) + + case .group: + try Contact + .fetchOrCreate(db, id: profileInfo.id, using: dependencies) + .upsert(db) + try Contact + .filter(id: profileInfo.id) + .updateAllAndConfig( + db, + contactChanges, + calledFromConfig: nil, + using: dependencies + ) + + default: break + } // Blocked message requests should be deleted if threadIsMessageRequest { @@ -345,7 +418,9 @@ public extension UIContextualAction { db, type: .hideContactConversationAndDeleteContent, threadId: threadViewModel.threadId, - calledFromConfigHandling: false + threadVariant: threadViewModel.threadVariant, + calledFromConfig: nil, + using: dependencies ) } } @@ -357,12 +432,27 @@ public extension UIContextualAction { switch threadIsMessageRequest { case false: performBlock(nil) case true: + let nameToUse: String = { + switch threadViewModel.threadVariant { + case .group: + return Profile.displayName( + for: .contact, + id: profileInfo.id, + name: profileInfo.profile?.name, + nickname: profileInfo.profile?.nickname, + suppressId: false + ) + + default: return threadViewModel.displayName + } + }() + let confirmationModal: ConfirmationModal = ConfirmationModal( info: ConfirmationModal.Info( title: "block".localized(), body: .attributedText( "blockDescription" - .put(key: "name", value: threadViewModel.displayName) + .put(key: "name", value: nameToUse) .localizedFormatted(baseFont: .systemFont(ofSize: Values.smallFontSize)) ), confirmTitle: "block".localized(), @@ -389,6 +479,7 @@ public extension UIContextualAction { iconHeight: Values.mediumFontSize, themeTintColor: .white, themeBackgroundColor: themeBackgroundColor, + accessibility: Accessibility(identifier: "Leave button"), side: side, actionIndex: targetIndex, indexPath: indexPath, @@ -405,11 +496,16 @@ public extension UIContextualAction { let confirmationModalExplanation: NSAttributedString = { switch (threadViewModel.threadVariant, threadViewModel.currentUserIsClosedGroupAdmin) { - case (.legacyGroup, true), (.group, true): - return "groupDeleteDescription" + case (.group, true): + return "groupLeaveDescriptionAdmin" .put(key: "group_name", value: threadViewModel.displayName) .localizedFormatted(baseFont: .boldSystemFont(ofSize: Values.smallFontSize)) - + + case (.legacyGroup, true): + return "groupLeaveDescription" + .put(key: "group_name", value: threadViewModel.displayName) + .localizedFormatted(baseFont: .boldSystemFont(ofSize: Values.smallFontSize)) + default: return "groupLeaveDescription" .put(key: "group_name", value: threadViewModel.displayName) @@ -433,13 +529,15 @@ public extension UIContextualAction { } }() - Storage.shared.writeAsync { db in + dependencies[singleton: .storage].writeAsync { db in do { try SessionThread.deleteOrLeave( db, type: deletionType, threadId: threadViewModel.threadId, - calledFromConfigHandling: false + threadVariant: threadViewModel.threadVariant, + calledFromConfig: nil, + using: dependencies ) } catch { DispatchQueue.main.async { @@ -462,7 +560,6 @@ public extension UIContextualAction { ) } } - } completionHandler(true) @@ -482,6 +579,7 @@ public extension UIContextualAction { icon: UIImage(named: "ic_bin"), themeTintColor: .white, themeBackgroundColor: themeBackgroundColor, + accessibility: Accessibility(identifier: "Delete button"), side: side, actionIndex: targetIndex, indexPath: indexPath, @@ -502,9 +600,10 @@ public extension UIContextualAction { }() let confirmationModalExplanation: NSAttributedString = { guard !isMessageRequest else { - return NSAttributedString( - string: "messageRequestsDelete".localized() - ) + switch threadViewModel.threadVariant { + case .group: return NSAttributedString(string: "groupInviteDelete".localized()) + default: return NSAttributedString(string: "messageRequestsDelete".localized()) + } } guard threadViewModel.currentUserIsClosedGroupAdmin == false else { @@ -547,12 +646,14 @@ public extension UIContextualAction { } }() - Storage.shared.writeAsync { db in + dependencies[singleton: .storage].writeAsync { db in try SessionThread.deleteOrLeave( db, type: deletionType, threadId: threadViewModel.threadId, - calledFromConfigHandling: false + threadVariant: threadViewModel.threadVariant, + calledFromConfig: nil, + using: dependencies ) } diff --git a/Session/Utilities/UIImage+Scaling.swift b/Session/Utilities/UIImage+Scaling.swift index 2645c3d4316..bc6208071f7 100644 --- a/Session/Utilities/UIImage+Scaling.swift +++ b/Session/Utilities/UIImage+Scaling.swift @@ -1,3 +1,4 @@ +import UIKit extension UIImage { diff --git a/Session/Utilities/UILabel+Interaction.swift b/Session/Utilities/UILabel+Interaction.swift index 2d12f2e518b..4c7a5e92c52 100644 --- a/Session/Utilities/UILabel+Interaction.swift +++ b/Session/Utilities/UILabel+Interaction.swift @@ -1,3 +1,4 @@ +import UIKit extension UILabel { diff --git a/Session/Utilities/UIResponder+OWS.swift b/Session/Utilities/UIResponder+OWS.swift index ce1d96c30fb..23d74b606f1 100644 --- a/Session/Utilities/UIResponder+OWS.swift +++ b/Session/Utilities/UIResponder+OWS.swift @@ -2,6 +2,8 @@ // Copyright (c) 2019 Open Whisper Systems. All rights reserved. // +import UIKit + // Based on https://stackoverflow.com/questions/1823317/get-the-current-first-responder-without-using-a-private-api/11768282#11768282 extension UIResponder { private weak static var firstResponder: UIResponder? diff --git a/Session/Utilities/UIStoryboard+OWS.swift b/Session/Utilities/UIStoryboard+OWS.swift index 26542a5c85e..c7fd38b82ee 100644 --- a/Session/Utilities/UIStoryboard+OWS.swift +++ b/Session/Utilities/UIStoryboard+OWS.swift @@ -2,7 +2,7 @@ // Copyright (c) 2018 Open Whisper Systems. All rights reserved. // -import Foundation +import UIKit extension UIStoryboard { private enum StoryboardName: String { diff --git a/SessionMessagingKit/Calls/CallManagerProtocol.swift b/SessionMessagingKit/Calls/CallManagerProtocol.swift index dcfc31aa4b5..a7498a7ae15 100644 --- a/SessionMessagingKit/Calls/CallManagerProtocol.swift +++ b/SessionMessagingKit/Calls/CallManagerProtocol.swift @@ -2,11 +2,30 @@ import Foundation import CallKit +import SessionUtilitiesKit + +// MARK: - Singleton + +public extension Singleton { + static let callManager: SingletonConfig = Dependencies.create( + identifier: "sessionCallManager", + createInstance: { _ in NoopSessionCallManager() } + ) +} + +// MARK: - CallManagerProtocol public protocol CallManagerProtocol { - var currentCall: CurrentCallProtocol? { get set } + var currentCall: CurrentCallProtocol? { get } + func setCurrentCall(_ call: CurrentCallProtocol?) + func reportIncomingCall(_ call: CurrentCallProtocol, callerName: String, completion: @escaping (Error?) -> Void) func reportCurrentCallEnded(reason: CXCallEndedReason?) + func suspendDatabaseIfCallEndedInBackground() + + func startCall(_ call: CurrentCallProtocol?, completion: ((Error?) -> Void)?) + func answerCall(_ call: CurrentCallProtocol?, completion: ((Error?) -> Void)?) + func endCall(_ call: CurrentCallProtocol?, completion: ((Error?) -> Void)?) func showCallUIForCall(caller: String, uuid: String, mode: CallMode, interactionId: Int64?) func handleICECandidates(message: CallMessage, sdpMLineIndexes: [UInt32], sdpMids: [String]) diff --git a/SessionMessagingKit/Calls/CurrentCallProtocol.swift b/SessionMessagingKit/Calls/CurrentCallProtocol.swift index c37a26bdd6e..bb6334c064f 100644 --- a/SessionMessagingKit/Calls/CurrentCallProtocol.swift +++ b/SessionMessagingKit/Calls/CurrentCallProtocol.swift @@ -3,6 +3,7 @@ import Foundation import GRDB import WebRTC +import SessionUtilitiesKit public protocol CurrentCallProtocol { var uuid: String { get } @@ -10,7 +11,7 @@ public protocol CurrentCallProtocol { var hasStartedConnecting: Bool { get set } var hasEnded: Bool { get set } - func updateCallMessage(mode: EndCallMode) + func updateCallMessage(mode: EndCallMode, using dependencies: Dependencies) func didReceiveRemoteSDP(sdp: RTCSessionDescription) func startSessionCall(_ db: Database) } diff --git a/SessionMessagingKit/Calls/NoopSessionCallManager.swift b/SessionMessagingKit/Calls/NoopSessionCallManager.swift new file mode 100644 index 00000000000..afa3e928060 --- /dev/null +++ b/SessionMessagingKit/Calls/NoopSessionCallManager.swift @@ -0,0 +1,25 @@ +// Copyright © 2024 Rangeproof Pty Ltd. All rights reserved. + +import Foundation +import CallKit + +internal struct NoopSessionCallManager: CallManagerProtocol { + var currentCall: CurrentCallProtocol? + + func setCurrentCall(_ call: CurrentCallProtocol?) {} + func reportIncomingCall(_ call: CurrentCallProtocol, callerName: String, completion: @escaping (Error?) -> Void) {} + func reportCurrentCallEnded(reason: CXCallEndedReason?) {} + func suspendDatabaseIfCallEndedInBackground() {} + + func startCall(_ call: CurrentCallProtocol?, completion: ((Error?) -> Void)?) {} + func answerCall(_ call: CurrentCallProtocol?, completion: ((Error?) -> Void)?) {} + func endCall(_ call: CurrentCallProtocol?, completion: ((Error?) -> Void)?) {} + + func showCallUIForCall(caller: String, uuid: String, mode: CallMode, interactionId: Int64?) {} + func handleICECandidates(message: CallMessage, sdpMLineIndexes: [UInt32], sdpMids: [String]) {} + func handleAnswerMessage(_ message: CallMessage) {} + + func currentWebRTCSessionMatches(callId: String) -> Bool { return false } + + func dismissAllCallUI() {} +} diff --git a/SessionMessagingKit/Configuration.swift b/SessionMessagingKit/Configuration.swift index c796e94eed2..a8803bd37aa 100644 --- a/SessionMessagingKit/Configuration.swift +++ b/SessionMessagingKit/Configuration.swift @@ -38,31 +38,42 @@ public enum SNMessagingKit: MigratableTarget { // Just to make the external API _018_DisappearingMessagesConfiguration.self, _019_ScheduleAppUpdateCheckJob.self, _020_AddMissingWhisperFlag.self, - _021_ReworkRecipientState.self + _021_ReworkRecipientState.self, + _022_GroupsRebuildChanges.self ] ] ) } - public static func configure() { + public static func configure(using dependencies: Dependencies) { // Configure the job executors - JobRunner.setExecutor(DisappearingMessagesJob.self, for: .disappearingMessages) - JobRunner.setExecutor(FailedMessageSendsJob.self, for: .failedMessageSends) - JobRunner.setExecutor(FailedAttachmentDownloadsJob.self, for: .failedAttachmentDownloads) - JobRunner.setExecutor(UpdateProfilePictureJob.self, for: .updateProfilePicture) - JobRunner.setExecutor(RetrieveDefaultOpenGroupRoomsJob.self, for: .retrieveDefaultOpenGroupRooms) - JobRunner.setExecutor(GarbageCollectionJob.self, for: .garbageCollection) - JobRunner.setExecutor(MessageSendJob.self, for: .messageSend) - JobRunner.setExecutor(MessageReceiveJob.self, for: .messageReceive) - JobRunner.setExecutor(NotifyPushServerJob.self, for: .notifyPushServer) - JobRunner.setExecutor(SendReadReceiptsJob.self, for: .sendReadReceipts) - JobRunner.setExecutor(AttachmentUploadJob.self, for: .attachmentUpload) - JobRunner.setExecutor(GroupLeavingJob.self, for: .groupLeaving) - JobRunner.setExecutor(AttachmentDownloadJob.self, for: .attachmentDownload) - JobRunner.setExecutor(ConfigurationSyncJob.self, for: .configurationSync) - JobRunner.setExecutor(ConfigMessageReceiveJob.self, for: .configMessageReceive) - JobRunner.setExecutor(ExpirationUpdateJob.self, for: .expirationUpdate) - JobRunner.setExecutor(GetExpirationJob.self, for: .getExpiration) - JobRunner.setExecutor(CheckForAppUpdatesJob.self, for: .checkForAppUpdates) + let executors: [Job.Variant: JobExecutor.Type] = [ + .disappearingMessages: DisappearingMessagesJob.self, + .failedMessageSends: FailedMessageSendsJob.self, + .failedAttachmentDownloads: FailedAttachmentDownloadsJob.self, + .updateProfilePicture: UpdateProfilePictureJob.self, + .retrieveDefaultOpenGroupRooms: RetrieveDefaultOpenGroupRoomsJob.self, + .garbageCollection: GarbageCollectionJob.self, + .messageSend: MessageSendJob.self, + .messageReceive: MessageReceiveJob.self, + .notifyPushServer: NotifyPushServerJob.self, + .sendReadReceipts: SendReadReceiptsJob.self, + .attachmentUpload: AttachmentUploadJob.self, + .groupLeaving: GroupLeavingJob.self, + .attachmentDownload: AttachmentDownloadJob.self, + .configurationSync: ConfigurationSyncJob.self, + .configMessageReceive: ConfigMessageReceiveJob.self, + .expirationUpdate: ExpirationUpdateJob.self, + .checkForAppUpdates: CheckForAppUpdatesJob.self, + .displayPictureDownload: DisplayPictureDownloadJob.self, + .getExpiration: GetExpirationJob.self, + .groupInviteMember: GroupInviteMemberJob.self, + .groupPromoteMember: GroupPromoteMemberJob.self, + .processPendingGroupMemberRemovals: ProcessPendingGroupMemberRemovalsJob.self + ] + + executors.forEach { variant, executor in + dependencies[singleton: .jobRunner].setExecutor(executor, for: variant) + } } } diff --git a/SessionMessagingKit/Crypto/Crypto+Attachments.swift b/SessionMessagingKit/Crypto/Crypto+Attachments.swift index e545a1a9595..254ae6916fa 100644 --- a/SessionMessagingKit/Crypto/Crypto+Attachments.swift +++ b/SessionMessagingKit/Crypto/Crypto+Attachments.swift @@ -29,9 +29,9 @@ public extension Crypto.Generator { } guard - var iv: [UInt8] = dependencies.crypto.generate(.randomBytes(aesCBCIvLength)), - var encryptionKey: [UInt8] = dependencies.crypto.generate(.randomBytes(aesKeySize)), - var hmacKey: [UInt8] = dependencies.crypto.generate(.randomBytes(hmac256KeyLength)) + var iv: [UInt8] = dependencies[singleton: .crypto].generate(.randomBytes(aesCBCIvLength)), + var encryptionKey: [UInt8] = dependencies[singleton: .crypto].generate(.randomBytes(aesKeySize)), + var hmacKey: [UInt8] = dependencies[singleton: .crypto].generate(.randomBytes(hmac256KeyLength)) else { Log.error("[Crypto] Failed to generate random data.") throw CryptoError.encryptionFailed diff --git a/SessionMessagingKit/Crypto/Crypto+LibSession.swift b/SessionMessagingKit/Crypto/Crypto+LibSession.swift new file mode 100644 index 00000000000..e212a20b935 --- /dev/null +++ b/SessionMessagingKit/Crypto/Crypto+LibSession.swift @@ -0,0 +1,190 @@ +// Copyright © 2023 Rangeproof Pty Ltd. All rights reserved. + +import Foundation +import SessionUtil +import SessionUtilitiesKit + +public extension Crypto.Generator { + static func tokenSubaccount( + config: LibSession.Config?, + groupSessionId: SessionId, + memberId: String + ) -> Crypto.Generator<[UInt8]> { + return Crypto.Generator( + id: "tokenSubaccount", + args: [config, groupSessionId, memberId] + ) { + guard case .groupKeys(let conf, _, _) = config else { throw LibSessionError.invalidConfigObject } + + var cMemberId: [CChar] = try memberId.cString(using: .utf8) ?? { throw LibSessionError.invalidCConversion }() + var tokenData: [UInt8] = [UInt8](repeating: 0, count: LibSession.sizeSubaccountBytes) + + guard groups_keys_swarm_subaccount_token( + conf, + &cMemberId, + &tokenData + ) else { throw LibSessionError.failedToMakeSubAccountInGroup } + + return tokenData + } + } + + static func memberAuthData( + config: LibSession.Config?, + groupSessionId: SessionId, + memberId: String + ) -> Crypto.Generator { + return Crypto.Generator( + id: "memberAuthData", + args: [config, groupSessionId, memberId] + ) { + guard case .groupKeys(let conf, _, _) = config else { throw LibSessionError.invalidConfigObject } + + var cMemberId: [CChar] = try memberId.cString(using: .utf8) ?? { throw LibSessionError.invalidCConversion }() + var authData: [UInt8] = [UInt8](repeating: 0, count: LibSession.sizeAuthDataBytes) + + guard groups_keys_swarm_make_subaccount( + conf, + &cMemberId, + &authData + ) else { throw LibSessionError.failedToMakeSubAccountInGroup } + + return .groupMember(groupSessionId: groupSessionId, authData: Data(authData)) + } + } + + static func signatureSubaccount( + config: LibSession.Config?, + verificationBytes: [UInt8], + memberAuthData: Data + ) -> Crypto.Generator { + return Crypto.Generator( + id: "signatureSubaccount", + args: [config, verificationBytes, memberAuthData] + ) { + guard case .groupKeys(let conf, _, _) = config else { throw LibSessionError.invalidConfigObject } + + var verificationBytes: [UInt8] = verificationBytes + var memberAuthData: [UInt8] = Array(memberAuthData) + var subaccount: [UInt8] = [UInt8](repeating: 0, count: LibSession.sizeSubaccountBytes) + var subaccountSig: [UInt8] = [UInt8](repeating: 0, count: LibSession.sizeSubaccountSigBytes) + var signature: [UInt8] = [UInt8](repeating: 0, count: LibSession.sizeSubaccountSignatureBytes) + + guard groups_keys_swarm_subaccount_sign_binary( + conf, + &verificationBytes, + verificationBytes.count, + &memberAuthData, + &subaccount, + &subaccountSig, + &signature + ) else { throw MessageSenderError.signingFailed } + + return Authentication.Signature.subaccount( + subaccount: subaccount, + subaccountSig: subaccountSig, + signature: signature + ) + } + } + + static func ciphertextForGroupMessage( + groupSessionId: SessionId, + message: [UInt8] + ) -> Crypto.Generator { + return Crypto.Generator( + id: "ciphertextForGroupMessage", + args: [groupSessionId, message] + ) { dependencies in + return try dependencies.mutate(cache: .libSession) { cache in + guard case .groupKeys(let conf, _, _) = cache.config(for: .groupKeys, sessionId: groupSessionId) else { + throw LibSessionError.invalidConfigObject + } + + var maybeCiphertext: UnsafeMutablePointer? = nil + var ciphertextLen: Int = 0 + groups_keys_encrypt_message( + conf, + message, + message.count, + &maybeCiphertext, + &ciphertextLen + ) + + guard + ciphertextLen > 0, + let ciphertext: Data = maybeCiphertext + .map({ Data(bytes: $0, count: ciphertextLen) }) + else { throw MessageSenderError.encryptionFailed } + + return ciphertext + } ?? { throw MessageSenderError.encryptionFailed }() + } + } + + static func plaintextForGroupMessage( + groupSessionId: SessionId, + ciphertext: [UInt8] + ) throws -> Crypto.Generator<(plaintext: Data, sender: String)> { + return Crypto.Generator( + id: "plaintextForGroupMessage", + args: [groupSessionId, ciphertext] + ) { dependencies in + return try dependencies.mutate(cache: .libSession) { cache in + guard case .groupKeys(let conf, _, _) = cache.config(for: .groupKeys, sessionId: groupSessionId) else { + throw LibSessionError.invalidConfigObject + } + + var cSessionId: [CChar] = [CChar](repeating: 0, count: 67) + var maybePlaintext: UnsafeMutablePointer? = nil + var plaintextLen: Int = 0 + let didDecrypt: Bool = groups_keys_decrypt_message( + conf, + ciphertext, + ciphertext.count, + &cSessionId, + &maybePlaintext, + &plaintextLen + ) + + // If we got a reported failure then just stop here + guard didDecrypt else { throw MessageReceiverError.decryptionFailed } + + // We need to manually free 'maybePlaintext' upon a successful decryption + defer { maybePlaintext?.deallocate() } + + guard + plaintextLen > 0, + let plaintext: Data = maybePlaintext + .map({ Data(bytes: $0, count: plaintextLen) }) + else { throw MessageReceiverError.decryptionFailed } + + return (plaintext, String(cString: cSessionId)) + } ?? { throw MessageReceiverError.decryptionFailed }() + } + } +} + +public extension Crypto.Verification { + static func memberAuthData( + groupSessionId: SessionId, + ed25519SecretKey: [UInt8], + memberAuthData: Data + ) -> Crypto.Verification { + return Crypto.Verification( + id: "memberAuthData", + args: [groupSessionId, ed25519SecretKey, memberAuthData] + ) { + guard var cGroupId: [CChar] = groupSessionId.hexString.cString(using: .utf8) else { return false } + + var cEd25519SecretKey: [UInt8] = ed25519SecretKey + var cAuthData: [UInt8] = Array(memberAuthData) + + return groups_keys_swarm_verify_subaccount( + &cGroupId, + &cEd25519SecretKey, + &cAuthData + ) + } + } +} diff --git a/SessionMessagingKit/Crypto/Crypto+SessionMessagingKit.swift b/SessionMessagingKit/Crypto/Crypto+SessionMessagingKit.swift index 2a24082788f..d8d090bf9e4 100644 --- a/SessionMessagingKit/Crypto/Crypto+SessionMessagingKit.swift +++ b/SessionMessagingKit/Crypto/Crypto+SessionMessagingKit.swift @@ -3,6 +3,7 @@ // stringlint:disable import Foundation +import CryptoKit import GRDB import SessionSnodeKit import SessionUtil @@ -27,9 +28,7 @@ public extension Crypto.Generator { let destinationX25519PublicKey: Data = try { switch destination { case .contact(let publicKey): return Data(SessionId(.standard, hex: publicKey).publicKey) - case .syncMessage: - return Data(SessionId(.standard, hex: getUserHexEncodedPublicKey(using: dependencies)).publicKey) - + case .syncMessage: return Data(dependencies[cache: .general].sessionId.publicKey) case .closedGroup(let groupPublicKey): return try ClosedGroupKeyPair.fetchLatestKeyPair(db, threadId: groupPublicKey)?.publicKey ?? { throw MessageSenderError.noKeyPair @@ -279,3 +278,56 @@ public extension Crypto.Generator { } } } + +// MARK: - DisplayPicture + +public extension Crypto.Generator { + static func encryptedDataDisplayPicture( + data: Data, + key: Data, + using dependencies: Dependencies + ) -> Crypto.Generator { + return Crypto.Generator(id: "encryptedDataDisplayPicture", args: [data, key]) { + // The key structure is: nonce || ciphertext || authTag + guard + key.count == DisplayPictureManager.aes256KeyByteLength, + let nonceData: Data = dependencies[singleton: .crypto] + .generate(.randomBytes(DisplayPictureManager.nonceLength)), + let nonce: AES.GCM.Nonce = try? AES.GCM.Nonce(data: nonceData), + let sealedData: AES.GCM.SealedBox = try? AES.GCM.seal( + data, + using: SymmetricKey(data: key), + nonce: nonce + ), + let encryptedContent: Data = sealedData.combined + else { throw CryptoError.failedToGenerateOutput } + + return encryptedContent + } + } + + static func decryptedDataDisplayPicture( + data: Data, + key: Data, + using dependencies: Dependencies + ) -> Crypto.Generator { + return Crypto.Generator(id: "decryptedDataDisplayPicture", args: [data, key]) { + guard key.count == DisplayPictureManager.aes256KeyByteLength else { throw CryptoError.failedToGenerateOutput } + + // The key structure is: nonce || ciphertext || authTag + let cipherTextLength: Int = (data.count - (DisplayPictureManager.nonceLength + DisplayPictureManager.tagLength)) + + guard + cipherTextLength > 0, + let sealedData: AES.GCM.SealedBox = try? AES.GCM.SealedBox( + nonce: AES.GCM.Nonce(data: data.subdata(in: 0..