From 67a54ca911cd7ba30976442d35076709d9f8774c Mon Sep 17 00:00:00 2001 From: Anton Makarov Date: Wed, 18 Apr 2018 13:39:35 +0300 Subject: [PATCH 1/8] 0.2.93 --- Nynja-Share/Resources/Info.plist | 2 +- Nynja/Resources/Info.plist | 2 +- Nynja/Services/MQTT/MQTTService.swift | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Nynja-Share/Resources/Info.plist b/Nynja-Share/Resources/Info.plist index d6920e2f7..cec9217ad 100644 --- a/Nynja-Share/Resources/Info.plist +++ b/Nynja-Share/Resources/Info.plist @@ -19,7 +19,7 @@ CFBundleShortVersionString 1.0 CFBundleVersion - 0.2.92 + 0.2.93 NSExtension NSExtensionAttributes diff --git a/Nynja/Resources/Info.plist b/Nynja/Resources/Info.plist index 6858b196d..7fc58e59a 100644 --- a/Nynja/Resources/Info.plist +++ b/Nynja/Resources/Info.plist @@ -19,7 +19,7 @@ CFBundleShortVersionString 1.0 CFBundleVersion - 0.2.92 + 0.2.93 Fabric APIKey diff --git a/Nynja/Services/MQTT/MQTTService.swift b/Nynja/Services/MQTT/MQTTService.swift index ae74acdbd..0013ef2e4 100644 --- a/Nynja/Services/MQTT/MQTTService.swift +++ b/Nynja/Services/MQTT/MQTTService.swift @@ -23,7 +23,7 @@ class MQTTService: NSObject, CocoaMQTTDelegate, ReachabilityServiceObserver { static let version = 4 - let currentHost = host.DemoTemp + let currentHost = host.Demo let port: UInt16 = 1883 -- GitLab From d05ceb4662f375a96a26863825c58cb56822c2f8 Mon Sep 17 00:00:00 2001 From: Anton Makarov Date: Sun, 17 Jun 2018 14:18:24 +0300 Subject: [PATCH 2/8] 0.2.108 prod --- Nynja-Share/Resources/Info.plist | 2 +- Nynja.xcodeproj/project.pbxproj | 14 ++++++++------ .../xcshareddata/xcschemes/NynjaRelease.xcscheme | 2 -- Nynja/Resources/Info.plist | 2 +- 4 files changed, 10 insertions(+), 10 deletions(-) diff --git a/Nynja-Share/Resources/Info.plist b/Nynja-Share/Resources/Info.plist index 7162d02e7..d7810d106 100644 --- a/Nynja-Share/Resources/Info.plist +++ b/Nynja-Share/Resources/Info.plist @@ -21,7 +21,7 @@ CFBundleShortVersionString 1.0 CFBundleVersion - 0.2.107 + 0.2.108 Config $(Config) NSExtension diff --git a/Nynja.xcodeproj/project.pbxproj b/Nynja.xcodeproj/project.pbxproj index 58eb97ad7..b67fffeb0 100644 --- a/Nynja.xcodeproj/project.pbxproj +++ b/Nynja.xcodeproj/project.pbxproj @@ -12409,10 +12409,11 @@ OTHER_SWIFT_FLAGS = "$(inherited) \"-D\" \"COCOAPODS\" -DSHARE_EXTENSION"; PRODUCT_BUNDLE_IDENTIFIER = "$(ExtensionBundleIdentifier)"; PRODUCT_NAME = "$(TARGET_NAME)"; - PROVISIONING_PROFILE = "0655762a-1aed-40b9-9a35-d41f90f770ff"; - PROVISIONING_PROFILE_SPECIFIER = ProductionBundle_AdhocExt; + PROVISIONING_PROFILE = "987d2d8b-d6f8-4bef-a6cc-30437a126285"; + PROVISIONING_PROFILE_SPECIFIER = ProductionBundle_AppstoreExt; SKIP_INSTALL = YES; - SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule"; + SWIFT_COMPILATION_MODE = wholemodule; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; SWIFT_VERSION = 4.0; TARGETED_DEVICE_FAMILY = "1,2"; }; @@ -12582,10 +12583,11 @@ LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; PRODUCT_BUNDLE_IDENTIFIER = "$(BundleIdentifier)"; PRODUCT_NAME = "$(TARGET_NAME)"; - PROVISIONING_PROFILE = "626e07ae-d6d5-49f1-891f-0ed0ac78c35c"; - PROVISIONING_PROFILE_SPECIFIER = ProductionBundle_Adhoc; + PROVISIONING_PROFILE = "36fef902-bd4f-4af6-8e35-4940cbc31680"; + PROVISIONING_PROFILE_SPECIFIER = ProductionBundle_Appstore; + SWIFT_COMPILATION_MODE = wholemodule; SWIFT_OBJC_BRIDGING_HEADER = "Nynja-Bridging-Header.h"; - SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule"; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; SWIFT_SWIFT3_OBJC_INFERENCE = Off; SWIFT_VERSION = 4.0; SWIFT_WHOLE_MODULE_OPTIMIZATION = YES; diff --git a/Nynja.xcodeproj/xcshareddata/xcschemes/NynjaRelease.xcscheme b/Nynja.xcodeproj/xcshareddata/xcschemes/NynjaRelease.xcscheme index 6d8c6e972..ef29f626a 100644 --- a/Nynja.xcodeproj/xcshareddata/xcschemes/NynjaRelease.xcscheme +++ b/Nynja.xcodeproj/xcshareddata/xcschemes/NynjaRelease.xcscheme @@ -26,7 +26,6 @@ buildConfiguration = "Release" selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB" selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB" - language = "" shouldUseLaunchSchemeArgsEnv = "YES"> CFBundleShortVersionString 1.0 CFBundleVersion - 0.2.107 + 0.2.108 Config $(Config) Fabric -- GitLab From b5e8b29421b83d29f29235be8914b0e09a25bcb1 Mon Sep 17 00:00:00 2001 From: Anton Makarov Date: Sun, 17 Jun 2018 14:30:23 +0300 Subject: [PATCH 3/8] MQTT --- Nynja/Services/MQTT/MQTTService.swift | 114 ++++++++++++++------------ 1 file changed, 62 insertions(+), 52 deletions(-) diff --git a/Nynja/Services/MQTT/MQTTService.swift b/Nynja/Services/MQTT/MQTTService.swift index 0013ef2e4..ac3c5b228 100644 --- a/Nynja/Services/MQTT/MQTTService.swift +++ b/Nynja/Services/MQTT/MQTTService.swift @@ -9,23 +9,23 @@ import Foundation import CocoaMQTT -enum host: String { - case Test = "52.36.43.142" - case Demo = "35.156.90.43" - case DemoTemp = "34.211.25.163" - case neLuboff = "192.168.88.71" - case YuriyOffice = "37.57.71.95" +struct Host { + let url: String + let port: UInt16 + + init() { + url = Bundle.main.serverUrl + port = Bundle.main.serverPort + } } -class MQTTService: NSObject, CocoaMQTTDelegate, ReachabilityServiceObserver { +class MQTTService: NSObject, CocoaMQTTDelegate, ReachabilityServiceObserver { var mqtt: CocoaMQTT? - - static let version = 4 - - let currentHost = host.Demo - let port: UInt16 = 1883 + static let version = Bundle.main.modelsVersion + + let host = Host() var push: String? @@ -37,7 +37,7 @@ class MQTTService: NSObject, CocoaMQTTDelegate, ReachabilityServiceObserver { private var subscribers = MQTTServiceSubscribers() private let subscribersQueue = DispatchQueue(label: "com.nynja.mobile.communicator.mqttservice.subscribers") - let showHandlers : Set = [] // = Set() + let showHandlers : Set = [] static let sharedInstance : MQTTService = { let instance = MQTTService() @@ -45,7 +45,6 @@ class MQTTService: NSObject, CocoaMQTTDelegate, ReachabilityServiceObserver { }() // MARK: - Session State - private let userDefaults: UserDefaults = .standard enum SessionState { @@ -73,19 +72,24 @@ class MQTTService: NSObject, CocoaMQTTDelegate, ReachabilityServiceObserver { } var isAuthenticated: Bool { - return StorageService.sharedInstance.hasToken() + return StorageService.sharedInstance.hasToken } + // MARK: - Init override init () { super.init() + state = fetchCurrentState() + } + + func initialize() { self.setup() myConnect() self.timer = Timer.scheduledTimer(timeInterval: 15, target: self, selector: #selector(runTimedCode), userInfo: nil, repeats: true) } - + func reachabilityStatusChanged(isReachable: Bool) { if !isReachable { mqtt?.disconnect() @@ -94,6 +98,16 @@ class MQTTService: NSObject, CocoaMQTTDelegate, ReachabilityServiceObserver { } } + func reachabilityStatusChanged(from: Network.Status?, to: Network.Status?) { + guard let from = from else { + return + } + + if (from == .wifi && to == .wwan) || (to == .wifi && from == .wwan) { + reconnect() + } + } + let semaphore = DispatchSemaphore(value: 1) func myConnect() { @@ -112,26 +126,27 @@ class MQTTService: NSObject, CocoaMQTTDelegate, ReachabilityServiceObserver { func setup() { ReachabilityService.sharedInstance.addRechabilityObserver(self) - if let token = StorageService.sharedInstance.getToken(), let clientID = StorageService.sharedInstance.getClientID(), token.length != 0 { + + if let token = StorageService.sharedInstance.token, let clientId = StorageService.sharedInstance.clientId, !token.isEmpty { mqtt?.delegate = nil - mqtt = CocoaMQTT(clientID: clientID, host: currentHost.rawValue, port: port) - let tok = String(data: token as Data, encoding: String.Encoding(rawValue: String.Encoding.utf8.rawValue)) - mqtt?.password = tok + mqtt = CocoaMQTT(clientID: clientId, host: host.url, port: host.port) + mqtt?.password = token mqtt?.username = "api" mqtt?.cleanSession = true } else { mqtt?.delegate = nil - mqtt = CocoaMQTT(clientID: "reg_\(deviceId)", host: currentHost.rawValue, port: port) + mqtt = CocoaMQTT(clientID: "reg_\(deviceId)", host: host.url, port: host.port) mqtt?.password = "" mqtt?.username = "api" mqtt?.cleanSession = false } + mqtt?.willMessage = CocoaMQTTWill(topic: "version/\(MQTTService.version)", message: "") mqtt?.keepAlive = 0 mqtt?.delegate = self print("--- setup clientID: \(mqtt?.clientID ?? "") and password: \(mqtt?.password ?? "")") } - + var isConnectedSuccess = false func mqtt(_ mqtt: CocoaMQTT, didConnect host: String, port: Int) { @@ -142,22 +157,17 @@ class MQTTService: NSObject, CocoaMQTTDelegate, ReachabilityServiceObserver { func mqtt(_ mqtt: CocoaMQTT, didConnectAck ack: CocoaMQTTConnAck) { if ack == .badUsernameOrPassword { - - if let actualToken = StorageService.sharedInstance.getToken() { - let actualPass = String(data: actualToken as Data, encoding: String.Encoding(rawValue: String.Encoding.utf8.rawValue)) - if actualPass == mqtt.password { + if let actualToken = StorageService.sharedInstance.token { + if actualToken == mqtt.password { #if !SHARE_EXTENSION - StorageService.sharedInstance.clearDatabase() - StorageService.sharedInstance.dropToken() - StorageService.sharedInstance.dropProfileID() - StorageService.sharedInstance.dropClientID() - - self.state = .notAuthenticated(isLoggedOutFromServer: true) - - MQTTService.sharedInstance.queue = Queue() - IoHandler.delegate?.sessionNotFound() - notifySubscribersDisconnect() - self.reconnect() + StorageService.sharedInstance.clearStorage() + + self.state = .notAuthenticated(isLoggedOutFromServer: true) + + MQTTService.sharedInstance.queue = Queue() + IoHandler.delegate?.sessionNotFound() + notifySubscribersDisconnect() + self.reconnect() #endif print("WTF>>>>>>>>>>>>>>>>>>>>>>>>>") } @@ -170,9 +180,9 @@ class MQTTService: NSObject, CocoaMQTTDelegate, ReachabilityServiceObserver { isConnectedSuccess = true #if !SHARE_EXTENSION -// if shouldResendLogout { -// logout() -// } + // if shouldResendLogout { + // logout() + // } #endif notifySubscribersConnect() @@ -197,20 +207,20 @@ class MQTTService: NSObject, CocoaMQTTDelegate, ReachabilityServiceObserver { private var shouldResendLogout: Bool { let storage = StorageService.sharedInstance - if !storage.hasToken(), case let .notAuthenticated(isLoggedOutFromServer) = self.state, !isLoggedOutFromServer { + if !storage.hasToken, case let .notAuthenticated(isLoggedOutFromServer) = self.state, !isLoggedOutFromServer { return true } return false } - -// func sendOld() { -// if !waitUpdatingToken { -// while let model = queue.dequeue() { -// publish(model: model) -// } -// } -// } + + // func sendOld() { + // if !waitUpdatingToken { + // while let model = queue.dequeue() { + // publish(model: model) + // } + // } + // } func reconnectWithTimer() { timer = Timer.scheduledTimer(timeInterval: 5, target: self, selector: #selector(runTimedCode), userInfo: nil, repeats: true) @@ -226,14 +236,14 @@ class MQTTService: NSObject, CocoaMQTTDelegate, ReachabilityServiceObserver { timer?.invalidate() mqtt?.disconnect() } - + func mqtt(_ mqtt: CocoaMQTT, didPublishMessage message: CocoaMQTTMessage, id: UInt16) { printMessage(msg: message, isSent: true) } func mqtt(_ mqtt: CocoaMQTT, didReceiveMessage message: CocoaMQTTMessage, id: UInt16 ) { - printMessage(msg: message, isSent: false) + printMessage(msg: message, isSent: false) HandlerService.handle(response: message) } @@ -241,7 +251,7 @@ class MQTTService: NSObject, CocoaMQTTDelegate, ReachabilityServiceObserver { print("--- Connection closed :( \n cleanSession: \(mqtt.cleanSession) username:\(mqtt.username ?? "") password: \(mqtt.password ?? "") clientID: \(mqtt.clientID )") if let error = err as NSError?, error.code == 7 { print("ATTTTTTTENTION \n\n\n AAAAA") - } + } } func mqtt(_ mqtt: CocoaMQTT, didSubscribeTopic topic: String) {} -- GitLab From c555eeec7827e0dcb5a38b4c07452938082d4d74 Mon Sep 17 00:00:00 2001 From: Anton Makarov Date: Wed, 1 Aug 2018 20:37:57 +0300 Subject: [PATCH 4/8] 0.2.108 Migration Start --- Nynja-Share/Resources/Info.plist | 118 +++++++----- Nynja.xcodeproj/project.pbxproj | 14 +- Nynja/Resources/DevConfig.xcconfig | 2 +- Nynja/Resources/Info.plist | 210 ++++++++++++---------- Nynja/Resources/PrereleaseConfig.xcconfig | 2 +- Nynja/Resources/ReleaseConfig.xcconfig | 2 +- 6 files changed, 193 insertions(+), 155 deletions(-) diff --git a/Nynja-Share/Resources/Info.plist b/Nynja-Share/Resources/Info.plist index cec9217ad..98b23533b 100644 --- a/Nynja-Share/Resources/Info.plist +++ b/Nynja-Share/Resources/Info.plist @@ -1,53 +1,73 @@ - - CFBundleDevelopmentRegion - $(DEVELOPMENT_LANGUAGE) - CFBundleDisplayName - Nynja - CFBundleExecutable - $(EXECUTABLE_NAME) - CFBundleIdentifier - $(PRODUCT_BUNDLE_IDENTIFIER) - CFBundleInfoDictionaryVersion - 6.0 - CFBundleName - $(PRODUCT_NAME) - CFBundlePackageType - XPC! - CFBundleShortVersionString - 1.0 - CFBundleVersion - 0.2.93 - NSExtension - - NSExtensionAttributes - - NSExtensionActivationRule - - NSExtensionActivationSupportsFileWithMaxCount - 1 - NSExtensionActivationSupportsImageWithMaxCount - 1 - NSExtensionActivationSupportsMovieWithMaxCount - 1 - NSExtensionActivationSupportsText - - NSExtensionActivationSupportsWebPageWithMaxCount - 1 - NSExtensionActivationSupportsWebURLWithMaxCount - 1 - - - NSExtensionMainStoryboard - MainInterface - NSExtensionPointIdentifier - com.apple.share-services - - UISupportedInterfaceOrientations - - UIInterfaceOrientationPortrait - - + + AppGroup + $(AppGroup) + CFBundleDevelopmentRegion + $(DEVELOPMENT_LANGUAGE) + CFBundleDisplayName + $(AppName) + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + XPC! + CFBundleShortVersionString + 1.0 + CFBundleVersion + 0.2.108 + Config + $(Config) + ModelsVersion + $(ModelsVersion) + NSExtension + + NSExtensionAttributes + + NSExtensionActivationRule + + NSExtensionActivationSupportsFileWithMaxCount + 1 + NSExtensionActivationSupportsImageWithMaxCount + 1 + NSExtensionActivationSupportsMovieWithMaxCount + 1 + NSExtensionActivationSupportsText + + NSExtensionActivationSupportsWebPageWithMaxCount + 1 + NSExtensionActivationSupportsWebURLWithMaxCount + 1 + + + NSExtensionMainStoryboard + MainInterface + NSExtensionPointIdentifier + com.apple.share-services + + ServerPort + $(ServerPort) + ServerURL + $(ServerURL) + UIAppFonts + + Avenir.ttc + LatoBlack.ttf + Myriad Pro Regular.ttf + NotoSans-Bold.ttf + NotoSans-Regular.ttf + NotoSans-Medium.ttf + NotoSans-Italic.ttf + + UISupportedInterfaceOrientations + + UIInterfaceOrientationPortrait + + diff --git a/Nynja.xcodeproj/project.pbxproj b/Nynja.xcodeproj/project.pbxproj index b67fffeb0..a00221524 100644 --- a/Nynja.xcodeproj/project.pbxproj +++ b/Nynja.xcodeproj/project.pbxproj @@ -12368,8 +12368,8 @@ CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; CODE_SIGN_ENTITLEMENTS = "Nynja-Share/Resources/Nynja-Share.entitlements"; - CODE_SIGN_IDENTITY = "iPhone Developer"; - "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + CODE_SIGN_IDENTITY = "iPhone Distribution"; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution"; CODE_SIGN_STYLE = Manual; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEVELOPMENT_TEAM = 9GKQ5AMF2B; @@ -12381,8 +12381,8 @@ OTHER_SWIFT_FLAGS = "$(inherited) \"-D\" \"COCOAPODS\" -DSHARE_EXTENSION"; PRODUCT_BUNDLE_IDENTIFIER = "$(ExtensionBundleIdentifier)"; PRODUCT_NAME = "$(TARGET_NAME)"; - PROVISIONING_PROFILE = "a8b63d08-5002-44f0-94d3-87ddff72770a"; - PROVISIONING_PROFILE_SPECIFIER = DevBundle_DevExt; + PROVISIONING_PROFILE = "99725127-a6e8-4f22-98eb-18164dfae6db"; + PROVISIONING_PROFILE_SPECIFIER = DevBundle_AdHocExt; SKIP_INSTALL = YES; SWIFT_VERSION = 4.0; TARGETED_DEVICE_FAMILY = "1,2"; @@ -12544,7 +12544,7 @@ CLANG_ENABLE_MODULES = YES; CODE_SIGN_ENTITLEMENTS = Nynja/Resources/Nynja.entitlements; CODE_SIGN_IDENTITY = "iPhone Distribution"; - "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution"; CODE_SIGN_STYLE = Manual; DEFINES_MODULE = YES; DEVELOPMENT_TEAM = 9GKQ5AMF2B; @@ -12554,8 +12554,8 @@ LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; PRODUCT_BUNDLE_IDENTIFIER = "$(BundleIdentifier)"; PRODUCT_NAME = "$(TARGET_NAME)"; - PROVISIONING_PROFILE = "74055a04-d4a5-4b52-910d-ca98d6c1666e"; - PROVISIONING_PROFILE_SPECIFIER = DevBundle_Dev; + PROVISIONING_PROFILE = "50dd2a41-0a8c-4a0a-a8b9-744fa4bf3bb4"; + PROVISIONING_PROFILE_SPECIFIER = DevBundle_adhoc; SWIFT_OBJC_BRIDGING_HEADER = "Nynja-Bridging-Header.h"; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; SWIFT_SWIFT3_OBJC_INFERENCE = Off; diff --git a/Nynja/Resources/DevConfig.xcconfig b/Nynja/Resources/DevConfig.xcconfig index 8192feb70..5a0f3293b 100644 --- a/Nynja/Resources/DevConfig.xcconfig +++ b/Nynja/Resources/DevConfig.xcconfig @@ -9,7 +9,7 @@ BundleIdentifier = com.nynja.dev.mobile.communicator ExtensionBundleIdentifier = com.nynja.dev.mobile.communicator.NynjaShare -ServerURL = 54.201.154.141 +ServerURL = 52.42.76.167 AppName = NYNJADev ServerPort = 1883 Config = dev diff --git a/Nynja/Resources/Info.plist b/Nynja/Resources/Info.plist index 7fc58e59a..31601dda1 100644 --- a/Nynja/Resources/Info.plist +++ b/Nynja/Resources/Info.plist @@ -1,100 +1,118 @@ - - CFBundleDevelopmentRegion - en - CFBundleDisplayName - NYNJA - CFBundleExecutable - $(EXECUTABLE_NAME) - CFBundleIdentifier - $(PRODUCT_BUNDLE_IDENTIFIER) - CFBundleInfoDictionaryVersion - 6.0 - CFBundleName - $(PRODUCT_NAME) - CFBundlePackageType - APPL - CFBundleShortVersionString - 1.0 - CFBundleVersion - 0.2.93 - Fabric - - APIKey - 595b68a8c4deb3533dcdfc24ca73fd3cffd99f3c - Kits - - - KitInfo - - KitName - Crashlytics - - - - LSApplicationQueriesSchemes - - comgooglemaps - googlechromes - - LSRequiresIPhoneOS - - NSAppTransportSecurity - - NSAllowsArbitraryLoads - - - NSCameraUsageDescription - NYNJA needs it to allow you make photos and video calls. - NSContactsUsageDescription - NYNJA needs it to allow you add new contacts from your phone contact book. - NSLocationAlwaysUsageDescription - NYNJA needs to know your location so that you can be able to share it. - NSLocationWhenInUseUsageDescription - NYNJA needs to know your location so that you can be able to share it. - NSMicrophoneUsageDescription - NYNJA needs it to allow you send audio messages and make voice calls. - NSPhotoLibraryUsageDescription - NYNJA needs it so that you can use your local images. - UIAppFonts - - Avenir.ttc - LatoBlack.ttf - Myriad Pro Regular.ttf - NotoSans-Bold.ttf - NotoSans-Regular.ttf - NotoSans-Medium.ttf - NotoSans-Italic.ttf - - UIBackgroundModes - - audio - fetch - remote-notification - voip - - UILaunchStoryboardName - LaunchScreen - UIRequiredDeviceCapabilities - - armv7 - - UIStatusBarStyle - UIStatusBarStyleLightContent - UISupportedInterfaceOrientations - - UIInterfaceOrientationPortrait - - UISupportedInterfaceOrientations~ipad - - UIInterfaceOrientationPortrait - UIInterfaceOrientationPortraitUpsideDown - UIInterfaceOrientationLandscapeLeft - UIInterfaceOrientationLandscapeRight - - UIViewControllerBasedStatusBarAppearance - - + + AppGroup + $(AppGroup) + CFBundleDevelopmentRegion + en + CFBundleDisplayName + $(AppName) + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + APPL + CFBundleShortVersionString + 1.0 + CFBundleVersion + 0.2.108 + ConfServerAddress + $(ConfServerAddress) + ConfServerPort + $(ConfServerPort) + Config + $(Config) + Fabric + + APIKey + 595b68a8c4deb3533dcdfc24ca73fd3cffd99f3c + Kits + + + KitInfo + + KitName + Crashlytics + + + + LSApplicationQueriesSchemes + + cydia + comgooglemapsurl + comgooglemaps + googlechromes + + LSRequiresIPhoneOS + + ModelsVersion + $(ModelsVersion) + NSAppTransportSecurity + + NSAllowsArbitraryLoads + + + NSCameraUsageDescription + NYNJA needs it to allow you make photos and video calls. + NSContactsUsageDescription + NYNJA needs it to allow you add new contacts from your phone contact book. + NSLocationAlwaysUsageDescription + NYNJA needs to know your location so that you can be able to share it. + NSLocationWhenInUseUsageDescription + NYNJA needs to know your location so that you can be able to share it. + NSMicrophoneUsageDescription + NYNJA needs it to allow you send audio messages and make voice calls. + NSPhotoLibraryAddUsageDescription + NYNJA needs it to save photos and video to your device. + NSPhotoLibraryUsageDescription + NYNJA needs it so that you can use your local images. + ServerPort + $(ServerPort) + ServerURL + $(ServerURL) + UIAppFonts + + Avenir.ttc + LatoBlack.ttf + Myriad Pro Regular.ttf + NotoSans-Bold.ttf + NotoSans-Regular.ttf + NotoSans-Medium.ttf + NotoSans-Italic.ttf + + UIBackgroundModes + + audio + fetch + remote-notification + voip + + UILaunchStoryboardName + LaunchScreen + UIRequiredDeviceCapabilities + + armv7 + + UIStatusBarStyle + UIStatusBarStyleLightContent + UISupportedInterfaceOrientations + + UIInterfaceOrientationPortrait + + UISupportedInterfaceOrientations~ipad + + UIInterfaceOrientationPortrait + UIInterfaceOrientationPortraitUpsideDown + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + UIViewControllerBasedStatusBarAppearance + + diff --git a/Nynja/Resources/PrereleaseConfig.xcconfig b/Nynja/Resources/PrereleaseConfig.xcconfig index d0dd36a25..15705d03b 100644 --- a/Nynja/Resources/PrereleaseConfig.xcconfig +++ b/Nynja/Resources/PrereleaseConfig.xcconfig @@ -9,7 +9,7 @@ BundleIdentifier = com.nynja.rc.mobile.communicator ExtensionBundleIdentifier = com.nynja.rc.mobile.communicator.NynjaShare -ServerURL = 34.211.25.163 +ServerURL = 52.12.24.0 AppName = NYNJARC ServerPort = 1883 Config = prerelease diff --git a/Nynja/Resources/ReleaseConfig.xcconfig b/Nynja/Resources/ReleaseConfig.xcconfig index 93e5b78e0..1d3706cf3 100644 --- a/Nynja/Resources/ReleaseConfig.xcconfig +++ b/Nynja/Resources/ReleaseConfig.xcconfig @@ -9,7 +9,7 @@ BundleIdentifier = com.nynja.mobile.communicator ExtensionBundleIdentifier = com.nynja.mobile.communicator.Nynja-Share -ServerURL = 35.156.90.43 +ServerURL = 52.12.24.0 AppName = NYNJA ServerPort = 1883 Config = release -- GitLab From fb11265827299ec8ca9716a8411b6ad49a564dea Mon Sep 17 00:00:00 2001 From: Anton Makarov Date: Thu, 6 Sep 2018 20:33:29 +0300 Subject: [PATCH 5/8] 0.2.153 (#1213) --- .../LogViewer.xcodeproj/project.pbxproj | 425 +++++ .../contents.xcworkspacedata | 7 + .../xcshareddata/IDEWorkspaceChecks.plist | 8 + .../contents.xcworkspacedata | 10 + .../xcshareddata/IDEWorkspaceChecks.plist | 8 + .../LogViewer/LogViewer/AppDelegate.swift | 26 + .../LogViewer/LogViewer/ArrayExtension.swift | 101 ++ .../AppIcon.appiconset/Contents.json | 58 + .../LogViewer/Assets.xcassets/Contents.json | 6 + .../LogViewer/Base.lproj/Main.storyboard | 116 ++ Frameworks/LogViewer/LogViewer/Bert.swift | 771 +++++++++ .../LogViewer/LogViewer/FileMenuCell.swift | 37 + .../LogViewer/LogViewer/FileMenuDS.swift | 33 + .../LogViewer/LogViewer/FileReader.swift | 69 + Frameworks/LogViewer/LogViewer/Info.plist | 32 + Frameworks/LogViewer/LogViewer/LogDS.swift | 58 + .../LogViewer/LogViewer.entitlements | 10 + .../LogViewer/LogViewer/StreamReader.swift | 89 + .../LogViewer/LogViewer/ViewController.swift | 236 +++ Frameworks/LogViewer/Podfile | 10 + .../NynjaUIKit.xcodeproj/project.pbxproj | 36 +- .../UICollectionView+ViewModel.swift | 1 + .../SupplementaryViewModel.swift | 2 +- Nynja-Share/Resources/Info.plist | 2 +- .../Services/Handlers/ContactHandler.swift | 15 +- Nynja-Share/UI/ActionsView.swift | 2 +- .../UI/ForwardSelectorInteractor.swift | 4 +- Nynja.xcodeproj/project.pbxproj | 1481 ++++++----------- .../xcschemes/NynjaChannels.xcscheme | 112 -- .../xcschemes/NynjaSticker.xcscheme | 101 -- .../xcschemes/NynjaTranslate.xcscheme | 112 -- Nynja/AmazonManager.swift | 8 +- Nynja/AppDelegate.swift | 10 +- Nynja/AudioManager.swift | 106 +- Nynja/AudioRecorder.swift | 4 +- Nynja/AuthHandler.swift | 2 +- Nynja/BadgeNumberService.swift | 5 +- Nynja/ChatBaseFactory.swift | 10 +- Nynja/ChatService.swift | 164 -- Nynja/ChatService/ChatService.swift | 249 +++ Nynja/CircleMenuControl/Core/CircleMenu.swift | 8 +- .../Core/Sector/SectionView.swift | 12 +- Nynja/ConnectionSubscriberService.swift | 4 +- Nynja/ContactDAO.swift | 32 +- Nynja/ContactDAOProtocol.swift | 4 +- Nynja/ConversationsProvider.swift | 4 +- Nynja/ConversationsProviding.swift | 1 + Nynja/ConvertMessage/ConvertMessageDAO.swift | 27 + .../ConvertMessageDAOProtocol.swift | 18 + Nynja/DB/Extensions/DatabaseExtension.swift | 32 +- Nynja/DB/Models/Base/DBModelProtocol.swift | 7 +- Nynja/DB/Models/DBContact.swift | 4 +- Nynja/DB/Models/DBContact.swift.orig | 171 -- Nynja/DB/Models/DBConvertMessage.swift | 98 ++ Nynja/DB/Models/DBJob.swift | 6 +- Nynja/DB/Models/DBLink.swift | 1 - Nynja/DB/Models/DBMessage.swift | 123 +- Nynja/DB/Models/DBMessageAction.swift | 16 +- Nynja/DB/Models/DBMessageEditAction.swift | 10 +- Nynja/DB/Models/DBRoom.swift | 8 +- Nynja/DB/Models/DBRoom.swift.orig | 210 --- Nynja/DB/Models/DBStar.swift | 31 +- Nynja/DB/Models/DBStarAction.swift | 45 + Nynja/DB/Models/DBStarMessage.swift | 2 +- Nynja/DB/Tables/Base/Table.swift | 26 +- Nynja/DB/Tables/Base/TableOrder.swift | 12 + Nynja/DB/Tables/ChatCheckpointTable.swift | 5 +- Nynja/DB/Tables/ContactTable.swift | 6 +- Nynja/DB/Tables/ConvertMessageTable.swift | 40 + Nynja/DB/Tables/DescTable.swift | 6 +- Nynja/DB/Tables/FeatureTable.swift | 8 +- Nynja/DB/Tables/JobMessageTable.swift | 10 +- Nynja/DB/Tables/JobTable.swift | 10 +- Nynja/DB/Tables/LinkTable.swift | 6 +- Nynja/DB/Tables/MemberTable.swift | 7 +- Nynja/DB/Tables/MessageActionTable.swift | 12 +- Nynja/DB/Tables/MessageEditActionTable.swift | 6 +- Nynja/DB/Tables/MessageLinkTable.swift | 8 +- Nynja/DB/Tables/MessageTable.swift | 6 +- Nynja/DB/Tables/MucTable.swift | 8 +- Nynja/DB/Tables/P2pTable.swift | 8 +- Nynja/DB/Tables/ProfileTable.swift | 6 +- Nynja/DB/Tables/RecentStickerTable.swift | 2 +- Nynja/DB/Tables/RoomMemberTable.swift | 6 +- Nynja/DB/Tables/RoomTable.swift | 5 +- Nynja/DB/Tables/RosterTable.swift | 8 +- Nynja/DB/Tables/ServiceTable.swift | 7 +- Nynja/DB/Tables/StarActionTable.swift | 36 + Nynja/DB/Tables/StarMessageTable.swift | 10 +- Nynja/DB/Tables/StarTable.swift | 8 +- Nynja/DB/Tables/StickerPackTable.swift | 2 +- Nynja/DB/Tables/SyncFileTable.swift | 6 +- Nynja/DB/Tables/TagTable.swift | 7 +- Nynja/DBManagerProtocol.swift | 6 +- Nynja/DatabaseManager.swift | 114 +- Nynja/Debug/DebugLogs.swift | 2 +- Nynja/DefaultMessageProcessingManager.swift | 29 +- Nynja/ExtendedStarHandler.swift | 4 + .../Extensions/BERT/StringAtomExtension.swift | 1 - Nynja/Extensions/CollectionsExtensions.swift | 2 +- Nynja/Extensions/Date+Extension.swift | 4 + Nynja/Extensions/JobExtension+BERT.swift | 7 +- .../Models/Desc/Desc+Construct.swift | 20 + .../Models/Desc/Desc+Messages.swift | 6 + .../Models/Desc/DescExtension.swift | 12 +- .../Models/Feature/FeatureExtension.swift | 13 + Nynja/Extensions/Models/JobExtension.swift | 8 +- Nynja/Extensions/Models/MemberExtension.swift | 4 + .../Models/Message/Message+DB.swift | 9 +- Nynja/Extensions/Models/P2P+DB.swift | 1 - Nynja/Extensions/Models/P2P+Opponent.swift | 21 + Nynja/Extensions/Models/P2pExtension.swift | 13 - Nynja/Extensions/Models/Room/Room+DB.swift | 6 +- Nynja/Extensions/Models/StarExtension.swift | 6 +- Nynja/Extensions/Range+Extension.swift | 24 + ...on.swift => BidirectionalCollection.swift} | 33 +- .../SwiftLibrary/Collection/Collection.swift | 32 + .../SwiftLibrary/String/String+Search.swift | 10 + Nynja/FeedDAO.swift | 14 + Nynja/FileManager.swift | 12 +- ...istoryRequestModelTypeRepresentable.swift} | 9 +- Nynja/HomeItemsFactory.swift | 2 +- Nynja/IdBuilder/IdBuilder.swift | 2 +- Nynja/KeychainService/KeychainService.swift | 4 + .../QueryFactory/QueryFactory.swift | 7 + .../MessageFactory/MessageFactory.swift | 23 +- Nynja/Library/UI/BaseVC/BaseVC.swift | 6 +- Nynja/Library/UI/BaseVC/BaseVC.swift.orig | 204 --- .../UI/BaseVC/LoadingInteractive.swift | 17 + .../ActionsView/ActionsView.swift | 2 +- .../Buttons/NynjaButton/BaseNynjaButton.swift | 4 +- .../NynjaContextMenuItemsFactory+Design.swift | 2 +- ...ynjaContextMenuItemsFactory+Messages.swift | 2 + .../Library/UI/Extensions/String+Split.swift | 44 + .../UI/Extensions/StringExtensions.swift | 37 +- .../UI/Extensions/UI/LabelExtensions.swift | 2 +- .../UI/Extensions/UI/UIFontExtension.swift | 27 +- .../UI/UIImageView/UIImageView+SetImage.swift | 42 - .../Library/UI/Extensions/URLExtensions.swift | 8 + .../ImagePreviewTransitionController.swift | 27 +- .../UI/ItemSelector/ItemSelectorCell.swift | 1 - .../UI/ItemSelector/ItemsSelector.swift | 16 +- .../EmptyStateView/CollectionState.swift | 2 +- .../Lists/EmptyStateView/EmptyStateView.swift | 13 +- .../EmptyStateView/EmptyStateViewModel.swift | 6 + .../Cell/ChatListMessageAccessoryView.swift | 2 +- .../Cell/ChatListMessageContentView.swift | 4 +- .../Cell/CounterView.swift | 2 +- .../DataSource/EmptyStateTableViewDS.swift | 16 +- .../MentionCounter/MentionCounterView.swift | 2 +- .../Library/UI/ReturnToCallContentView.swift | 124 +- Nynja/Library/UI/ReturnToCallView.swift | 2 +- .../UI/TextInput/InputBar/InputBar.swift | 22 +- .../InputContent/TextInputContent.swift | 4 +- .../UI/TextInput/InputField/CodeField.swift | 2 +- .../InputField/NynjaSearchField.swift | 5 + .../TextInputBar/ALTextInputBar.swift | 18 +- .../Material/Config/NynjaMTIConfig.swift | 6 +- .../Material/MaterialTextField.swift | 8 +- .../Wheel/ItemModels/WheelItemModel.swift | 2 +- .../Subviews/MediaInfoView.swift | 6 +- .../WheelImageFullItemPreview.swift | 27 +- .../UI/WheelContainer/WheelContainer.swift | 23 +- Nynja/LogService/LogService.swift | 53 +- Nynja/LogService/LogWriter.swift | 76 + Nynja/MessageActionDAO.swift | 19 +- Nynja/MessageActionDAOProtocol.swift | 5 +- Nynja/MessageBackgroundTaskHandler.swift | 144 +- Nynja/MessageDAO.swift | 97 +- Nynja/MessageDAOProtocol.swift | 17 +- Nynja/MessageEditActionDAO.swift | 15 +- Nynja/MessageEditActionDAOProtocol.swift | 4 +- .../MessageEditService.swift | 4 +- Nynja/MessagesProcessingManager.swift | 2 + Nynja/MigrationManager.swift | 120 +- Nynja/Models/ChatModel.swift | 2 +- .../AddContactByUsernameProtocols.swift | 9 +- .../AddContactByUsernameInteractor.swift | 79 +- .../AddContactByUsernamePresenter.swift | 13 +- .../AddContactByUsernameViewController.swift | 5 +- .../AddContactByUsernameWireframe.swift | 2 +- .../AddContactViaPhoneProtocols.swift | 4 +- .../AddContactViaPhoneInteractor.swift | 76 +- .../AddContactViaPhonePresenter.swift | 10 +- .../AddContactViaPhoneViewController.swift | 4 +- .../AddContactViaPhoneWireframe.swift | 2 +- .../AddParticipantsProtocols.swift | 8 +- .../AddParticipantsInteractor.swift | 50 +- .../Presenter/AddParticipantsPresenter.swift | 21 +- .../View/AddParticipantsViewController.swift | 77 +- .../View/ParticipantsActionsDelegate.swift | 2 + .../View/TableView/ParticipantsDelegate.swift | 12 + .../WireFrame/AddParticipantsWireframe.swift | 10 +- .../View/AsigningInterpreterLayout.swift | 4 +- .../Auth/Interactor/AuthInteractor.swift | 8 +- Nynja/Modules/Auth/View/LoginView.swift | 2 +- .../Call/CallInProgressProtocols.swift | 29 +- .../Interactor/CallInProgressInteractor.swift | 133 +- .../Presenter/CallInProgressPresenter.swift | 71 +- .../CallInProgressNavigationController.swift | 35 - .../View/CallInProgressViewController.swift | 149 +- .../WireFrame/CallInProgressWireframe.swift | 9 +- Nynja/Modules/Call/View/BottomCallView.swift | 2 + .../Interactor/NewChannelInteractor.swift | 22 +- .../SubscribersSelectorPresenter.swift | 2 +- .../SubscribersSelectorViewController.swift | 2 +- .../Presenter/ChannelsListPresenter.swift | 2 +- .../View/ChannelsListViewController.swift | 6 +- .../ChatsList/ChatsListProtocols.swift | 6 +- .../Interactor/ChatsListInteractor.swift | 84 +- .../Presenter/ChatsListPresenter.swift | 44 +- .../View/ChatsListViewController.swift | 97 +- .../WireFrame/ChatsListWireframe.swift | 8 +- .../Interactor/ContactsInteractor.swift | 2 +- .../View/TableView/Cell/ContactCell.swift | 2 +- .../ContactsViewController.swift | 3 - .../Interactor/CreateGroupInteractor.swift | 18 +- .../Interactor/FavoritesInteractor.swift | 34 +- .../View/FavoritesViewController.swift | 32 +- .../CameraVideoPreviewInteractor.swift | 4 +- .../Interactor/GalleryInteractor.swift | 4 +- .../MultiplePreviewInteractor.swift | 2 +- .../ForwardSelectorProtocols.swift | 1 - .../ForwardSelectorInteractor.swift | 13 - .../Presenter/ForwardSelectorPresenter.swift | 6 +- .../View/GroupRulesViewController.swift | 2 +- .../GroupStorage/GroupStorageProtocols.swift | 7 +- .../Interactor/GroupStorageInteractor.swift | 66 +- .../Presenter/GroupStorageListItems.swift | 33 +- .../Presenter/GroupStoragePresenter.swift | 10 +- .../Collection/GroupStorageCollectionVC.swift | 4 +- .../View/Collection/GroupVideosCell.swift | 2 +- .../View/Files/GroupFilesCell.swift | 4 +- .../View/Files/GroupFilesListVC.swift | 4 +- .../GroupStorage/View/GroupLinksListVC.swift | 6 +- .../GroupStorage/View/GroupStorageCell.swift | 2 + .../View/GroupStorageViewController.swift | 15 +- .../WireFrame/GroupStorageWireframe.swift | 4 +- .../GroupsList/GroupsListProtocols.swift | 6 +- .../Interactor/GroupsListInteractor.swift | 36 +- .../Presenter/GroupsListPresenter.swift | 32 +- .../View/GroupsListViewController.swift | 96 +- .../History/View/HistoryCell.swift.orig | 233 --- .../History/View/HistoryViewController.swift | 3 - .../View/ImagePreviewViewController.swift | 30 +- .../AlertTextFieldViewController.swift | 1 + .../AlertTextFieldViewControllerLayout.swift | 6 +- .../View/InterpretationLayout.swift | 4 +- .../View/LanguagePickerDelegate.swift | 2 +- .../Cell/InterpretationTypeCellLayout.swift | 6 +- .../View/LanguageSelectorViewController.swift | 4 - .../ChatLanguageSettingsPresenter.swift | 17 +- .../Interactor/LogOutputInteractor.swift | 39 + .../LogOutput/LogOutputProtocols.swift | 63 + .../Presenter/LogOutputPresenter.swift | 33 + .../Modules/LogOutput/View/LogOutputDS.swift | 36 + .../LogOutput/View/LogOutputView.swift | 121 ++ .../LogOutput/View/Logoutputcell.swift | 43 + .../Wireframe/LogOutputWireFrame.swift | 37 + .../Main/Interactor/MainInteractor.swift | 36 +- .../Main/Interactor/MainInteractor.swift.orig | 99 -- Nynja/Modules/Main/MainProtocols.swift | 11 +- .../Main/Presenter/MainPresenter.swift | 39 +- .../Main/View/MainNavigationItem.swift | 11 +- .../View/MainViewController+Container.swift | 11 +- .../MainViewController+NavigateProtocol.swift | 9 +- .../Main/View/MainViewController.swift | 12 +- .../Modules/Main/View/NavigateProtocol.swift | 1 + .../Main/WireFrame/MainWireframe.swift | 82 +- .../Modules/Map/View/MapViewController.swift | 2 +- .../Interactor/MessageInteractor+Fetch.swift | 23 +- .../MessageInteractor+Forward.swift | 50 + .../MessageInteractor+History.swift | 107 ++ .../MessageInteractor+Mentions.swift | 2 +- .../MessageInteractor+StorageSubscriber.swift | 110 +- .../MessageInteractor+Transcription.swift | 117 +- .../Interactor/MessageInteractor.swift | 276 +-- .../Configurations/ChatConfiguration.swift | 5 +- .../InputController/MentionController.swift | 65 +- .../Message/Presenter/MessagePresenter.swift | 118 +- .../Message/Protocols/MessageProtocols.swift | 34 +- .../Protocols/VoiceAudioInteractive.swift | 118 ++ .../Message/View/MessageVC+CellDelegate.swift | 104 +- Nynja/Modules/Message/View/MessageVC.swift | 94 +- .../Message/View/MessageVCLayout.swift | 2 +- .../Views/CallInfoView/CallInfoView.swift | 4 +- ...essageCollectionViewLayoutAttributes.swift | 16 + ...essageCollectionViewLayoutAttributes.swift | 16 + .../Layout/MessageCollectionViewLayout.swift | 42 + ...ReversedMessageCollectionViewLayout.swift} | 63 +- .../MessageCollectionViewDataSource.swift | 36 +- .../MessageCollectionViewDelegate.swift | 12 +- .../CollectionView/ProgressIdentifier.swift | 18 + .../Panel/MentionPanelView.swift | 18 +- .../ChatCells/BaseChatCell/BaseChatCell.swift | 36 +- .../BaseChatCell/BaseChatCellLayout.swift | 3 +- .../BaseChatCell/OponentChatCell.swift | 12 +- .../Cells/Models/BaseChatCellModel.swift | 2 +- .../Cells/Models/RepliedMessageModel.swift | 7 + .../Views/TableView/Cells/SystemCell.swift | 28 +- .../View/Views/TableView/Cells/TimeCell.swift | 37 +- .../Views/TableView/Cells/UnreadCell.swift | 36 +- .../Views/Base/MessageBaseImageView.swift | 2 +- .../Views/Converting/ConvertionInfoView.swift | 4 +- .../Cells/Views/FileTransferInfoView.swift | 2 +- .../Views/Message/MessageContactView.swift | 54 +- .../Cells/Views/Message/MessageFileView.swift | 2 +- .../Views/Message/MessageForwardView.swift | 2 +- .../Views/Message/MessageImageView.swift | 6 +- .../Views/Message/MessagePaymentView.swift | 4 +- .../Cells/Views/Message/MessageTextView.swift | 2 +- .../Views/Message/MessageVideoView.swift | 6 +- .../Views/Message/MessageVoiceView.swift | 3 + .../Views/Reply/MessageRepliedView.swift | 7 +- .../Cells/Views/Reply/ReplyInfoView.swift | 4 +- .../Message/WireFrame/MessageWireframe.swift | 4 +- .../Interactor/OtherUserInteractor.swift | 2 +- .../OtherUser/OtherUserProtocols.swift.orig | 109 -- .../Presenter/OtherUserPresenter.swift.orig | 170 -- .../View/TableView/OtherUserTableViewDS.swift | 2 +- .../WireFrame/OtherUserWireFrame.swift | 2 +- .../View/QRCodeReaderViewController.swift | 6 +- .../Replies/View/RepliesVC+CellDelegate.swift | 8 +- Nynja/Modules/Replies/View/RepliesVC.swift | 21 +- .../ScheduleMessageInteractor.swift | 9 +- .../Views/MessageContent/AudioItemView.swift | 2 +- .../Views/MessageContent/TextItemView.swift | 2 +- .../Interactor/SelectCountryInteractor.swift | 2 +- .../DataAndStorageTableDelegate.swift | 2 +- .../NotificationAlertSoundsInteractor.swift | 2 +- .../Interactor/SettingsGroupInteractor.swift | 8 +- .../Presenter/SettingsGroupPresenter.swift | 2 +- .../WireFrame/SettingsGroupWireFrame.swift | 2 +- .../Splash/Interactor/SplashInteractor.swift | 11 +- .../Interactor/SplashInteractor.swift.orig | 68 - .../Splash/View/SplashViewController.swift | 4 +- .../Splash/WireFrame/SplashWireframe.swift | 1 - .../Interactor/StickersInputInteractor.swift | 17 +- .../Presenter/StickersInputPresenter.swift | 33 +- .../Stickers/StickersInputProtocols.swift | 17 +- .../StickerPackHeaderModel.swift | 4 + .../StickerPreviewContainerView.swift | 9 + .../Content/StickerDetailsPreviewView.swift | 11 + .../Content/StickerImagePreviewView.swift | 9 + .../StickersInputViewController.swift | 17 +- .../TimeZoneSelectorInteractor.swift | 4 +- Nynja/MotionManager/MotionManager.swift | 117 ++ Nynja/NotificationManager.swift | 4 +- Nynja/OptionsItemsFactory.swift | 5 +- Nynja/P2pChatItemsFactory.swift | 29 +- Nynja/ProgressModel.swift | 5 + .../HistoryRequestModelFactory.swift | 101 +- .../marketplace_interpretation.pdf | Bin 6447 -> 4834 bytes .../Contents.json | 12 + .../ic_marketplace_wheel_context_menu.pdf | Bin 0 -> 6309 bytes Nynja/Resources/ChannelsConfig.xcconfig | 17 - Nynja/Resources/Constants.swift | 4 +- Nynja/Resources/Info.plist | 2 +- Nynja/Resources/Sounds/Call/ringback.m4a | Bin 0 -> 19987 bytes Nynja/Resources/Sounds/Sounds.json | 4 + Nynja/Resources/StickersConfig.xcconfig | 17 - Nynja/Resources/ThirdPartyServices.swift | 10 +- Nynja/Resources/TranslateConfig.xcconfig | 18 - Nynja/Resources/en.lproj/Localizable.strings | 8 +- Nynja/RoomDAO.swift | 42 +- Nynja/RoomDAOProtocol.swift | 4 +- .../Anti-debugging/AntiDebuggingService.swift | 43 + .../DebuggingDetector/DebuggingDetector.swift | 15 + .../DebuggingDetectorProtocol.swift | 18 + .../Mechanisms/DDMechanism.swift | 11 + .../Mechanisms/DDSysctlMechanism.swift | 33 + .../DebuggingPreventing.swift | 11 + .../DetectorDebuggingPreventer.swift | 20 + .../PtraceDebuggingPreventer.swift | 40 + .../JailbreakDetector/JailbreakDetector.swift | 0 .../JailbreakDetectorProtocol.swift | 0 .../Mechanisms/JDCydiaUrlMechanism.swift | 0 .../Mechanisms/JDFileBasedMechanism.swift | 0 .../JDFilePermissionMechanism.swift | 0 .../Mechanisms/JDMechanism.swift | 0 .../UIDevice+Jailbreak.swift | 0 Nynja/ServerModel/Model/Contact.swift | 2 + Nynja/ServerModel/Model/Message.swift | 6 + Nynja/ServerModel/Model/Room.swift | 4 +- Nynja/ServerModel/Spec/History_Spec.swift | 7 +- Nynja/Services/Amazon+FileSync.swift | 11 +- Nynja/Services/Aps.swift | 25 +- .../HandleServices/ContactHandler.swift | 45 +- .../HandleServices/HandlerService.swift.orig | 136 -- .../HandleServices/HistoryHandler.swift | 119 +- .../HandleServices/MessageHandler.swift | 93 +- .../MessageHandlerSubscriber.swift | 24 + .../HandleServices/ProfileHandler.swift | 78 +- .../Services/HandleServices/RoomHandler.swift | 59 +- .../Services/HandleServices/StarHandler.swift | 4 +- Nynja/Services/LocationService.swift | 4 +- Nynja/Services/MQTT/MQTTService.swift | 61 +- Nynja/Services/MQTT/MQTTServiceHelper.swift | 2 +- Nynja/Services/MQTT/MQTTServiceSchedule.swift | 4 +- Nynja/Services/Member/MemberDAO.swift | 8 + Nynja/Services/Member/MemberDAOProtocol.swift | 5 + .../Operations/DownloadOperation.swift | 7 +- .../Operations/UploadOperation.swift | 5 +- .../MessageSendingService.swift | 37 +- .../Services/Models/HistoryRequestModel.swift | 38 +- Nynja/Services/NynjaCommunicatorService.swift | 350 ++-- Nynja/Services/PushService.swift | 62 +- .../TranscribeNetworkService.swift | 18 +- Nynja/Services/ReachabilityService.swift | 6 +- .../ResourceManager/ResourceManager.swift | 12 +- .../ServiceFactory/ServiceFactory.swift | 18 +- Nynja/Services/SoundService.swift | 189 ++- .../StickersProvider/StickersProvider.swift | 10 + .../StickersProvider/StickersProviding.swift | 1 + Nynja/Services/StorageService.swift | 53 +- ...tion.swift => AudioConvertOperation.swift} | 41 +- .../AudioLongTranscribeOperation.swift | 62 + ...ioLongTranscribeProccessingOperation.swift | 100 ++ .../AudioShortTranscribeOperation.swift | 69 + .../AudioTranscribeSendOperation.swift | 56 + .../Operations /AudioUploadOperation.swift | 38 +- .../TranscribeLongAudioOperation.swift | 47 - ...nscribeLongAudioProccessingOperation.swift | 81 - .../Operations /TranscribeOperation.swift | 39 + .../TranscribeShortAudioOperation.swift | 54 - .../TranscribeService/TranscribeService.swift | 291 +++- .../Manager/WCDataManager.swift | 55 +- .../Manager/WCDataManagerProtocol.swift | 2 +- Nynja/SoundBundle.swift | 4 +- Nynja/SoundPlayer.swift | 2 +- Nynja/StarActionDAO.swift | 40 + Nynja/StarActionDAOProtocol.swift | 20 + Nynja/StarDAO.swift | 11 + Nynja/StarDAOProtocol.swift | 5 +- Nynja/StorageService+UserInfo.swift | 6 +- Nynja/SyncFileManager/SyncFileManager.swift | 10 +- Nynja/ThumbnailGenerator.swift | 2 +- Nynja/TransferInfo.swift | 4 + Nynja/TransferManager.swift | 188 ++- Nynja/UserInfo.swift | 1 + Nynja/WCBaseItemsFactory.swift | 8 +- Nynja/WrappedTaskOperation.swift | 28 + .../Models/HistoryRequestModelTests.swift | 59 +- Podfile | 4 +- .../Contact/Contact+BaseChatModel.swift | 2 +- .../Models/Contact/ContactExtension.swift | 4 + .../Models/Message/Message+Factory.swift | 116 ++ .../Models/Message/Message+Files.swift | 68 + .../Models/Message/MessageExtension.swift | 171 +- .../Models/Message/MessageIdentifiers.swift | 10 + Shared/Library/Models/BaseChatModel.swift | 1 + Shared/Services/Handlers/IoHandler.swift | 3 +- 452 files changed, 10166 insertions(+), 5890 deletions(-) create mode 100644 Frameworks/LogViewer/LogViewer.xcodeproj/project.pbxproj create mode 100644 Frameworks/LogViewer/LogViewer.xcodeproj/project.xcworkspace/contents.xcworkspacedata create mode 100644 Frameworks/LogViewer/LogViewer.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist create mode 100644 Frameworks/LogViewer/LogViewer.xcworkspace/contents.xcworkspacedata create mode 100644 Frameworks/LogViewer/LogViewer.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist create mode 100644 Frameworks/LogViewer/LogViewer/AppDelegate.swift create mode 100644 Frameworks/LogViewer/LogViewer/ArrayExtension.swift create mode 100644 Frameworks/LogViewer/LogViewer/Assets.xcassets/AppIcon.appiconset/Contents.json create mode 100644 Frameworks/LogViewer/LogViewer/Assets.xcassets/Contents.json create mode 100644 Frameworks/LogViewer/LogViewer/Base.lproj/Main.storyboard create mode 100644 Frameworks/LogViewer/LogViewer/Bert.swift create mode 100644 Frameworks/LogViewer/LogViewer/FileMenuCell.swift create mode 100644 Frameworks/LogViewer/LogViewer/FileMenuDS.swift create mode 100644 Frameworks/LogViewer/LogViewer/FileReader.swift create mode 100644 Frameworks/LogViewer/LogViewer/Info.plist create mode 100644 Frameworks/LogViewer/LogViewer/LogDS.swift create mode 100644 Frameworks/LogViewer/LogViewer/LogViewer.entitlements create mode 100644 Frameworks/LogViewer/LogViewer/StreamReader.swift create mode 100644 Frameworks/LogViewer/LogViewer/ViewController.swift create mode 100644 Frameworks/LogViewer/Podfile delete mode 100644 Nynja.xcodeproj/xcshareddata/xcschemes/NynjaChannels.xcscheme delete mode 100644 Nynja.xcodeproj/xcshareddata/xcschemes/NynjaSticker.xcscheme delete mode 100644 Nynja.xcodeproj/xcshareddata/xcschemes/NynjaTranslate.xcscheme delete mode 100644 Nynja/ChatService.swift create mode 100644 Nynja/ChatService/ChatService.swift create mode 100644 Nynja/ConvertMessage/ConvertMessageDAO.swift create mode 100644 Nynja/ConvertMessage/ConvertMessageDAOProtocol.swift delete mode 100644 Nynja/DB/Models/DBContact.swift.orig create mode 100644 Nynja/DB/Models/DBConvertMessage.swift delete mode 100644 Nynja/DB/Models/DBRoom.swift.orig create mode 100644 Nynja/DB/Models/DBStarAction.swift create mode 100644 Nynja/DB/Tables/Base/TableOrder.swift create mode 100644 Nynja/DB/Tables/ConvertMessageTable.swift create mode 100644 Nynja/DB/Tables/StarActionTable.swift create mode 100644 Nynja/Extensions/Models/Desc/Desc+Construct.swift create mode 100644 Nynja/Extensions/Models/P2P+Opponent.swift rename Nynja/Extensions/SwiftLibrary/Collection/{CollectionExtension.swift => BidirectionalCollection.swift} (50%) create mode 100644 Nynja/Extensions/SwiftLibrary/Collection/Collection.swift rename Nynja/{HistoryRequestModelTypeProtocol.swift => HistoryRequestModelTypeRepresentable.swift} (69%) delete mode 100644 Nynja/Library/UI/BaseVC/BaseVC.swift.orig create mode 100644 Nynja/Library/UI/BaseVC/LoadingInteractive.swift create mode 100644 Nynja/Library/UI/Extensions/String+Split.swift create mode 100644 Nynja/LogService/LogWriter.swift delete mode 100644 Nynja/Modules/Call/CallScreens/CallInProgress/View/CallInProgressNavigationController.swift delete mode 100644 Nynja/Modules/History/View/HistoryCell.swift.orig create mode 100644 Nynja/Modules/LogOutput/Interactor/LogOutputInteractor.swift create mode 100644 Nynja/Modules/LogOutput/LogOutputProtocols.swift create mode 100644 Nynja/Modules/LogOutput/Presenter/LogOutputPresenter.swift create mode 100644 Nynja/Modules/LogOutput/View/LogOutputDS.swift create mode 100644 Nynja/Modules/LogOutput/View/LogOutputView.swift create mode 100644 Nynja/Modules/LogOutput/View/Logoutputcell.swift create mode 100644 Nynja/Modules/LogOutput/Wireframe/LogOutputWireFrame.swift delete mode 100644 Nynja/Modules/Main/Interactor/MainInteractor.swift.orig create mode 100644 Nynja/Modules/Message/Interactor/MessageInteractor+Forward.swift create mode 100644 Nynja/Modules/Message/Interactor/MessageInteractor+History.swift create mode 100644 Nynja/Modules/Message/Protocols/VoiceAudioInteractive.swift create mode 100644 Nynja/Modules/Message/View/Views/CollectionView/Attributes/MessageCollectionViewLayoutAttributes.swift create mode 100644 Nynja/Modules/Message/View/Views/CollectionView/Attributes/ReversedMessageCollectionViewLayoutAttributes.swift create mode 100644 Nynja/Modules/Message/View/Views/CollectionView/Layout/MessageCollectionViewLayout.swift rename Nynja/Modules/Message/View/Views/CollectionView/{MessageCollectionViewLayout.swift => Layout/ReversedMessageCollectionViewLayout.swift} (54%) create mode 100644 Nynja/Modules/Message/View/Views/CollectionView/ProgressIdentifier.swift delete mode 100644 Nynja/Modules/OtherUser/OtherUserProtocols.swift.orig delete mode 100644 Nynja/Modules/OtherUser/Presenter/OtherUserPresenter.swift.orig delete mode 100644 Nynja/Modules/Splash/Interactor/SplashInteractor.swift.orig create mode 100644 Nynja/MotionManager/MotionManager.swift create mode 100644 Nynja/Resources/Assets.xcassets/ic_marketplace_wheel_context_menu.imageset/Contents.json create mode 100644 Nynja/Resources/Assets.xcassets/ic_marketplace_wheel_context_menu.imageset/ic_marketplace_wheel_context_menu.pdf delete mode 100644 Nynja/Resources/ChannelsConfig.xcconfig create mode 100644 Nynja/Resources/Sounds/Call/ringback.m4a delete mode 100644 Nynja/Resources/StickersConfig.xcconfig delete mode 100644 Nynja/Resources/TranslateConfig.xcconfig create mode 100644 Nynja/Security/Anti-debugging/AntiDebuggingService.swift create mode 100644 Nynja/Security/Anti-debugging/DebuggingDetector/DebuggingDetector.swift create mode 100644 Nynja/Security/Anti-debugging/DebuggingDetector/DebuggingDetectorProtocol.swift create mode 100644 Nynja/Security/Anti-debugging/DebuggingDetector/Mechanisms/DDMechanism.swift create mode 100644 Nynja/Security/Anti-debugging/DebuggingDetector/Mechanisms/DDSysctlMechanism.swift create mode 100644 Nynja/Security/Anti-debugging/DebuggingPreventer/DebuggingPreventing.swift create mode 100644 Nynja/Security/Anti-debugging/DebuggingPreventer/DetectorDebuggingPreventer.swift create mode 100644 Nynja/Security/Anti-debugging/DebuggingPreventer/PtraceDebuggingPreventer.swift rename Nynja/{ => Security}/JailbreakDetector/JailbreakDetector.swift (100%) rename Nynja/{ => Security}/JailbreakDetector/JailbreakDetectorProtocol.swift (100%) rename Nynja/{ => Security}/JailbreakDetector/Mechanisms/JDCydiaUrlMechanism.swift (100%) rename Nynja/{ => Security}/JailbreakDetector/Mechanisms/JDFileBasedMechanism.swift (100%) rename Nynja/{ => Security}/JailbreakDetector/Mechanisms/JDFilePermissionMechanism.swift (100%) rename Nynja/{ => Security}/JailbreakDetector/Mechanisms/JDMechanism.swift (100%) rename Nynja/{ => Security}/JailbreakDetector/UIDevice+Jailbreak.swift (100%) delete mode 100644 Nynja/Services/HandleServices/HandlerService.swift.orig create mode 100644 Nynja/Services/HandleServices/MessageHandlerSubscriber.swift rename Nynja/Services/TranscribeService/Operations /{AudioConvertionOperation.swift => AudioConvertOperation.swift} (58%) create mode 100644 Nynja/Services/TranscribeService/Operations /AudioLongTranscribeOperation.swift create mode 100644 Nynja/Services/TranscribeService/Operations /AudioLongTranscribeProccessingOperation.swift create mode 100644 Nynja/Services/TranscribeService/Operations /AudioShortTranscribeOperation.swift create mode 100644 Nynja/Services/TranscribeService/Operations /AudioTranscribeSendOperation.swift delete mode 100644 Nynja/Services/TranscribeService/Operations /TranscribeLongAudioOperation.swift delete mode 100644 Nynja/Services/TranscribeService/Operations /TranscribeLongAudioProccessingOperation.swift create mode 100644 Nynja/Services/TranscribeService/Operations /TranscribeOperation.swift delete mode 100644 Nynja/Services/TranscribeService/Operations /TranscribeShortAudioOperation.swift create mode 100644 Nynja/StarActionDAO.swift create mode 100644 Nynja/StarActionDAOProtocol.swift create mode 100644 Nynja/WrappedTaskOperation.swift create mode 100644 Shared/Library/Extensions/Models/Message/Message+Factory.swift create mode 100644 Shared/Library/Extensions/Models/Message/Message+Files.swift create mode 100644 Shared/Library/Extensions/Models/Message/MessageIdentifiers.swift diff --git a/Frameworks/LogViewer/LogViewer.xcodeproj/project.pbxproj b/Frameworks/LogViewer/LogViewer.xcodeproj/project.pbxproj new file mode 100644 index 000000000..ed625304d --- /dev/null +++ b/Frameworks/LogViewer/LogViewer.xcodeproj/project.pbxproj @@ -0,0 +1,425 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 50; + objects = { + +/* Begin PBXBuildFile section */ + 26053110212739A4002E1CF1 /* StreamReader.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2605310F212739A4002E1CF1 /* StreamReader.swift */; }; + 260D68292125E5F10072F11F /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 260D68282125E5F10072F11F /* AppDelegate.swift */; }; + 260D682B2125E5F10072F11F /* ViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 260D682A2125E5F10072F11F /* ViewController.swift */; }; + 260D682D2125E5F30072F11F /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 260D682C2125E5F30072F11F /* Assets.xcassets */; }; + 260D68302125E5F30072F11F /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 260D682E2125E5F30072F11F /* Main.storyboard */; }; + 260D68392125E8050072F11F /* FileMenuDS.swift in Sources */ = {isa = PBXBuildFile; fileRef = 260D68382125E8050072F11F /* FileMenuDS.swift */; }; + 260D683B2125E8740072F11F /* FileMenuCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 260D683A2125E8740072F11F /* FileMenuCell.swift */; }; + 266B85B9212609CA00464B96 /* ArrayExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 266B85B8212609CA00464B96 /* ArrayExtension.swift */; }; + 266B85BB21260F5700464B96 /* Bert.swift in Sources */ = {isa = PBXBuildFile; fileRef = 266B85BA21260F5700464B96 /* Bert.swift */; }; + 26D9DE5F212601AD0073E61C /* LogDS.swift in Sources */ = {isa = PBXBuildFile; fileRef = 26D9DE5E212601AD0073E61C /* LogDS.swift */; }; + 26D9DE61212603E80073E61C /* FileReader.swift in Sources */ = {isa = PBXBuildFile; fileRef = 26D9DE60212603E80073E61C /* FileReader.swift */; }; + 6A14F9C863596AD6863CB5BC /* Pods_LogViewer.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D53CFD93FB395E5B8EDEF63A /* Pods_LogViewer.framework */; }; +/* End PBXBuildFile section */ + +/* Begin PBXFileReference section */ + 1E99CF57910781DF707D9852 /* Pods-LogViewer.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-LogViewer.debug.xcconfig"; path = "Pods/Target Support Files/Pods-LogViewer/Pods-LogViewer.debug.xcconfig"; sourceTree = ""; }; + 2605310F212739A4002E1CF1 /* StreamReader.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StreamReader.swift; sourceTree = ""; }; + 260D68252125E5F10072F11F /* LogViewer.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = LogViewer.app; sourceTree = BUILT_PRODUCTS_DIR; }; + 260D68282125E5F10072F11F /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; + 260D682A2125E5F10072F11F /* ViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewController.swift; sourceTree = ""; }; + 260D682C2125E5F30072F11F /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; + 260D682F2125E5F30072F11F /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; + 260D68312125E5F30072F11F /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + 260D68322125E5F30072F11F /* LogViewer.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = LogViewer.entitlements; sourceTree = ""; }; + 260D68382125E8050072F11F /* FileMenuDS.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FileMenuDS.swift; sourceTree = ""; }; + 260D683A2125E8740072F11F /* FileMenuCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FileMenuCell.swift; sourceTree = ""; }; + 266B85B8212609CA00464B96 /* ArrayExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ArrayExtension.swift; sourceTree = ""; }; + 266B85BA21260F5700464B96 /* Bert.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Bert.swift; sourceTree = ""; }; + 26D9DE5E212601AD0073E61C /* LogDS.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LogDS.swift; sourceTree = ""; }; + 26D9DE60212603E80073E61C /* FileReader.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FileReader.swift; sourceTree = ""; }; + 2F3B0D34DB1E128A07CDFCDE /* Pods-LogViewer.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-LogViewer.release.xcconfig"; path = "Pods/Target Support Files/Pods-LogViewer/Pods-LogViewer.release.xcconfig"; sourceTree = ""; }; + D53CFD93FB395E5B8EDEF63A /* Pods_LogViewer.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_LogViewer.framework; sourceTree = BUILT_PRODUCTS_DIR; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + 260D68222125E5F10072F11F /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 6A14F9C863596AD6863CB5BC /* Pods_LogViewer.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 260D681C2125E5F10072F11F = { + isa = PBXGroup; + children = ( + 260D68272125E5F10072F11F /* LogViewer */, + 260D68262125E5F10072F11F /* Products */, + 582B9CB94522C76D387B59B4 /* Pods */, + 63838EB16C276D3E7548548C /* Frameworks */, + ); + sourceTree = ""; + }; + 260D68262125E5F10072F11F /* Products */ = { + isa = PBXGroup; + children = ( + 260D68252125E5F10072F11F /* LogViewer.app */, + ); + name = Products; + sourceTree = ""; + }; + 260D68272125E5F10072F11F /* LogViewer */ = { + isa = PBXGroup; + children = ( + 260D68282125E5F10072F11F /* AppDelegate.swift */, + 260D682A2125E5F10072F11F /* ViewController.swift */, + 260D683A2125E8740072F11F /* FileMenuCell.swift */, + 260D68382125E8050072F11F /* FileMenuDS.swift */, + 26D9DE5E212601AD0073E61C /* LogDS.swift */, + 26D9DE60212603E80073E61C /* FileReader.swift */, + 2605310F212739A4002E1CF1 /* StreamReader.swift */, + 266B85B8212609CA00464B96 /* ArrayExtension.swift */, + 266B85BA21260F5700464B96 /* Bert.swift */, + 260D682C2125E5F30072F11F /* Assets.xcassets */, + 260D682E2125E5F30072F11F /* Main.storyboard */, + 260D68312125E5F30072F11F /* Info.plist */, + 260D68322125E5F30072F11F /* LogViewer.entitlements */, + ); + path = LogViewer; + sourceTree = ""; + }; + 582B9CB94522C76D387B59B4 /* Pods */ = { + isa = PBXGroup; + children = ( + 1E99CF57910781DF707D9852 /* Pods-LogViewer.debug.xcconfig */, + 2F3B0D34DB1E128A07CDFCDE /* Pods-LogViewer.release.xcconfig */, + ); + name = Pods; + sourceTree = ""; + }; + 63838EB16C276D3E7548548C /* Frameworks */ = { + isa = PBXGroup; + children = ( + D53CFD93FB395E5B8EDEF63A /* Pods_LogViewer.framework */, + ); + name = Frameworks; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + 260D68242125E5F10072F11F /* LogViewer */ = { + isa = PBXNativeTarget; + buildConfigurationList = 260D68352125E5F30072F11F /* Build configuration list for PBXNativeTarget "LogViewer" */; + buildPhases = ( + 1A7405D04F345C65CC715D1C /* [CP] Check Pods Manifest.lock */, + 260D68212125E5F10072F11F /* Sources */, + 260D68222125E5F10072F11F /* Frameworks */, + 260D68232125E5F10072F11F /* Resources */, + 982DFEC435C303C31A2CFC5C /* [CP] Embed Pods Frameworks */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = LogViewer; + productName = LogViewer; + productReference = 260D68252125E5F10072F11F /* LogViewer.app */; + productType = "com.apple.product-type.application"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + 260D681D2125E5F10072F11F /* Project object */ = { + isa = PBXProject; + attributes = { + LastSwiftUpdateCheck = 0930; + LastUpgradeCheck = 0930; + ORGANIZATIONNAME = "Anton M"; + TargetAttributes = { + 260D68242125E5F10072F11F = { + CreatedOnToolsVersion = 9.3.1; + }; + }; + }; + buildConfigurationList = 260D68202125E5F10072F11F /* Build configuration list for PBXProject "LogViewer" */; + compatibilityVersion = "Xcode 9.3"; + developmentRegion = en; + hasScannedForEncodings = 0; + knownRegions = ( + en, + Base, + ); + mainGroup = 260D681C2125E5F10072F11F; + productRefGroup = 260D68262125E5F10072F11F /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + 260D68242125E5F10072F11F /* LogViewer */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + 260D68232125E5F10072F11F /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 260D682D2125E5F30072F11F /* Assets.xcassets in Resources */, + 260D68302125E5F30072F11F /* Main.storyboard in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXShellScriptBuildPhase section */ + 1A7405D04F345C65CC715D1C /* [CP] Check Pods Manifest.lock */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + "${PODS_PODFILE_DIR_PATH}/Podfile.lock", + "${PODS_ROOT}/Manifest.lock", + ); + name = "[CP] Check Pods Manifest.lock"; + outputPaths = ( + "$(DERIVED_FILE_DIR)/Pods-LogViewer-checkManifestLockResult.txt", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; + showEnvVarsInLog = 0; + }; + 982DFEC435C303C31A2CFC5C /* [CP] Embed Pods Frameworks */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + "${SRCROOT}/Pods/Target Support Files/Pods-LogViewer/Pods-LogViewer-frameworks.sh", + "${BUILT_PRODUCTS_DIR}/SnapKit/SnapKit.framework", + ); + name = "[CP] Embed Pods Frameworks"; + outputPaths = ( + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/SnapKit.framework", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"${SRCROOT}/Pods/Target Support Files/Pods-LogViewer/Pods-LogViewer-frameworks.sh\"\n"; + showEnvVarsInLog = 0; + }; +/* End PBXShellScriptBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + 260D68212125E5F10072F11F /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 26D9DE61212603E80073E61C /* FileReader.swift in Sources */, + 26D9DE5F212601AD0073E61C /* LogDS.swift in Sources */, + 266B85BB21260F5700464B96 /* Bert.swift in Sources */, + 26053110212739A4002E1CF1 /* StreamReader.swift in Sources */, + 260D682B2125E5F10072F11F /* ViewController.swift in Sources */, + 260D68292125E5F10072F11F /* AppDelegate.swift in Sources */, + 266B85B9212609CA00464B96 /* ArrayExtension.swift in Sources */, + 260D683B2125E8740072F11F /* FileMenuCell.swift in Sources */, + 260D68392125E8050072F11F /* FileMenuDS.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin PBXVariantGroup section */ + 260D682E2125E5F30072F11F /* Main.storyboard */ = { + isa = PBXVariantGroup; + children = ( + 260D682F2125E5F30072F11F /* Base */, + ); + name = Main.storyboard; + sourceTree = ""; + }; +/* End PBXVariantGroup section */ + +/* Begin XCBuildConfiguration section */ + 260D68332125E5F30072F11F /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; + CLANG_CXX_LIBRARY = "libc++"; + 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_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_IDENTITY = "Mac Developer"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = dwarf; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; + GCC_C_LANGUAGE_STANDARD = gnu11; + GCC_DYNAMIC_NO_PIC = NO; + GCC_NO_COMMON_BLOCKS = YES; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + 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; + MACOSX_DEPLOYMENT_TARGET = 10.13; + MTL_ENABLE_DEBUG_INFO = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = macosx; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + }; + name = Debug; + }; + 260D68342125E5F30072F11F /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; + CLANG_CXX_LIBRARY = "libc++"; + 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_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_IDENTITY = "Mac Developer"; + 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; + MACOSX_DEPLOYMENT_TARGET = 10.13; + MTL_ENABLE_DEBUG_INFO = NO; + SDKROOT = macosx; + SWIFT_COMPILATION_MODE = wholemodule; + SWIFT_OPTIMIZATION_LEVEL = "-O"; + }; + name = Release; + }; + 260D68362125E5F30072F11F /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 1E99CF57910781DF707D9852 /* Pods-LogViewer.debug.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CODE_SIGN_ENTITLEMENTS = LogViewer/LogViewer.entitlements; + CODE_SIGN_STYLE = Automatic; + COMBINE_HIDPI_IMAGES = YES; + DEVELOPMENT_TEAM = 9GKQ5AMF2B; + INFOPLIST_FILE = LogViewer/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/../Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = nynja.mobilecommunicator.LogViewer; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 4.0; + }; + name = Debug; + }; + 260D68372125E5F30072F11F /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 2F3B0D34DB1E128A07CDFCDE /* Pods-LogViewer.release.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CODE_SIGN_ENTITLEMENTS = LogViewer/LogViewer.entitlements; + CODE_SIGN_STYLE = Automatic; + COMBINE_HIDPI_IMAGES = YES; + DEVELOPMENT_TEAM = 9GKQ5AMF2B; + INFOPLIST_FILE = LogViewer/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/../Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = nynja.mobilecommunicator.LogViewer; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 4.0; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + 260D68202125E5F10072F11F /* Build configuration list for PBXProject "LogViewer" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 260D68332125E5F30072F11F /* Debug */, + 260D68342125E5F30072F11F /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 260D68352125E5F30072F11F /* Build configuration list for PBXNativeTarget "LogViewer" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 260D68362125E5F30072F11F /* Debug */, + 260D68372125E5F30072F11F /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + }; + rootObject = 260D681D2125E5F10072F11F /* Project object */; +} diff --git a/Frameworks/LogViewer/LogViewer.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/Frameworks/LogViewer/LogViewer.xcodeproj/project.xcworkspace/contents.xcworkspacedata new file mode 100644 index 000000000..18192d50e --- /dev/null +++ b/Frameworks/LogViewer/LogViewer.xcodeproj/project.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,7 @@ + + + + + diff --git a/Frameworks/LogViewer/LogViewer.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/Frameworks/LogViewer/LogViewer.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 000000000..18d981003 --- /dev/null +++ b/Frameworks/LogViewer/LogViewer.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/Frameworks/LogViewer/LogViewer.xcworkspace/contents.xcworkspacedata b/Frameworks/LogViewer/LogViewer.xcworkspace/contents.xcworkspacedata new file mode 100644 index 000000000..b46b6ac9a --- /dev/null +++ b/Frameworks/LogViewer/LogViewer.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,10 @@ + + + + + + + diff --git a/Frameworks/LogViewer/LogViewer.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/Frameworks/LogViewer/LogViewer.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 000000000..18d981003 --- /dev/null +++ b/Frameworks/LogViewer/LogViewer.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/Frameworks/LogViewer/LogViewer/AppDelegate.swift b/Frameworks/LogViewer/LogViewer/AppDelegate.swift new file mode 100644 index 000000000..c0aa898b7 --- /dev/null +++ b/Frameworks/LogViewer/LogViewer/AppDelegate.swift @@ -0,0 +1,26 @@ +// +// AppDelegate.swift +// LogViewer +// +// Created by Anton M on 16.08.2018. +// Copyright © 2018 Anton M. All rights reserved. +// + +import Cocoa + +@NSApplicationMain +class AppDelegate: NSObject, NSApplicationDelegate { + + + + func applicationDidFinishLaunching(_ aNotification: Notification) { + // Insert code here to initialize your application + } + + func applicationWillTerminate(_ aNotification: Notification) { + // Insert code here to tear down your application + } + + +} + diff --git a/Frameworks/LogViewer/LogViewer/ArrayExtension.swift b/Frameworks/LogViewer/LogViewer/ArrayExtension.swift new file mode 100644 index 000000000..d0e4581d8 --- /dev/null +++ b/Frameworks/LogViewer/LogViewer/ArrayExtension.swift @@ -0,0 +1,101 @@ +// +// ArrayExtension.swift +// LogViewer +// +// Created by Anton M on 16.08.2018. +// Copyright © 2018 Anton M. All rights reserved. +// + +import Foundation + +public extension Array where Element == UInt8 { + public func toBase64() -> String? { + return Data(bytes: self).base64EncodedString() + } + + public init(base64: String) { + self.init() + + guard let decodedData = Data(base64Encoded: base64) else { + return + } + + append(contentsOf: decodedData.bytes) + } +} + +extension Array where Element == UInt8 { + public init(hex: String) { + self.init(reserveCapacity: hex.unicodeScalars.lazy.underestimatedCount) + var buffer: UInt8? + var skip = hex.hasPrefix("0x") ? 2 : 0 + for char in hex.unicodeScalars.lazy { + guard skip == 0 else { + skip -= 1 + continue + } + guard char.value >= 48 && char.value <= 102 else { + removeAll() + return + } + let v: UInt8 + let c: UInt8 = UInt8(char.value) + switch c { + case let c where c <= 57: + v = c - 48 + case let c where c >= 65 && c <= 70: + v = c - 55 + case let c where c >= 97: + v = c - 87 + default: + removeAll() + return + } + if let b = buffer { + append(b << 4 | v) + buffer = nil + } else { + buffer = v + } + } + if let b = buffer { + append(b) + } + } + + public func toHexString() -> String { + return `lazy`.reduce("") { + var s = String($1, radix: 16) + if s.count == 1 { + s = "0" + s + } + return $0 + s + } + } +} + +extension Array { + public init(reserveCapacity: Int) { + self = Array() + self.reserveCapacity(reserveCapacity) + } + + var slice: ArraySlice { + return self[self.startIndex ..< self.endIndex] + } +} + + +extension Data { + public init(hex: String) { + self.init(bytes: Array(hex: hex)) + } + + public var bytes: Array { + return Array(self) + } + + public func toHexString() -> String { + return bytes.toHexString() + } +} diff --git a/Frameworks/LogViewer/LogViewer/Assets.xcassets/AppIcon.appiconset/Contents.json b/Frameworks/LogViewer/LogViewer/Assets.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 000000000..2db2b1c7c --- /dev/null +++ b/Frameworks/LogViewer/LogViewer/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -0,0 +1,58 @@ +{ + "images" : [ + { + "idiom" : "mac", + "size" : "16x16", + "scale" : "1x" + }, + { + "idiom" : "mac", + "size" : "16x16", + "scale" : "2x" + }, + { + "idiom" : "mac", + "size" : "32x32", + "scale" : "1x" + }, + { + "idiom" : "mac", + "size" : "32x32", + "scale" : "2x" + }, + { + "idiom" : "mac", + "size" : "128x128", + "scale" : "1x" + }, + { + "idiom" : "mac", + "size" : "128x128", + "scale" : "2x" + }, + { + "idiom" : "mac", + "size" : "256x256", + "scale" : "1x" + }, + { + "idiom" : "mac", + "size" : "256x256", + "scale" : "2x" + }, + { + "idiom" : "mac", + "size" : "512x512", + "scale" : "1x" + }, + { + "idiom" : "mac", + "size" : "512x512", + "scale" : "2x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Frameworks/LogViewer/LogViewer/Assets.xcassets/Contents.json b/Frameworks/LogViewer/LogViewer/Assets.xcassets/Contents.json new file mode 100644 index 000000000..da4a164c9 --- /dev/null +++ b/Frameworks/LogViewer/LogViewer/Assets.xcassets/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Frameworks/LogViewer/LogViewer/Base.lproj/Main.storyboard b/Frameworks/LogViewer/LogViewer/Base.lproj/Main.storyboard new file mode 100644 index 000000000..0e7515743 --- /dev/null +++ b/Frameworks/LogViewer/LogViewer/Base.lproj/Main.storyboard @@ -0,0 +1,116 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Frameworks/LogViewer/LogViewer/Bert.swift b/Frameworks/LogViewer/LogViewer/Bert.swift new file mode 100644 index 000000000..21fc91eee --- /dev/null +++ b/Frameworks/LogViewer/LogViewer/Bert.swift @@ -0,0 +1,771 @@ +// +// AppDelegate.swift +// Nynja +// +// Created by Anton Makarov on 15.05.17. +// Copyright © 2017 TecSynt Solutions. All rights reserved. +// + +import Foundation + +enum BertError: Error { + case NotValidBertObject + case NotValidErlangTerm + case UnexpectedErlangType + case IntegerValueToLarge + case AtomLengthToLarge +} + +enum BertType: UInt8 { + case Version = 131 + case SmallAtom = 115 + case Atom = 100 + case Binary = 109 + case SmallInteger = 97 + case Integer = 98 + case SmallBig = 110 + // case LargetBig = 111 + // case Float = 99 + case NewFloat = 70 + case String = 107 + // case Port = 102 + // case Pid = 103 + case SmallTuple = 104 + case LargeTuple = 105 + case List = 108 + // case Reference = 101 + // case NewReference = 114 + case Nil = 106 +} + +class BertObject { + var type: UInt8 = 0 + + var description : String { + get { + return "BertObject" + } + } +} + +class BertAtom: BertObject { + var value = "" + + init (fromString string: String) { + value = string + } + + override var description : String { + get { + if value == "" { + return "[]" + } + return value + } + } +} + +class BertBool: BertObject { + var value: Bool + + init (fromBool b: Bool) { + value = b + } + override var description : String { + get { + if value.description == "" { + return "[]" + } + return value.description + } + } +} + +class BertUndefined: BertAtom { + init () { + super.init(fromString: "undefined") + } + + override var description : String { + get { + if value.description == "" { + return "[]" + } + return value.description + } + } +} + +class BertBinary: BertObject { + var value: NSData + + init (fromNSData d: NSData) { + value = d + } + + override var description : String { + get { + let d = String(data: value as Data, encoding: String.Encoding.utf8) + return d ?? "__Can't GET data__" + } + } +} + +class BertNumber: BertObject { + var value: Int64 + + init (fromUInt8 i: UInt8) { + value = Int64(i) + } + + init (fromInt32 i: Int32) { + value = Int64(i) + } + + init (fromInt64 i: Int64) { + value = i + } + + override var description : String { + get { + return value.description + } + } +} + +class BertFloat: BertObject { + var value: Double + + init (fromDouble d: Double) { + value = d + } + override var description : String { + get { + return value.description + } + } +} + +class BertNil: BertObject { + + override var description : String { + get { + return "NIL" + } + } +} + +class BertString: BertObject { + var value: String + + init (fromString s: String) { + value = s + } + + override var description : String { + get { + return value + } + } +} + +class BertTuple: BertObject { + var elements: [BertObject] + + override var description : String { + get { + var res = "{" + for i in 0.. NSData { + let length = try getEncodeSize(object: object) + var offset: Int = 0 + var data = [UInt8](repeating: 0, count: length + 1) + + data[offset] = BertType.Version.rawValue + offset += 1 + try encodeInner(object: object, data: &data, offset: &offset) + + return NSData(bytes: data, length: length + 1) + } + + class func getObjectClassName(object: BertObject) -> String { + let className = NSStringFromClass(object_getClass(object)!) + let classNameArr = className.components(separatedBy: ".") + + return classNameArr.last! + } + + class func getEncodeSize (object: BertObject) throws -> Int { + switch getObjectClassName(object: object) { + case "BertBool": + let bool = (object as! BertBool) + return 1 + 2 + (bool.value ? 4 : 5) + case "BertUndefined": + return 1 + 2 + 9 + case "BertAtom": + let atom = (object as! BertAtom) + return 1 + 2 + atom.value.characters.count + case "BertBinary": + let binary = (object as! BertBinary) + return 1 + 4 + binary.value.length + case "BertNil": + return 1 + case "BertNumber": + let number = (object as! BertNumber) + if number.value >= 0 && number.value <= 255 { + return 1 + 1 + } + if number.value >= -2147483648 && number.value <= 2147483647 { + return 1 + 4 + } + return 1 + 1 + 8 + case "BertFloat": + return 1 + 8 + case "BertString": + // TODO: implement encoding for length > 0xFF + let string = (object as! BertString) + return 1 + 2 + string.value.characters.count + case "BertTuple": + let tuple = (object as! BertTuple) + var n = 0 + for element in tuple.elements { + n += try getEncodeSize(object: element) + } + return 1 + (tuple.elements.count <= 255 ? 1 : 4) + n + case "BertList": + let list = (object as! BertList) + var n = 0 + for element in list.elements { + n += try getEncodeSize(object: element) + } + return 1 + 4 + 1 + n + default: + throw BertError.UnexpectedErlangType + } + } + + class func encodeInner(object: BertObject, data: inout [UInt8], offset: inout Int) throws { + switch getObjectClassName(object: object) { + case "BertAtom": encodeAtom(atom: object as! BertAtom, data: &data, offset: &offset) + case "BertBool": encodeBool(bool: object as! BertBool, data: &data, offset: &offset) + case "BertUndefined": encodeAtom(atom: object as! BertUndefined, data: &data, offset: &offset) + case "BertBinary": encodeBinary(binary: object as! BertBinary, data: &data, offset: &offset) + case "BertNumber": encodeNumber(number: object as! BertNumber, data: &data, offset: &offset) + case "BertFloat": encodeFloat(float: object as! BertFloat, data: &data, offset: &offset) + case "BertString": encodeString(string: object as! BertString, data: &data, offset: &offset) + case "BertTuple": try encodeTuple(tuple: object as! BertTuple, data: &data, offset: &offset) + case "BertList": try encodeList(list: object as! BertList, data: &data, offset: &offset) + case "BertNil": encodeNil(data: &data, offset: &offset) + default: + throw BertError.UnexpectedErlangType + } + } + + class func encodeAtom(atom: BertAtom, data: inout [UInt8], offset: inout Int) { + let length = UInt16(atom.value.characters.count) + data[offset] = BertType.Atom.rawValue + offset += 1 + writeUInt16(i: length, data: &data, offset: &offset) + + memcpy(&data[offset], (atom.value as NSString).utf8String, Int(length)) + offset += Int(length) + } + + class func encodeBool(bool: BertBool, data: inout [UInt8], offset: inout Int) { + let atom = BertAtom(fromString: (bool.value ? "true" : "false")) + encodeAtom(atom: atom, data: &data, offset: &offset) + } + + class func encodeNil(data: inout [UInt8], offset: inout Int) { + data[offset] = BertType.Nil.rawValue + offset += 1 + // writeUInt16(i: 2, data: &data, offset: &offset) + + //memcpy(&data[offset], (atom.value as NSString).utf8String, 1) + //offset += 1 + } + + class func encodeBinary(binary: BertBinary, data: inout [UInt8], offset: inout Int) { + data[offset] = BertType.Binary.rawValue + offset += 1 + let length = UInt32(binary.value.length) + + writeUInt32(i: length, data: &data, offset: &offset) + + memcpy(&data[offset], binary.value.bytes, Int(length)) + offset += Int(length) + } + + class func encodeNumber(number: BertNumber, data: inout [UInt8], offset: inout Int) { + if number.value >= 0 && number.value <= 255 { + data[offset] = BertType.SmallInteger.rawValue + offset += 1 + data[offset] = UInt8(number.value) + offset += 1 + } else if number.value >= -2147483648 && number.value <= 2147483647 { + data[offset] = BertType.Integer.rawValue + offset += 1 + if number.value > 0 { + writeUInt32(i: UInt32(number.value), data: &data, offset: &offset) + } else { + writeInt32(i: Int32(number.value), data: &data, offset: &offset) + } + } else { + data[offset] = BertType.SmallBig.rawValue + offset += 1 + var i: UInt64 = UInt64(number.value < 0 ? -number.value : number.value) + var n = 0 + var pos = offset + 2 //arity, sign + + while (i > 0) { + data[pos] = UInt8(i % 256) + pos += 1 + i = UInt64(floor(Double(i / 256))) + n += 1 + } + data[offset] = UInt8(n) + offset += 1 + let temp = UInt8(Int(number.value < 0 ? 1:0)) + data[offset] = temp + offset = pos + } + } + + class func encodeFloat(float: BertFloat, data: inout [UInt8], offset: inout Int) { + data[offset] = BertType.NewFloat.rawValue + offset += 1 + let bytes = withUnsafePointer(to: &float.value) { + $0.withMemoryRebound(to: UInt8.self, capacity: 1, { + Array(UnsafeBufferPointer(start: $0, count: MemoryLayout.size)) + }) + } + var i = UnsafePointer(bytes).withMemoryRebound(to: UInt64.self, capacity: 1) { + $0.pointee + }.bigEndian + + memcpy(&data[offset],&i,MemoryLayout.size) + + offset += 8 + } + + class func encodeString(string: BertString, data: inout [UInt8], offset: inout Int) { + data[offset] = BertType.String.rawValue + offset += 1 + writeUInt16(i: UInt16(string.value.characters.count), data: &data, offset: &offset) + + memcpy(&data[offset], (string.value as NSString).utf8String, string.value.characters.count) + offset += string.value.characters.count + } + + class func encodeTuple(tuple: BertTuple, data: inout [UInt8], offset: inout Int) throws { + if tuple.elements.count <= 255 { + data[offset] = BertType.SmallTuple.rawValue + offset += 1 + data[offset] = UInt8(tuple.elements.count) + offset += 1 + } else { + data[offset] = BertType.LargeTuple.rawValue + offset += 1 + writeUInt32(i: UInt32(tuple.elements.count), data: &data, offset: &offset) + } + + for element in tuple.elements { + try encodeInner(object: element, data: &data, offset: &offset) + } + } + + class func encodeList(list: BertList, data: inout [UInt8], offset: inout Int) throws { + data[offset] = BertType.List.rawValue + offset += 1 + writeUInt32(i: UInt32(list.elements.count), data: &data, offset: &offset) + + for element in list.elements { + try encodeInner(object: element, data: &data, offset: &offset) + } + data[offset] = BertType.Nil.rawValue + offset += 1 + } + + class func writeUInt16(i: UInt16, data: inout [UInt8], offset: inout Int) { + data[offset] = UInt8((i & 0xFF00) >> 8) + offset += 1 + data[offset] = UInt8(i & 0xFF) + offset += 1 + } + + class func writeUInt32(i: UInt32, data: inout [UInt8], offset: inout Int) { + data[offset] = UInt8((i & 0xFF000000) >> 24) + offset += 1 + data[offset] = UInt8((i & 0xFF0000) >> 16) + offset += 1 + data[offset] = UInt8((i & 0xFF00) >> 8) + offset += 1 + data[offset] = UInt8(i & 0xFF) + offset += 1 + } + + class func writeInt32(i: Int32, data: inout [UInt8], offset: inout Int) { + data[offset] = UInt8(i >> 24 & 0xFF) + offset += 1 + data[offset] = UInt8(i >> 16 & 0xFF) + offset += 1 + data[offset] = UInt8(i >> 8 & 0xFF) + offset += 1 + data[offset] = UInt8(i >> 0 & 0xFF) + offset += 1 + } + + class func decode (data: NSData) throws -> BertObject { + if data.length == 0 { + return BertUndefined() + } + + var offset = 0 + var buffer = [UInt8](repeating: 0, count: 1) + data.getBytes(&buffer, range: NSMakeRange(offset, 1)) + offset += 1 + let header = buffer[0] + + if header != BertType.Version.rawValue { + throw BertError.NotValidErlangTerm + } + + var printBuffer = [UInt8](repeating: 0, count: data.length) + data.getBytes(&printBuffer, length: data.length) + + return try decodeInner(data: data, offset: &offset) + } + + class func decodeInner (data: NSData, offset: inout Int) throws -> BertObject { + var buffer = [UInt8](repeating: 0, count: 1) + + data.getBytes(&buffer, range: NSMakeRange(offset, 1)) + let type = buffer[0] + + switch type { + case BertType.Atom.rawValue: return try decodeAtom(data: data, offset: &offset) + case BertType.SmallAtom.rawValue: return try decodeAtom(data: data, offset: &offset) + case BertType.Binary.rawValue: return decodeBinary(data: data, offset: &offset) + case BertType.SmallInteger.rawValue: return try decodeNumber(data: data, offset: &offset) + case BertType.Integer.rawValue: return try decodeNumber(data: data, offset: &offset) + case BertType.SmallBig.rawValue: return try decodeNumber(data: data, offset: &offset) + case BertType.NewFloat.rawValue: return decodeDouble(data: data, offset: &offset) + case BertType.String.rawValue: return decodeString(data: data, offset: &offset) + case BertType.SmallTuple.rawValue: return try decodeTuple(data: data, offset: &offset) + case BertType.LargeTuple.rawValue: return try decodeTuple(data: data, offset: &offset) + case BertType.List.rawValue: return try decodeList(data: data, offset: &offset) + case BertType.Nil.rawValue: return decodeNil(data: data, offset: &offset) + default: + throw BertError.UnexpectedErlangType + } + } + + class func decodeAtom(data: NSData, offset: inout Int) throws -> BertObject { + var buffer = [UInt8](repeating: 0, count: 2) + + data.getBytes(&buffer, range: NSMakeRange(offset, 1)) + offset += 1 + let type = buffer[0] + var n: Int + + switch type { + case BertType.Atom.rawValue: + data.getBytes(&buffer, range: NSMakeRange(offset, 2)) + offset += 2 + let u16 = UnsafePointer(buffer).withMemoryRebound(to: UInt16.self, capacity: 1) { + $0.pointee + } + n = Int(u16.bigEndian) + case BertType.SmallAtom.rawValue: + data.getBytes(&buffer, range: NSMakeRange(offset, 1)) + offset += 1 + n = Int(buffer[0]) + default: + throw BertError.UnexpectedErlangType + } + + var buffer1 = [UInt8](repeating: 0, count: n) + data.getBytes(&buffer1, range: NSMakeRange(offset, n)) + offset += n + + let value = NSString(bytes: buffer1, length: n, encoding: String.Encoding.utf8.rawValue)! as String + + switch value { + case "true": return BertBool(fromBool: true) + case "false": return BertBool(fromBool: false) + case "undefined": return BertUndefined() + default: return BertAtom(fromString: value) + } + } + + class func decodeBinary(data: NSData, offset: inout Int) -> BertObject { + offset += 1 + + var buffer = [UInt8](repeating: 0, count: 4) + + data.getBytes(&buffer, range: NSMakeRange(offset, 4)) + + offset += 4 + let u16 = UnsafePointer(buffer).withMemoryRebound(to: UInt32.self, capacity: 1) { + $0.pointee + } + let length = Int(u16.bigEndian) + var dataBuffer = [UInt8](repeating: 0, count: length) + data.getBytes(&dataBuffer, range: NSMakeRange(offset, length)) + offset += length + + return BertBinary(fromNSData: NSData(bytes: dataBuffer, length: length)) + } + + class func decodeNumber(data: NSData, offset: inout Int) throws -> BertObject { + var buffer = [UInt8](repeating: 0, count: 10) + + data.getBytes(&buffer, range: NSMakeRange(offset, 1)) + offset += 1 + let type = buffer[0] + + switch type { + case BertType.SmallInteger.rawValue: + data.getBytes(&buffer, range: NSMakeRange(offset, 1)) + offset += 1 + let value = buffer[0] + return BertNumber(fromUInt8: value) + case BertType.Integer.rawValue: + data.getBytes(&buffer, range: NSMakeRange(offset, 4)) + offset += 4 + let u16 = UnsafePointer(buffer).withMemoryRebound(to: Int32.self, capacity: 1) { + $0.pointee + } + let value = Int32(u16.bigEndian) + return BertNumber(fromInt32: value) + case BertType.SmallBig.rawValue: + data.getBytes(&buffer, range: NSMakeRange(offset, 1)) + offset += 1 + let arity = buffer[0] + if (arity > 7) { + throw BertError.IntegerValueToLarge + } + + data.getBytes(&buffer, range: NSMakeRange(offset, 1)) + offset += 1 + let sign = buffer[0] + + var value: Int64 = 0 + var n: Int64 = 1 + for i in 0...arity-1 { + data.getBytes(&buffer, range:NSMakeRange(offset, 1)) + offset += 1 + let v = Int64(buffer[0]) + value += v * n + if i+1 != arity { + n *= 256 + } + } + + if sign > 0 { + value = -value + } + return BertNumber(fromInt64: Int64(value)) + default: + throw BertError.UnexpectedErlangType + } + } + + class func decodeDouble(data: NSData, offset: inout Int) -> BertObject { + offset += 1 + var buffer = [UInt8](repeating: 0, count: 8) + data.getBytes(&buffer, range: NSMakeRange(offset, 8)) + offset += 8 + var i = Int64(UnsafePointer(buffer).withMemoryRebound(to: UInt64.self, capacity: 1) { + $0.pointee + }.bigEndian) + var d: Double = 0 + memcpy(&d, &i, MemoryLayout.size) + return BertFloat(fromDouble: d) + } + + class func decodeString(data: NSData, offset: inout Int) -> BertObject { + offset += 1 + + var buffer = [UInt8](repeating: 0, count: 2) + data.getBytes(&buffer, range: NSMakeRange(offset, 2)) + offset += 2 + let length = Int(UnsafePointer(buffer).withMemoryRebound(to: UInt16.self, capacity: 1) { + $0.pointee + }.bigEndian) + + var stringBuffer = [UInt8](repeating: 0, count: length) + + data.getBytes(&stringBuffer, range: NSMakeRange(offset, length)) + offset += length + + var result = [BertNumber]() + for i in stringBuffer { + result.append(BertNumber(fromUInt8: i)) + } + + return BertList(fromElements: result)//BertString(fromString: String(bytes: stringBuffer, encoding: String.Encoding.utf8)!) + } + + class func decodeTuple(data: NSData, offset: inout Int) throws -> BertObject { + var buffer = [UInt8](repeating: 0, count: 4) + var n: Int + + data.getBytes(&buffer, range: NSMakeRange(offset, 1)) + offset += 1 + let type = buffer[0] + + switch type { + case BertType.SmallTuple.rawValue: + data.getBytes(&buffer, range: NSMakeRange(offset, 1)) + offset += 1 + n = Int(buffer[0]) + case BertType.LargeTuple.rawValue: + data.getBytes(&buffer, range: NSMakeRange(offset, 4)) + offset += 4 + n = Int(UnsafePointer(buffer).withMemoryRebound(to: UInt32.self, capacity: 1) { + $0.pointee + }.bigEndian) + default: + throw BertError.UnexpectedErlangType + } + + var elements = [BertObject]() + if n > 0 { + for _ in 0...n-1 { + elements.append(try decodeInner(data: data, offset: &offset)) + } + } + + return BertTuple(fromElements: elements) + } + + class func decodeList(data: NSData, offset: inout Int) throws -> BertObject { + offset+=1 + + var buffer = [UInt8](repeating: 0, count: 4) + + data.getBytes(&buffer, range: NSMakeRange(offset, 4)) + offset += 4 + let n = Int(UnsafePointer(buffer).withMemoryRebound(to: UInt32.self, capacity: 1) { + $0.pointee + }.bigEndian) + var elements = [BertObject]() + if n > 0 { + for _ in 0...n-1 { + elements.append(try decodeInner(data: data, offset: &offset)) + } + } + + if data.length > offset { + data.getBytes(&buffer, range: NSMakeRange(offset, 1)) + if (buffer[0] == BertType.Nil.rawValue) { + offset += 1 + } + } + + return BertList(fromElements: elements) + } + + class func decodeNil(data: NSData, offset: inout Int) -> BertObject { + offset += 1 + return BertNil() + } + + class func getBin(_ value: String?) -> BertObject { + var result: BertObject = BertNil() + if let v = value { + result = BertBinary(fromNSData: v.data(using: String.Encoding.utf8)! as NSData) + } + return result + } + + class func getBin(_ value: Int64?) -> BertObject { + var result: BertObject = BertNil() + if let v = value { + result = BertNumber(fromInt64: v) + } + return result + } + + class func getBin(_ value: BertBinConvertible?) -> BertObject { + var result: BertObject = BertNil() + if let v = value { + result = v.getBin() + } + return result + } +} + +import Foundation + +protocol BertBinConvertible { + func getBin() -> BertObject +} + +extension String: BertBinConvertible { + + func getBin() -> BertObject { + return BertBinary(fromNSData: data(using: String.Encoding.utf8)! as NSData) + } +} + +extension Int64: BertBinConvertible { + + func getBin() -> BertObject { + return BertNumber(fromInt64: self) + } +} + +extension Optional: BertBinConvertible where Wrapped: BertBinConvertible { + + func getBin() -> BertObject { + var result: BertObject = BertNil() + if let wrapped = self { + result = wrapped.getBin() + } + return result + } +} + diff --git a/Frameworks/LogViewer/LogViewer/FileMenuCell.swift b/Frameworks/LogViewer/LogViewer/FileMenuCell.swift new file mode 100644 index 000000000..0591ae8ad --- /dev/null +++ b/Frameworks/LogViewer/LogViewer/FileMenuCell.swift @@ -0,0 +1,37 @@ +// +// FileMenuCell.swift +// LogViewer +// +// Created by Anton M on 16.08.2018. +// Copyright © 2018 Anton M. All rights reserved. +// + +import Cocoa + +final class FileMenuCell: NSCollectionViewItem { + + lazy var lbl: NSTextField = { + let _lbl = NSTextField(labelWithString: "") + _lbl.backgroundColor = .clear + _lbl.isEditable = false + _lbl.textColor = NSColor.black + _lbl.font = NSFont(name: "Arial Italic", size: 10) + self.view.addSubview(_lbl) + _lbl.snp.makeConstraints({ (make) in + make.center.equalTo(self.view) + }) + return _lbl + }() + + func setup(text: String) { + lbl.stringValue = text + lbl.sizeToFit() + } + + override func loadView() { + self.view = NSView() + self.view.wantsLayer = true + self.view.layer?.backgroundColor = NSColor.lightGray.cgColor + } + +} diff --git a/Frameworks/LogViewer/LogViewer/FileMenuDS.swift b/Frameworks/LogViewer/LogViewer/FileMenuDS.swift new file mode 100644 index 000000000..039673be4 --- /dev/null +++ b/Frameworks/LogViewer/LogViewer/FileMenuDS.swift @@ -0,0 +1,33 @@ +// +// FileMenuDS.swift +// LogViewer +// +// Created by Anton M on 16.08.2018. +// Copyright © 2018 Anton M. All rights reserved. +// + +import Cocoa + +class FileMenuDS: NSObject, NSCollectionViewDataSource { + + var files: [URL] = [] + + func collectionView(_ collectionView: NSCollectionView, numberOfItemsInSection section: Int) -> Int { + return files.count + 1 + } + + func collectionView(_ collectionView: NSCollectionView, itemForRepresentedObjectAt indexPath: IndexPath) -> NSCollectionViewItem { + let cell = collectionView.makeItem( + withIdentifier: NSUserInterfaceItemIdentifier(rawValue: "Cell"), + for: indexPath + ) as! FileMenuCell + if indexPath.last == files.count { + cell.setup(text: "+") + } else { + let url = files[indexPath.last!] + cell.setup(text: "\(url.lastPathComponent)") + } + return cell + } + +} diff --git a/Frameworks/LogViewer/LogViewer/FileReader.swift b/Frameworks/LogViewer/LogViewer/FileReader.swift new file mode 100644 index 000000000..db8bbfd2b --- /dev/null +++ b/Frameworks/LogViewer/LogViewer/FileReader.swift @@ -0,0 +1,69 @@ +// +// FileReader.swift +// LogViewer +// +// Created by Anton M on 16.08.2018. +// Copyright © 2018 Anton M. All rights reserved. +// + +import Foundation + +class FileReader { + static let shared = FileReader() + + private init() { + + } + + func getData(url: URL, callback: (([Log]?)->Void)?) { + DispatchQueue.global(qos: .utility).async { + var result: [Log]? = nil + if let aStreamReader = StreamReader(path: url.path, delimiter: ";") { + defer { + aStreamReader.close() + } + while let line = aStreamReader.nextLine() { + guard let data = Data(base64Encoded: line) as NSData?, + let bert = try? Bert.decode(data: data) as? BertTuple else { continue } + guard let tuple = bert, let log = Log(tuple: tuple) else { continue } + if result == nil { + result = [log] + } else { + result!.append(log) + } + } + callback?(result) + } + } + } +} + +struct Log { + var timestamp: Int64? + var title: String? + var description: String? + var thread: String? + + init?(tuple: BertTuple) { + let els = tuple.elements + + guard els.count >= 4, (els[0] as? BertAtom)?.value == "Log" else { return nil } + if els.count >= 4 { + timestamp = (els[3] as? BertNumber)?.value + title = (els[1] as? BertBinary)?.string() + description = (els[2] as? BertBinary)?.string()?.replacingOccurrences(of: "\n", with: " ") + thread = "UNDEFINED" + } + if els.count == 5 { + thread = (els[4] as? BertBinary)?.string() + } + } + +} + +extension BertBinary { + func string() -> String? { + let data = self.value as Data + return String(data: data, encoding: .utf8) + } +} diff --git a/Frameworks/LogViewer/LogViewer/Info.plist b/Frameworks/LogViewer/LogViewer/Info.plist new file mode 100644 index 000000000..e6a8efa01 --- /dev/null +++ b/Frameworks/LogViewer/LogViewer/Info.plist @@ -0,0 +1,32 @@ + + + + + CFBundleDevelopmentRegion + $(DEVELOPMENT_LANGUAGE) + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIconFile + + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + APPL + CFBundleShortVersionString + 1.0 + CFBundleVersion + 1 + LSMinimumSystemVersion + $(MACOSX_DEPLOYMENT_TARGET) + NSHumanReadableCopyright + Copyright © 2018 Anton M. All rights reserved. + NSMainStoryboardFile + Main + NSPrincipalClass + NSApplication + + diff --git a/Frameworks/LogViewer/LogViewer/LogDS.swift b/Frameworks/LogViewer/LogViewer/LogDS.swift new file mode 100644 index 000000000..e02517f89 --- /dev/null +++ b/Frameworks/LogViewer/LogViewer/LogDS.swift @@ -0,0 +1,58 @@ +// +// LogDS.swift +// LogViewer +// +// Created by Anton M on 16.08.2018. +// Copyright © 2018 Anton M. All rights reserved. +// + +import Cocoa + +class LogDS: NSObject, NSTableViewDataSource { + var source: [Log] = [] { + didSet { + self.titles.removeAll() + source.forEach { (log) in + if log.title != nil { + self.titles.insert(log.title!) + } + } + self.filter = Set() + + } + } + + var filteredSource: [Log] = [] + + var titles: Set = Set() + var filter: Set = Set() { + didSet { + self.filteredSource = source.filter() { !filter.contains($0.title ?? "") } + } + } + + func numberOfRows(in tableView: NSTableView) -> Int { + return filteredSource.count + } + + func tableView(_ tableView: NSTableView, objectValueFor tableColumn: NSTableColumn?, row: Int) -> Any? { + let log = filteredSource[row] + switch tableColumn!.identifier { + case NSUserInterfaceItemIdentifier(rawValue: "TimeStamp"): + let timestamp = TimeInterval(log.timestamp ?? 0) + let date = Date(timeIntervalSince1970: timestamp) + let formatter = DateFormatter() + formatter.dateFormat = "dd-MM-yyyy HH:mm:ss" + return formatter.string(from: date) + case NSUserInterfaceItemIdentifier(rawValue: "Title"): + return log.title + case NSUserInterfaceItemIdentifier(rawValue: "Desc"): + return log.description + case NSUserInterfaceItemIdentifier(rawValue: "Thread"): + return log.thread + default: return "" + } + } + + +} diff --git a/Frameworks/LogViewer/LogViewer/LogViewer.entitlements b/Frameworks/LogViewer/LogViewer/LogViewer.entitlements new file mode 100644 index 000000000..f2ef3ae02 --- /dev/null +++ b/Frameworks/LogViewer/LogViewer/LogViewer.entitlements @@ -0,0 +1,10 @@ + + + + + com.apple.security.app-sandbox + + com.apple.security.files.user-selected.read-only + + + diff --git a/Frameworks/LogViewer/LogViewer/StreamReader.swift b/Frameworks/LogViewer/LogViewer/StreamReader.swift new file mode 100644 index 000000000..68f7200b7 --- /dev/null +++ b/Frameworks/LogViewer/LogViewer/StreamReader.swift @@ -0,0 +1,89 @@ +// +// StreamReader.swift +// LogViewer +// +// Created by Anton M on 17.08.2018. +// Copyright © 2018 Anton M. All rights reserved. +// + +import Foundation + +class StreamReader { + + let encoding : String.Encoding + let chunkSize : Int + var fileHandle : FileHandle! + let delimData : Data + var buffer : Data + var atEof : Bool + + init?(path: String, delimiter: String = "\n", encoding: String.Encoding = .utf8, + chunkSize: Int = 4096) { + + guard let fileHandle = FileHandle(forReadingAtPath: path), + let delimData = delimiter.data(using: encoding) else { + return nil + } + self.encoding = encoding + self.chunkSize = chunkSize + self.fileHandle = fileHandle + self.delimData = delimData + self.buffer = Data(capacity: chunkSize) + self.atEof = false + } + + deinit { + self.close() + } + + /// Return next line, or nil on EOF. + func nextLine() -> String? { + precondition(fileHandle != nil, "Attempt to read from closed file") + + // Read data chunks from file until a line delimiter is found: + while !atEof { + if let range = buffer.range(of: delimData) { + // Convert complete line (excluding the delimiter) to a string: + let line = String(data: buffer.subdata(in: 0.. 0 { + buffer.append(tmpData) + } else { + // EOF or read error. + atEof = true + if buffer.count > 0 { + // Buffer contains last line in file (not terminated by delimiter). + let line = String(data: buffer as Data, encoding: encoding) + buffer.count = 0 + return line + } + } + } + return nil + } + + /// Start reading from the beginning of file. + func rewind() -> Void { + fileHandle.seek(toFileOffset: 0) + buffer.count = 0 + atEof = false + } + + /// Close the underlying file. No reading must be done after calling this method. + func close() -> Void { + fileHandle?.closeFile() + fileHandle = nil + } +} + +extension StreamReader : Sequence { + func makeIterator() -> AnyIterator { + return AnyIterator { + return self.nextLine() + } + } +} diff --git a/Frameworks/LogViewer/LogViewer/ViewController.swift b/Frameworks/LogViewer/LogViewer/ViewController.swift new file mode 100644 index 000000000..ca42777a3 --- /dev/null +++ b/Frameworks/LogViewer/LogViewer/ViewController.swift @@ -0,0 +1,236 @@ +// +// ViewController.swift +// LogViewer +// +// Created by Anton M on 16.08.2018. +// Copyright © 2018 Anton M. All rights reserved. +// + +import Cocoa +import SnapKit + +class ViewController: NSViewController, NSCollectionViewDelegate, NSTableViewDelegate { + + let fileDatasource = FileMenuDS() + let logDS = LogDS() + let filterMenu = NSApp.menu?.items.last + + lazy var files: NSCollectionView = { + let collection = NSCollectionView(frame: NSRect(x: 0, y: 0, width: 0, height: 0)) + collection.backgroundColors = [NSColor.darkGray] + let layout = NSCollectionViewFlowLayout() + layout.itemSize = CGSize(width: 130, height: 40) + layout.scrollDirection = .horizontal + layout.minimumLineSpacing = 2 + collection.collectionViewLayout = layout + collection.allowsMultipleSelection = true + collection.isSelectable = true + collection.register( + FileMenuCell.self, + forItemWithIdentifier: NSUserInterfaceItemIdentifier(rawValue: "Cell") + ) + return collection + }() + + let tableViewColumnNamesArray: [[String: String]] = [ + ["columnIdentifier":"TimeStamp","columnTitle":"Time Stamp","columnType":"text","columnMaxWidth":"150","columnMinWidth":"150"], + ["columnIdentifier":"Title","columnTitle":"Title","columnType":"text","columnMaxWidth":"100","columnMinWidth":"100"], + ["columnIdentifier":"Thread","columnTitle":"Thread","columnType":"text","columnMaxWidth":"150","columnMinWidth":"150"], + ["columnIdentifier":"Desc","columnTitle":"Description","columnType":"text","columnMaxWidth":"500","columnMinWidth":"100"] + ] + + lazy var data: NSTableView = { + let tbl = NSTableView() + tbl.backgroundColor = NSColor.green + for var col in self.tableViewColumnNamesArray { + let column = NSTableColumn(identifier: NSUserInterfaceItemIdentifier(rawValue: col["columnIdentifier"]!)) + column.headerCell.title = col["columnTitle"]! + column.width = CGFloat(Int(col["columnMaxWidth"]!)!) + column.minWidth = CGFloat(Int(col["columnMinWidth"]!)!) + tbl.addTableColumn(column) + } + return tbl + }() + + lazy var scrollView: NSScrollView = { + let _scrV = NSScrollView() + _scrV.documentView = files + view.addSubview(_scrV) + _scrV.snp.makeConstraints({ (make) in + make.left.right.top.equalTo(self.view) + make.height.equalTo(40) + }) + return _scrV + }() + + lazy var scrollView2: NSScrollView = { + let _scrV = NSScrollView() + _scrV.documentView = data + view.addSubview(_scrV) + _scrV.snp.makeConstraints({ (make) in + make.left.right.equalTo(self.view) + make.bottom.equalTo(self.textView.snp.top) + make.top.equalTo(separator.snp.bottom) +// make.height.equalTo(NSScreen.main?.frame.height ?? 1000) + }) + return _scrV + }() + + lazy var separator: NSView = { + let v = NSView() + v.wantsLayer = true + v.layer?.backgroundColor = NSColor.black.cgColor + self.view.addSubview(v) + v.snp.makeConstraints({ (make) in + make.left.right.equalTo(self.view) + make.top.equalTo(scrollView.snp.bottom) + make.height.equalTo(1) + }) + return v + }() + + private let textStorage = NSTextStorage() + + lazy var textView: NSTextView = { + let layoutManager = NSLayoutManager() + textStorage.addLayoutManager(layoutManager) + let textContainer = NSTextContainer(containerSize: NSSize(width: 1000, height: CGFloat(FLT_MAX))) + layoutManager.addTextContainer(textContainer) + let _textView = NSTextView(frame: NSRect(x: 0, y: 0, width: 1000, height: 200), textContainer: textContainer) + _textView.isEditable = false + _textView.isSelectable = true + _textView.minSize = NSMakeSize(0, 200) + _textView.maxSize = NSMakeSize(CGFloat(FLT_MAX), CGFloat(FLT_MAX)) + _textView.isVerticallyResizable = true + _textView.isHorizontallyResizable = false + _textView.autoresizingMask = [.width] + return _textView + }() + + lazy var scrollView3: NSScrollView = { + let _scrV = NSScrollView() + _scrV.documentView = textView + _scrV.borderType = .lineBorder + _scrV.hasVerticalScroller = true + _scrV.hasHorizontalScroller = false + _scrV.autoresizingMask = [NSView.AutoresizingMask.width, NSView.AutoresizingMask.height] + view.addSubview(_scrV) + _scrV.snp.makeConstraints({ (make) in + make.left.right.bottom.equalTo(self.view) + make.height.equalTo(200) + }) + return _scrV + }() + + override func viewDidLoad() { + super.viewDidLoad() + scrollView3.isHidden = false + data.dataSource = logDS + files.dataSource = fileDatasource + files.delegate = self + scrollView2.isHidden = false + data.delegate = self + self.filterMenu?.isEnabled = false + self.view.layer?.backgroundColor = NSColor.darkGray.cgColor + // Do any additional setup after loading the view. + } + + override var representedObject: Any? { + didSet { + // Update the view, if already loaded. + } + } + + func collectionView(_ collectionView: NSCollectionView, didSelectItemsAt indexPaths: Set) { + guard let itemIndex = indexPaths.first?.last else { return } + if itemIndex == fileDatasource.files.count { + addNewFile() + collectionView.deselectAll(nil) + } else { + let file = fileDatasource.files[itemIndex] + updateLogs(url: file) + } + } + + func addNewFile() { + guard let link = openFile() else { return } + fileDatasource.files.append(link) + updateLogs(url: link) + files.reloadData() + } + + private func updateLogs(url: URL) { + FileReader.shared.getData(url: url) { result in + if result != nil { + DispatchQueue.main.async { [weak self] in + self?.logDS.source = result! + if let titles = self?.logDS.titles { + let titles = Array(titles) + self?.setupFilter(titles: titles) + } else { + self?.setupFilter(titles: [String]()) + } + self?.data.reloadData() + } + } + } + } + + func setupFilter(titles: [String]) { + if titles.count > 0 { + self.filterMenu?.isEnabled = true + } else { + self.filterMenu?.isEnabled = false + } + if let menu = self.filterMenu?.submenu as? NSMenu { + menu.removeAllItems() + for i in 0.. URL? { + let dialog = NSOpenPanel(); + + dialog.title = "Choose a .log file"; + dialog.showsResizeIndicator = true; + dialog.showsHiddenFiles = false; + dialog.canChooseDirectories = false; + dialog.canCreateDirectories = false; + dialog.allowsMultipleSelection = false; + dialog.allowedFileTypes = ["log"]; + + if (dialog.runModal() == .OK) { + return dialog.url + } else { + return nil + } + } + + func tableViewSelectionDidChange(_ notification: Notification) { + let row = data.selectedRow + if row != -1 { + textStorage.mutableString.setString("") + let log = logDS.filteredSource[row].description?.replacingOccurrences(of: "\n", with: " ") ?? "" + let str = NSAttributedString(string: "\(log)") + textStorage.append(str) + } + } +} + diff --git a/Frameworks/LogViewer/Podfile b/Frameworks/LogViewer/Podfile new file mode 100644 index 000000000..b84dc45cb --- /dev/null +++ b/Frameworks/LogViewer/Podfile @@ -0,0 +1,10 @@ +# Uncomment the next line to define a global platform for your project +# platform :ios, '9.0' + +target 'LogViewer' do + # Comment the next line if you're not using Swift and don't want to use dynamic frameworks + use_frameworks! + + # Pods for LogViewer + pod 'SnapKit', '~> 4.0.0' +end diff --git a/Frameworks/NynjaUIKit/NynjaUIKit.xcodeproj/project.pbxproj b/Frameworks/NynjaUIKit/NynjaUIKit.xcodeproj/project.pbxproj index d227aa966..f64833620 100644 --- a/Frameworks/NynjaUIKit/NynjaUIKit.xcodeproj/project.pbxproj +++ b/Frameworks/NynjaUIKit/NynjaUIKit.xcodeproj/project.pbxproj @@ -505,7 +505,11 @@ DYLIB_INSTALL_NAME_BASE = "@rpath"; INFOPLIST_FILE = NynjaUIKit/Info.plist; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; - LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@loader_path/Frameworks", + ); PRODUCT_BUNDLE_IDENTIFIER = com.nynja.mobile.communicator.NynjaUIKit; PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; PROVISIONING_PROFILE_SPECIFIER = ""; @@ -586,7 +590,11 @@ DYLIB_INSTALL_NAME_BASE = "@rpath"; INFOPLIST_FILE = NynjaUIKit/Info.plist; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; - LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@loader_path/Frameworks", + ); PRODUCT_BUNDLE_IDENTIFIER = com.nynja.mobile.communicator.NynjaUIKit; PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; PROVISIONING_PROFILE_SPECIFIER = ""; @@ -667,7 +675,11 @@ DYLIB_INSTALL_NAME_BASE = "@rpath"; INFOPLIST_FILE = NynjaUIKit/Info.plist; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; - LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@loader_path/Frameworks", + ); PRODUCT_BUNDLE_IDENTIFIER = com.nynja.mobile.communicator.NynjaUIKit; PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; PROVISIONING_PROFILE_SPECIFIER = ""; @@ -748,7 +760,11 @@ DYLIB_INSTALL_NAME_BASE = "@rpath"; INFOPLIST_FILE = NynjaUIKit/Info.plist; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; - LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@loader_path/Frameworks", + ); PRODUCT_BUNDLE_IDENTIFIER = com.nynja.mobile.communicator.NynjaUIKit; PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; PROVISIONING_PROFILE_SPECIFIER = ""; @@ -829,7 +845,11 @@ DYLIB_INSTALL_NAME_BASE = "@rpath"; INFOPLIST_FILE = NynjaUIKit/Info.plist; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; - LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@loader_path/Frameworks", + ); PRODUCT_BUNDLE_IDENTIFIER = com.nynja.mobile.communicator.NynjaUIKit; PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; PROVISIONING_PROFILE_SPECIFIER = ""; @@ -910,7 +930,11 @@ DYLIB_INSTALL_NAME_BASE = "@rpath"; INFOPLIST_FILE = NynjaUIKit/Info.plist; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; - LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@loader_path/Frameworks", + ); PRODUCT_BUNDLE_IDENTIFIER = com.nynja.mobile.communicator.NynjaUIKit; PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; PROVISIONING_PROFILE_SPECIFIER = ""; diff --git a/Frameworks/NynjaUIKit/NynjaUIKit/Core/Collection/Extensions/UICollectionView+ViewModel.swift b/Frameworks/NynjaUIKit/NynjaUIKit/Core/Collection/Extensions/UICollectionView+ViewModel.swift index e70499029..0522c8add 100755 --- a/Frameworks/NynjaUIKit/NynjaUIKit/Core/Collection/Extensions/UICollectionView+ViewModel.swift +++ b/Frameworks/NynjaUIKit/NynjaUIKit/Core/Collection/Extensions/UICollectionView+ViewModel.swift @@ -38,6 +38,7 @@ extension UICollectionView { let identifier = type(of: viewModel).uniqueIdentifier let kind = type(of: viewModel).supplementaryKind let view = dequeueReusableSupplementaryView(ofKind: kind, withReuseIdentifier: identifier, for: indexPath) + view.accessibilityIdentifier = viewModel.accessibilityIdentifier(for: indexPath) viewModel.setup(view: view) return view } diff --git a/Frameworks/NynjaUIKit/NynjaUIKit/Core/Collection/ViewModels/SupplementaryView/SupplementaryViewModel.swift b/Frameworks/NynjaUIKit/NynjaUIKit/Core/Collection/ViewModels/SupplementaryView/SupplementaryViewModel.swift index 3d5ca8e81..193a6c513 100644 --- a/Frameworks/NynjaUIKit/NynjaUIKit/Core/Collection/ViewModels/SupplementaryView/SupplementaryViewModel.swift +++ b/Frameworks/NynjaUIKit/NynjaUIKit/Core/Collection/ViewModels/SupplementaryView/SupplementaryViewModel.swift @@ -10,7 +10,7 @@ import UIKit public typealias AnySupplementaryView = UICollectionReusableView -public protocol AnySupplementaryViewModel: Reusable { +public protocol AnySupplementaryViewModel: Reusable, AccessibilityConfigurable { static var supplementaryKind: String { get } func setup(view: AnySupplementaryView) } diff --git a/Nynja-Share/Resources/Info.plist b/Nynja-Share/Resources/Info.plist index b50a69636..241f06e9a 100644 --- a/Nynja-Share/Resources/Info.plist +++ b/Nynja-Share/Resources/Info.plist @@ -21,7 +21,7 @@ CFBundleShortVersionString 1.0 CFBundleVersion - 0.2.138 + 0.2.153 Config $(Config) ModelsVersion diff --git a/Nynja-Share/Services/Handlers/ContactHandler.swift b/Nynja-Share/Services/Handlers/ContactHandler.swift index 829a00be9..a2041fabf 100644 --- a/Nynja-Share/Services/Handlers/ContactHandler.swift +++ b/Nynja-Share/Services/Handlers/ContactHandler.swift @@ -9,22 +9,21 @@ import Foundation protocol ContactHandlerDelegate: class { - func getContactSuccess(contact: Contact) + func didReceiveUpdatedContact(contact: Contact) } extension ContactHandlerDelegate { - func getContactSuccess(contact: Contact) {} + func didReceiveUpdatedContact(contact: Contact) { } } // TODO: need to think about this. It is share extension. class ContactHandler: BaseHandler { - static weak var delegate :ContactHandlerDelegate? + + static weak var delegate: ContactHandlerDelegate? static func executeHandle(data: BertTuple) { - guard let contact = get_Contact().parse(bert: data) as? Contact, - let status = contact.originalStatus else { - return + guard let contact = get_Contact().parse(bert: data) as? Contact, contact.originalStatus != nil else { + return } - delegate?.getContactSuccess(contact: contact) + delegate?.didReceiveUpdatedContact(contact: contact) } - } diff --git a/Nynja-Share/UI/ActionsView.swift b/Nynja-Share/UI/ActionsView.swift index 0f12464b1..21e883580 100644 --- a/Nynja-Share/UI/ActionsView.swift +++ b/Nynja-Share/UI/ActionsView.swift @@ -177,7 +177,7 @@ class ActionsView: UIView { button.setTitle(title, for: .normal) } - button.titleLabel?.font = UIFont.systemFont(ofSize: 10)//UIFont.init(fontName: Constants.fonts.medium, height: Constraints.buttons.labelHeight) + button.titleLabel?.font = UIFont.systemFont(ofSize: 10) button.addTarget(self, action: #selector(actionButtonTapped(_:)), for: .touchUpInside) diff --git a/Nynja-Share/UI/ForwardSelectorInteractor.swift b/Nynja-Share/UI/ForwardSelectorInteractor.swift index 1916839cc..68c11487c 100644 --- a/Nynja-Share/UI/ForwardSelectorInteractor.swift +++ b/Nynja-Share/UI/ForwardSelectorInteractor.swift @@ -85,7 +85,7 @@ final class ForwardSelectorInteractor: ForwardSelectorInteractorInputProtocol, P func connectToServer() { if let token = StorageService.sharedInstance.token { - LogService.log(topic: .MQTT, text: "token: \(token)") + LogService.log(topic: .MQTT) { return "token: \(token)" } _ = MQTTService.sharedInstance.connectFromExtension() } else { handlerServerSignals?(.noToken) @@ -335,7 +335,7 @@ final class ForwardSelectorInteractor: ForwardSelectorInteractorInputProtocol, P //MARK: - ContactHandlerDelegate - func getContactSuccess(contact: Contact) { + func didReceiveUpdatedContact(contact: Contact) { if [.ban, .banned].contains(contact.originalStatus) { contacts = contacts?.filter{ $0.content.contact?.phoneId != contact.phoneId } presenter.updateContactsIfNeeded() diff --git a/Nynja.xcodeproj/project.pbxproj b/Nynja.xcodeproj/project.pbxproj index f66a59051..52d749b8c 100644 --- a/Nynja.xcodeproj/project.pbxproj +++ b/Nynja.xcodeproj/project.pbxproj @@ -133,9 +133,19 @@ 2604C0962069163C0051E4FB /* HandlerServiceProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = A4A242412060336400B0A804 /* HandlerServiceProtocol.swift */; }; 26052C7A20FCE7E000E7A6A0 /* LogService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 26052C7920FCE7E000E7A6A0 /* LogService.swift */; }; 26052C7B20FCE7E000E7A6A0 /* LogService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 26052C7920FCE7E000E7A6A0 /* LogService.swift */; }; + 2605311B212740FD002E1CF1 /* LogOutputProtocols.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2605311A212740FD002E1CF1 /* LogOutputProtocols.swift */; }; + 2605311D21274116002E1CF1 /* LogOutputView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2605311C21274116002E1CF1 /* LogOutputView.swift */; }; + 2605311F21274124002E1CF1 /* LogOutputInteractor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2605311E21274124002E1CF1 /* LogOutputInteractor.swift */; }; + 2605312121274133002E1CF1 /* LogOutputPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2605312021274133002E1CF1 /* LogOutputPresenter.swift */; }; + 26053123212741C2002E1CF1 /* LogOutputWireFrame.swift in Sources */ = {isa = PBXBuildFile; fileRef = 26053122212741C2002E1CF1 /* LogOutputWireFrame.swift */; }; + 260531262127455C002E1CF1 /* MotionManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 260531252127455C002E1CF1 /* MotionManager.swift */; }; + 2605312921298BEF002E1CF1 /* Logoutputcell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2605312821298BEF002E1CF1 /* Logoutputcell.swift */; }; + 2605312B21299198002E1CF1 /* LogOutputDS.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2605312A21299198002E1CF1 /* LogOutputDS.swift */; }; 260552A61F9E1CD100D68DE6 /* SearchHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 260552A51F9E1CD100D68DE6 /* SearchHandler.swift */; }; 260629712056EF2800CB8F65 /* LinksCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 260629702056EF2800CB8F65 /* LinksCell.swift */; }; 2606F3BC20BFE20500CF7F15 /* MessageInteractor+Translation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2606F3BB20BFE20400CF7F15 /* MessageInteractor+Translation.swift */; }; + 260D67D92124616A0072F11F /* LogWriter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 260D67D82124616A0072F11F /* LogWriter.swift */; }; + 260D67DF2125A2FE0072F11F /* LogWriter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 260D67D82124616A0072F11F /* LogWriter.swift */; }; 2610D4642076516900E6E2B2 /* Array+Feature.swift in Sources */ = {isa = PBXBuildFile; fileRef = 26DCB25320692237001EF0AB /* Array+Feature.swift */; }; 26131E02210399BA00BE94F9 /* TranscribeService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 26131E01210399BA00BE94F9 /* TranscribeService.swift */; }; 26142B1120472ECD004E5FE4 /* MessageLinkTable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 26142B1020472ECD004E5FE4 /* MessageLinkTable.swift */; }; @@ -150,6 +160,7 @@ 26245F44204EF67C00C8D3DD /* PresenterApearingProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 26245F43204EF67B00C8D3DD /* PresenterApearingProtocol.swift */; }; 2625DBF620EFC52E00E01C05 /* AudioFileConvertOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2625DBF520EFC52D00E01C05 /* AudioFileConvertOperation.swift */; }; 2625DBF820EFC5DE00E01C05 /* FourCharCode+StringLiteralConvertible.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2625DBF720EFC5DE00E01C05 /* FourCharCode+StringLiteralConvertible.swift */; }; + 2625F29F212463E8007C42B5 /* ProgressIdentifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2625F29E212463E8007C42B5 /* ProgressIdentifier.swift */; }; 262D43872033417F002F1E45 /* FriendExtansion+BERT.swift in Sources */ = {isa = PBXBuildFile; fileRef = 262D43862033417F002F1E45 /* FriendExtansion+BERT.swift */; }; 262D438820335225002F1E45 /* FriendRequstModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A2374DA1F26458300701045 /* FriendRequstModel.swift */; }; 262D4389203352D4002F1E45 /* FriendExtansion+BERT.swift in Sources */ = {isa = PBXBuildFile; fileRef = 262D43862033417F002F1E45 /* FriendExtansion+BERT.swift */; }; @@ -172,6 +183,8 @@ 263529182075730500DC6FBD /* actExtension+BERT.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2683F75D203F36140003181A /* actExtension+BERT.swift */; }; 263A60AC1FB4F8F7006F9D52 /* ParticipantsDataSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = 263A60AB1FB4F8F7006F9D52 /* ParticipantsDataSource.swift */; }; 263A60AE1FB51C22006F9D52 /* MemberExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 263A60AD1FB51C22006F9D52 /* MemberExtension.swift */; }; + 263C04E92132E2FF00B8F0BE /* WrappedTaskOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 263C04E82132E2FF00B8F0BE /* WrappedTaskOperation.swift */; }; + 263C04EB2132E56E00B8F0BE /* TranscribeOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 263C04EA2132E56E00B8F0BE /* TranscribeOperation.swift */; }; 263D66271FE829CC00A509F8 /* RoomExtension+BERT.swift in Sources */ = {isa = PBXBuildFile; fileRef = 263D66261FE829CC00A509F8 /* RoomExtension+BERT.swift */; }; 263D662A1FE8359900A509F8 /* RoomExtension+BERT.swift in Sources */ = {isa = PBXBuildFile; fileRef = 263D66261FE829CC00A509F8 /* RoomExtension+BERT.swift */; }; 263D662D1FE8D03400A509F8 /* TypingModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 263D662C1FE8D03400A509F8 /* TypingModel.swift */; }; @@ -211,8 +224,6 @@ 2648C41E2069B5B300863614 /* ChangeNumberItemsFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2648C41D2069B5B200863614 /* ChangeNumberItemsFactory.swift */; }; 264C808620DBF397003532FA /* DBFeatureFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 264C808520DBF397003532FA /* DBFeatureFactory.swift */; }; 264FFA901FC590580028243D /* Nynja-Share.appex in Embed App Extensions */ = {isa = PBXBuildFile; fileRef = 357809A31F9765CF00C9680C /* Nynja-Share.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; }; - 264FFA951FC5912E0028243D /* Table.swift in Sources */ = {isa = PBXBuildFile; fileRef = E7417E981FBED91100E5C124 /* Table.swift */; }; - 264FFA961FC5913A0028243D /* ProfileTable.swift in Sources */ = {isa = PBXBuildFile; fileRef = E70938361FBEDA2B006CCDC6 /* ProfileTable.swift */; }; 264FFA971FC591600028243D /* Describable.swift in Sources */ = {isa = PBXBuildFile; fileRef = E709383E1FBEE41D006CCDC6 /* Describable.swift */; }; 264FFA981FC5917D0028243D /* WheelItemModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3AE0A8401F20321A008A04F3 /* WheelItemModel.swift */; }; 2651093F20ADB81100F1B38B /* NotificationSettingProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2651093E20ADB81100F1B38B /* NotificationSettingProtocol.swift */; }; @@ -249,6 +260,8 @@ 266F04CB2015050400B97A83 /* DBStarMessage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 266F04CA2015050400B97A83 /* DBStarMessage.swift */; }; 266F04CF201541BC00B97A83 /* StarExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 266F04CE201541BC00B97A83 /* StarExtension.swift */; }; 26770A571FFD6CAC009AC870 /* SharedParameters.swift in Sources */ = {isa = PBXBuildFile; fileRef = 26770A561FFD6CAC009AC870 /* SharedParameters.swift */; }; + 26771CC1212ECE08006112B5 /* ConvertMessageTable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 26771CC0212ECE08006112B5 /* ConvertMessageTable.swift */; }; + 26771CC3212ED109006112B5 /* DBConvertMessage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 26771CC2212ED109006112B5 /* DBConvertMessage.swift */; }; 26791A7C207639E7001A87B8 /* AuthModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3AC321771EEAC4C10068F3C8 /* AuthModel.swift */; }; 267BE2831FDE905D00C47E18 /* SettingsProtocols.swift in Sources */ = {isa = PBXBuildFile; fileRef = 267BE2821FDE905D00C47E18 /* SettingsProtocols.swift */; }; 267BE2851FDE983400C47E18 /* SettingsGroupVC.swift in Sources */ = {isa = PBXBuildFile; fileRef = 267BE2841FDE983400C47E18 /* SettingsGroupVC.swift */; }; @@ -288,10 +301,10 @@ 2689CDEA20C48AD8007816B9 /* TranslationManualView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2689CDE920C48AD8007816B9 /* TranslationManualView.swift */; }; 268C340C2106709A00F1472A /* GoogleService-Info.plist in Resources */ = {isa = PBXBuildFile; fileRef = 268C340B2106709A00F1472A /* GoogleService-Info.plist */; }; 268C340F21067BAD00F1472A /* AudioUploadOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 268C340E21067BAD00F1472A /* AudioUploadOperation.swift */; }; - 268C341121067F1D00F1472A /* TranscribeLongAudioOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 268C341021067F1D00F1472A /* TranscribeLongAudioOperation.swift */; }; + 268C341121067F1D00F1472A /* AudioLongTranscribeOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 268C341021067F1D00F1472A /* AudioLongTranscribeOperation.swift */; }; 268C3413210688B200F1472A /* TranscribeLongRequestData.swift in Sources */ = {isa = PBXBuildFile; fileRef = 268C3412210688B200F1472A /* TranscribeLongRequestData.swift */; }; 268C34152107479600F1472A /* TranscribeLongResponseData.swift in Sources */ = {isa = PBXBuildFile; fileRef = 268C34142107479600F1472A /* TranscribeLongResponseData.swift */; }; - 268C341721074AD000F1472A /* TranscribeLongAudioProccessingOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 268C341621074AD000F1472A /* TranscribeLongAudioProccessingOperation.swift */; }; + 268C341721074AD000F1472A /* AudioLongTranscribeProccessingOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 268C341621074AD000F1472A /* AudioLongTranscribeProccessingOperation.swift */; }; 268C341921074D6C00F1472A /* TranscribeLongOperationResponseData.swift in Sources */ = {isa = PBXBuildFile; fileRef = 268C341821074D6C00F1472A /* TranscribeLongOperationResponseData.swift */; }; 268C341C21075B4700F1472A /* Cancelable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 268C341B21075B4700F1472A /* Cancelable.swift */; }; 268C62E32008DA0900433705 /* UIImageExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A1DFD7D1F5370A600F3A3D8 /* UIImageExtensions.swift */; }; @@ -333,6 +346,7 @@ 26ABCA4A211B321100EA4782 /* Bundle+provision.swift in Sources */ = {isa = PBXBuildFile; fileRef = 26ABCA49211B321100EA4782 /* Bundle+provision.swift */; }; 26ABF27B2059FE2500438975 /* URL+valid.swift in Sources */ = {isa = PBXBuildFile; fileRef = 26ABF27A2059FE2500438975 /* URL+valid.swift */; }; 26AC7B641F9F79D400D448AE /* NavigateProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 26AC7B631F9F79D400D448AE /* NavigateProtocol.swift */; }; + 26ACC5CE212C3DDB008455E8 /* AudioTranscribeSendOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 26ACC5CD212C3DDB008455E8 /* AudioTranscribeSendOperation.swift */; }; 26AD28371FFB0AE3009E4580 /* StorageSubscriber.swift in Sources */ = {isa = PBXBuildFile; fileRef = 26AD28361FFB0AE3009E4580 /* StorageSubscriber.swift */; }; 26AD28391FFB0AF9009E4580 /* StorageObserver.swift in Sources */ = {isa = PBXBuildFile; fileRef = 26AD28381FFB0AF9009E4580 /* StorageObserver.swift */; }; 26B06C8020602643005BF9AF /* CarouselPickerCollectionViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 26B06C7F20602643005BF9AF /* CarouselPickerCollectionViewCell.swift */; }; @@ -385,11 +399,13 @@ 26C1A3ED2031D3030009F7F0 /* OtherUserContainerViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 26C1A3EC2031D3030009F7F0 /* OtherUserContainerViewController.swift */; }; 26C1A3F02031D9E60009F7F0 /* OtherUserTableViewDS.swift in Sources */ = {isa = PBXBuildFile; fileRef = 26C1A3EF2031D9E60009F7F0 /* OtherUserTableViewDS.swift */; }; 26C1A3F32031EED30009F7F0 /* OtherUserHeaderView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 26C1A3F22031EED30009F7F0 /* OtherUserHeaderView.swift */; }; - 26CD3FDB2104D19D00597E62 /* TranscribeShortAudioOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 26CD3FDA2104D19D00597E62 /* TranscribeShortAudioOperation.swift */; }; - 26CD3FDD2104D1DD00597E62 /* AudioConvertionOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 26CD3FDC2104D1DD00597E62 /* AudioConvertionOperation.swift */; }; + 26CD3FDB2104D19D00597E62 /* AudioShortTranscribeOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 26CD3FDA2104D19D00597E62 /* AudioShortTranscribeOperation.swift */; }; + 26CD3FDD2104D1DD00597E62 /* AudioConvertOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 26CD3FDC2104D1DD00597E62 /* AudioConvertOperation.swift */; }; 26D238E9781B604B721C6643 /* ScheduleMessageViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5245254E61C6EB3C6ACF4D2C /* ScheduleMessageViewController.swift */; }; 26D35AB81FD0EFA800A5D513 /* AudioPlayer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 26D35AB71FD0EFA800A5D513 /* AudioPlayer.swift */; }; 26D621F42069778400595E13 /* ChatWheelItemView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 26D621F32069778400595E13 /* ChatWheelItemView.swift */; }; + 26D6D227212EDA6600EA2419 /* ConvertMessageDAOProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 26D6D226212EDA6600EA2419 /* ConvertMessageDAOProtocol.swift */; }; + 26D6D229212EDADC00EA2419 /* ConvertMessageDAO.swift in Sources */ = {isa = PBXBuildFile; fileRef = 26D6D228212EDADC00EA2419 /* ConvertMessageDAO.swift */; }; 26D8317520EA65200067C5B4 /* TranslationInfo.swift in Sources */ = {isa = PBXBuildFile; fileRef = 26D8317420EA65200067C5B4 /* TranslationInfo.swift */; }; 26DAE5D01FFAF45800EDF412 /* BackgroundModeSubscriber.swift in Sources */ = {isa = PBXBuildFile; fileRef = 26DAE5CF1FFAF45800EDF412 /* BackgroundModeSubscriber.swift */; }; 26DAE5D21FFAF7EE00EDF412 /* BackgroundModeService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 26DAE5D11FFAF7EE00EDF412 /* BackgroundModeService.swift */; }; @@ -610,6 +626,7 @@ 5A6237362268CC9BD4792230 /* EditUsernameViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8B772E08B9E40EB48DD87082 /* EditUsernameViewController.swift */; }; 5AD8110B5B87B1AB9F1C5B52 /* CreateGroupPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8CBACEAABEE65D7EC5572C4E /* CreateGroupPresenter.swift */; }; 5B5EE777EF301CFC1FDCF307 /* CreateGroupInteractor.swift in Sources */ = {isa = PBXBuildFile; fileRef = AFC76E2B3DD0BCA0A622A5CD /* CreateGroupInteractor.swift */; }; + 5BBEF53C212DE09F00F10768 /* ringback.m4a in Resources */ = {isa = PBXBuildFile; fileRef = 5BBEF53B212DE09F00F10768 /* ringback.m4a */; }; 5BC1D37320D3B3D9002A44B3 /* NynjaCommunicatorService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5BC1D37220D3B3D8002A44B3 /* NynjaCommunicatorService.swift */; }; 5BC1D37920D3B4A8002A44B3 /* GroupCollectionViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5BC1D37420D3B4A6002A44B3 /* GroupCollectionViewCell.swift */; }; 5BC1D37B20D3B4A8002A44B3 /* GroupAddParticipantsCollectionViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5BC1D37620D3B4A7002A44B3 /* GroupAddParticipantsCollectionViewCell.swift */; }; @@ -723,9 +740,13 @@ 850FC611203312FA00832D87 /* ForwardSelectorViewControllerLayout.swift in Sources */ = {isa = PBXBuildFile; fileRef = 850FC610203312FA00832D87 /* ForwardSelectorViewControllerLayout.swift */; }; 8511D3712034427F00B2A620 /* UIView+SafeArea.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8511D3702034427F00B2A620 /* UIView+SafeArea.swift */; }; 8511D3742034596E00B2A620 /* Collection+ViewLayout.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8511D3732034596E00B2A620 /* Collection+ViewLayout.swift */; }; - 8512349221221B9E000129A2 /* CollectionExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8512349121221B9E000129A2 /* CollectionExtension.swift */; }; + 8512349221221B9E000129A2 /* Collection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8512349121221B9E000129A2 /* Collection.swift */; }; 8514D52220EE48930002378A /* NynjaContextMenuItemsFactory+Design.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8514D52120EE48930002378A /* NynjaContextMenuItemsFactory+Design.swift */; }; 8514D52420EE48A30002378A /* NynjaContextMenuItemsFactory+Messages.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8514D52320EE48A30002378A /* NynjaContextMenuItemsFactory+Messages.swift */; }; + 8514DE892136A50100718DD8 /* DBStarAction.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8514DE882136A50100718DD8 /* DBStarAction.swift */; }; + 8514DE8C2136A5FD00718DD8 /* StarActionTable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8514DE8B2136A5FD00718DD8 /* StarActionTable.swift */; }; + 8514DE8F2136A9A900718DD8 /* StarActionDAO.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8514DE8E2136A9A900718DD8 /* StarActionDAO.swift */; }; + 8514DE912136A9CB00718DD8 /* StarActionDAOProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8514DE902136A9CB00718DD8 /* StarActionDAOProtocol.swift */; }; 8514F17220EA219E00883513 /* ContextMenuItemsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8514F16620EA219E00883513 /* ContextMenuItemsView.swift */; }; 8514F17320EA219E00883513 /* ContextMenuConfiguration+Favorites.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8514F16720EA219E00883513 /* ContextMenuConfiguration+Favorites.swift */; }; 8514F17420EA219E00883513 /* ContextMenuNextCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8514F16920EA219E00883513 /* ContextMenuNextCell.swift */; }; @@ -761,6 +782,9 @@ 8528E50E2072835E00A8644A /* AudioDurationFormatter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8528E50D2072835E00A8644A /* AudioDurationFormatter.swift */; }; 852DF26120371FB400A4F8B6 /* FileExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 852DF26020371FB400A4F8B6 /* FileExtension.swift */; }; 852DF263203720E600A4F8B6 /* FileIcons.swift in Sources */ = {isa = PBXBuildFile; fileRef = 852DF262203720E600A4F8B6 /* FileIcons.swift */; }; + 852E847121345FB400FD3841 /* MessageCollectionViewLayoutAttributes.swift in Sources */ = {isa = PBXBuildFile; fileRef = 852E847021345FB400FD3841 /* MessageCollectionViewLayoutAttributes.swift */; }; + 852E8473213460E800FD3841 /* ReversedMessageCollectionViewLayoutAttributes.swift in Sources */ = {isa = PBXBuildFile; fileRef = 852E8472213460E800FD3841 /* ReversedMessageCollectionViewLayoutAttributes.swift */; }; + 852E8475213462F000FD3841 /* ReversedMessageCollectionViewLayout.swift in Sources */ = {isa = PBXBuildFile; fileRef = 852E8474213462F000FD3841 /* ReversedMessageCollectionViewLayout.swift */; }; 853801242052C848002C6960 /* TextCheckmarkTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 853801232052C848002C6960 /* TextCheckmarkTableViewCell.swift */; }; 853801262052C853002C6960 /* TextCheckmarkCellModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 853801252052C853002C6960 /* TextCheckmarkCellModel.swift */; }; 853801282052CCAD002C6960 /* SoundCellModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 853801272052CCAD002C6960 /* SoundCellModel.swift */; }; @@ -799,6 +823,36 @@ 85433F25204D596D00B373A7 /* WebFullScreenInteractor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 85433F20204D596D00B373A7 /* WebFullScreenInteractor.swift */; }; 85433F26204D596D00B373A7 /* WebFullScreenWireFrame.swift in Sources */ = {isa = PBXBuildFile; fileRef = 85433F21204D596D00B373A7 /* WebFullScreenWireFrame.swift */; }; 85433F2C204D5AA500B373A7 /* NynjaCloseButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 85433F2B204D5AA500B373A7 /* NynjaCloseButton.swift */; }; + 85458CD9212D6FED00BA8814 /* String+Split.swift in Sources */ = {isa = PBXBuildFile; fileRef = 85458CD8212D6FED00BA8814 /* String+Split.swift */; }; + 85458CDA212D6FFE00BA8814 /* String+Split.swift in Sources */ = {isa = PBXBuildFile; fileRef = 85458CD8212D6FED00BA8814 /* String+Split.swift */; }; + 85458CDB212D6FFE00BA8814 /* String+Split.swift in Sources */ = {isa = PBXBuildFile; fileRef = 85458CD8212D6FED00BA8814 /* String+Split.swift */; }; + 85458CDC212D6FFF00BA8814 /* String+Split.swift in Sources */ = {isa = PBXBuildFile; fileRef = 85458CD8212D6FED00BA8814 /* String+Split.swift */; }; + 85458CE2212D730E00BA8814 /* MessageIdentifiers.swift in Sources */ = {isa = PBXBuildFile; fileRef = 85458CE1212D730E00BA8814 /* MessageIdentifiers.swift */; }; + 85458CE3212D731200BA8814 /* MessageIdentifiers.swift in Sources */ = {isa = PBXBuildFile; fileRef = 85458CE1212D730E00BA8814 /* MessageIdentifiers.swift */; }; + 85458CE4212D731300BA8814 /* MessageIdentifiers.swift in Sources */ = {isa = PBXBuildFile; fileRef = 85458CE1212D730E00BA8814 /* MessageIdentifiers.swift */; }; + 85458CE5212D731300BA8814 /* MessageIdentifiers.swift in Sources */ = {isa = PBXBuildFile; fileRef = 85458CE1212D730E00BA8814 /* MessageIdentifiers.swift */; }; + 85458CE6212D73CF00BA8814 /* P2pExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = E7C9CEC41FCC245F0090C2E0 /* P2pExtension.swift */; }; + 85458CE8212D73E600BA8814 /* MucExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = E7C9CEC61FCC249D0090C2E0 /* MucExtension.swift */; }; + 85458CE9212D740D00BA8814 /* MessageExtension+BERT.swift in Sources */ = {isa = PBXBuildFile; fileRef = A4F3DAB12084949F00FF71C7 /* MessageExtension+BERT.swift */; }; + 85458CEA212D742300BA8814 /* muc.swift in Sources */ = {isa = PBXBuildFile; fileRef = A42CE4D320692EDA000889CC /* muc.swift */; }; + 85458CEB212D742A00BA8814 /* p2p.swift in Sources */ = {isa = PBXBuildFile; fileRef = A42CE4E820692EDA000889CC /* p2p.swift */; }; + 85458CED212D74B400BA8814 /* P2P+Opponent.swift in Sources */ = {isa = PBXBuildFile; fileRef = 85458CEC212D74B400BA8814 /* P2P+Opponent.swift */; }; + 85458CEE212D74C000BA8814 /* P2P+Opponent.swift in Sources */ = {isa = PBXBuildFile; fileRef = 85458CEC212D74B400BA8814 /* P2P+Opponent.swift */; }; + 85458CF0212D75CE00BA8814 /* p2pExtension+BERT.swift in Sources */ = {isa = PBXBuildFile; fileRef = 26B32B8F1FE20B6F00888A0A /* p2pExtension+BERT.swift */; }; + 85458CF1212D75D300BA8814 /* mucExtension+BERT.swift in Sources */ = {isa = PBXBuildFile; fileRef = 26B32B921FE20B8B00888A0A /* mucExtension+BERT.swift */; }; + 85458CF3212D762900BA8814 /* Message+Factory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 85458CF2212D762900BA8814 /* Message+Factory.swift */; }; + 85458CF5212D770100BA8814 /* Message+Files.swift in Sources */ = {isa = PBXBuildFile; fileRef = 85458CF4212D770100BA8814 /* Message+Files.swift */; }; + 85458CF6212D770900BA8814 /* Message+Files.swift in Sources */ = {isa = PBXBuildFile; fileRef = 85458CF4212D770100BA8814 /* Message+Files.swift */; }; + 85458CF7212D771700BA8814 /* MessageExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = A45F114320B421AB00F45004 /* MessageExtension.swift */; }; + 85458CF9212D785A00BA8814 /* DescExtension+BERT.swift in Sources */ = {isa = PBXBuildFile; fileRef = 26B32B951FE20BAB00888A0A /* DescExtension+BERT.swift */; }; + 85458CFA212D79F500BA8814 /* Message+Factory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 85458CF2212D762900BA8814 /* Message+Factory.swift */; }; + 85458CFB212D7B2100BA8814 /* FeatureExtension+BERT.swift in Sources */ = {isa = PBXBuildFile; fileRef = 26C061BF1FEAA04A00A2EBE4 /* FeatureExtension+BERT.swift */; }; + 85458CFD212D7B8C00BA8814 /* Desc+Construct.swift in Sources */ = {isa = PBXBuildFile; fileRef = 85458CFC212D7B8C00BA8814 /* Desc+Construct.swift */; }; + 85458CFE212D7BCA00BA8814 /* Desc+Construct.swift in Sources */ = {isa = PBXBuildFile; fileRef = 85458CFC212D7B8C00BA8814 /* Desc+Construct.swift */; }; + 85458CFF212D7BCB00BA8814 /* Desc+Construct.swift in Sources */ = {isa = PBXBuildFile; fileRef = 85458CFC212D7B8C00BA8814 /* Desc+Construct.swift */; }; + 85458D00212D7C0C00BA8814 /* Int+AnyObject.swift in Sources */ = {isa = PBXBuildFile; fileRef = 85D669E120BD955F00FBD803 /* Int+AnyObject.swift */; }; + 85458D01212D7C1A00BA8814 /* StringAtomExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B052CAF203614D400BC2A9B /* StringAtomExtension.swift */; }; + 8546D464213EEC4E0024FE66 /* BidirectionalCollection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8546D463213EEC4E0024FE66 /* BidirectionalCollection.swift */; }; 854751492093BDD300F8D5F8 /* CollectionViewScrollProxy.swift in Sources */ = {isa = PBXBuildFile; fileRef = 854751482093BDD300F8D5F8 /* CollectionViewScrollProxy.swift */; }; 85482844204E915400DCBEC8 /* PrivacyTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 85482843204E915400DCBEC8 /* PrivacyTableViewCell.swift */; }; 85482846204E918000DCBEC8 /* PrivacyCellModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 85482845204E918000DCBEC8 /* PrivacyCellModel.swift */; }; @@ -812,7 +866,7 @@ 854CFB08210704AE00FBC133 /* CGRectExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 854CFB07210704AE00FBC133 /* CGRectExtensions.swift */; }; 854D13D8211B2E7200E139FC /* MessageCollectionViewLayout.swift in Sources */ = {isa = PBXBuildFile; fileRef = 854D13D7211B2E7200E139FC /* MessageCollectionViewLayout.swift */; }; 854FC1CB204468FC00B12BE5 /* CarouselFlowLayout.swift in Sources */ = {isa = PBXBuildFile; fileRef = 854FC1CA204468FC00B12BE5 /* CarouselFlowLayout.swift */; }; - 855792D52122206400D0AB57 /* CollectionExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8512349121221B9E000129A2 /* CollectionExtension.swift */; }; + 855792D52122206400D0AB57 /* Collection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8512349121221B9E000129A2 /* Collection.swift */; }; 8557987F2093200D007050B8 /* StickerMenuActionCollectionViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8557987D2093200D007050B8 /* StickerMenuActionCollectionViewCell.swift */; }; 855798802093200D007050B8 /* StickerMenuActionCellModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8557987E2093200D007050B8 /* StickerMenuActionCellModel.swift */; }; 85579882209322A8007050B8 /* StickerMenuDataSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = 85579881209322A8007050B8 /* StickerMenuDataSource.swift */; }; @@ -820,6 +874,7 @@ 8557988820932401007050B8 /* StickerStaticMenuActionCellModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8557988620932401007050B8 /* StickerStaticMenuActionCellModel.swift */; }; 8557989C209368E7007050B8 /* StickerPackHeaderView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8557989A209368E7007050B8 /* StickerPackHeaderView.swift */; }; 8557989D209368E7007050B8 /* StickerPackHeaderModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8557989B209368E7007050B8 /* StickerPackHeaderModel.swift */; }; + 855A393D213E76E20002B8DC /* LoadingInteractive.swift in Sources */ = {isa = PBXBuildFile; fileRef = 855A393C213E76E20002B8DC /* LoadingInteractive.swift */; }; 855AC532208E441500DC2335 /* StickersInputPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 855AC52D208E441500DC2335 /* StickersInputPresenter.swift */; }; 855AC533208E441500DC2335 /* StickersInputViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 855AC52E208E441500DC2335 /* StickersInputViewController.swift */; }; 855AC534208E441500DC2335 /* StickersInputProtocols.swift in Sources */ = {isa = PBXBuildFile; fileRef = 855AC52F208E441500DC2335 /* StickersInputProtocols.swift */; }; @@ -827,6 +882,7 @@ 855AC536208E441500DC2335 /* StickersInputWireFrame.swift in Sources */ = {isa = PBXBuildFile; fileRef = 855AC531208E441500DC2335 /* StickersInputWireFrame.swift */; }; 855AC53F208E45AA00DC2335 /* StickerCollectionViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 855AC53D208E45AA00DC2335 /* StickerCollectionViewCell.swift */; }; 855AC540208E45AA00DC2335 /* StickerCellModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 855AC53E208E45AA00DC2335 /* StickerCellModel.swift */; }; + 855C9FE62125B4C0000E3429 /* MessageHandlerSubscriber.swift in Sources */ = {isa = PBXBuildFile; fileRef = 855C9FE52125B4C0000E3429 /* MessageHandlerSubscriber.swift */; }; 855EF421202CC6F800541BE3 /* GetExtendedStarsModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 855EF420202CC6F800541BE3 /* GetExtendedStarsModel.swift */; }; 855EF423202CC85300541BE3 /* MQTTServiceStars.swift in Sources */ = {isa = PBXBuildFile; fileRef = 855EF422202CC85300541BE3 /* MQTTServiceStars.swift */; }; 855EF425202CCADB00541BE3 /* ExtendedStarHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 855EF424202CCADB00541BE3 /* ExtendedStarHandler.swift */; }; @@ -835,6 +891,7 @@ 8562853620D164B5000C9739 /* ScaleAnimatableGrid.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8562853520D164B5000C9739 /* ScaleAnimatableGrid.swift */; }; 8562853920D166E5000C9739 /* CollectionPreviewState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8562853820D166E5000C9739 /* CollectionPreviewState.swift */; }; 8562853B20D16C61000C9739 /* LongPressClosureRecognizer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8562853A20D16C61000C9739 /* LongPressClosureRecognizer.swift */; }; + 85629ECA2137EF2400A79C97 /* VoiceAudioInteractive.swift in Sources */ = {isa = PBXBuildFile; fileRef = 85629EC92137EF2400A79C97 /* VoiceAudioInteractive.swift */; }; 8566771C20C139A000DD4204 /* DebugLogs.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8566771B20C139A000DD4204 /* DebugLogs.swift */; }; 8566771E20C1579C00DD4204 /* StorageSubscriberReference.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8566771D20C1579C00DD4204 /* StorageSubscriberReference.swift */; }; 8566772020C1924500DD4204 /* MessageInteractor+MessageHandlerSubscriber.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8566771F20C1924500DD4204 /* MessageInteractor+MessageHandlerSubscriber.swift */; }; @@ -905,6 +962,8 @@ 859C42AA2056B05D00AE3797 /* SoundBundle.swift in Sources */ = {isa = PBXBuildFile; fileRef = 859C42A92056B05D00AE3797 /* SoundBundle.swift */; }; 859C42AD2056BF9F00AE3797 /* incoming_message.mp3 in Resources */ = {isa = PBXBuildFile; fileRef = 859C42AC2056BF9F00AE3797 /* incoming_message.mp3 */; }; 859F9B4C2035CB1E009D017A /* ForwardContent.swift in Sources */ = {isa = PBXBuildFile; fileRef = 859F9B4B2035CB1E009D017A /* ForwardContent.swift */; }; + 85B0013221270DEC000C89FE /* TableOrder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 85B0013121270DEC000C89FE /* TableOrder.swift */; }; + 85B0013421272694000C89FE /* MessageInteractor+History.swift in Sources */ = {isa = PBXBuildFile; fileRef = 85B0013321272694000C89FE /* MessageInteractor+History.swift */; }; 85B750A120334A2B00AD6013 /* ForwardTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 85B750A020334A2B00AD6013 /* ForwardTableViewCell.swift */; }; 85BA176120BEA7BD001EF8AC /* StickerPreviewContainerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 85BA176020BEA7BD001EF8AC /* StickerPreviewContainerView.swift */; }; 85BEC0E12063F91C0098C99C /* TimeZoneCellModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 85BEC0E02063F91C0098C99C /* TimeZoneCellModel.swift */; }; @@ -1002,7 +1061,6 @@ 99B9D27D2F0EFE051E6581ED /* CreateGroupProtocols.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDE9DC6ADA0E71241C49A328 /* CreateGroupProtocols.swift */; }; 9BC9657620FF042E00052AE1 /* CallInProgressProtocols.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9BC9657520FF042D00052AE1 /* CallInProgressProtocols.swift */; }; 9BD8E3F120EF7898001384EC /* CallInProgressViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9BD8E3EF20EF7898001384EC /* CallInProgressViewController.swift */; }; - 9BD8E3F820EF8874001384EC /* CallInProgressNavigationController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9BD8E3F720EF8874001384EC /* CallInProgressNavigationController.swift */; }; 9BD8E40720F3576F001384EC /* CallInProgressWireframe.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9BD8E40620F3576F001384EC /* CallInProgressWireframe.swift */; }; 9BD8E41120F39AE3001384EC /* CallInProgressPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9BD8E41020F39AE3001384EC /* CallInProgressPresenter.swift */; }; 9BD8E41320F3A2E2001384EC /* CallInProgressInteractor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9BD8E41220F3A2E2001384EC /* CallInProgressInteractor.swift */; }; @@ -1348,7 +1406,6 @@ A44B4D5920CE9BDF00CA700A /* ImageCellViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = A44B4D4D20CE9BDF00CA700A /* ImageCellViewModel.swift */; }; A44B4D5A20CE9BDF00CA700A /* SwitchCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = A44B4D4E20CE9BDF00CA700A /* SwitchCell.swift */; }; A44B4D5B20CE9BDF00CA700A /* ImageCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = A44B4D4F20CE9BDF00CA700A /* ImageCell.swift */; }; - A44B4D6220CEA24100CA700A /* ChannelsConfig.xcconfig in Resources */ = {isa = PBXBuildFile; fileRef = A44B4D6120CEA24100CA700A /* ChannelsConfig.xcconfig */; }; A4569873060C49904EF8C555 /* EditGroupPhotoViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 40444524B52370D471DC9141 /* EditGroupPhotoViewController.swift */; }; A458FABB20EB87BF0075D55E /* ActionContainerContent.swift in Sources */ = {isa = PBXBuildFile; fileRef = A458FABA20EB87BF0075D55E /* ActionContainerContent.swift */; }; A458FABD20EB8B320075D55E /* MessageChannelActionsProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = A458FABC20EB8B320075D55E /* MessageChannelActionsProtocol.swift */; }; @@ -1463,17 +1520,26 @@ A4679BBB20B305360021FE9C /* LinkField.swift in Sources */ = {isa = PBXBuildFile; fileRef = A4679BB820B305360021FE9C /* LinkField.swift */; }; A4688DFA20650FF50013660D /* DBObserver.swift in Sources */ = {isa = PBXBuildFile; fileRef = A4688DF920650FF50013660D /* DBObserver.swift */; }; A4688DFC20652DE30013660D /* StorageChange.swift in Sources */ = {isa = PBXBuildFile; fileRef = A4688DFB20652DE30013660D /* StorageChange.swift */; }; - A46CF04321147BAE0072F185 /* HistoryRequestModelTypeProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = A46CF04221147BAE0072F185 /* HistoryRequestModelTypeProtocol.swift */; }; + A46C362F2121995800172773 /* DebuggingDetectorProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = A46C362E2121995800172773 /* DebuggingDetectorProtocol.swift */; }; + A46C36312121996000172773 /* DebuggingDetector.swift in Sources */ = {isa = PBXBuildFile; fileRef = A46C36302121996000172773 /* DebuggingDetector.swift */; }; + A46C36342121999100172773 /* DDMechanism.swift in Sources */ = {isa = PBXBuildFile; fileRef = A46C36332121999100172773 /* DDMechanism.swift */; }; + A46C363721219A9000172773 /* DDSysctlMechanism.swift in Sources */ = {isa = PBXBuildFile; fileRef = A46C363621219A9000172773 /* DDSysctlMechanism.swift */; }; + A46CF04321147BAE0072F185 /* HistoryRequestModelTypeRepresentable.swift in Sources */ = {isa = PBXBuildFile; fileRef = A46CF04221147BAE0072F185 /* HistoryRequestModelTypeRepresentable.swift */; }; A47785A220D18D4A0053E0D2 /* BaseView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E70402BC1FF6972B00182D81 /* BaseView.swift */; }; A47785A420D286680053E0D2 /* ChannelChatItemsFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = A47785A320D286680053E0D2 /* ChannelChatItemsFactory.swift */; }; A477CE7D2061236800081D34 /* MessageLinkDAOProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = A477CE7C2061236800081D34 /* MessageLinkDAOProtocol.swift */; }; A477CE8420613A5A00081D34 /* StarMessageDAO.swift in Sources */ = {isa = PBXBuildFile; fileRef = A477CE8320613A5A00081D34 /* StarMessageDAO.swift */; }; A481BD1C20EE72CB008FFED8 /* ReplyCounterDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = A481BD1B20EE72CB008FFED8 /* ReplyCounterDelegate.swift */; }; A481BD1F20EE73BD008FFED8 /* InfoInjectableConstants.swift in Sources */ = {isa = PBXBuildFile; fileRef = A481BD1E20EE73BD008FFED8 /* InfoInjectableConstants.swift */; }; + A4868F2F2121D349001F624E /* DetectorDebuggingPreventer.swift in Sources */ = {isa = PBXBuildFile; fileRef = A4868F2E2121D349001F624E /* DetectorDebuggingPreventer.swift */; }; + A4868F312121D360001F624E /* DebuggingPreventing.swift in Sources */ = {isa = PBXBuildFile; fileRef = A4868F302121D360001F624E /* DebuggingPreventing.swift */; }; + A4868F362121DE26001F624E /* PtraceDebuggingPreventer.swift in Sources */ = {isa = PBXBuildFile; fileRef = A4868F352121DE26001F624E /* PtraceDebuggingPreventer.swift */; }; + A4868F3A2121E22C001F624E /* AntiDebuggingService.swift in Sources */ = {isa = PBXBuildFile; fileRef = A4868F392121E22C001F624E /* AntiDebuggingService.swift */; }; A48BF1C220A1CA390076D892 /* Array+Table.swift in Sources */ = {isa = PBXBuildFile; fileRef = A48BF1C120A1CA390076D892 /* Array+Table.swift */; }; A48C153F20EF765E002DA994 /* MQTTServiceLink.swift in Sources */ = {isa = PBXBuildFile; fileRef = A48C153E20EF765E002DA994 /* MQTTServiceLink.swift */; }; A48C154220EF76EE002DA994 /* LinkExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = A48C154120EF76EE002DA994 /* LinkExtension.swift */; }; A48C154420EF7A15002DA994 /* LinkHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = A48C154320EF7A15002DA994 /* LinkHandler.swift */; }; + A49381AA21355EE1006D28DD /* MessageInteractor+Forward.swift in Sources */ = {isa = PBXBuildFile; fileRef = A49381A921355EE1006D28DD /* MessageInteractor+Forward.swift */; }; A497F56720EFA538005CC60F /* HandlerFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = A497F56620EFA537005CC60F /* HandlerFactory.swift */; }; A497F56A20EFA80B005CC60F /* HandlerFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = A497F56920EFA80B005CC60F /* HandlerFactory.swift */; }; A497F56B20EFA86C005CC60F /* HandlerService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A1DC7381EF151C8006A8E9F /* HandlerService.swift */; }; @@ -2030,6 +2096,13 @@ remoteGlobalIDString = 357809A21F9765CF00C9680C; remoteInfo = "Nynja-Share"; }; + 26DC81EC213838CD003E5FD9 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 85C65C6620EE58EC00C468B2 /* NynjaUIKit.xcodeproj */; + proxyType = 1; + remoteGlobalIDString = 8514D4C020EE27080002378A; + remoteInfo = NynjaUIKit; + }; 5B80A2842102177B0008D6AD /* PBXContainerItemProxy */ = { isa = PBXContainerItemProxy; containerPortal = 85C65C6620EE58EC00C468B2 /* NynjaUIKit.xcodeproj */; @@ -2233,9 +2306,18 @@ 260313AE20A0A50D009AC66D /* TranslationService.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TranslationService.swift; sourceTree = ""; }; 260313C720A0BC80009AC66D /* Array+LangExtended.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "Array+LangExtended.swift"; sourceTree = ""; }; 26052C7920FCE7E000E7A6A0 /* LogService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LogService.swift; sourceTree = ""; }; + 2605311A212740FD002E1CF1 /* LogOutputProtocols.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LogOutputProtocols.swift; sourceTree = ""; }; + 2605311C21274116002E1CF1 /* LogOutputView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LogOutputView.swift; sourceTree = ""; }; + 2605311E21274124002E1CF1 /* LogOutputInteractor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LogOutputInteractor.swift; sourceTree = ""; }; + 2605312021274133002E1CF1 /* LogOutputPresenter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LogOutputPresenter.swift; sourceTree = ""; }; + 26053122212741C2002E1CF1 /* LogOutputWireFrame.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LogOutputWireFrame.swift; sourceTree = ""; }; + 260531252127455C002E1CF1 /* MotionManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MotionManager.swift; sourceTree = ""; }; + 2605312821298BEF002E1CF1 /* Logoutputcell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Logoutputcell.swift; sourceTree = ""; }; + 2605312A21299198002E1CF1 /* LogOutputDS.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LogOutputDS.swift; sourceTree = ""; }; 260552A51F9E1CD100D68DE6 /* SearchHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = SearchHandler.swift; path = Services/HandleServices/SearchHandler.swift; sourceTree = ""; }; 260629702056EF2800CB8F65 /* LinksCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LinksCell.swift; sourceTree = ""; }; 2606F3BB20BFE20400CF7F15 /* MessageInteractor+Translation.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "MessageInteractor+Translation.swift"; sourceTree = ""; }; + 260D67D82124616A0072F11F /* LogWriter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LogWriter.swift; sourceTree = ""; }; 26131E01210399BA00BE94F9 /* TranscribeService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TranscribeService.swift; sourceTree = ""; }; 26142B1020472ECD004E5FE4 /* MessageLinkTable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageLinkTable.swift; sourceTree = ""; }; 26142B1220473BFD004E5FE4 /* DBMessageLink.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DBMessageLink.swift; sourceTree = ""; }; @@ -2248,10 +2330,10 @@ 26245F43204EF67B00C8D3DD /* PresenterApearingProtocol.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PresenterApearingProtocol.swift; sourceTree = ""; }; 2625DBF520EFC52D00E01C05 /* AudioFileConvertOperation.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AudioFileConvertOperation.swift; sourceTree = ""; }; 2625DBF720EFC5DE00E01C05 /* FourCharCode+StringLiteralConvertible.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "FourCharCode+StringLiteralConvertible.swift"; sourceTree = ""; }; + 2625F29E212463E8007C42B5 /* ProgressIdentifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProgressIdentifier.swift; sourceTree = ""; }; 262D43862033417F002F1E45 /* FriendExtansion+BERT.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "FriendExtansion+BERT.swift"; sourceTree = ""; }; 2631C511207A4C0C00F9AA55 /* AudioRecorder.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AudioRecorder.swift; sourceTree = ""; }; 2632139020D797F500C31144 /* TranslationViewProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TranslationViewProtocol.swift; sourceTree = ""; }; - 2632139220D7B71200C31144 /* TranslateConfig.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = TranslateConfig.xcconfig; sourceTree = ""; }; 2633EF6D205212F700DB3868 /* MemberDAOProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MemberDAOProtocol.swift; sourceTree = ""; }; 2633EF6F2052130400DB3868 /* MemberDAO.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MemberDAO.swift; sourceTree = ""; }; 263409332119CFE2002F8D8F /* RecordContainer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RecordContainer.swift; sourceTree = ""; }; @@ -2265,6 +2347,8 @@ 263529142075729400DC6FBD /* Job+DB.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Job+DB.swift"; sourceTree = ""; }; 263A60AB1FB4F8F7006F9D52 /* ParticipantsDataSource.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ParticipantsDataSource.swift; sourceTree = ""; }; 263A60AD1FB51C22006F9D52 /* MemberExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MemberExtension.swift; sourceTree = ""; }; + 263C04E82132E2FF00B8F0BE /* WrappedTaskOperation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WrappedTaskOperation.swift; sourceTree = ""; }; + 263C04EA2132E56E00B8F0BE /* TranscribeOperation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TranscribeOperation.swift; sourceTree = ""; }; 263D66261FE829CC00A509F8 /* RoomExtension+BERT.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = "RoomExtension+BERT.swift"; path = "Nynja/MQTTModels/RoomExtension+BERT.swift"; sourceTree = SOURCE_ROOT; }; 263D662C1FE8D03400A509F8 /* TypingModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = TypingModel.swift; path = Services/Models/TypingModel.swift; sourceTree = ""; }; 263D662F1FE8D20100A509F8 /* TypingExtension+BERT.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = "TypingExtension+BERT.swift"; path = "Nynja/MQTTModels/TypingExtension+BERT.swift"; sourceTree = SOURCE_ROOT; }; @@ -2329,6 +2413,8 @@ 266F04CA2015050400B97A83 /* DBStarMessage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DBStarMessage.swift; sourceTree = ""; }; 266F04CE201541BC00B97A83 /* StarExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StarExtension.swift; sourceTree = ""; }; 26770A561FFD6CAC009AC870 /* SharedParameters.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SharedParameters.swift; sourceTree = ""; }; + 26771CC0212ECE08006112B5 /* ConvertMessageTable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConvertMessageTable.swift; sourceTree = ""; }; + 26771CC2212ED109006112B5 /* DBConvertMessage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DBConvertMessage.swift; sourceTree = ""; }; 267BE2821FDE905D00C47E18 /* SettingsProtocols.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsProtocols.swift; sourceTree = ""; }; 267BE2841FDE983400C47E18 /* SettingsGroupVC.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsGroupVC.swift; sourceTree = ""; }; 267BE28D1FDE9FCC00C47E18 /* SettingsGroupWireFrame.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsGroupWireFrame.swift; sourceTree = ""; }; @@ -2357,10 +2443,10 @@ 2689CDE920C48AD8007816B9 /* TranslationManualView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TranslationManualView.swift; sourceTree = ""; }; 268C340B2106709A00F1472A /* GoogleService-Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = "GoogleService-Info.plist"; sourceTree = ""; }; 268C340E21067BAD00F1472A /* AudioUploadOperation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AudioUploadOperation.swift; sourceTree = ""; }; - 268C341021067F1D00F1472A /* TranscribeLongAudioOperation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TranscribeLongAudioOperation.swift; sourceTree = ""; }; + 268C341021067F1D00F1472A /* AudioLongTranscribeOperation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AudioLongTranscribeOperation.swift; sourceTree = ""; }; 268C3412210688B200F1472A /* TranscribeLongRequestData.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TranscribeLongRequestData.swift; sourceTree = ""; }; 268C34142107479600F1472A /* TranscribeLongResponseData.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TranscribeLongResponseData.swift; sourceTree = ""; }; - 268C341621074AD000F1472A /* TranscribeLongAudioProccessingOperation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TranscribeLongAudioProccessingOperation.swift; sourceTree = ""; }; + 268C341621074AD000F1472A /* AudioLongTranscribeProccessingOperation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AudioLongTranscribeProccessingOperation.swift; sourceTree = ""; }; 268C341821074D6C00F1472A /* TranscribeLongOperationResponseData.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TranscribeLongOperationResponseData.swift; sourceTree = ""; }; 268C341B21075B4700F1472A /* Cancelable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Cancelable.swift; sourceTree = ""; }; 269666171FB57963009E41C1 /* RoomHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = RoomHandler.swift; path = Services/HandleServices/RoomHandler.swift; sourceTree = ""; }; @@ -2380,6 +2466,7 @@ 26ABCA49211B321100EA4782 /* Bundle+provision.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Bundle+provision.swift"; sourceTree = ""; }; 26ABF27A2059FE2500438975 /* URL+valid.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "URL+valid.swift"; sourceTree = ""; }; 26AC7B631F9F79D400D448AE /* NavigateProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NavigateProtocol.swift; sourceTree = ""; }; + 26ACC5CD212C3DDB008455E8 /* AudioTranscribeSendOperation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AudioTranscribeSendOperation.swift; sourceTree = ""; }; 26AD28361FFB0AE3009E4580 /* StorageSubscriber.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StorageSubscriber.swift; sourceTree = ""; }; 26AD28381FFB0AF9009E4580 /* StorageObserver.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StorageObserver.swift; sourceTree = ""; }; 26B06C7F20602643005BF9AF /* CarouselPickerCollectionViewCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CarouselPickerCollectionViewCell.swift; sourceTree = ""; }; @@ -2407,13 +2494,14 @@ 26C1A3EC2031D3030009F7F0 /* OtherUserContainerViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OtherUserContainerViewController.swift; sourceTree = ""; }; 26C1A3EF2031D9E60009F7F0 /* OtherUserTableViewDS.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OtherUserTableViewDS.swift; sourceTree = ""; }; 26C1A3F22031EED30009F7F0 /* OtherUserHeaderView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OtherUserHeaderView.swift; sourceTree = ""; }; - 26CD3FDA2104D19D00597E62 /* TranscribeShortAudioOperation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TranscribeShortAudioOperation.swift; sourceTree = ""; }; - 26CD3FDC2104D1DD00597E62 /* AudioConvertionOperation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AudioConvertionOperation.swift; sourceTree = ""; }; + 26CD3FDA2104D19D00597E62 /* AudioShortTranscribeOperation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AudioShortTranscribeOperation.swift; sourceTree = ""; }; + 26CD3FDC2104D1DD00597E62 /* AudioConvertOperation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AudioConvertOperation.swift; sourceTree = ""; }; 26D35AB71FD0EFA800A5D513 /* AudioPlayer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AudioPlayer.swift; sourceTree = ""; }; 26D621F32069778400595E13 /* ChatWheelItemView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatWheelItemView.swift; sourceTree = ""; }; + 26D6D226212EDA6600EA2419 /* ConvertMessageDAOProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConvertMessageDAOProtocol.swift; sourceTree = ""; }; + 26D6D228212EDADC00EA2419 /* ConvertMessageDAO.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConvertMessageDAO.swift; sourceTree = ""; }; 26D8317420EA65200067C5B4 /* TranslationInfo.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TranslationInfo.swift; sourceTree = ""; }; 26D87FB9E1E4B20C47456AF6 /* Pods-Nynja-Share.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Nynja-Share.release.xcconfig"; path = "Pods/Target Support Files/Pods-Nynja-Share/Pods-Nynja-Share.release.xcconfig"; sourceTree = ""; }; - 26D8F42B20D7E074001602E9 /* StickersConfig.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = StickersConfig.xcconfig; sourceTree = ""; }; 26DAE5CF1FFAF45800EDF412 /* BackgroundModeSubscriber.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BackgroundModeSubscriber.swift; sourceTree = ""; }; 26DAE5D11FFAF7EE00EDF412 /* BackgroundModeService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BackgroundModeService.swift; sourceTree = ""; }; 26DAE5D31FFAF91100EDF412 /* DefaultBackgroundModeService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DefaultBackgroundModeService.swift; sourceTree = ""; }; @@ -2630,6 +2718,7 @@ 5A48445E21178E33000657ED /* AlertTextFieldViewControllerLayout.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AlertTextFieldViewControllerLayout.swift; sourceTree = ""; }; 5AEEB3D82E9CF02760DA4CE7 /* Pods-Nynja-Share.channels.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Nynja-Share.channels.xcconfig"; path = "Pods/Target Support Files/Pods-Nynja-Share/Pods-Nynja-Share.channels.xcconfig"; sourceTree = ""; }; 5B377AA90A6B6BA0120C31F1 /* EditProfileProtocols.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = EditProfileProtocols.swift; sourceTree = ""; }; + 5BBEF53B212DE09F00F10768 /* ringback.m4a */ = {isa = PBXFileReference; lastKnownFileType = file; path = ringback.m4a; sourceTree = ""; }; 5BC1D37220D3B3D8002A44B3 /* NynjaCommunicatorService.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = NynjaCommunicatorService.swift; path = Services/NynjaCommunicatorService.swift; sourceTree = ""; }; 5BC1D37420D3B4A6002A44B3 /* GroupCollectionViewCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GroupCollectionViewCell.swift; sourceTree = ""; }; 5BC1D37620D3B4A7002A44B3 /* GroupAddParticipantsCollectionViewCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GroupAddParticipantsCollectionViewCell.swift; sourceTree = ""; }; @@ -2745,9 +2834,13 @@ 850FC610203312FA00832D87 /* ForwardSelectorViewControllerLayout.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ForwardSelectorViewControllerLayout.swift; sourceTree = ""; }; 8511D3702034427F00B2A620 /* UIView+SafeArea.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIView+SafeArea.swift"; sourceTree = ""; }; 8511D3732034596E00B2A620 /* Collection+ViewLayout.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Collection+ViewLayout.swift"; sourceTree = ""; }; - 8512349121221B9E000129A2 /* CollectionExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CollectionExtension.swift; sourceTree = ""; }; + 8512349121221B9E000129A2 /* Collection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Collection.swift; sourceTree = ""; }; 8514D52120EE48930002378A /* NynjaContextMenuItemsFactory+Design.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NynjaContextMenuItemsFactory+Design.swift"; sourceTree = ""; }; 8514D52320EE48A30002378A /* NynjaContextMenuItemsFactory+Messages.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NynjaContextMenuItemsFactory+Messages.swift"; sourceTree = ""; }; + 8514DE882136A50100718DD8 /* DBStarAction.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DBStarAction.swift; sourceTree = ""; }; + 8514DE8B2136A5FD00718DD8 /* StarActionTable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StarActionTable.swift; sourceTree = ""; }; + 8514DE8E2136A9A900718DD8 /* StarActionDAO.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StarActionDAO.swift; sourceTree = ""; }; + 8514DE902136A9CB00718DD8 /* StarActionDAOProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StarActionDAOProtocol.swift; sourceTree = ""; }; 8514F16620EA219E00883513 /* ContextMenuItemsView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ContextMenuItemsView.swift; sourceTree = ""; }; 8514F16720EA219E00883513 /* ContextMenuConfiguration+Favorites.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "ContextMenuConfiguration+Favorites.swift"; sourceTree = ""; }; 8514F16920EA219E00883513 /* ContextMenuNextCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ContextMenuNextCell.swift; sourceTree = ""; }; @@ -2779,6 +2872,9 @@ 8528E50D2072835E00A8644A /* AudioDurationFormatter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AudioDurationFormatter.swift; sourceTree = ""; }; 852DF26020371FB400A4F8B6 /* FileExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FileExtension.swift; sourceTree = ""; }; 852DF262203720E600A4F8B6 /* FileIcons.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FileIcons.swift; sourceTree = ""; }; + 852E847021345FB400FD3841 /* MessageCollectionViewLayoutAttributes.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MessageCollectionViewLayoutAttributes.swift; sourceTree = ""; }; + 852E8472213460E800FD3841 /* ReversedMessageCollectionViewLayoutAttributes.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReversedMessageCollectionViewLayoutAttributes.swift; sourceTree = ""; }; + 852E8474213462F000FD3841 /* ReversedMessageCollectionViewLayout.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReversedMessageCollectionViewLayout.swift; sourceTree = ""; }; 853801232052C848002C6960 /* TextCheckmarkTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TextCheckmarkTableViewCell.swift; sourceTree = ""; }; 853801252052C853002C6960 /* TextCheckmarkCellModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TextCheckmarkCellModel.swift; sourceTree = ""; }; 853801272052CCAD002C6960 /* SoundCellModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SoundCellModel.swift; sourceTree = ""; }; @@ -2814,6 +2910,13 @@ 85433F20204D596D00B373A7 /* WebFullScreenInteractor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WebFullScreenInteractor.swift; sourceTree = ""; }; 85433F21204D596D00B373A7 /* WebFullScreenWireFrame.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WebFullScreenWireFrame.swift; sourceTree = ""; }; 85433F2B204D5AA500B373A7 /* NynjaCloseButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NynjaCloseButton.swift; sourceTree = ""; }; + 85458CD8212D6FED00BA8814 /* String+Split.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "String+Split.swift"; sourceTree = ""; }; + 85458CE1212D730E00BA8814 /* MessageIdentifiers.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageIdentifiers.swift; sourceTree = ""; }; + 85458CEC212D74B400BA8814 /* P2P+Opponent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "P2P+Opponent.swift"; sourceTree = ""; }; + 85458CF2212D762900BA8814 /* Message+Factory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Message+Factory.swift"; sourceTree = ""; }; + 85458CF4212D770100BA8814 /* Message+Files.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Message+Files.swift"; sourceTree = ""; }; + 85458CFC212D7B8C00BA8814 /* Desc+Construct.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Desc+Construct.swift"; sourceTree = ""; }; + 8546D463213EEC4E0024FE66 /* BidirectionalCollection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BidirectionalCollection.swift; sourceTree = ""; }; 854751482093BDD300F8D5F8 /* CollectionViewScrollProxy.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CollectionViewScrollProxy.swift; sourceTree = ""; }; 85482843204E915400DCBEC8 /* PrivacyTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PrivacyTableViewCell.swift; sourceTree = ""; }; 85482845204E918000DCBEC8 /* PrivacyCellModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PrivacyCellModel.swift; sourceTree = ""; }; @@ -2834,6 +2937,7 @@ 8557988620932401007050B8 /* StickerStaticMenuActionCellModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StickerStaticMenuActionCellModel.swift; sourceTree = ""; }; 8557989A209368E7007050B8 /* StickerPackHeaderView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StickerPackHeaderView.swift; sourceTree = ""; }; 8557989B209368E7007050B8 /* StickerPackHeaderModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StickerPackHeaderModel.swift; sourceTree = ""; }; + 855A393C213E76E20002B8DC /* LoadingInteractive.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = LoadingInteractive.swift; path = BaseVC/LoadingInteractive.swift; sourceTree = ""; }; 855AC52D208E441500DC2335 /* StickersInputPresenter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StickersInputPresenter.swift; sourceTree = ""; }; 855AC52E208E441500DC2335 /* StickersInputViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StickersInputViewController.swift; sourceTree = ""; }; 855AC52F208E441500DC2335 /* StickersInputProtocols.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StickersInputProtocols.swift; sourceTree = ""; }; @@ -2841,6 +2945,7 @@ 855AC531208E441500DC2335 /* StickersInputWireFrame.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StickersInputWireFrame.swift; sourceTree = ""; }; 855AC53D208E45AA00DC2335 /* StickerCollectionViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StickerCollectionViewCell.swift; sourceTree = ""; }; 855AC53E208E45AA00DC2335 /* StickerCellModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StickerCellModel.swift; sourceTree = ""; }; + 855C9FE52125B4C0000E3429 /* MessageHandlerSubscriber.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = MessageHandlerSubscriber.swift; path = Services/HandleServices/MessageHandlerSubscriber.swift; sourceTree = ""; }; 855EF420202CC6F800541BE3 /* GetExtendedStarsModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GetExtendedStarsModel.swift; sourceTree = ""; }; 855EF422202CC85300541BE3 /* MQTTServiceStars.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MQTTServiceStars.swift; sourceTree = ""; }; 855EF424202CCADB00541BE3 /* ExtendedStarHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ExtendedStarHandler.swift; sourceTree = ""; }; @@ -2849,6 +2954,7 @@ 8562853520D164B5000C9739 /* ScaleAnimatableGrid.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ScaleAnimatableGrid.swift; sourceTree = ""; }; 8562853820D166E5000C9739 /* CollectionPreviewState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CollectionPreviewState.swift; sourceTree = ""; }; 8562853A20D16C61000C9739 /* LongPressClosureRecognizer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LongPressClosureRecognizer.swift; sourceTree = ""; }; + 85629EC92137EF2400A79C97 /* VoiceAudioInteractive.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VoiceAudioInteractive.swift; sourceTree = ""; }; 8566771B20C139A000DD4204 /* DebugLogs.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DebugLogs.swift; sourceTree = ""; }; 8566771D20C1579C00DD4204 /* StorageSubscriberReference.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StorageSubscriberReference.swift; sourceTree = ""; }; 8566771F20C1924500DD4204 /* MessageInteractor+MessageHandlerSubscriber.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "MessageInteractor+MessageHandlerSubscriber.swift"; sourceTree = ""; }; @@ -2918,6 +3024,8 @@ 859C42A92056B05D00AE3797 /* SoundBundle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SoundBundle.swift; sourceTree = ""; }; 859C42AC2056BF9F00AE3797 /* incoming_message.mp3 */ = {isa = PBXFileReference; lastKnownFileType = audio.mp3; path = incoming_message.mp3; sourceTree = ""; }; 859F9B4B2035CB1E009D017A /* ForwardContent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ForwardContent.swift; sourceTree = ""; }; + 85B0013121270DEC000C89FE /* TableOrder.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TableOrder.swift; sourceTree = ""; }; + 85B0013321272694000C89FE /* MessageInteractor+History.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "MessageInteractor+History.swift"; sourceTree = ""; }; 85B750A020334A2B00AD6013 /* ForwardTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ForwardTableViewCell.swift; sourceTree = ""; }; 85BA176020BEA7BD001EF8AC /* StickerPreviewContainerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StickerPreviewContainerView.swift; sourceTree = ""; }; 85BEC0E02063F91C0098C99C /* TimeZoneCellModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimeZoneCellModel.swift; sourceTree = ""; }; @@ -3013,7 +3121,6 @@ 9B810991D7143259040DCA31 /* LanguageSettingsViewController.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = LanguageSettingsViewController.swift; sourceTree = ""; }; 9BC9657520FF042D00052AE1 /* CallInProgressProtocols.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CallInProgressProtocols.swift; sourceTree = ""; }; 9BD8E3EF20EF7898001384EC /* CallInProgressViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CallInProgressViewController.swift; sourceTree = ""; }; - 9BD8E3F720EF8874001384EC /* CallInProgressNavigationController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CallInProgressNavigationController.swift; sourceTree = ""; }; 9BD8E40620F3576F001384EC /* CallInProgressWireframe.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CallInProgressWireframe.swift; sourceTree = ""; }; 9BD8E41020F39AE3001384EC /* CallInProgressPresenter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CallInProgressPresenter.swift; sourceTree = ""; }; 9BD8E41220F3A2E2001384EC /* CallInProgressInteractor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CallInProgressInteractor.swift; sourceTree = ""; }; @@ -3236,7 +3343,6 @@ A44B4D4D20CE9BDF00CA700A /* ImageCellViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ImageCellViewModel.swift; sourceTree = ""; }; A44B4D4E20CE9BDF00CA700A /* SwitchCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SwitchCell.swift; sourceTree = ""; }; A44B4D4F20CE9BDF00CA700A /* ImageCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ImageCell.swift; sourceTree = ""; }; - A44B4D6120CEA24100CA700A /* ChannelsConfig.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; path = ChannelsConfig.xcconfig; sourceTree = ""; }; A458FABA20EB87BF0075D55E /* ActionContainerContent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ActionContainerContent.swift; sourceTree = ""; }; A458FABC20EB8B320075D55E /* MessageChannelActionsProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageChannelActionsProtocol.swift; sourceTree = ""; }; A458FABE20EB8BB50075D55E /* MessageInteractor+ChannelActions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "MessageInteractor+ChannelActions.swift"; sourceTree = ""; }; @@ -3336,17 +3442,26 @@ A4679BB820B305360021FE9C /* LinkField.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LinkField.swift; sourceTree = ""; }; A4688DF920650FF50013660D /* DBObserver.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DBObserver.swift; sourceTree = ""; }; A4688DFB20652DE30013660D /* StorageChange.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StorageChange.swift; sourceTree = ""; }; - A46CF04221147BAE0072F185 /* HistoryRequestModelTypeProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HistoryRequestModelTypeProtocol.swift; sourceTree = ""; }; + A46C362E2121995800172773 /* DebuggingDetectorProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DebuggingDetectorProtocol.swift; sourceTree = ""; }; + A46C36302121996000172773 /* DebuggingDetector.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DebuggingDetector.swift; sourceTree = ""; }; + A46C36332121999100172773 /* DDMechanism.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DDMechanism.swift; sourceTree = ""; }; + A46C363621219A9000172773 /* DDSysctlMechanism.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DDSysctlMechanism.swift; sourceTree = ""; }; + A46CF04221147BAE0072F185 /* HistoryRequestModelTypeRepresentable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HistoryRequestModelTypeRepresentable.swift; sourceTree = ""; }; A47785A320D286680053E0D2 /* ChannelChatItemsFactory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChannelChatItemsFactory.swift; sourceTree = ""; }; A477CE7C2061236800081D34 /* MessageLinkDAOProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageLinkDAOProtocol.swift; sourceTree = ""; }; A477CE8120613A0900081D34 /* StarMessageDAOProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StarMessageDAOProtocol.swift; sourceTree = ""; }; A477CE8320613A5A00081D34 /* StarMessageDAO.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StarMessageDAO.swift; sourceTree = ""; }; A481BD1B20EE72CB008FFED8 /* ReplyCounterDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReplyCounterDelegate.swift; sourceTree = ""; }; A481BD1E20EE73BD008FFED8 /* InfoInjectableConstants.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InfoInjectableConstants.swift; sourceTree = ""; }; + A4868F2E2121D349001F624E /* DetectorDebuggingPreventer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DetectorDebuggingPreventer.swift; sourceTree = ""; }; + A4868F302121D360001F624E /* DebuggingPreventing.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DebuggingPreventing.swift; sourceTree = ""; }; + A4868F352121DE26001F624E /* PtraceDebuggingPreventer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PtraceDebuggingPreventer.swift; sourceTree = ""; }; + A4868F392121E22C001F624E /* AntiDebuggingService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AntiDebuggingService.swift; sourceTree = ""; }; A48BF1C120A1CA390076D892 /* Array+Table.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Array+Table.swift"; sourceTree = ""; }; A48C153E20EF765E002DA994 /* MQTTServiceLink.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MQTTServiceLink.swift; sourceTree = ""; }; A48C154120EF76EE002DA994 /* LinkExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LinkExtension.swift; sourceTree = ""; }; A48C154320EF7A15002DA994 /* LinkHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LinkHandler.swift; sourceTree = ""; }; + A49381A921355EE1006D28DD /* MessageInteractor+Forward.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "MessageInteractor+Forward.swift"; sourceTree = ""; }; A497F56620EFA537005CC60F /* HandlerFactory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HandlerFactory.swift; sourceTree = ""; }; A497F56920EFA80B005CC60F /* HandlerFactory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HandlerFactory.swift; sourceTree = ""; }; A497F56D20EFA8B6005CC60F /* ErrorsHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ErrorsHandler.swift; sourceTree = ""; }; @@ -4443,10 +4558,65 @@ isa = PBXGroup; children = ( 26052C7920FCE7E000E7A6A0 /* LogService.swift */, + 260D67D82124616A0072F11F /* LogWriter.swift */, ); path = LogService; sourceTree = ""; }; + 260531122127407A002E1CF1 /* LogOutput */ = { + isa = PBXGroup; + children = ( + 26053119212740DE002E1CF1 /* View */, + 26053117212740C7002E1CF1 /* Interactor */, + 26053115212740B8002E1CF1 /* Presenter */, + 2605311321274095002E1CF1 /* Wireframe */, + 2605311A212740FD002E1CF1 /* LogOutputProtocols.swift */, + ); + path = LogOutput; + sourceTree = ""; + }; + 2605311321274095002E1CF1 /* Wireframe */ = { + isa = PBXGroup; + children = ( + 26053122212741C2002E1CF1 /* LogOutputWireFrame.swift */, + ); + path = Wireframe; + sourceTree = ""; + }; + 26053115212740B8002E1CF1 /* Presenter */ = { + isa = PBXGroup; + children = ( + 2605312021274133002E1CF1 /* LogOutputPresenter.swift */, + ); + path = Presenter; + sourceTree = ""; + }; + 26053117212740C7002E1CF1 /* Interactor */ = { + isa = PBXGroup; + children = ( + 2605311E21274124002E1CF1 /* LogOutputInteractor.swift */, + ); + path = Interactor; + sourceTree = ""; + }; + 26053119212740DE002E1CF1 /* View */ = { + isa = PBXGroup; + children = ( + 2605311C21274116002E1CF1 /* LogOutputView.swift */, + 2605312A21299198002E1CF1 /* LogOutputDS.swift */, + 2605312821298BEF002E1CF1 /* Logoutputcell.swift */, + ); + path = View; + sourceTree = ""; + }; + 260531242127454E002E1CF1 /* MotionManager */ = { + isa = PBXGroup; + children = ( + 260531252127455C002E1CF1 /* MotionManager.swift */, + ); + path = MotionManager; + sourceTree = ""; + }; 2607270B203C59D600290545 /* Cells */ = { isa = PBXGroup; children = ( @@ -4477,11 +4647,13 @@ 26131E002103998D00BE94F9 /* Operations */ = { isa = PBXGroup; children = ( - 268C341621074AD000F1472A /* TranscribeLongAudioProccessingOperation.swift */, - 268C341021067F1D00F1472A /* TranscribeLongAudioOperation.swift */, - 26CD3FDA2104D19D00597E62 /* TranscribeShortAudioOperation.swift */, - 26CD3FDC2104D1DD00597E62 /* AudioConvertionOperation.swift */, + 26ACC5CD212C3DDB008455E8 /* AudioTranscribeSendOperation.swift */, + 268C341621074AD000F1472A /* AudioLongTranscribeProccessingOperation.swift */, + 268C341021067F1D00F1472A /* AudioLongTranscribeOperation.swift */, + 26CD3FDA2104D19D00597E62 /* AudioShortTranscribeOperation.swift */, 268C340E21067BAD00F1472A /* AudioUploadOperation.swift */, + 26CD3FDC2104D1DD00597E62 /* AudioConvertOperation.swift */, + 263C04EA2132E56E00B8F0BE /* TranscribeOperation.swift */, ); path = "Operations "; sourceTree = ""; @@ -4850,6 +5022,15 @@ path = Transcription; sourceTree = ""; }; + 26771CC4212ED978006112B5 /* ConvertMessage */ = { + isa = PBXGroup; + children = ( + 26D6D226212EDA6600EA2419 /* ConvertMessageDAOProtocol.swift */, + 26D6D228212EDADC00EA2419 /* ConvertMessageDAO.swift */, + ); + path = ConvertMessage; + sourceTree = ""; + }; 267BE27D1FDE900900C47E18 /* SettingsGroup */ = { isa = PBXGroup; children = ( @@ -5433,6 +5614,7 @@ 269666171FB57963009E41C1 /* RoomHandler.swift */, 3A771CA91F191B38008D968A /* ProfileHandler.swift */, 3A19FEAC1F3B7F1D00ACE750 /* MessageHandler.swift */, + 855C9FE52125B4C0000E3429 /* MessageHandlerSubscriber.swift */, 3A2374D81F262A1600701045 /* ContactHandler.swift */, 3A237BCC1F30E5D400C42B6E /* RosterHandler.swift */, 3A1EB9A41F3A848A00658E93 /* HistoryHandler.swift */, @@ -5480,6 +5662,7 @@ 3A8045D91F60E18E00AED866 /* Queue.swift */, 266AE8C2203496B60096A12C /* AsyncOperation.swift */, F112B18F20E0FBE800B06E3E /* AsyncBlockOperation.swift */, + 263C04E82132E2FF00B8F0BE /* WrappedTaskOperation.swift */, ); name = Library; sourceTree = ""; @@ -5487,11 +5670,13 @@ 3A768E1C1ECD152300108F7C /* Services */ = { isa = PBXGroup; children = ( + 4B0DBA892137F6F800D79163 /* ChatService */, + 260531242127454E002E1CF1 /* MotionManager */, + A46C362C212198D500172773 /* Security */, FE2D7CCB211C71AD00520D78 /* WalletService */, 8513A88621184F9A00B5CA4A /* Audio */, 26B32B5E1FE170FE00888A0A /* MigrationManager.swift */, A409B1CD2108D4720051C20B /* KeychainService */, - A4CB151C2103725900C3B68B /* JailbreakDetector */, 26131DFF2103998D00BE94F9 /* TranscribeService */, 26052C7820FCE7C100E7A6A0 /* LogService */, A458FAC020EBA4D70075D55E /* MuteChatService */, @@ -5540,7 +5725,6 @@ 2652D6171FA85B28005E62C7 /* ImageSelector.swift */, 26B5F7411FB0FF7B00CEC6AE /* FileManager.swift */, 00F7B349202B350A00E443E1 /* TimeZoneManager.swift */, - 4B5A714C204F069000A551F5 /* ChatService.swift */, A4166F5B205FE3670008F231 /* JobService.swift */, 26DCB255206924B3001EF0AB /* FeatureFactory.swift */, 8509AC61206A54420089089B /* ResponseResult.swift */, @@ -5608,6 +5792,7 @@ 3A82187D1EDEEDF400337B05 /* AlertManager.swift */, 3A2A99821EFAD2FB002749B3 /* PageControl.swift */, 3A2171501EFB25C400F34B8B /* BaseVC.swift */, + 855A393C213E76E20002B8DC /* LoadingInteractive.swift */, 6DEEE1921F1F9CF6000FAF09 /* UIViewController+Child.swift */, 26F47051201B7248005D3192 /* ReturnToCallView.swift */, 3AF8E26E1F42E33300D81390 /* ReturnToCallContentView.swift */, @@ -5695,11 +5880,8 @@ 6D36F8E41F0ADBD300FA1AC8 /* Localizable.strings */, F1313AFE20888CAB00E04092 /* DevConfig.xcconfig */, F10AFE9A20EF8B9A00C7CE83 /* DevAutoTests.xcconfig */, - A44B4D6120CEA24100CA700A /* ChannelsConfig.xcconfig */, - 2632139220D7B71200C31144 /* TranslateConfig.xcconfig */, F1313AFF20888CB800E04092 /* PrereleaseConfig.xcconfig */, F1313B0020888CC400E04092 /* ReleaseConfig.xcconfig */, - 26D8F42B20D7E074001602E9 /* StickersConfig.xcconfig */, F1313B0120888FE600E04092 /* ThirdPartyServices.swift */, A4B544FA20EFC0AD00EB7B0F /* StatusCodes.strings */, ); @@ -5774,6 +5956,7 @@ 269D9DEF1FC3AF0D00324263 /* CGSizeExtension.swift */, 854CFB07210704AE00FBC133 /* CGRectExtensions.swift */, A4BE4AB42068E98C00C041D1 /* ALTextInputBar+Trim.swift */, + 85458CD8212D6FED00BA8814 /* String+Split.swift */, ); path = Extensions; sourceTree = ""; @@ -5875,6 +6058,7 @@ 49E75E252CE2F3C96A626230 /* Modules */ = { isa = PBXGroup; children = ( + 260531122127407A002E1CF1 /* LogOutput */, FBCE83C320E52351003B7558 /* Payment */, FBF0E38320E5232E00B6FB59 /* WalletBalances */, 855AC52C208E435700DC2335 /* Stickers */, @@ -5986,11 +6170,13 @@ 4B8996E2204EEC2C00DCB183 /* Message */, 4B8996E8204EF32400DCB183 /* MessageAction */, 265F5D2A209B8B45008ACCC8 /* EditMessageAction */, + 8514DE8D2136A98900718DD8 /* StarAction */, 4B8996CB204ED30B00DCB183 /* Star */, A477CE80206139F400081D34 /* StarMessage */, 4B8996D6204EDA5E00DCB183 /* Job */, 85CB25DD20D723EE00D5E565 /* StickerPack */, A477CE7B2061235700081D34 /* MessageLink */, + 26771CC4212ED978006112B5 /* ConvertMessage */, ); name = DAO; sourceTree = ""; @@ -6084,6 +6270,14 @@ name = Chat; sourceTree = ""; }; + 4B0DBA892137F6F800D79163 /* ChatService */ = { + isa = PBXGroup; + children = ( + 4B5A714C204F069000A551F5 /* ChatService.swift */, + ); + path = ChatService; + sourceTree = ""; + }; 4B1D7DFF2029C4A900703228 /* Options */ = { isa = PBXGroup; children = ( @@ -7093,7 +7287,8 @@ 8512349021221B82000129A2 /* Collection */ = { isa = PBXGroup; children = ( - 8512349121221B9E000129A2 /* CollectionExtension.swift */, + 8512349121221B9E000129A2 /* Collection.swift */, + 8546D463213EEC4E0024FE66 /* BidirectionalCollection.swift */, ); path = Collection; sourceTree = ""; @@ -7115,6 +7310,15 @@ path = ContextMenu; sourceTree = ""; }; + 8514DE8D2136A98900718DD8 /* StarAction */ = { + isa = PBXGroup; + children = ( + 8514DE902136A9CB00718DD8 /* StarActionDAOProtocol.swift */, + 8514DE8E2136A9A900718DD8 /* StarActionDAO.swift */, + ); + name = StarAction; + sourceTree = ""; + }; 8514F16520EA219E00883513 /* ContextMenuOLD */ = { isa = PBXGroup; children = ( @@ -7216,6 +7420,24 @@ path = ControlsContainer; sourceTree = ""; }; + 852E84762134631900FD3841 /* Layout */ = { + isa = PBXGroup; + children = ( + 854D13D7211B2E7200E139FC /* MessageCollectionViewLayout.swift */, + 852E8474213462F000FD3841 /* ReversedMessageCollectionViewLayout.swift */, + ); + path = Layout; + sourceTree = ""; + }; + 852E84772134632A00FD3841 /* Attributes */ = { + isa = PBXGroup; + children = ( + 852E847021345FB400FD3841 /* MessageCollectionViewLayoutAttributes.swift */, + 852E8472213460E800FD3841 /* ReversedMessageCollectionViewLayoutAttributes.swift */, + ); + path = Attributes; + sourceTree = ""; + }; 853801212052C813002C6960 /* ViewController */ = { isa = PBXGroup; children = ( @@ -7289,6 +7511,7 @@ children = ( E7302A921FC83477002892F8 /* DescExtension.swift */, 853E594E20D6AED2007799B9 /* Desc+Messages.swift */, + 85458CFC212D7B8C00BA8814 /* Desc+Construct.swift */, 853E595020D6AF59007799B9 /* Desc+Room.swift */, 26C0C1DE2073D9B600C530DA /* Desc+DB.swift */, ); @@ -7511,10 +7734,12 @@ 854D13D6211B2E6200E139FC /* CollectionView */ = { isa = PBXGroup; children = ( - 854D13D7211B2E7200E139FC /* MessageCollectionViewLayout.swift */, + 852E84772134632A00FD3841 /* Attributes */, + 852E84762134631900FD3841 /* Layout */, 8540A330211B34B4007F65AF /* MessageCollectionViewDataSource.swift */, 8540A332211B35A4007F65AF /* MessageCollectionViewDelegate.swift */, 85D77806211D9B980044E72F /* ScrollPosition.swift */, + 2625F29E212463E8007C42B5 /* ProgressIdentifier.swift */, ); path = CollectionView; sourceTree = ""; @@ -7724,6 +7949,7 @@ 8580BAC420BD983400239D9D /* MessageSearchProtocols.swift */, 8580BAC520BD983400239D9D /* MentionTransitionProtocol.swift */, A458FABC20EB8B320075D55E /* MessageChannelActionsProtocol.swift */, + 85629EC92137EF2400A79C97 /* VoiceAudioInteractive.swift */, ); path = Protocols; sourceTree = ""; @@ -7943,6 +8169,7 @@ isa = PBXGroup; children = ( 3A1146671ED6F047006BA132 /* ring.mp3 */, + 5BBEF53B212DE09F00F10768 /* ringback.m4a */, ); path = Call; sourceTree = ""; @@ -8432,7 +8659,6 @@ 9BD8E3FE20F354A7001384EC /* View */ = { isa = PBXGroup; children = ( - 9BD8E3F720EF8874001384EC /* CallInProgressNavigationController.swift */, 9BD8E3EF20EF7898001384EC /* CallInProgressViewController.swift */, ); path = View; @@ -8486,13 +8712,6 @@ path = WireFrame; sourceTree = ""; }; - A33FA59FE338E9660AB10CD1 /* Presenter */ = { - isa = PBXGroup; - children = ( - ); - path = Presenter; - sourceTree = ""; - }; A402A1C720DE68C0005BFA20 /* TopSwipable */ = { isa = PBXGroup; children = ( @@ -9463,6 +9682,8 @@ A45F10FE20B4218D00F45004 /* MessageInteractor+Utils.swift */, A45F110020B4218D00F45004 /* PresenceStatusProvider.swift */, A458FABE20EB8BB50075D55E /* MessageInteractor+ChannelActions.swift */, + 85B0013321272694000C89FE /* MessageInteractor+History.swift */, + A49381A921355EE1006D28DD /* MessageInteractor+Forward.swift */, ); path = Interactor; sourceTree = ""; @@ -9470,7 +9691,10 @@ A45F114220B421AB00F45004 /* Message */ = { isa = PBXGroup; children = ( + 85458CE1212D730E00BA8814 /* MessageIdentifiers.swift */, A45F114320B421AB00F45004 /* MessageExtension.swift */, + 85458CF4212D770100BA8814 /* Message+Files.swift */, + 85458CF2212D762900BA8814 /* Message+Factory.swift */, ); path = Message; sourceTree = ""; @@ -9634,11 +9858,39 @@ path = LinkField; sourceTree = ""; }; + A46C362C212198D500172773 /* Security */ = { + isa = PBXGroup; + children = ( + A4868F2C2121D2E8001F624E /* Anti-debugging */, + A4CB151C2103725900C3B68B /* JailbreakDetector */, + ); + path = Security; + sourceTree = ""; + }; + A46C362D2121991100172773 /* DebuggingDetector */ = { + isa = PBXGroup; + children = ( + A46C36322121998300172773 /* Mechanisms */, + A46C362E2121995800172773 /* DebuggingDetectorProtocol.swift */, + A46C36302121996000172773 /* DebuggingDetector.swift */, + ); + path = DebuggingDetector; + sourceTree = ""; + }; + A46C36322121998300172773 /* Mechanisms */ = { + isa = PBXGroup; + children = ( + A46C36332121999100172773 /* DDMechanism.swift */, + A46C363621219A9000172773 /* DDSysctlMechanism.swift */, + ); + path = Mechanisms; + sourceTree = ""; + }; A46CF04121147B9D0072F185 /* HistoryRequestModel */ = { isa = PBXGroup; children = ( 3A3FD2821F39E0A000B6958F /* HistoryRequestModel.swift */, - A46CF04221147BAE0072F185 /* HistoryRequestModelTypeProtocol.swift */, + A46CF04221147BAE0072F185 /* HistoryRequestModelTypeRepresentable.swift */, ); name = HistoryRequestModel; sourceTree = ""; @@ -9672,6 +9924,33 @@ path = Base; sourceTree = ""; }; + A4868F2C2121D2E8001F624E /* Anti-debugging */ = { + isa = PBXGroup; + children = ( + A4868F392121E22C001F624E /* AntiDebuggingService.swift */, + A4868F2D2121D335001F624E /* DebuggingPreventer */, + A46C362D2121991100172773 /* DebuggingDetector */, + ); + path = "Anti-debugging"; + sourceTree = ""; + }; + A4868F2D2121D335001F624E /* DebuggingPreventer */ = { + isa = PBXGroup; + children = ( + A4868F302121D360001F624E /* DebuggingPreventing.swift */, + A4868F2E2121D349001F624E /* DetectorDebuggingPreventer.swift */, + A4868F352121DE26001F624E /* PtraceDebuggingPreventer.swift */, + ); + path = DebuggingPreventer; + sourceTree = ""; + }; + A4875961212DC26500AD454A /* Database */ = { + isa = PBXGroup; + children = ( + ); + path = Database; + sourceTree = ""; + }; A48C154020EF76DB002DA994 /* Link */ = { isa = PBXGroup; children = ( @@ -9882,13 +10161,6 @@ path = States; sourceTree = ""; }; - A4D0787972A19641165C28B6 /* WireFrame */ = { - isa = PBXGroup; - children = ( - ); - path = WireFrame; - sourceTree = ""; - }; A4ED79AE20C803E800A41F67 /* DataSource */ = { isa = PBXGroup; children = ( @@ -10319,9 +10591,6 @@ 5BC1D38320D3B670002A44B3 /* CallCreatorMediator.swift */, 9BC9657520FF042D00052AE1 /* CallInProgressProtocols.swift */, A895793051E246613AC4F30F /* View */, - A33FA59FE338E9660AB10CD1 /* Presenter */, - D6365C3D94F0150AFA59F586 /* Interactor */, - A4D0787972A19641165C28B6 /* WireFrame */, ); path = Call; sourceTree = ""; @@ -10579,13 +10848,6 @@ path = Interactor; sourceTree = ""; }; - D6365C3D94F0150AFA59F586 /* Interactor */ = { - isa = PBXGroup; - children = ( - ); - path = Interactor; - sourceTree = ""; - }; D6ABBEEBA2D71463B8110D50 /* WebView */ = { isa = PBXGroup; children = ( @@ -10856,16 +11118,18 @@ E78EFB8A1FC8876F00C44975 /* DBMember.swift */, E7AE41671FCC596300C3ED5D /* DBRoomMember.swift */, E70F78BA1FD6CB5600385565 /* DBChatCheckpoint.swift */, - 26541F712007B93400AAEACF /* DBMessageAction.swift */, 269848CF200FB82A00590D6F /* DBStar.swift */, 266F04CA2015050400B97A83 /* DBStarMessage.swift */, 0008E92120347A7B003E316E /* DBJob.swift */, 0008E92320347A8E003E316E /* DBJobMessage.swift */, 26142B1220473BFD004E5FE4 /* DBMessageLink.swift */, - FE58F9B2208F0583004AFDD3 /* DBMessageEditAction.swift */, 852003F520D4194A007C0036 /* DBRecentSticker.swift */, 853E595620D70F9A007799B9 /* DBStickerPack.swift */, A415132520DBE3A800C2C01F /* DBLink.swift */, + 26541F712007B93400AAEACF /* DBMessageAction.swift */, + FE58F9B2208F0583004AFDD3 /* DBMessageEditAction.swift */, + 26771CC2212ED109006112B5 /* DBConvertMessage.swift */, + 8514DE882136A50100718DD8 /* DBStarAction.swift */, ); path = Models; sourceTree = ""; @@ -10901,15 +11165,17 @@ E7302A941FC86424002892F8 /* P2pTable.swift */, E7302A961FC8642F002892F8 /* MucTable.swift */, E70F78B81FD6C64E00385565 /* ChatCheckpointTable.swift */, - 26541F732007B9A200AAEACF /* MessageActionTable.swift */, 269848CD200FB59800590D6F /* StarMessageTable.swift */, 4B1F122F203C8DDE00D61D21 /* JobTable.swift */, 004581202036073000F8E413 /* JobMessageTable.swift */, 26142B1020472ECD004E5FE4 /* MessageLinkTable.swift */, - FE58F9B0208F00FE004AFDD3 /* MessageEditActionTable.swift */, 852003F720D419E9007C0036 /* RecentStickerTable.swift */, 853E595820D711B1007799B9 /* StickerPackTable.swift */, A415132720DBE40F00C2C01F /* LinkTable.swift */, + 26541F732007B9A200AAEACF /* MessageActionTable.swift */, + FE58F9B0208F00FE004AFDD3 /* MessageEditActionTable.swift */, + 26771CC0212ECE08006112B5 /* ConvertMessageTable.swift */, + 8514DE8B2136A5FD00718DD8 /* StarActionTable.swift */, ); path = Tables; sourceTree = ""; @@ -11216,6 +11482,7 @@ isa = PBXGroup; children = ( E7417E981FBED91100E5C124 /* Table.swift */, + 85B0013121270DEC000C89FE /* TableOrder.swift */, E709383E1FBEE41D006CCDC6 /* Describable.swift */, ); path = Base; @@ -11292,6 +11559,7 @@ 853E595C20D71E73007799B9 /* StickerPack */, E7C9CEC41FCC245F0090C2E0 /* P2pExtension.swift */, 26C0C1E12073DA2E00C530DA /* P2P+DB.swift */, + 85458CEC212D74B400BA8814 /* P2P+Opponent.swift */, E7C9CEC61FCC249D0090C2E0 /* MucExtension.swift */, 26C0C1E32073DA3A00C530DA /* Muc+DB.swift */, E757B53C1FE9225C00467BA2 /* TypingExtension.swift */, @@ -12472,6 +12740,7 @@ FE21ACA82113AA7F006010A0 /* NynjaIntegrationTests */ = { isa = PBXGroup; children = ( + A4875961212DC26500AD454A /* Database */, FE21ACB62113AAF7006010A0 /* Services */, FE21ACAB2113AA7F006010A0 /* Info.plist */, ); @@ -12543,6 +12812,7 @@ buildRules = ( ); dependencies = ( + 26DC81ED213838CD003E5FD9 /* PBXTargetDependency */, ); name = "Nynja-Share"; productName = "Nynja-Share"; @@ -12731,7 +13001,6 @@ isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; files = ( - A44B4D6220CEA24100CA700A /* ChannelsConfig.xcconfig in Resources */, 6D36F8E21F0ADBD300FA1AC8 /* Localizable.strings in Resources */, E77B9B7F1FDEC6E20035CA12 /* NotoSans-Regular.ttf in Resources */, E77B9B7E1FDEC6E20035CA12 /* NotoSans-Medium.ttf in Resources */, @@ -12748,6 +13017,7 @@ E77B9B7C1FDEC6E20035CA12 /* NotoSans-Bold.ttf in Resources */, F10AFE9B20EF8B9B00C7CE83 /* DevAutoTests.xcconfig in Resources */, 00F7B348202B317000E443E1 /* timezones.json in Resources */, + 5BBEF53C212DE09F00F10768 /* ringback.m4a in Resources */, 3A2843291EF9317100EFE21A /* Avenir.ttc in Resources */, 859C42A5205691FB00AE3797 /* Sounds.json in Resources */, E77B9B7D1FDEC6E20035CA12 /* NotoSans-Italic.ttf in Resources */, @@ -12838,10 +13108,11 @@ "${BUILT_PRODUCTS_DIR}/CryptoSwift/CryptoSwift.framework", "${BUILT_PRODUCTS_DIR}/GRDBCipher/GRDBCipher.framework", "${BUILT_PRODUCTS_DIR}/GTMSessionFetcher/GTMSessionFetcher.framework", - "${BUILT_PRODUCTS_DIR}/GoogleToolboxForMac/GoogleToolboxForMac.framework", + "${BUILT_PRODUCTS_DIR}/GoogleUtilities/GoogleUtilities.framework", "${BUILT_PRODUCTS_DIR}/JTAppleCalendar/JTAppleCalendar.framework", "${BUILT_PRODUCTS_DIR}/MDFTextAccessibility/MDFTextAccessibility.framework", "${BUILT_PRODUCTS_DIR}/MaterialComponents/MaterialComponents.framework", + "${BUILT_PRODUCTS_DIR}/MulticastDelegateSwift/MulticastDelegateSwift.framework", "${PODS_ROOT}/NynjaSDK/NynjaSDK.framework", "${BUILT_PRODUCTS_DIR}/QRCode/QRCode.framework", "${BUILT_PRODUCTS_DIR}/SDWebImage/SDWebImage.framework", @@ -12862,10 +13133,11 @@ "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/CryptoSwift.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/GRDBCipher.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/GTMSessionFetcher.framework", - "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/GoogleToolboxForMac.framework", + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/GoogleUtilities.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/JTAppleCalendar.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/MDFTextAccessibility.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/MaterialComponents.framework", + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/MulticastDelegateSwift.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/NynjaSDK.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/QRCode.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/SDWebImage.framework", @@ -13028,6 +13300,7 @@ A497F56B20EFA86C005CC60F /* HandlerService.swift in Sources */, A4F3DAAA208493C100FF71C7 /* StorageService.swift in Sources */, A42CE56C20692EDB000889CC /* p2p.swift in Sources */, + 85458CF6212D770900BA8814 /* Message+Files.swift in Sources */, A42CE5F620692EDB000889CC /* Person_Spec.swift in Sources */, A42CE57220692EDB000889CC /* chain.swift in Sources */, 269848CC200EA0ED00590D6F /* StarModels.swift in Sources */, @@ -13062,11 +13335,10 @@ 26A856262074C5BC00C642EA /* ActionsView.swift in Sources */, 4B736D4720237C140028F2CB /* CGSizeExtension.swift in Sources */, A45F115520B4224100F45004 /* Room+BaseChatModel.swift in Sources */, - 855792D52122206400D0AB57 /* CollectionExtension.swift in Sources */, + 855792D52122206400D0AB57 /* Collection.swift in Sources */, E785F15A1FF3E38D006C52D9 /* UIImageView+Rounded.swift in Sources */, A42CE56A20692EDB000889CC /* Typing.swift in Sources */, 26EEA5482091F84E0066D3B0 /* CollectionsExtensions.swift in Sources */, - 264FFA951FC5912E0028243D /* Table.swift in Sources */, A42CE5D820692EDB000889CC /* error_Spec.swift in Sources */, 261522642084D5EC00AF72A5 /* Testable.swift in Sources */, 26FF00A51FCC2EC4002170B1 /* MQTTServiceAuth.swift in Sources */, @@ -13141,6 +13413,7 @@ A42CE5BE20692EDB000889CC /* Search_Spec.swift in Sources */, 35B1AB821F9FB06500E65233 /* AttachmentModel.swift in Sources */, A42CE5D420692EDB000889CC /* cur_Spec.swift in Sources */, + 85458CFA212D79F500BA8814 /* Message+Factory.swift in Sources */, A42CE5E820692EDB000889CC /* Auth_Spec.swift in Sources */, A415132320DBD5C100C2C01F /* Link_Spec.swift in Sources */, A42CE5C620692EDB000889CC /* reader_Spec.swift in Sources */, @@ -13184,6 +13457,7 @@ A42CE61420692EDB000889CC /* io_Spec.swift in Sources */, A42CE59020692EDB000889CC /* Task.swift in Sources */, A42CE58A20692EDB000889CC /* boundaryEvent.swift in Sources */, + 85458CDA212D6FFE00BA8814 /* String+Split.swift in Sources */, A42CE5A420692EDB000889CC /* Search.swift in Sources */, 26A8562F2074CBC600C642EA /* UIView+Gradient.swift in Sources */, 26A856292074C7BE00C642EA /* ActionsView+Action.swift in Sources */, @@ -13197,6 +13471,7 @@ A42CE55E20692EDB000889CC /* Person.swift in Sources */, A42CE5E620692EDB000889CC /* iter_Spec.swift in Sources */, 264FFA971FC591600028243D /* Describable.swift in Sources */, + 85458CFE212D7BCA00BA8814 /* Desc+Construct.swift in Sources */, A42CE59E20692EDB000889CC /* Job.swift in Sources */, A42CE5FA20692EDB000889CC /* container_Spec.swift in Sources */, 26A856382075080400C642EA /* ForwardSelectorPresenter.swift in Sources */, @@ -13210,6 +13485,7 @@ 262D438820335225002F1E45 /* FriendRequstModel.swift in Sources */, 267D465B20AB4CC100D42242 /* TimeZoneLocal.swift in Sources */, A42CE56020692EDB000889CC /* Auth.swift in Sources */, + 85458CEE212D74C000BA8814 /* P2P+Opponent.swift in Sources */, A42CE57820692EDB000889CC /* Member.swift in Sources */, A42CE55420692EDB000889CC /* process.swift in Sources */, 263D662A1FE8359900A509F8 /* RoomExtension+BERT.swift in Sources */, @@ -13290,6 +13566,7 @@ F11DF06920BD9E7900F3E005 /* NavigationProtocol.swift in Sources */, 26A8562A2074C7FB00C642EA /* UIView+SafeArea.swift in Sources */, 26C0C1CD2073C94400C530DA /* ForwardSelectorDisplayMode.swift in Sources */, + 260D67DF2125A2FE0072F11F /* LogWriter.swift in Sources */, 26C0C1E92073DB1000C530DA /* Localizable.swift in Sources */, 268C62E32008DA0900433705 /* UIImageExtensions.swift in Sources */, A4330A552109D60D0060BD93 /* QueryFactoryProtocol.swift in Sources */, @@ -13297,7 +13574,6 @@ 853E595420D6B214007799B9 /* Desc+Messages.swift in Sources */, A42CE5CC20692EDB000889CC /* Member_Spec.swift in Sources */, A4E6D14C208F043400519472 /* GApiResponse.swift in Sources */, - 264FFA961FC5913A0028243D /* ProfileTable.swift in Sources */, A43B25B420AB1E3C00FF8107 /* TextField.swift in Sources */, A42CE5BA20692EDB000889CC /* boundaryEvent_Spec.swift in Sources */, A42CE54C20692EDB000889CC /* Desc.swift in Sources */, @@ -13307,6 +13583,7 @@ A42CE58220692EDB000889CC /* CDR.swift in Sources */, A45F115220B4224100F45004 /* ContactExtension.swift in Sources */, A4B544EB20EFB36100EB7B0F /* errors.swift in Sources */, + 85458CE3212D731200BA8814 /* MessageIdentifiers.swift in Sources */, 26C0C1EF2073DE2600C530DA /* ForwardSelectorProtocols+ShareExt.swift in Sources */, 26A8563A20750B5D00C642EA /* ScheduleInfo.swift in Sources */, A45F115420B4224100F45004 /* RoomExtension.swift in Sources */, @@ -13348,10 +13625,12 @@ A42D51B6206A361400EEB952 /* iter.swift in Sources */, 2603139E20A0A4BA009AC66D /* ChatLanguageSettingsPresenter.swift in Sources */, 4B1D7DFC2029C37900703228 /* FavoritesItemsFactory.swift in Sources */, + 26771CC3212ED109006112B5 /* DBConvertMessage.swift in Sources */, 269848CA200E9F1300590D6F /* StarModels.swift in Sources */, 4B058F0D204EAEC3004C7D9F /* RoomDAO.swift in Sources */, F11DF05F20BD93FB00F3E005 /* UIViewExtensions.swift in Sources */, 4B1D7E112029FF5000703228 /* Array+WheelItemModel.swift in Sources */, + A46C36342121999100172773 /* DDMechanism.swift in Sources */, 854A4B302080D6C400759152 /* CellWithImageTableViewCell.swift in Sources */, 3A8045D81F60C98200AED866 /* MQTTServiceHelper.swift in Sources */, 8E9601971FF2EC8100E0C21D /* GroupFilesListVC.swift in Sources */, @@ -13423,6 +13702,7 @@ 001F0CF5202C38FA006B4304 /* TimeZoneCell.swift in Sources */, C9C694FD201FA55800A57297 /* SwipeBackHelper.swift in Sources */, 85CE26D820C5593600553FE7 /* HapticSelectionFeedbackGenerator.swift in Sources */, + A49381AA21355EE1006D28DD /* MessageInteractor+Forward.swift in Sources */, 26FA4210201821B400E6F6EC /* StarHandler.swift in Sources */, FBCE83E120E52496003B7558 /* ProfileServices.swift in Sources */, 26C0C1E42073DA3A00C530DA /* Muc+DB.swift in Sources */, @@ -13435,6 +13715,7 @@ 3A19FEAD1F3B7F1D00ACE750 /* MessageHandler.swift in Sources */, 2632139120D797F500C31144 /* TranslationViewProtocol.swift in Sources */, A409B1D22108D4A80051C20B /* QueryFactoryProtocol.swift in Sources */, + 85B0013221270DEC000C89FE /* TableOrder.swift in Sources */, 2603139920A0A4B9009AC66D /* LanguageSelectorTableActionDelegate.swift in Sources */, 8580BAE720BD9A5600239D9D /* SeparatorView.swift in Sources */, 8E6C4BE21FF559F8009C8374 /* GroupStorageListVC.swift in Sources */, @@ -13448,6 +13729,7 @@ 00E86469204D519600844FF1 /* LanguageSettingsItemsFactory.swift in Sources */, A4F3DAB22084949F00FF71C7 /* MessageExtension+BERT.swift in Sources */, F105C6BA20A1347E0091786A /* PhotoPreviewProtocols.swift in Sources */, + 85458CF5212D770100BA8814 /* Message+Files.swift in Sources */, E721306F1F9A384900D88103 /* AlignableLabel.swift in Sources */, A4C9300420B323B700D6FB0F /* RoomExtension.swift in Sources */, 4B06D30E2028A349003B275B /* ChatsItemsFactory.swift in Sources */, @@ -13561,6 +13843,7 @@ 26C1A3F02031D9E60009F7F0 /* OtherUserTableViewDS.swift in Sources */, A45F110D20B4218D00F45004 /* SendingStatus.swift in Sources */, A45F114720B421AB00F45004 /* MessageExtension.swift in Sources */, + 8546D464213EEC4E0024FE66 /* BidirectionalCollection.swift in Sources */, B79FA02E2107731400F286BF /* MarketplaceInteractor.swift in Sources */, F10B0E1B20B4412100528E7A /* GalleryViewController.swift in Sources */, E78EFB891FC867B200C44975 /* DBMuc.swift in Sources */, @@ -13577,6 +13860,7 @@ E74EC9EF1FC2DE23007268E6 /* MemberTable.swift in Sources */, 8514F17220EA219E00883513 /* ContextMenuItemsView.swift in Sources */, 268C3413210688B200F1472A /* TranscribeLongRequestData.swift in Sources */, + A46C36312121996000172773 /* DebuggingDetector.swift in Sources */, A42D52C0206A53AA00EEB952 /* error_Spec.swift in Sources */, 3AA13C761F2252F900BE5D8F /* SearchModel.swift in Sources */, A43B259D20AB1DFA00FF8107 /* PhoneField.swift in Sources */, @@ -13624,7 +13908,7 @@ E734831A1F9F39400090A4DB /* CellModel.swift in Sources */, B74BB00321076AFA0049CD27 /* CircleMenuItem.swift in Sources */, 6D485DDF1F0ACA4700E12FB1 /* UIImageView+Rounded.swift in Sources */, - 26CD3FDB2104D19D00597E62 /* TranscribeShortAudioOperation.swift in Sources */, + 26CD3FDB2104D19D00597E62 /* AudioShortTranscribeOperation.swift in Sources */, 40C2631343E285717633ADFA /* AuthPresenter.swift in Sources */, A42D51A7206A361400EEB952 /* log.swift in Sources */, DAE89B7EFAB308A6B48AF5EC /* AuthInteractor.swift in Sources */, @@ -13632,6 +13916,7 @@ 26C1A3E32031A95D0009F7F0 /* OtherUserProtocols.swift in Sources */, 8503B528205046A6006F0593 /* NotificationSettingsInteractor.swift in Sources */, B79FA02F2107731400F286BF /* MarketplaceWireFrame.swift in Sources */, + 8514DE8F2136A9A900718DD8 /* StarActionDAO.swift in Sources */, A4C9300520B323B700D6FB0F /* Room+BaseChatModel.swift in Sources */, E73483211F9F78DC0090A4DB /* ProfileSectionFooterView.swift in Sources */, 3AE0A84B1F20321A008A04F3 /* Wheel.swift in Sources */, @@ -13745,6 +14030,7 @@ 26588E6720A20E49000D3E1A /* Customizable.swift in Sources */, 8541BD6B206CE3A40093EF1E /* ChatPlaceholderWheelItemModel.swift in Sources */, F11786DE20A9ED65007A9A1B /* DownloadOperation.swift in Sources */, + 852E8473213460E800FD3841 /* ReversedMessageCollectionViewLayoutAttributes.swift in Sources */, 005886CD2030F41700FE2E89 /* NynjaTimeAmPmDelegate.swift in Sources */, 8514F17820EA219F00883513 /* ContextMenu.swift in Sources */, A432CF1320B4347D00993AFB /* InputInfoContainer.swift in Sources */, @@ -13781,6 +14067,7 @@ C9DF574C2023BE92006B990A /* SelectCountryHeaderView.swift in Sources */, E7A3DAB31F9DE9CB00856133 /* ConfigurableCell.swift in Sources */, A49CC1D420E4A9ED00879D41 /* InputBar+ContentType.swift in Sources */, + A46C363721219A9000172773 /* DDSysctlMechanism.swift in Sources */, 8502DB522061030100613C8C /* WheelPositionCellModel.swift in Sources */, E7EE893F1F83D85B009D37F9 /* Layout.swift in Sources */, F10B0E1520B43FC400528E7A /* GalleryProtocols.swift in Sources */, @@ -13836,6 +14123,7 @@ 260313A820A0A4BA009AC66D /* LabeledHeaderView.swift in Sources */, 2652D6161FA82EFE005E62C7 /* EditProfileVCLayout.swift in Sources */, A432CF1920B4347D00993AFB /* UIView+Animate.swift in Sources */, + 855A393D213E76E20002B8DC /* LoadingInteractive.swift in Sources */, E70189BB1F9107AD00CA7005 /* ProximitySensorManager.swift in Sources */, A497F56F20EFA8BC005CC60F /* ErrorsHandler.swift in Sources */, 0062D9442062EC4100B915AC /* ShareNynjaHeaderView.swift in Sources */, @@ -13863,6 +14151,7 @@ 2648C3E62069B49000863614 /* UITextField+Extension.swift in Sources */, A432CF1520B4347D00993AFB /* FloatingPlaceholderContainer.swift in Sources */, E70F78BB1FD6CB5600385565 /* DBChatCheckpoint.swift in Sources */, + 263C04E92132E2FF00B8F0BE /* WrappedTaskOperation.swift in Sources */, A42D52BE206A53AA00EEB952 /* cur_Spec.swift in Sources */, A402A1CE20DE6B38005BFA20 /* BaseButton.swift in Sources */, E74597751FA2222600D3C88C /* NavigationView.swift in Sources */, @@ -13914,6 +14203,7 @@ F11786CD20A8E4FD007A9A1B /* CameraVideoPreviewInteractor.swift in Sources */, A4679BBA20B305360021FE9C /* LinkValidator.swift in Sources */, 4BEE89D69CACB85ABEE9046F /* QRCodeGeneratorPresenter.swift in Sources */, + 2605311B212740FD002E1CF1 /* LogOutputProtocols.swift in Sources */, FBCE841420E525A6003B7558 /* NetworkService.swift in Sources */, A409B1CF2108D48E0051C20B /* QueryFactory.swift in Sources */, A42D52B7206A53AA00EEB952 /* reader_Spec.swift in Sources */, @@ -13939,16 +14229,16 @@ 85788C4420442385003600C9 /* BuildNumberPresenter.swift in Sources */, 3AE0A84D1F20321A008A04F3 /* WheelItemView.swift in Sources */, A4C4D6C220F38447005F757A /* Identifiable.swift in Sources */, - 268C341721074AD000F1472A /* TranscribeLongAudioProccessingOperation.swift in Sources */, + 268C341721074AD000F1472A /* AudioLongTranscribeProccessingOperation.swift in Sources */, 85018419204946C900F324A1 /* ThemeCollectionViewCell.swift in Sources */, A45F112120B4218D00F45004 /* InfoDateView.swift in Sources */, - 9BD8E3F820EF8874001384EC /* CallInProgressNavigationController.swift in Sources */, A4688DFA20650FF50013660D /* DBObserver.swift in Sources */, 263409342119CFE2002F8D8F /* RecordContainer.swift in Sources */, 853FB0682049B193000996C5 /* SupportProtocols.swift in Sources */, A42D51B1206A361400EEB952 /* Typing.swift in Sources */, A42D52D3206A53AB00EEB952 /* Test_Spec.swift in Sources */, E7F2CFE21F5EEF1E00806E43 /* PermissionManager.swift in Sources */, + 26ACC5CE212C3DDB008455E8 /* AudioTranscribeSendOperation.swift in Sources */, F6A317F954DA5B46BFD50E3C /* QRCodeGeneratorWireframe.swift in Sources */, F11786D720A9AABD007A9A1B /* Injectable.swift in Sources */, A9C6233FE6A819AAA64C1A35 /* QRCodeReaderProtocols.swift in Sources */, @@ -14001,6 +14291,7 @@ C9C695032022306D00A57297 /* SelectCountryTableDataSource.swift in Sources */, 8584C90F20920F3C001A0BBB /* StickerGridCellModel.swift in Sources */, A42D51A4206A361400EEB952 /* Feature.swift in Sources */, + 26D6D227212EDA6600EA2419 /* ConvertMessageDAOProtocol.swift in Sources */, 260552A61F9E1CD100D68DE6 /* SearchHandler.swift in Sources */, F1607B1F20B21A9D00BDF60A /* CameraViewController.swift in Sources */, 853E595B20D71E6C007799B9 /* StickerPack+DB.swift in Sources */, @@ -14078,6 +14369,7 @@ F117872520ACF2DB007A9A1B /* QualityName.swift in Sources */, A45F111B20B4218D00F45004 /* MessagePlaceView.swift in Sources */, A42D52C4206A53AA00EEB952 /* error2_Spec.swift in Sources */, + 85458CD9212D6FED00BA8814 /* String+Split.swift in Sources */, 00E9824E205C2604008BF03D /* SessionItemView.swift in Sources */, 8520040920D4F9B4007C0036 /* MessageStickerRepliedView.swift in Sources */, 00102F40202C8E5300A877A9 /* NynjaCalendarView.swift in Sources */, @@ -14110,6 +14402,7 @@ 8572C3BB2092366100E4840C /* StickerCollectionDataSource.swift in Sources */, E7FF40B41F96089B00810D1C /* AudioManagerDelegate.swift in Sources */, E735853D1F6C2705003354B5 /* Geometry.swift in Sources */, + 85B0013421272694000C89FE /* MessageInteractor+History.swift in Sources */, F119E67720D27E990043A532 /* ImagePreviewCVCell.swift in Sources */, 260313A720A0A4BA009AC66D /* ChatLanguageSettingsTableDataSource.swift in Sources */, 260313A520A0A4BA009AC66D /* BaseCell.swift in Sources */, @@ -14136,6 +14429,7 @@ A42D51A5206A361400EEB952 /* act.swift in Sources */, E7C36C371FC469E600740630 /* ProfileExtension.swift in Sources */, 26ED2C1820042683002DBBE8 /* RepliesDS.swift in Sources */, + 2605311F21274124002E1CF1 /* LogOutputInteractor.swift in Sources */, 850C0B2820E01F7F003341D0 /* NotificationCenter+WheelNotifications.swift in Sources */, 619C44B00CC7B169077CDEC2 /* EditProfileProtocols.swift in Sources */, 26ABCA4A211B321100EA4782 /* Bundle+provision.swift in Sources */, @@ -14166,6 +14460,7 @@ F117871D20ACF1D0007A9A1B /* CameraSettingsService.swift in Sources */, 26245F3F204EF58E00C8D3DD /* BasePresenter.swift in Sources */, 85052E5220D1A62500BCC386 /* StickerPreviewingContent.swift in Sources */, + 852E847121345FB400FD3841 /* MessageCollectionViewLayoutAttributes.swift in Sources */, A4330A6E2109EBA70060BD93 /* CountriesProvider.swift in Sources */, 26142B1320473BFD004E5FE4 /* DBMessageLink.swift in Sources */, 68B66BDEEFD73CDC331AC840 /* EditProfilePresenter.swift in Sources */, @@ -14177,6 +14472,7 @@ E785F1551FF3DDC8006C52D9 /* GroupRulesViewControllerConstants.swift in Sources */, 853FB0692049B193000996C5 /* SupportInteractor.swift in Sources */, 26DCB2442064B9BA001EF0AB /* ContactsViewController.swift in Sources */, + 85458CED212D74B400BA8814 /* P2P+Opponent.swift in Sources */, E79061B41FBF10AA009FD83A /* MessageTable.swift in Sources */, 00EB7872206E286A00E3FB03 /* WCReusableViews.swift in Sources */, 8ED0F3C11FBC5CB1004916AB /* Contact+DialogCellModel.swift in Sources */, @@ -14201,6 +14497,7 @@ A4B544FF20EFC1BA00EB7B0F /* StatusCode.swift in Sources */, 001169B5201A0B02001B435F /* MapSearchCell.swift in Sources */, 26342CB420ECFAB600D2196B /* MessageInteractor+Transcription.swift in Sources */, + 8514DE892136A50100718DD8 /* DBStarAction.swift in Sources */, 00102F3E202C8E3A00A877A9 /* NynjaTimeControl.swift in Sources */, 267BE2941FDEA24000C47E18 /* SettingsGroupDS.swift in Sources */, 260629712056EF2800CB8F65 /* LinksCell.swift in Sources */, @@ -14235,6 +14532,7 @@ 26B32B601FE170FE00888A0A /* MigrationManager.swift in Sources */, 986BE2204D6D0813B13618B1 /* AddContactViaPhonePresenter.swift in Sources */, 265AEA171FE9AFD400AC4806 /* MemberModel.swift in Sources */, + 2605311D21274116002E1CF1 /* LogOutputView.swift in Sources */, 263529152075729400DC6FBD /* Job+DB.swift in Sources */, 8520040B20D4FB06007C0036 /* ReplyInfoView.swift in Sources */, E77D58991F98B94E00FBE926 /* ProfileTablewViewDS.swift in Sources */, @@ -14255,11 +14553,13 @@ 260313AA20A0A4BA009AC66D /* ChatLanguageSettingsViewController.swift in Sources */, 859B862F204820DC003272B2 /* ThemePickerInteractor.swift in Sources */, 263A60AC1FB4F8F7006F9D52 /* ParticipantsDataSource.swift in Sources */, + 852E8475213462F000FD3841 /* ReversedMessageCollectionViewLayout.swift in Sources */, 2AC52C9C5598DB3C4D3D9364 /* AddContactViaPhoneWireframe.swift in Sources */, 8504DEAD2069438D006722AC /* Media.swift in Sources */, 26B06C8020602643005BF9AF /* CarouselPickerCollectionViewCell.swift in Sources */, C9C6952E202349DA00A57297 /* SelectCountryCellLayout.swift in Sources */, 3A237BCD1F30E5D400C42B6E /* RosterHandler.swift in Sources */, + A4868F2F2121D349001F624E /* DetectorDebuggingPreventer.swift in Sources */, 26610F592015458D00609F77 /* LocationFullWheelItemView.swift in Sources */, A45F110A20B4218D00F45004 /* RecordingStatus.swift in Sources */, 4B1D7E032029C80800703228 /* ByContactsItemsFactory.swift in Sources */, @@ -14269,6 +14569,7 @@ 3A237BC91F30AB0F00C42B6E /* EditProfileVC.swift in Sources */, A406E39A210B457300435B3E /* DictionaryExtension.swift in Sources */, 2661D1331F373D5900F3E125 /* WheelConfiguration.swift in Sources */, + 263C04EB2132E56E00B8F0BE /* TranscribeOperation.swift in Sources */, A42CE5AD20692EDB000889CC /* StringAtom.swift in Sources */, A4CB1520210372DF00C3B68B /* JDMechanism.swift in Sources */, 8E23E0882006853000A59B8C /* GroupVideosListVC.swift in Sources */, @@ -14319,7 +14620,7 @@ 269848CE200FB59800590D6F /* StarMessageTable.swift in Sources */, A42D51C5206A361400EEB952 /* History.swift in Sources */, 267BE2AD1FE13AB600C47E18 /* ParticipantsProtocols.swift in Sources */, - 268C341121067F1D00F1472A /* TranscribeLongAudioOperation.swift in Sources */, + 268C341121067F1D00F1472A /* AudioLongTranscribeOperation.swift in Sources */, A4ED79B020C8041500A41F67 /* TableViewDataSourceProxy.swift in Sources */, A45F113E20B4218D00F45004 /* MessageInteractor+Utils.swift in Sources */, A4CB153521038A7A00C3B68B /* UIDevice+Jailbreak.swift in Sources */, @@ -14424,7 +14725,8 @@ 32868DE11F31CB7D0028B260 /* ChatsListWireframe.swift in Sources */, F105C69D209F71BF0091786A /* CameraWireframe.swift in Sources */, A458FABF20EB8BB50075D55E /* MessageInteractor+ChannelActions.swift in Sources */, - 26CD3FDD2104D1DD00597E62 /* AudioConvertionOperation.swift in Sources */, + 26CD3FDD2104D1DD00597E62 /* AudioConvertOperation.swift in Sources */, + 26771CC1212ECE08006112B5 /* ConvertMessageTable.swift in Sources */, 8514F17C20EA219F00883513 /* ContextMenuConfiguration.swift in Sources */, FBCE840D20E525A6003B7558 /* HTTPResponseResult.swift in Sources */, A43B259F20AB1DFA00FF8107 /* PickerCell.swift in Sources */, @@ -14438,6 +14740,7 @@ 266AE8C3203496B60096A12C /* AsyncOperation.swift in Sources */, F119E67220D24BE40043A532 /* MultiplePreviewInteractor.swift in Sources */, 8503B525205046A6006F0593 /* NotificationSettingsPresenter.swift in Sources */, + 85458CFD212D7B8C00BA8814 /* Desc+Construct.swift in Sources */, 85433F2C204D5AA500B373A7 /* NynjaCloseButton.swift in Sources */, A44B4D5A20CE9BDF00CA700A /* SwitchCell.swift in Sources */, F1607B3020B2FD5A00BDF60A /* QRNotificationVIew.swift in Sources */, @@ -14482,7 +14785,7 @@ 267C1D5920404EDB0087808F /* AlertImageViewController.swift in Sources */, 0008E91B20333A38003E316E /* JobHandler.swift in Sources */, B750EF042046D69C00A99F9C /* SpeedMesurement.swift in Sources */, - 8512349221221B9E000129A2 /* CollectionExtension.swift in Sources */, + 8512349221221B9E000129A2 /* Collection.swift in Sources */, 853D55B220CE66180080659F /* StickersInputData.swift in Sources */, 260313A320A0A4BA009AC66D /* ActionCellViewModel.swift in Sources */, E764919C1F7A5485001E741C /* MainWheelContainerDataSource.swift in Sources */, @@ -14497,6 +14800,7 @@ 26342CAF20ECD16A00D2196B /* TranscribeShortResponseData.swift in Sources */, E743B5881FB08F0F00F72F92 /* ParticipantsAvatarCell.swift in Sources */, D883A2CBD629A340B27997EF /* SplashViewController.swift in Sources */, + 26D6D229212EDADC00EA2419 /* ConvertMessageDAO.swift in Sources */, 85991DB52113437D0056F3E0 /* UITextView+Extensions.swift in Sources */, 69309CB4317F99B9C299F7D6 /* SplashPresenter.swift in Sources */, 85482844204E915400DCBEC8 /* PrivacyTableViewCell.swift in Sources */, @@ -14511,7 +14815,9 @@ C9B8BEF7204DD7890018748C /* SettingsDataAndStorageLayout.swift in Sources */, A49E6C4220D9A27D007D85F5 /* MainViewController+Container.swift in Sources */, 85788C4620442392003600C9 /* BuildNumberInteractor.swift in Sources */, + 2605312921298BEF002E1CF1 /* Logoutputcell.swift in Sources */, 6B3D349607A18D5650BF47E6 /* SplashInteractor.swift in Sources */, + 260D67D92124616A0072F11F /* LogWriter.swift in Sources */, 859B863720485F01003272B2 /* CarouselPickerViewController.swift in Sources */, 8566771E20C1579C00DD4204 /* StorageSubscriberReference.swift in Sources */, F117871420ACF018007A9A1B /* CameraSettingsWireframe.swift in Sources */, @@ -14534,6 +14840,7 @@ F1D4A4A020762A1D00F31089 /* Configurable.swift in Sources */, A45F110F20B4218D00F45004 /* ChatInitialMessage.swift in Sources */, C9B8BEFE204DEBD00018748C /* DataDownloadAndUsageMode.swift in Sources */, + 2625F29F212463E8007C42B5 /* ProgressIdentifier.swift in Sources */, A44B4D5220CE9BDF00CA700A /* SettingCellProtocol.swift in Sources */, 0008E9152032D6B7003E316E /* JobExtension+BERT.swift in Sources */, 85D66A0220BD963C00FBD803 /* MentionInfo.swift in Sources */, @@ -14551,7 +14858,7 @@ 73BFE52F809536A538E6A55E /* ImagePreviewViewController.swift in Sources */, 850C0B2620E00C3E003341D0 /* UIScreen+Keyboard.swift in Sources */, 8503B51B205036F2006F0593 /* BaseNynjaButton.swift in Sources */, - A46CF04321147BAE0072F185 /* HistoryRequestModelTypeProtocol.swift in Sources */, + A46CF04321147BAE0072F185 /* HistoryRequestModelTypeRepresentable.swift in Sources */, 8538012C2052E29D002C6960 /* SoundTableHeaderView.swift in Sources */, 3A8045D31F60C8E200AED866 /* MQTTServiceProfile.swift in Sources */, B7F505192061158800C28FA1 /* SettingsArrowCellViewModel.swift in Sources */, @@ -14581,7 +14888,9 @@ A43B25A620AB1DFA00FF8107 /* RecordingAudioWaveform.swift in Sources */, 26C1A3EB2031AAD20009F7F0 /* OtherUserInteractor.swift in Sources */, 26D8317520EA65200067C5B4 /* TranslationInfo.swift in Sources */, + A4868F3A2121E22C001F624E /* AntiDebuggingService.swift in Sources */, 85D66A0020BD963C00FBD803 /* InputTextMessage.swift in Sources */, + A4868F312121D360001F624E /* DebuggingPreventing.swift in Sources */, A45F114920B421AB00F45004 /* ContactExtension.swift in Sources */, 26B32B931FE20B8B00888A0A /* mucExtension+BERT.swift in Sources */, A43B25D720AB1EE400FF8107 /* NewChannelWireFrame.swift in Sources */, @@ -14626,6 +14935,7 @@ A42D52AD206A53AA00EEB952 /* log_Spec.swift in Sources */, F6150A15F8A3E399EEB2C724 /* MapWireframe.swift in Sources */, 853E595920D711B1007799B9 /* StickerPackTable.swift in Sources */, + 8514DE8C2136A5FD00718DD8 /* StarActionTable.swift in Sources */, 4B1D7E092029D86600703228 /* CreateGroupItemsFactory.swift in Sources */, 85052E5720D1A90D00BCC386 /* StickerImagePreviewView.swift in Sources */, 8580BACC20BD984500239D9D /* MessageEditInfo.swift in Sources */, @@ -14664,6 +14974,7 @@ E745A24B20061AD400D7EF42 /* DatabaseExtension.swift in Sources */, A42D51BF206A361400EEB952 /* Roster.swift in Sources */, 9BD8E41320F3A2E2001384EC /* CallInProgressInteractor.swift in Sources */, + 85629ECA2137EF2400A79C97 /* VoiceAudioInteractive.swift in Sources */, 265EB71D20A86A1900C1483E /* ConvertionProgressModel.swift in Sources */, 85D66A0320BD963C00FBD803 /* NSAttributedStringKey+Mention.swift in Sources */, E7417E991FBED91100E5C124 /* Table.swift in Sources */, @@ -14737,6 +15048,7 @@ 859B862E204820DC003272B2 /* ThemePickerProtocols.swift in Sources */, 1D1D5634D125333796D14E10 /* AddParticipantsPresenter.swift in Sources */, 26EA201320BECDA600FBB9CA /* ConversationLanguageSettingServiceProtocol.swift in Sources */, + A46C362F2121995800172773 /* DebuggingDetectorProtocol.swift in Sources */, 26C1A3F32031EED30009F7F0 /* OtherUserHeaderView.swift in Sources */, 00772A49F4B53A5EB669E8F2 /* AddParticipantsInteractor.swift in Sources */, A44B4D5420CE9BDF00CA700A /* ArrowCellViewModel.swift in Sources */, @@ -14796,12 +15108,15 @@ 855798802093200D007050B8 /* StickerMenuActionCellModel.swift in Sources */, 853D55B620CE6BF60080659F /* StickerPackageViewModel.swift in Sources */, 26C0C1D12073CB5800C530DA /* ForwardTargets+Schedule.swift in Sources */, + A4868F362121DE26001F624E /* PtraceDebuggingPreventer.swift in Sources */, FB15A8B220A04B05005DE3EC /* ImagePreviewTransitionController.swift in Sources */, 85D669E420BD956000FBD803 /* Int+AnyObject.swift in Sources */, A4569873060C49904EF8C555 /* EditGroupPhotoViewController.swift in Sources */, 8502DB502061030000613C8C /* WheelPositionPickerWireFrame.swift in Sources */, 854D13D8211B2E7200E139FC /* MessageCollectionViewLayout.swift in Sources */, 0062D9452062EC4100B915AC /* InviteFriendsDS.swift in Sources */, + 85458CF3212D762900BA8814 /* Message+Factory.swift in Sources */, + 85458CE2212D730E00BA8814 /* MessageIdentifiers.swift in Sources */, F117872620ACF2DB007A9A1B /* VideoQuality.swift in Sources */, FBDA34E920921079009F4FB6 /* KeyboardLayoutGuide.swift in Sources */, 267BE2901FDEA0A700C47E18 /* SettingsGroupPresenter.swift in Sources */, @@ -14892,6 +15207,7 @@ 40D11B34597AE40C8B71E59C /* AddContactByUsernameInteractor.swift in Sources */, 26DCB24C2064B9CC001EF0AB /* ContactCellModel.swift in Sources */, A42D52AF206A53AA00EEB952 /* Room_Spec.swift in Sources */, + 26053123212741C2002E1CF1 /* LogOutputWireFrame.swift in Sources */, A45F112B20B4218D00F45004 /* MessageContainerView.swift in Sources */, A7285B8B56BFCA857AD9BA8A /* AddContactByUsernameWireframe.swift in Sources */, 2603139A20A0A4B9009AC66D /* LanguageSelectorTableDelegate.swift in Sources */, @@ -14944,9 +15260,12 @@ 24AC9EAFA26353C7B95B60BF /* DateTimePickerPresenter.swift in Sources */, 85788C48204423A4003600C9 /* BuildNumberWireFrame.swift in Sources */, 5894F4C605B66B55F21D406E /* DateTimePickerInteractor.swift in Sources */, + 8514DE912136A9CB00718DD8 /* StarActionDAOProtocol.swift in Sources */, 00E98254205C2726008BF03D /* SessionFooterView.swift in Sources */, 8580BABE20BD981900239D9D /* MessageInteractor+Mentions.swift in Sources */, A45F112F20B4218D00F45004 /* BaseChatCellDelegate.swift in Sources */, + 2605312B21299198002E1CF1 /* LogOutputDS.swift in Sources */, + 2605312121274133002E1CF1 /* LogOutputPresenter.swift in Sources */, E3E22BD2755EAE3DBBCE2E9D /* DateTimePickerWireframe.swift in Sources */, 850FC5F22032F33900832D87 /* ForwardSelectorViewController.swift in Sources */, A42D52D0206A53AB00EEB952 /* Friend_Spec.swift in Sources */, @@ -14964,9 +15283,11 @@ C6B308C6734EFB77892832A0 /* SecurityPresenter.swift in Sources */, A42D52B4206A53AA00EEB952 /* ok_Spec.swift in Sources */, B74BAFFD21076AFA0049CD27 /* Sector.swift in Sources */, + 260531262127455C002E1CF1 /* MotionManager.swift in Sources */, 8E54E93EA25B11D417A6100E /* SecurityInteractor.swift in Sources */, A43B259420AB1DFA00FF8107 /* InputBar.swift in Sources */, F11DF06820BD996200F3E005 /* NavigationProtocol.swift in Sources */, + 855C9FE62125B4C0000E3429 /* MessageHandlerSubscriber.swift in Sources */, 2CB54DD94DA23D7160F36472 /* SecurityWireframe.swift in Sources */, 26EA201520BECDCA00FBB9CA /* ConversationLanguageSettingService.swift in Sources */, F11786ED20AC383D007A9A1B /* CollapsedView.swift in Sources */, @@ -14980,16 +15301,26 @@ buildActionMask = 2147483647; files = ( FB816EF520B5B87300093DCD /* Bert.swift in Sources */, + 85458CE8212D73E600BA8814 /* MucExtension.swift in Sources */, + 85458CFF212D7BCB00BA8814 /* Desc+Construct.swift in Sources */, FB816EF220B5B6F100093DCD /* BaseMQTTModel.swift in Sources */, FB816EF020B5B36D00093DCD /* HistoryRequestModelTests.swift in Sources */, + 85458CEA212D742300BA8814 /* muc.swift in Sources */, A4CB153B21039C1100C3B68B /* JailbreakDetectorProtocol.swift in Sources */, + 85458D01212D7C1A00BA8814 /* StringAtomExtension.swift in Sources */, A4AB8E522105EC46005F9B0C /* TextField.swift in Sources */, + 85458D00212D7C0C00BA8814 /* Int+AnyObject.swift in Sources */, FB816EF820B5B89700093DCD /* StringAtom.swift in Sources */, A4CB153A21039BD800C3B68B /* JailbreakDetectorTest.swift in Sources */, FB816EF420B5B85E00093DCD /* Room.swift in Sources */, A4AB8E562105ECD7005F9B0C /* TextViewTest.swift in Sources */, + 85458CFB212D7B2100BA8814 /* FeatureExtension+BERT.swift in Sources */, + 85458CF9212D785A00BA8814 /* DescExtension+BERT.swift in Sources */, A49EE6D7210B110800B700B1 /* Link.swift in Sources */, + 85458CE4212D731300BA8814 /* MessageIdentifiers.swift in Sources */, + 85458CE9212D740D00BA8814 /* MessageExtension+BERT.swift in Sources */, A4330A562109D60D0060BD93 /* QueryFactoryProtocol.swift in Sources */, + 85458CE6212D73CF00BA8814 /* P2pExtension.swift in Sources */, A4330A582109D6130060BD93 /* QueryFactory.swift in Sources */, A49EE6DC210B257000B700B1 /* UserInfo.swift in Sources */, A49EE6D8210B110C00B700B1 /* Service.swift in Sources */, @@ -14998,10 +15329,15 @@ 852003FC20D45B48007C0036 /* BertBinConvertible.swift in Sources */, FB816EF320B5B85900093DCD /* Contact.swift in Sources */, A4AB8E542105EC9A005F9B0C /* InputsCachePolicy.swift in Sources */, + 85458CDB212D6FFE00BA8814 /* String+Split.swift in Sources */, A4AB8E532105EC4B005F9B0C /* TextView.swift in Sources */, FB816EFA20B5B8B000093DCD /* Member.swift in Sources */, A4CB153C21039C1B00C3B68B /* JDMechanism.swift in Sources */, + 85458CF1212D75D300BA8814 /* mucExtension+BERT.swift in Sources */, + 85458CEB212D742A00BA8814 /* p2p.swift in Sources */, + 85458CF7212D771700BA8814 /* MessageExtension.swift in Sources */, FB816EF620B5B88C00093DCD /* Message.swift in Sources */, + 85458CF0212D75CE00BA8814 /* p2pExtension+BERT.swift in Sources */, A49EE6DB210B254A00B700B1 /* UserInfoTest.swift in Sources */, A406E39C210B482100435B3E /* DictionaryExtension.swift in Sources */, A4123DD72102324900AF585A /* KeychainService.swift in Sources */, @@ -15021,6 +15357,8 @@ FE21ACBC2113AB92006010A0 /* QueryFactoryProtocol.swift in Sources */, FE21ACBD2113ABBF006010A0 /* DictionaryExtension.swift in Sources */, FE21ACBA2113AB4B006010A0 /* KeychainService.swift in Sources */, + 85458CDC212D6FFF00BA8814 /* String+Split.swift in Sources */, + 85458CE5212D731300BA8814 /* MessageIdentifiers.swift in Sources */, FE21ACBB2113AB7C006010A0 /* QueryFactory.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; @@ -15033,6 +15371,11 @@ target = 357809A21F9765CF00C9680C /* Nynja-Share */; targetProxy = 264FFA911FC590580028243D /* PBXContainerItemProxy */; }; + 26DC81ED213838CD003E5FD9 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + name = NynjaUIKit; + targetProxy = 26DC81EC213838CD003E5FD9 /* PBXContainerItemProxy */; + }; 85C65C7220EE5A2800C468B2 /* PBXTargetDependency */ = { isa = PBXTargetDependency; name = NynjaUIKit; @@ -15091,106 +15434,45 @@ /* End PBXVariantGroup section */ /* Begin XCBuildConfiguration section */ - 26D8F42C20D7E0BA001602E9 /* Stickers */ = { - isa = XCBuildConfiguration; - baseConfigurationReference = 26D8F42B20D7E074001602E9 /* StickersConfig.xcconfig */; - buildSettings = { - ALWAYS_SEARCH_USER_PATHS = NO; - CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES; - CLANG_ANALYZER_NONNULL = YES; - CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; - CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; - CLANG_CXX_LIBRARY = "libc++"; - CLANG_ENABLE_MODULES = YES; - CLANG_ENABLE_OBJC_ARC = YES; - CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; - CLANG_WARN_BOOL_CONVERSION = YES; - CLANG_WARN_COMMA = YES; - CLANG_WARN_CONSTANT_CONVERSION = 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_LITERAL_CONVERSION = YES; - CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; - CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; - CLANG_WARN_STRICT_PROTOTYPES = YES; - CLANG_WARN_SUSPICIOUS_MOVE = YES; - CLANG_WARN_UNREACHABLE_CODE = YES; - CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; - "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; - COPY_PHASE_STRIP = NO; - DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; - ENABLE_STRICT_OBJC_MSGSEND = YES; - ENABLE_TESTABILITY = YES; - GCC_C_LANGUAGE_STANDARD = gnu99; - GCC_DYNAMIC_NO_PIC = NO; - GCC_NO_COMMON_BLOCKS = YES; - GCC_OPTIMIZATION_LEVEL = 0; - GCC_PREPROCESSOR_DEFINITIONS = ( - "DEBUG=1", - "$(inherited)", - ); - 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; - IPHONEOS_DEPLOYMENT_TARGET = 10.3; - MTL_ENABLE_DEBUG_INFO = YES; - ONLY_ACTIVE_ARCH = YES; - SDKROOT = iphoneos; - SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; - SWIFT_OPTIMIZATION_LEVEL = "-Onone"; - SWIFT_SWIFT3_OBJC_INFERENCE = Off; - SWIFT_WHOLE_MODULE_OPTIMIZATION = YES; - TARGETED_DEVICE_FAMILY = "1,2"; - }; - name = Stickers; - }; - 26D8F42D20D7E0BA001602E9 /* Stickers */ = { + 357809AE1F9765CF00C9680C /* Dev */ = { isa = XCBuildConfiguration; - baseConfigurationReference = EE1EF22666DC92AE739E2DA5 /* Pods-Nynja.stickers.xcconfig */; + baseConfigurationReference = A169D8E4AB2003F96040DD7A /* Pods-Nynja-Share.dev.xcconfig */; buildSettings = { ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = "$(inherited)"; - ASSETCATALOG_COMPILER_APPICON_NAME = AppIconChannels; - CLANG_ENABLE_MODULES = YES; - CODE_SIGN_ENTITLEMENTS = Nynja/Resources/Nynja.entitlements; - CODE_SIGN_IDENTITY = "iPhone Distribution"; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CODE_SIGN_ENTITLEMENTS = "Nynja-Share/Resources/Nynja-Share.entitlements"; + CODE_SIGN_IDENTITY = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; CODE_SIGN_STYLE = Manual; - DEFINES_MODULE = YES; + DEBUG_INFORMATION_FORMAT = dwarf; DEVELOPMENT_TEAM = 9GKQ5AMF2B; ENABLE_BITCODE = NO; - INFOPLIST_FILE = "$(SRCROOT)/Nynja/Resources/Info.plist"; + GCC_C_LANGUAGE_STANDARD = gnu11; + INFOPLIST_FILE = "Nynja-Share/Resources/Info.plist"; IPHONEOS_DEPLOYMENT_TARGET = 10.0; - LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; - PRODUCT_BUNDLE_IDENTIFIER = "$(BundleIdentifier)"; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @executable_path/../../Frameworks"; + OTHER_SWIFT_FLAGS = "$(inherited) \"-D\" \"COCOAPODS\" -DSHARE_EXTENSION -Xfrontend -warn-long-expression-type-checking=20 -Xfrontend -warn-long-function-bodies=20"; + PRODUCT_BUNDLE_IDENTIFIER = "$(ExtensionBundleIdentifier)"; PRODUCT_NAME = "$(TARGET_NAME)"; - PROVISIONING_PROFILE = "dc7cf190-1393-4a29-84c9-ece23e0d0f16"; - PROVISIONING_PROFILE_SPECIFIER = DevBundle_Dev; - SWIFT_OBJC_BRIDGING_HEADER = "Nynja-Bridging-Header.h"; - SWIFT_OPTIMIZATION_LEVEL = "-Onone"; - SWIFT_SWIFT3_OBJC_INFERENCE = Off; + PROVISIONING_PROFILE = "69f3dc99-df33-4a29-8a8a-8d93926a3535"; + PROVISIONING_PROFILE_SPECIFIER = DevBundle_DevExt; + SKIP_INSTALL = YES; SWIFT_VERSION = 4.0; - SWIFT_WHOLE_MODULE_OPTIMIZATION = YES; + TARGETED_DEVICE_FAMILY = "1,2"; }; - name = Stickers; + name = Dev; }; - 26D8F42E20D7E0BA001602E9 /* Stickers */ = { + 357809AF1F9765CF00C9680C /* Release */ = { isa = XCBuildConfiguration; - baseConfigurationReference = 0C816517E842C8136C67FFD2 /* Pods-Nynja-Share.stickers.xcconfig */; + baseConfigurationReference = 26D87FB9E1E4B20C47456AF6 /* Pods-Nynja-Share.release.xcconfig */; buildSettings = { ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = "$(inherited)"; CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; CODE_SIGN_ENTITLEMENTS = "Nynja-Share/Resources/Nynja-Share.entitlements"; CODE_SIGN_IDENTITY = "iPhone Developer"; - "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution"; CODE_SIGN_STYLE = Manual; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEVELOPMENT_TEAM = 9GKQ5AMF2B; @@ -15202,31 +15484,32 @@ OTHER_SWIFT_FLAGS = "$(inherited) \"-D\" \"COCOAPODS\" -DSHARE_EXTENSION"; PRODUCT_BUNDLE_IDENTIFIER = "$(ExtensionBundleIdentifier)"; PRODUCT_NAME = "$(TARGET_NAME)"; - PROVISIONING_PROFILE = "a8b63d08-5002-44f0-94d3-87ddff72770a"; - PROVISIONING_PROFILE_SPECIFIER = DevBundle_DevExt; + PROVISIONING_PROFILE = "2a318f9e-d0ab-41dc-968a-e1cb13de4de5"; + PROVISIONING_PROFILE_SPECIFIER = ProductionBundle_AppstoreExt; SKIP_INSTALL = YES; + SWIFT_COMPILATION_MODE = wholemodule; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; SWIFT_VERSION = 4.0; TARGETED_DEVICE_FAMILY = "1,2"; }; - name = Stickers; + name = Release; }; - 26D8F42F20D7E0BA001602E9 /* Stickers */ = { + 3ABCE8FD1EC9330D00A80B15 /* Dev */ = { isa = XCBuildConfiguration; - baseConfigurationReference = E49C16524294773D8EE07534 /* Pods-NynjaUnitTests.stickers.xcconfig */; + baseConfigurationReference = F1313AFE20888CAB00E04092 /* DevConfig.xcconfig */; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES; CLANG_ANALYZER_NONNULL = YES; CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; - CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; CLANG_CXX_LIBRARY = "libc++"; 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; @@ -15234,23 +15517,19 @@ 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_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_IDENTITY = "iPhone Developer"; - CODE_SIGN_STYLE = Automatic; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; COPY_PHASE_STRIP = NO; - DEBUG_INFORMATION_FORMAT = dwarf; - DEVELOPMENT_TEAM = 9GKQ5AMF2B; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; ENABLE_STRICT_OBJC_MSGSEND = YES; ENABLE_TESTABILITY = YES; - GCC_C_LANGUAGE_STANDARD = gnu11; + GCC_C_LANGUAGE_STANDARD = gnu99; GCC_DYNAMIC_NO_PIC = NO; GCC_NO_COMMON_BLOCKS = YES; GCC_OPTIMIZATION_LEVEL = 0; @@ -15264,634 +15543,21 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - INFOPLIST_FILE = NynjaUnitTests/Info.plist; - IPHONEOS_DEPLOYMENT_TARGET = 11.3; - LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; + IPHONEOS_DEPLOYMENT_TARGET = 10.3; MTL_ENABLE_DEBUG_INFO = YES; ONLY_ACTIVE_ARCH = YES; - PRODUCT_BUNDLE_IDENTIFIER = com.nynja.mobile.communicator.NynjaUnitTests; - PRODUCT_NAME = "$(TARGET_NAME)"; SDKROOT = iphoneos; SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; - SWIFT_VERSION = 4.0; + SWIFT_SWIFT3_OBJC_INFERENCE = Off; + SWIFT_WHOLE_MODULE_OPTIMIZATION = YES; TARGETED_DEVICE_FAMILY = "1,2"; }; - name = Stickers; + name = Dev; }; - 26E3229420E52C4A00271413 /* Translate */ = { - isa = XCBuildConfiguration; - baseConfigurationReference = 2632139220D7B71200C31144 /* TranslateConfig.xcconfig */; - buildSettings = { - ALWAYS_SEARCH_USER_PATHS = NO; - CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES; - CLANG_ANALYZER_NONNULL = YES; - CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; - CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; - CLANG_CXX_LIBRARY = "libc++"; - CLANG_ENABLE_MODULES = YES; - CLANG_ENABLE_OBJC_ARC = YES; - CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; - CLANG_WARN_BOOL_CONVERSION = YES; - CLANG_WARN_COMMA = YES; - CLANG_WARN_CONSTANT_CONVERSION = 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_LITERAL_CONVERSION = YES; - CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; - CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; - CLANG_WARN_STRICT_PROTOTYPES = YES; - CLANG_WARN_SUSPICIOUS_MOVE = YES; - CLANG_WARN_UNREACHABLE_CODE = YES; - CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; - "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; - COPY_PHASE_STRIP = NO; - DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; - ENABLE_STRICT_OBJC_MSGSEND = YES; - ENABLE_TESTABILITY = YES; - GCC_C_LANGUAGE_STANDARD = gnu99; - GCC_DYNAMIC_NO_PIC = NO; - GCC_NO_COMMON_BLOCKS = YES; - GCC_OPTIMIZATION_LEVEL = 0; - GCC_PREPROCESSOR_DEFINITIONS = ( - "DEBUG=1", - "$(inherited)", - ); - 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; - IPHONEOS_DEPLOYMENT_TARGET = 10.3; - MTL_ENABLE_DEBUG_INFO = YES; - ONLY_ACTIVE_ARCH = YES; - SDKROOT = iphoneos; - SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; - SWIFT_OPTIMIZATION_LEVEL = "-Onone"; - SWIFT_SWIFT3_OBJC_INFERENCE = Off; - SWIFT_WHOLE_MODULE_OPTIMIZATION = YES; - TARGETED_DEVICE_FAMILY = "1,2"; - }; - name = Translate; - }; - 26E3229520E52C4A00271413 /* Translate */ = { - isa = XCBuildConfiguration; - baseConfigurationReference = 718EF22D86A9656BB6ED89D5 /* Pods-Nynja.translate.xcconfig */; - buildSettings = { - ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = "$(inherited)"; - ASSETCATALOG_COMPILER_APPICON_NAME = AppIconSpotify; - CLANG_ENABLE_MODULES = YES; - CODE_SIGN_ENTITLEMENTS = Nynja/Resources/Nynja.entitlements; - CODE_SIGN_IDENTITY = "iPhone Developer"; - "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; - CODE_SIGN_STYLE = Manual; - DEFINES_MODULE = YES; - DEVELOPMENT_TEAM = 9GKQ5AMF2B; - ENABLE_BITCODE = NO; - INFOPLIST_FILE = "$(SRCROOT)/Nynja/Resources/Info.plist"; - IPHONEOS_DEPLOYMENT_TARGET = 10.0; - LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; - PRODUCT_BUNDLE_IDENTIFIER = "$(BundleIdentifier)"; - PRODUCT_NAME = "$(TARGET_NAME)"; - PROVISIONING_PROFILE = "dc7cf190-1393-4a29-84c9-ece23e0d0f16"; - PROVISIONING_PROFILE_SPECIFIER = DevBundle_Dev; - SWIFT_OBJC_BRIDGING_HEADER = "Nynja-Bridging-Header.h"; - SWIFT_OPTIMIZATION_LEVEL = "-Onone"; - SWIFT_SWIFT3_OBJC_INFERENCE = Off; - SWIFT_VERSION = 4.0; - SWIFT_WHOLE_MODULE_OPTIMIZATION = YES; - }; - name = Translate; - }; - 26E3229620E52C4A00271413 /* Translate */ = { - isa = XCBuildConfiguration; - baseConfigurationReference = CCA291E1CE928BC100DD6353 /* Pods-Nynja-Share.translate.xcconfig */; - buildSettings = { - ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = "$(inherited)"; - CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; - CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; - CODE_SIGN_ENTITLEMENTS = "Nynja-Share/Resources/Nynja-Share.entitlements"; - CODE_SIGN_IDENTITY = "iPhone Developer"; - "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; - CODE_SIGN_STYLE = Manual; - DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; - DEVELOPMENT_TEAM = 9GKQ5AMF2B; - ENABLE_BITCODE = NO; - GCC_C_LANGUAGE_STANDARD = gnu11; - INFOPLIST_FILE = "Nynja-Share/Resources/Info.plist"; - IPHONEOS_DEPLOYMENT_TARGET = 10.0; - LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @executable_path/../../Frameworks"; - OTHER_SWIFT_FLAGS = "$(inherited) \"-D\" \"COCOAPODS\" -DSHARE_EXTENSION"; - PRODUCT_BUNDLE_IDENTIFIER = "$(ExtensionBundleIdentifier)"; - PRODUCT_NAME = "$(TARGET_NAME)"; - PROVISIONING_PROFILE = "469318b6-446b-4fef-9cc5-c149cdd8df2c"; - PROVISIONING_PROFILE_SPECIFIER = DevBundle_DevExt; - SKIP_INSTALL = YES; - SWIFT_VERSION = 4.0; - TARGETED_DEVICE_FAMILY = "1,2"; - }; - name = Translate; - }; - 26E3229720E52C4A00271413 /* Translate */ = { - isa = XCBuildConfiguration; - baseConfigurationReference = F46A5D92A279FA0A509DA508 /* Pods-NynjaUnitTests.translate.xcconfig */; - buildSettings = { - ALWAYS_SEARCH_USER_PATHS = NO; - CLANG_ANALYZER_NONNULL = YES; - CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; - CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; - CLANG_CXX_LIBRARY = "libc++"; - 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_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_IDENTITY = "iPhone Developer"; - CODE_SIGN_STYLE = Automatic; - COPY_PHASE_STRIP = NO; - DEBUG_INFORMATION_FORMAT = dwarf; - DEVELOPMENT_TEAM = 9GKQ5AMF2B; - ENABLE_STRICT_OBJC_MSGSEND = YES; - ENABLE_TESTABILITY = YES; - GCC_C_LANGUAGE_STANDARD = gnu11; - GCC_DYNAMIC_NO_PIC = NO; - GCC_NO_COMMON_BLOCKS = YES; - GCC_OPTIMIZATION_LEVEL = 0; - GCC_PREPROCESSOR_DEFINITIONS = ( - "DEBUG=1", - "$(inherited)", - ); - 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; - INFOPLIST_FILE = NynjaUnitTests/Info.plist; - IPHONEOS_DEPLOYMENT_TARGET = 11.3; - LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; - MTL_ENABLE_DEBUG_INFO = YES; - ONLY_ACTIVE_ARCH = YES; - PRODUCT_BUNDLE_IDENTIFIER = com.nynja.mobile.communicator.NynjaUnitTests; - PRODUCT_NAME = "$(TARGET_NAME)"; - SDKROOT = iphoneos; - SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; - SWIFT_OPTIMIZATION_LEVEL = "-Onone"; - SWIFT_VERSION = 4.0; - TARGETED_DEVICE_FAMILY = "1,2"; - }; - name = Translate; - }; - 357809AE1F9765CF00C9680C /* Dev */ = { - isa = XCBuildConfiguration; - baseConfigurationReference = A169D8E4AB2003F96040DD7A /* Pods-Nynja-Share.dev.xcconfig */; - buildSettings = { - ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = "$(inherited)"; - CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; - CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; - CODE_SIGN_ENTITLEMENTS = "Nynja-Share/Resources/Nynja-Share.entitlements"; - CODE_SIGN_IDENTITY = "iPhone Developer"; - "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; - CODE_SIGN_STYLE = Manual; - DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; - DEVELOPMENT_TEAM = 9GKQ5AMF2B; - ENABLE_BITCODE = NO; - GCC_C_LANGUAGE_STANDARD = gnu11; - INFOPLIST_FILE = "Nynja-Share/Resources/Info.plist"; - IPHONEOS_DEPLOYMENT_TARGET = 10.0; - LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @executable_path/../../Frameworks"; - OTHER_SWIFT_FLAGS = "$(inherited) \"-D\" \"COCOAPODS\" -DSHARE_EXTENSION"; - PRODUCT_BUNDLE_IDENTIFIER = "$(ExtensionBundleIdentifier)"; - PRODUCT_NAME = "$(TARGET_NAME)"; - PROVISIONING_PROFILE = "69f3dc99-df33-4a29-8a8a-8d93926a3535"; - PROVISIONING_PROFILE_SPECIFIER = DevBundle_DevExt; - SKIP_INSTALL = YES; - SWIFT_VERSION = 4.0; - TARGETED_DEVICE_FAMILY = "1,2"; - }; - name = Dev; - }; - 357809AF1F9765CF00C9680C /* Release */ = { - isa = XCBuildConfiguration; - baseConfigurationReference = 26D87FB9E1E4B20C47456AF6 /* Pods-Nynja-Share.release.xcconfig */; - buildSettings = { - ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = "$(inherited)"; - CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; - CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; - CODE_SIGN_ENTITLEMENTS = "Nynja-Share/Resources/Nynja-Share.entitlements"; - CODE_SIGN_IDENTITY = "iPhone Distribution"; - "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution"; - CODE_SIGN_STYLE = Manual; - DEVELOPMENT_TEAM = 9GKQ5AMF2B; - ENABLE_BITCODE = NO; - GCC_C_LANGUAGE_STANDARD = gnu11; - INFOPLIST_FILE = "Nynja-Share/Resources/Info.plist"; - IPHONEOS_DEPLOYMENT_TARGET = 10.0; - LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @executable_path/../../Frameworks"; - OTHER_SWIFT_FLAGS = "$(inherited) \"-D\" \"COCOAPODS\" -DSHARE_EXTENSION"; - PRODUCT_BUNDLE_IDENTIFIER = "$(ExtensionBundleIdentifier)"; - PRODUCT_NAME = "$(TARGET_NAME)"; - PROVISIONING_PROFILE = "2a318f9e-d0ab-41dc-968a-e1cb13de4de5"; - PROVISIONING_PROFILE_SPECIFIER = ProductionBundle_AppstoreExt; - SKIP_INSTALL = YES; - SWIFT_COMPILATION_MODE = wholemodule; - SWIFT_OPTIMIZATION_LEVEL = "-Onone"; - SWIFT_VERSION = 4.0; - TARGETED_DEVICE_FAMILY = "1,2"; - }; - name = Release; - }; - 3ABCE8FD1EC9330D00A80B15 /* Dev */ = { - isa = XCBuildConfiguration; - baseConfigurationReference = F1313AFE20888CAB00E04092 /* DevConfig.xcconfig */; - buildSettings = { - ALWAYS_SEARCH_USER_PATHS = NO; - CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES; - CLANG_ANALYZER_NONNULL = YES; - CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; - CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; - CLANG_CXX_LIBRARY = "libc++"; - CLANG_ENABLE_MODULES = YES; - CLANG_ENABLE_OBJC_ARC = YES; - CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; - CLANG_WARN_BOOL_CONVERSION = YES; - CLANG_WARN_COMMA = YES; - CLANG_WARN_CONSTANT_CONVERSION = 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_LITERAL_CONVERSION = YES; - CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; - CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; - CLANG_WARN_STRICT_PROTOTYPES = YES; - CLANG_WARN_SUSPICIOUS_MOVE = YES; - CLANG_WARN_UNREACHABLE_CODE = YES; - CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; - "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; - COPY_PHASE_STRIP = NO; - DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; - ENABLE_STRICT_OBJC_MSGSEND = YES; - ENABLE_TESTABILITY = YES; - GCC_C_LANGUAGE_STANDARD = gnu99; - GCC_DYNAMIC_NO_PIC = NO; - GCC_NO_COMMON_BLOCKS = YES; - GCC_OPTIMIZATION_LEVEL = 0; - GCC_PREPROCESSOR_DEFINITIONS = ( - "DEBUG=1", - "$(inherited)", - ); - 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; - IPHONEOS_DEPLOYMENT_TARGET = 10.3; - MTL_ENABLE_DEBUG_INFO = YES; - ONLY_ACTIVE_ARCH = YES; - SDKROOT = iphoneos; - SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; - SWIFT_OPTIMIZATION_LEVEL = "-Onone"; - SWIFT_SWIFT3_OBJC_INFERENCE = Off; - SWIFT_WHOLE_MODULE_OPTIMIZATION = YES; - TARGETED_DEVICE_FAMILY = "1,2"; - }; - name = Dev; - }; - 3ABCE8FE1EC9330D00A80B15 /* Release */ = { - isa = XCBuildConfiguration; - baseConfigurationReference = F1313B0020888CC400E04092 /* ReleaseConfig.xcconfig */; - buildSettings = { - ALWAYS_SEARCH_USER_PATHS = NO; - CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES; - CLANG_ANALYZER_NONNULL = YES; - CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; - CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; - CLANG_CXX_LIBRARY = "libc++"; - CLANG_ENABLE_MODULES = YES; - CLANG_ENABLE_OBJC_ARC = YES; - CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; - CLANG_WARN_BOOL_CONVERSION = YES; - CLANG_WARN_COMMA = YES; - CLANG_WARN_CONSTANT_CONVERSION = 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_LITERAL_CONVERSION = YES; - CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; - CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; - CLANG_WARN_STRICT_PROTOTYPES = YES; - CLANG_WARN_SUSPICIOUS_MOVE = YES; - CLANG_WARN_UNREACHABLE_CODE = YES; - CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; - "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; - COPY_PHASE_STRIP = NO; - DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; - ENABLE_NS_ASSERTIONS = NO; - ENABLE_STRICT_OBJC_MSGSEND = YES; - GCC_C_LANGUAGE_STANDARD = gnu99; - GCC_NO_COMMON_BLOCKS = YES; - GCC_OPTIMIZATION_LEVEL = 0; - 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; - IPHONEOS_DEPLOYMENT_TARGET = 10.3; - MTL_ENABLE_DEBUG_INFO = NO; - SDKROOT = iphoneos; - SWIFT_OPTIMIZATION_LEVEL = "-Onone"; - SWIFT_SWIFT3_OBJC_INFERENCE = Off; - SWIFT_WHOLE_MODULE_OPTIMIZATION = YES; - TARGETED_DEVICE_FAMILY = "1,2"; - VALIDATE_PRODUCT = YES; - }; - name = Release; - }; - 3ABCE9001EC9330D00A80B15 /* Dev */ = { - isa = XCBuildConfiguration; - baseConfigurationReference = 643B61A129DD7717EF6B856A /* Pods-Nynja.dev.xcconfig */; - buildSettings = { - ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = "$(inherited)"; - ASSETCATALOG_COMPILER_APPICON_NAME = AppIconDev; - CLANG_ENABLE_MODULES = YES; - CODE_SIGN_ENTITLEMENTS = Nynja/Resources/Nynja.entitlements; - CODE_SIGN_IDENTITY = "iPhone Developer"; - "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; - CODE_SIGN_STYLE = Manual; - DEFINES_MODULE = YES; - DEVELOPMENT_TEAM = 9GKQ5AMF2B; - ENABLE_BITCODE = NO; - INFOPLIST_FILE = "$(SRCROOT)/Nynja/Resources/Info.plist"; - IPHONEOS_DEPLOYMENT_TARGET = 10.0; - LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; - PRODUCT_BUNDLE_IDENTIFIER = "$(BundleIdentifier)"; - PRODUCT_NAME = "$(TARGET_NAME)"; - PROVISIONING_PROFILE = "dc81b7c8-7ca8-4e18-9fee-d0f512f7c8ca"; - PROVISIONING_PROFILE_SPECIFIER = DevBundle_Dev; - SWIFT_OBJC_BRIDGING_HEADER = "Nynja-Bridging-Header.h"; - SWIFT_OPTIMIZATION_LEVEL = "-Onone"; - SWIFT_SWIFT3_OBJC_INFERENCE = Off; - SWIFT_VERSION = 4.0; - SWIFT_WHOLE_MODULE_OPTIMIZATION = YES; - }; - name = Dev; - }; - 3ABCE9011EC9330D00A80B15 /* Release */ = { - isa = XCBuildConfiguration; - baseConfigurationReference = 26E8D53526B7E2B0A1C265D4 /* Pods-Nynja.release.xcconfig */; - buildSettings = { - ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = "$(inherited)"; - ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; - CLANG_ENABLE_MODULES = YES; - CODE_SIGN_ENTITLEMENTS = Nynja/Resources/Nynja.entitlements; - CODE_SIGN_IDENTITY = "iPhone Distribution"; - "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution"; - CODE_SIGN_STYLE = Manual; - DEFINES_MODULE = YES; - DEVELOPMENT_TEAM = 9GKQ5AMF2B; - ENABLE_BITCODE = NO; - INFOPLIST_FILE = "$(SRCROOT)/Nynja/Resources/Info.plist"; - IPHONEOS_DEPLOYMENT_TARGET = 10.0; - LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; - PRODUCT_BUNDLE_IDENTIFIER = "$(BundleIdentifier)"; - PRODUCT_NAME = "$(TARGET_NAME)"; - PROVISIONING_PROFILE = "f7246486-87f5-46b9-aeed-3ddf15f9a589"; - PROVISIONING_PROFILE_SPECIFIER = ProductionBundle_Appstore; - SWIFT_COMPILATION_MODE = wholemodule; - SWIFT_OBJC_BRIDGING_HEADER = "Nynja-Bridging-Header.h"; - SWIFT_OPTIMIZATION_LEVEL = "-Onone"; - SWIFT_SWIFT3_OBJC_INFERENCE = Off; - SWIFT_VERSION = 4.0; - SWIFT_WHOLE_MODULE_OPTIMIZATION = YES; - }; - name = Release; - }; - 85631BF820EFC9140002BE51 /* Translate */ = { - isa = XCBuildConfiguration; - baseConfigurationReference = 2632139220D7B71200C31144 /* TranslateConfig.xcconfig */; - buildSettings = { - ALWAYS_SEARCH_USER_PATHS = NO; - CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES; - CLANG_ANALYZER_NONNULL = YES; - CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; - CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; - CLANG_CXX_LIBRARY = "libc++"; - CLANG_ENABLE_MODULES = YES; - CLANG_ENABLE_OBJC_ARC = YES; - CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; - CLANG_WARN_BOOL_CONVERSION = YES; - CLANG_WARN_COMMA = YES; - CLANG_WARN_CONSTANT_CONVERSION = 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_LITERAL_CONVERSION = YES; - CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; - CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; - CLANG_WARN_STRICT_PROTOTYPES = YES; - CLANG_WARN_SUSPICIOUS_MOVE = YES; - CLANG_WARN_UNREACHABLE_CODE = YES; - CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; - "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; - COPY_PHASE_STRIP = NO; - DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; - ENABLE_STRICT_OBJC_MSGSEND = YES; - ENABLE_TESTABILITY = YES; - GCC_C_LANGUAGE_STANDARD = gnu99; - GCC_DYNAMIC_NO_PIC = NO; - GCC_NO_COMMON_BLOCKS = YES; - GCC_OPTIMIZATION_LEVEL = 0; - GCC_PREPROCESSOR_DEFINITIONS = ( - "DEBUG=1", - "$(inherited)", - ); - 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; - IPHONEOS_DEPLOYMENT_TARGET = 10.3; - MTL_ENABLE_DEBUG_INFO = YES; - ONLY_ACTIVE_ARCH = YES; - SDKROOT = iphoneos; - SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; - SWIFT_OPTIMIZATION_LEVEL = "-Onone"; - SWIFT_SWIFT3_OBJC_INFERENCE = Off; - SWIFT_WHOLE_MODULE_OPTIMIZATION = YES; - TARGETED_DEVICE_FAMILY = "1,2"; - }; - name = Translate; - }; - 85631BF920EFC9140002BE51 /* Translate */ = { - isa = XCBuildConfiguration; - baseConfigurationReference = 718EF22D86A9656BB6ED89D5 /* Pods-Nynja.translate.xcconfig */; - buildSettings = { - ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; - ASSETCATALOG_COMPILER_APPICON_NAME = AppIconChannels; - CLANG_ENABLE_MODULES = YES; - CODE_SIGN_ENTITLEMENTS = Nynja/Resources/Nynja.entitlements; - CODE_SIGN_IDENTITY = "iPhone Developer"; - "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; - CODE_SIGN_STYLE = Manual; - DEFINES_MODULE = YES; - DEVELOPMENT_TEAM = 9GKQ5AMF2B; - ENABLE_BITCODE = NO; - INFOPLIST_FILE = "$(SRCROOT)/Nynja/Resources/Info.plist"; - IPHONEOS_DEPLOYMENT_TARGET = 10.0; - LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; - PRODUCT_BUNDLE_IDENTIFIER = "$(BundleIdentifier)"; - PRODUCT_NAME = "$(TARGET_NAME)"; - PROVISIONING_PROFILE = "dc81b7c8-7ca8-4e18-9fee-d0f512f7c8ca"; - PROVISIONING_PROFILE_SPECIFIER = DevBundle_Dev; - SWIFT_OBJC_BRIDGING_HEADER = "Nynja-Bridging-Header.h"; - SWIFT_OPTIMIZATION_LEVEL = "-Onone"; - SWIFT_SWIFT3_OBJC_INFERENCE = Off; - SWIFT_VERSION = 4.0; - SWIFT_WHOLE_MODULE_OPTIMIZATION = YES; - }; - name = Translate; - }; - 85631BFA20EFC9140002BE51 /* Translate */ = { - isa = XCBuildConfiguration; - baseConfigurationReference = CCA291E1CE928BC100DD6353 /* Pods-Nynja-Share.translate.xcconfig */; - buildSettings = { - ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = "$(inherited)"; - CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; - CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; - CODE_SIGN_ENTITLEMENTS = "Nynja-Share/Resources/Nynja-Share.entitlements"; - CODE_SIGN_IDENTITY = "iPhone Developer"; - "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; - CODE_SIGN_STYLE = Manual; - DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; - DEVELOPMENT_TEAM = 9GKQ5AMF2B; - ENABLE_BITCODE = NO; - GCC_C_LANGUAGE_STANDARD = gnu11; - INFOPLIST_FILE = "Nynja-Share/Resources/Info.plist"; - IPHONEOS_DEPLOYMENT_TARGET = 10.0; - LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @executable_path/../../Frameworks"; - OTHER_SWIFT_FLAGS = "$(inherited) \"-D\" \"COCOAPODS\" -DSHARE_EXTENSION"; - PRODUCT_BUNDLE_IDENTIFIER = "$(ExtensionBundleIdentifier)"; - PRODUCT_NAME = "$(TARGET_NAME)"; - PROVISIONING_PROFILE = "a8b63d08-5002-44f0-94d3-87ddff72770a"; - PROVISIONING_PROFILE_SPECIFIER = DevBundle_DevExt; - SKIP_INSTALL = YES; - SWIFT_VERSION = 4.0; - TARGETED_DEVICE_FAMILY = "1,2"; - }; - name = Translate; - }; - 85631BFB20EFC9140002BE51 /* Translate */ = { - isa = XCBuildConfiguration; - baseConfigurationReference = F46A5D92A279FA0A509DA508 /* Pods-NynjaUnitTests.translate.xcconfig */; - buildSettings = { - ALWAYS_SEARCH_USER_PATHS = NO; - CLANG_ANALYZER_NONNULL = YES; - CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; - CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; - CLANG_CXX_LIBRARY = "libc++"; - 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_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_IDENTITY = "iPhone Developer"; - CODE_SIGN_STYLE = Automatic; - COPY_PHASE_STRIP = NO; - DEBUG_INFORMATION_FORMAT = dwarf; - DEVELOPMENT_TEAM = 9GKQ5AMF2B; - ENABLE_STRICT_OBJC_MSGSEND = YES; - ENABLE_TESTABILITY = YES; - GCC_C_LANGUAGE_STANDARD = gnu11; - GCC_DYNAMIC_NO_PIC = NO; - GCC_NO_COMMON_BLOCKS = YES; - GCC_OPTIMIZATION_LEVEL = 0; - GCC_PREPROCESSOR_DEFINITIONS = ( - "DEBUG=1", - "$(inherited)", - ); - 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; - INFOPLIST_FILE = NynjaUnitTests/Info.plist; - IPHONEOS_DEPLOYMENT_TARGET = 11.3; - LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; - MTL_ENABLE_DEBUG_INFO = YES; - ONLY_ACTIVE_ARCH = YES; - PRODUCT_BUNDLE_IDENTIFIER = com.nynja.mobile.communicator.NynjaUnitTests; - PRODUCT_NAME = "$(TARGET_NAME)"; - SDKROOT = iphoneos; - SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; - SWIFT_OPTIMIZATION_LEVEL = "-Onone"; - SWIFT_VERSION = 4.0; - TARGETED_DEVICE_FAMILY = "1,2"; - }; - name = Translate; - }; - A44B4D5D20CEA20000CA700A /* Channels */ = { + 3ABCE8FE1EC9330D00A80B15 /* Release */ = { isa = XCBuildConfiguration; - baseConfigurationReference = A44B4D6120CEA24100CA700A /* ChannelsConfig.xcconfig */; + baseConfigurationReference = F1313B0020888CC400E04092 /* ReleaseConfig.xcconfig */; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES; @@ -15922,16 +15588,11 @@ "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; COPY_PHASE_STRIP = NO; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; ENABLE_STRICT_OBJC_MSGSEND = YES; - ENABLE_TESTABILITY = YES; GCC_C_LANGUAGE_STANDARD = gnu99; - GCC_DYNAMIC_NO_PIC = NO; GCC_NO_COMMON_BLOCKS = YES; GCC_OPTIMIZATION_LEVEL = 0; - GCC_PREPROCESSOR_DEFINITIONS = ( - "DEBUG=1", - "$(inherited)", - ); GCC_WARN_64_TO_32_BIT_CONVERSION = YES; GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; GCC_WARN_UNDECLARED_SELECTOR = YES; @@ -15939,143 +15600,79 @@ GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; IPHONEOS_DEPLOYMENT_TARGET = 10.3; - MTL_ENABLE_DEBUG_INFO = YES; - ONLY_ACTIVE_ARCH = YES; + MTL_ENABLE_DEBUG_INFO = NO; SDKROOT = iphoneos; - SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; SWIFT_SWIFT3_OBJC_INFERENCE = Off; SWIFT_WHOLE_MODULE_OPTIMIZATION = YES; TARGETED_DEVICE_FAMILY = "1,2"; + VALIDATE_PRODUCT = YES; }; - name = Channels; + name = Release; }; - A44B4D5E20CEA20000CA700A /* Channels */ = { + 3ABCE9001EC9330D00A80B15 /* Dev */ = { isa = XCBuildConfiguration; - baseConfigurationReference = 35BAFB8584DC4B34650DE4EB /* Pods-Nynja.channels.xcconfig */; + baseConfigurationReference = 643B61A129DD7717EF6B856A /* Pods-Nynja.dev.xcconfig */; buildSettings = { ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = "$(inherited)"; - ASSETCATALOG_COMPILER_APPICON_NAME = AppIconChannels; + ASSETCATALOG_COMPILER_APPICON_NAME = AppIconDev; CLANG_ENABLE_MODULES = YES; CODE_SIGN_ENTITLEMENTS = Nynja/Resources/Nynja.entitlements; - CODE_SIGN_IDENTITY = "iPhone Distribution"; + CODE_SIGN_IDENTITY = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; CODE_SIGN_STYLE = Manual; + DEBUG_INFORMATION_FORMAT = dwarf; DEFINES_MODULE = YES; DEVELOPMENT_TEAM = 9GKQ5AMF2B; ENABLE_BITCODE = NO; INFOPLIST_FILE = "$(SRCROOT)/Nynja/Resources/Info.plist"; IPHONEOS_DEPLOYMENT_TARGET = 10.0; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; + OTHER_SWIFT_FLAGS = "$(inherited) -D SQLITE_HAS_CODEC -D GRDBCIPHER $(inherited) \"-D\" \"COCOAPODS\" -Xfrontend -debug-time-function-bodies -Xfrontend -warn-long-expression-type-checking=20 -Xfrontend -warn-long-function-bodies=20"; PRODUCT_BUNDLE_IDENTIFIER = "$(BundleIdentifier)"; PRODUCT_NAME = "$(TARGET_NAME)"; - PROVISIONING_PROFILE = "45e50eb3-5cd2-425f-8671-712de29dc416"; - PROVISIONING_PROFILE_SPECIFIER = NynjaDev_Dev; + PROVISIONING_PROFILE = "dc81b7c8-7ca8-4e18-9fee-d0f512f7c8ca"; + PROVISIONING_PROFILE_SPECIFIER = DevBundle_Dev; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; SWIFT_OBJC_BRIDGING_HEADER = "Nynja-Bridging-Header.h"; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; SWIFT_SWIFT3_OBJC_INFERENCE = Off; SWIFT_VERSION = 4.0; SWIFT_WHOLE_MODULE_OPTIMIZATION = YES; }; - name = Channels; + name = Dev; }; - A44B4D5F20CEA20000CA700A /* Channels */ = { + 3ABCE9011EC9330D00A80B15 /* Release */ = { isa = XCBuildConfiguration; - baseConfigurationReference = 5AEEB3D82E9CF02760DA4CE7 /* Pods-Nynja-Share.channels.xcconfig */; + baseConfigurationReference = 26E8D53526B7E2B0A1C265D4 /* Pods-Nynja.release.xcconfig */; buildSettings = { ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = "$(inherited)"; - CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; - CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; - CODE_SIGN_ENTITLEMENTS = "Nynja-Share/Resources/Nynja-Share.entitlements"; + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; + CODE_SIGN_ENTITLEMENTS = Nynja/Resources/Nynja.entitlements; CODE_SIGN_IDENTITY = "iPhone Developer"; - "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution"; CODE_SIGN_STYLE = Manual; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + DEFINES_MODULE = YES; DEVELOPMENT_TEAM = 9GKQ5AMF2B; ENABLE_BITCODE = NO; - GCC_C_LANGUAGE_STANDARD = gnu11; - INFOPLIST_FILE = "Nynja-Share/Resources/Info.plist"; + INFOPLIST_FILE = "$(SRCROOT)/Nynja/Resources/Info.plist"; IPHONEOS_DEPLOYMENT_TARGET = 10.0; - LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @executable_path/../../Frameworks"; - OTHER_SWIFT_FLAGS = "$(inherited) \"-D\" \"COCOAPODS\" -DSHARE_EXTENSION"; - PRODUCT_BUNDLE_IDENTIFIER = "$(ExtensionBundleIdentifier)"; - PRODUCT_NAME = "$(TARGET_NAME)"; - PROVISIONING_PROFILE = "a8b63d08-5002-44f0-94d3-87ddff72770a"; - PROVISIONING_PROFILE_SPECIFIER = DevBundle_DevExt; - SKIP_INSTALL = YES; - SWIFT_VERSION = 4.0; - TARGETED_DEVICE_FAMILY = "1,2"; - }; - name = Channels; - }; - A44B4D6020CEA20000CA700A /* Channels */ = { - isa = XCBuildConfiguration; - baseConfigurationReference = 9E82188EE0AC1D1C05470692 /* Pods-NynjaUnitTests.channels.xcconfig */; - buildSettings = { - ALWAYS_SEARCH_USER_PATHS = NO; - CLANG_ANALYZER_NONNULL = YES; - CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; - CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; - CLANG_CXX_LIBRARY = "libc++"; - 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_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_IDENTITY = "iPhone Developer"; - CODE_SIGN_STYLE = Automatic; - COPY_PHASE_STRIP = NO; - DEBUG_INFORMATION_FORMAT = dwarf; - DEVELOPMENT_TEAM = 9GKQ5AMF2B; - ENABLE_STRICT_OBJC_MSGSEND = YES; - ENABLE_TESTABILITY = YES; - GCC_C_LANGUAGE_STANDARD = gnu11; - GCC_DYNAMIC_NO_PIC = NO; - GCC_NO_COMMON_BLOCKS = YES; - GCC_OPTIMIZATION_LEVEL = 0; - GCC_PREPROCESSOR_DEFINITIONS = ( - "DEBUG=1", - "$(inherited)", - ); - 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; - INFOPLIST_FILE = NynjaUnitTests/Info.plist; - IPHONEOS_DEPLOYMENT_TARGET = 11.3; - LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; - MTL_ENABLE_DEBUG_INFO = YES; - ONLY_ACTIVE_ARCH = YES; - PRODUCT_BUNDLE_IDENTIFIER = com.nynja.mobile.communicator.NynjaUnitTests; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; + PRODUCT_BUNDLE_IDENTIFIER = "$(BundleIdentifier)"; PRODUCT_NAME = "$(TARGET_NAME)"; - SDKROOT = iphoneos; - SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; + PROVISIONING_PROFILE = "f7246486-87f5-46b9-aeed-3ddf15f9a589"; + PROVISIONING_PROFILE_SPECIFIER = ProductionBundle_Appstore; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = RELEASE; + SWIFT_COMPILATION_MODE = wholemodule; + SWIFT_OBJC_BRIDGING_HEADER = "Nynja-Bridging-Header.h"; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_SWIFT3_OBJC_INFERENCE = Off; SWIFT_VERSION = 4.0; - TARGETED_DEVICE_FAMILY = "1,2"; + SWIFT_WHOLE_MODULE_OPTIMIZATION = YES; }; - name = Channels; + name = Release; }; F10AFE9C20EF8BBE00C7CE83 /* DevAutoTests */ = { isa = XCBuildConfiguration; @@ -16149,6 +15746,7 @@ CODE_SIGN_IDENTITY = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; CODE_SIGN_STYLE = Manual; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEFINES_MODULE = YES; DEVELOPMENT_TEAM = 9GKQ5AMF2B; ENABLE_BITCODE = NO; @@ -16337,6 +15935,7 @@ CODE_SIGN_IDENTITY = "iPhone Distribution"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution"; CODE_SIGN_STYLE = Manual; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEFINES_MODULE = YES; DEVELOPMENT_TEAM = 9GKQ5AMF2B; ENABLE_BITCODE = NO; @@ -16574,33 +16173,6 @@ }; name = Dev; }; - FE21ACAF2113AA7F006010A0 /* Translate */ = { - isa = XCBuildConfiguration; - buildSettings = { - BUNDLE_LOADER = "$(TEST_HOST)"; - CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; - CLANG_ENABLE_MODULES = YES; - CLANG_ENABLE_OBJC_WEAK = YES; - CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; - CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; - CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; - CODE_SIGN_IDENTITY = "iPhone Developer"; - CODE_SIGN_STYLE = Automatic; - DEVELOPMENT_TEAM = 9GKQ5AMF2B; - GCC_C_LANGUAGE_STANDARD = gnu11; - INFOPLIST_FILE = NynjaIntegrationTests/Info.plist; - IPHONEOS_DEPLOYMENT_TARGET = 11.3; - LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; - PRODUCT_BUNDLE_IDENTIFIER = com.nynja.dev.mobile.communicator.NynjaIntegrationTests; - PRODUCT_NAME = "$(TARGET_NAME)"; - SWIFT_COMPILATION_MODE = wholemodule; - SWIFT_OPTIMIZATION_LEVEL = "-Onone"; - SWIFT_VERSION = 4.0; - TARGETED_DEVICE_FAMILY = "1,2"; - TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Nynja.app/Nynja"; - }; - name = Translate; - }; FE21ACB02113AA7F006010A0 /* DevAutoTests */ = { isa = XCBuildConfiguration; buildSettings = { @@ -16628,60 +16200,6 @@ }; name = DevAutoTests; }; - FE21ACB12113AA7F006010A0 /* Stickers */ = { - isa = XCBuildConfiguration; - buildSettings = { - BUNDLE_LOADER = "$(TEST_HOST)"; - CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; - CLANG_ENABLE_MODULES = YES; - CLANG_ENABLE_OBJC_WEAK = YES; - CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; - CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; - CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; - CODE_SIGN_IDENTITY = "iPhone Developer"; - CODE_SIGN_STYLE = Automatic; - DEVELOPMENT_TEAM = 9GKQ5AMF2B; - GCC_C_LANGUAGE_STANDARD = gnu11; - INFOPLIST_FILE = NynjaIntegrationTests/Info.plist; - IPHONEOS_DEPLOYMENT_TARGET = 11.3; - LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; - PRODUCT_BUNDLE_IDENTIFIER = com.nynja.dev.mobile.communicator.NynjaIntegrationTests; - PRODUCT_NAME = "$(TARGET_NAME)"; - SWIFT_COMPILATION_MODE = wholemodule; - SWIFT_OPTIMIZATION_LEVEL = "-Onone"; - SWIFT_VERSION = 4.0; - TARGETED_DEVICE_FAMILY = "1,2"; - TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Nynja.app/Nynja"; - }; - name = Stickers; - }; - FE21ACB22113AA7F006010A0 /* Channels */ = { - isa = XCBuildConfiguration; - buildSettings = { - BUNDLE_LOADER = "$(TEST_HOST)"; - CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; - CLANG_ENABLE_MODULES = YES; - CLANG_ENABLE_OBJC_WEAK = YES; - CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; - CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; - CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; - CODE_SIGN_IDENTITY = "iPhone Developer"; - CODE_SIGN_STYLE = Automatic; - DEVELOPMENT_TEAM = 9GKQ5AMF2B; - GCC_C_LANGUAGE_STANDARD = gnu11; - INFOPLIST_FILE = NynjaIntegrationTests/Info.plist; - IPHONEOS_DEPLOYMENT_TARGET = 11.3; - LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; - PRODUCT_BUNDLE_IDENTIFIER = com.nynja.dev.mobile.communicator.NynjaIntegrationTests; - PRODUCT_NAME = "$(TARGET_NAME)"; - SWIFT_COMPILATION_MODE = wholemodule; - SWIFT_OPTIMIZATION_LEVEL = "-Onone"; - SWIFT_VERSION = 4.0; - TARGETED_DEVICE_FAMILY = "1,2"; - TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Nynja.app/Nynja"; - }; - name = Channels; - }; FE21ACB32113AA7F006010A0 /* Prerelease */ = { isa = XCBuildConfiguration; buildSettings = { @@ -16743,11 +16261,7 @@ isa = XCConfigurationList; buildConfigurations = ( 357809AE1F9765CF00C9680C /* Dev */, - 85631BFA20EFC9140002BE51 /* Translate */, F10AFE9E20EF8BBE00C7CE83 /* DevAutoTests */, - 26E3229620E52C4A00271413 /* Translate */, - 26D8F42E20D7E0BA001602E9 /* Stickers */, - A44B4D5F20CEA20000CA700A /* Channels */, F1313AFD20888BD300E04092 /* Prerelease */, 357809AF1F9765CF00C9680C /* Release */, ); @@ -16758,11 +16272,7 @@ isa = XCConfigurationList; buildConfigurations = ( 3ABCE8FD1EC9330D00A80B15 /* Dev */, - 85631BF820EFC9140002BE51 /* Translate */, F10AFE9C20EF8BBE00C7CE83 /* DevAutoTests */, - 26E3229420E52C4A00271413 /* Translate */, - 26D8F42C20D7E0BA001602E9 /* Stickers */, - A44B4D5D20CEA20000CA700A /* Channels */, F1313AFB20888BD300E04092 /* Prerelease */, 3ABCE8FE1EC9330D00A80B15 /* Release */, ); @@ -16773,11 +16283,7 @@ isa = XCConfigurationList; buildConfigurations = ( 3ABCE9001EC9330D00A80B15 /* Dev */, - 85631BF920EFC9140002BE51 /* Translate */, F10AFE9D20EF8BBE00C7CE83 /* DevAutoTests */, - 26E3229520E52C4A00271413 /* Translate */, - 26D8F42D20D7E0BA001602E9 /* Stickers */, - A44B4D5E20CEA20000CA700A /* Channels */, F1313AFC20888BD300E04092 /* Prerelease */, 3ABCE9011EC9330D00A80B15 /* Release */, ); @@ -16788,11 +16294,7 @@ isa = XCConfigurationList; buildConfigurations = ( F1C37AB0209A1BF4005EA197 /* Dev */, - 85631BFB20EFC9140002BE51 /* Translate */, F10AFE9F20EF8BBE00C7CE83 /* DevAutoTests */, - 26E3229720E52C4A00271413 /* Translate */, - 26D8F42F20D7E0BA001602E9 /* Stickers */, - A44B4D6020CEA20000CA700A /* Channels */, F1C37AB1209A1BF4005EA197 /* Prerelease */, F1C37AB2209A1BF4005EA197 /* Release */, ); @@ -16803,10 +16305,7 @@ isa = XCConfigurationList; buildConfigurations = ( FE21ACAE2113AA7F006010A0 /* Dev */, - FE21ACAF2113AA7F006010A0 /* Translate */, FE21ACB02113AA7F006010A0 /* DevAutoTests */, - FE21ACB12113AA7F006010A0 /* Stickers */, - FE21ACB22113AA7F006010A0 /* Channels */, FE21ACB32113AA7F006010A0 /* Prerelease */, FE21ACB42113AA7F006010A0 /* Release */, ); diff --git a/Nynja.xcodeproj/xcshareddata/xcschemes/NynjaChannels.xcscheme b/Nynja.xcodeproj/xcshareddata/xcschemes/NynjaChannels.xcscheme deleted file mode 100644 index df48c6a3d..000000000 --- a/Nynja.xcodeproj/xcshareddata/xcschemes/NynjaChannels.xcscheme +++ /dev/null @@ -1,112 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/Nynja.xcodeproj/xcshareddata/xcschemes/NynjaSticker.xcscheme b/Nynja.xcodeproj/xcshareddata/xcschemes/NynjaSticker.xcscheme deleted file mode 100644 index c38dd3e77..000000000 --- a/Nynja.xcodeproj/xcshareddata/xcschemes/NynjaSticker.xcscheme +++ /dev/null @@ -1,101 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/Nynja.xcodeproj/xcshareddata/xcschemes/NynjaTranslate.xcscheme b/Nynja.xcodeproj/xcshareddata/xcschemes/NynjaTranslate.xcscheme deleted file mode 100644 index f3527d4fb..000000000 --- a/Nynja.xcodeproj/xcshareddata/xcschemes/NynjaTranslate.xcscheme +++ /dev/null @@ -1,112 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/Nynja/AmazonManager.swift b/Nynja/AmazonManager.swift index 54ade0e3a..ad255b3e1 100644 --- a/Nynja/AmazonManager.swift +++ b/Nynja/AmazonManager.swift @@ -73,10 +73,10 @@ final class AmazonManager { case .cancelled, .paused: break default: - LogService.log(topic: .amazon, text: "Error downloading: \(String(describing: downloadRequest?.key)) Error: \(error)") + LogService.log(topic: .amazon) { return "Error downloading: \(String(describing: downloadRequest?.key)) Error: \(error)" } } } else { - LogService.log(topic: .amazon, text: "Error downloading: \(String(describing: downloadRequest?.key)) Error: \(error)") + LogService.log(topic: .amazon) { return "Error downloading: \(String(describing: downloadRequest?.key)) Error: \(error)" } } return nil } @@ -146,10 +146,10 @@ final class AmazonManager { case .cancelled: self.processingURLs[filePath.absoluteString] = nil default: - LogService.log(topic: .amazon, text: "Error uploading: \(String(describing: uploadRequest.key)) Error: \(error)") + LogService.log(topic: .amazon) { return "Error uploading: \(String(describing: uploadRequest.key)) Error: \(error)" } } } else { - LogService.log(topic: .amazon, text: "Error uploading: \(String(describing: uploadRequest.key)) Error: \(error)") + LogService.log(topic: .amazon) { return "Error uploading: \(String(describing: uploadRequest.key)) Error: \(error)" } } return nil } diff --git a/Nynja/AppDelegate.swift b/Nynja/AppDelegate.swift index ade8b9e23..bd68ff5ad 100644 --- a/Nynja/AppDelegate.swift +++ b/Nynja/AppDelegate.swift @@ -32,10 +32,13 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD }() private let storageService = StorageService.sharedInstance + private let antiDebuggingService = AntiDebuggingService() func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool { UNUserNotificationCenter.current().delegate = self - + + antiDebuggingService.startTracking() + configureDependencies() migrateFromV5toV6() @@ -43,8 +46,8 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD configureWindow() - LogService.log(topic: .system, text: "Avaliable logs:\n\(LogService.allValuesStrings)") - + LogService.log(topic: .system) { return "Avaliable logs:\n\(LogService.allValuesStrings)" } + MotionManager.shared.startAccelerometers() return true } @@ -129,6 +132,7 @@ private extension AppDelegate { private func wipeStorage() { if !storageService.wasRun { + LogService.log(topic: .db) { return "Clear storage: AppDelegate - if it is first runs" } storageService.clearStorage() storageService.wasRun = true } diff --git a/Nynja/AudioManager.swift b/Nynja/AudioManager.swift index 328ae465c..1fedb65bd 100644 --- a/Nynja/AudioManager.swift +++ b/Nynja/AudioManager.swift @@ -9,6 +9,7 @@ import AVFoundation enum Speaker { + case unknown case soft case loud } @@ -21,9 +22,9 @@ extension SpeakerDelegate { func speakerUpdated(state: Speaker) {} } -class AudioManager: NSObject, AVAudioPlayerDelegate { +final class AudioManager: NSObject, AVAudioPlayerDelegate { - static let sharedInstance : AudioManager = { + static let sharedInstance: AudioManager = { let instance = AudioManager() return instance }() @@ -46,15 +47,18 @@ class AudioManager: NSObject, AVAudioPlayerDelegate { private let audioSessionManager = AudioSessionManager.shared + private var needAdjust : Bool = true /// Determines type of speaker - /// Default: .loud - var speaker: Speaker = .loud { + /// Default: .unknown + var speaker: Speaker = .unknown { didSet { - if adjustSpeaker() { - speakerDelegate?.speakerUpdated(state: speaker) - } else { - speakerDelegate?.speakerUpdated(state: oldValue) + if (needAdjust && oldValue != speaker) { + if adjustSpeaker() { + speakerDelegate?.speakerUpdated(state: speaker) + } else { + speakerDelegate?.speakerUpdated(state: oldValue) + } } } } @@ -77,11 +81,21 @@ class AudioManager: NSObject, AVAudioPlayerDelegate { override init() { super.init() configureAudioSession() + NotificationCenter.default.addObserver(self, + selector: #selector(audioSessionRouteChange(notification:)), + name: .AVAudioSessionRouteChange, + object: nil) } private func configureAudioSession() { audioSession = AVAudioSession.sharedInstance() - adjustSpeaker() + var isSpeaker: Bool = false + for output in audioSession.currentRoute.outputs where output.portType == AVAudioSessionPortBuiltInSpeaker { + isSpeaker = true + break + } + + speaker = isSpeaker ? .loud : .soft } @@ -93,9 +107,7 @@ class AudioManager: NSObject, AVAudioPlayerDelegate { } func pause() { - // Check that audio is playing if let player = audioPlayer, player.isPlaying { - // Pause playing player.pause() invalidateProgressTimer() } @@ -126,7 +138,6 @@ class AudioManager: NSObject, AVAudioPlayerDelegate { } private func _play() { - // Check that audio is paused guard let player = audioPlayer, !player.isPlaying else { return } @@ -148,10 +159,8 @@ class AudioManager: NSObject, AVAudioPlayerDelegate { } private func invalidateProgressTimer() { - if progressTimer != nil { - progressTimer?.invalidate() - progressTimer = nil - } + progressTimer?.invalidate() + progressTimer = nil } @objc private func updateProgress(_ timer: Timer) { @@ -183,7 +192,7 @@ class AudioManager: NSObject, AVAudioPlayerDelegate { try audioSession.overrideOutputAudioPort(.none) return true } catch { - LogService.log(topic: .audioSystem, text: error.localizedDescription) + LogService.log(topic: .audioSystem) { return error.localizedDescription } } case .loud: do { @@ -193,8 +202,10 @@ class AudioManager: NSObject, AVAudioPlayerDelegate { try audioSession.overrideOutputAudioPort(.speaker) return true } catch { - LogService.log(topic: .audioSystem, text: error.localizedDescription) + LogService.log(topic: .audioSystem) { return error.localizedDescription } } + case .unknown: + LogService.log(topic: .audioSystem) { return "unexpected case of speaker" } } return false } @@ -205,10 +216,65 @@ class AudioManager: NSObject, AVAudioPlayerDelegate { func audioPlayerDidFinishPlaying(_ player: AVAudioPlayer, successfully flag: Bool) { audioSessionManager.stopPlayback() + invalidateProgressTimer() player.stop() player.currentTime = 0.0 - invalidateProgressTimer() + + + if let url = currentUrl { + delegate?.didFinishPlayingAudio(self, with: url) + } + } + + // MARK: - Route Changes + @objc func audioSessionRouteChange(notification: Notification) { + guard let userInfo = notification.userInfo, + let reasonValue = userInfo[AVAudioSessionRouteChangeReasonKey] as? UInt, + let reason = AVAudioSessionRouteChangeReason(rawValue:reasonValue) else { + return + } + + switch reason { + case .unknown: + LogService.log(topic: .audioSystem) { return "audioSessionRouteChange reason unknown" } + case .newDeviceAvailable: + LogService.log(topic: .audioSystem) { return "audioSessionRouteChange reason newDeviceAvailable" } + for output in audioSession.currentRoute.outputs where output.portType == AVAudioSessionPortHeadphones { + break + } + case .oldDeviceUnavailable: + LogService.log(topic: .audioSystem) { return "audioSessionRouteChange reason oldDeviceUnavailable" } + + case .categoryChange: + LogService.log(topic: .audioSystem) { return "audioSessionRouteChange reason categoryChange" } + LogService.log(topic: .audioSystem) { return "AVAudioSession Category: \(audioSession.category)" } + + case .override: + LogService.log(topic: .audioSystem) { return "audioSessionRouteChange reason override" } + + case .wakeFromSleep: + LogService.log(topic: .audioSystem) { return "audioSessionRouteChange reason wakeFromSleep" } + + case .noSuitableRouteForCategory: + LogService.log(topic: .audioSystem) { return "audioSessionRouteChange reason noSuitableRouteForCategory" } + + case .routeConfigurationChange: + LogService.log(topic: .audioSystem) { return "audioSessionRouteChange reason routeConfigurationChange" } + } + + var isSpeaker: Bool = false + for output in audioSession.currentRoute.outputs { + LogService.log(topic: .audioSystem) { return "audioSessionRoute: \( output.portName )" } + if output.portType == AVAudioSessionPortBuiltInSpeaker { + isSpeaker = true + } + } + - self.delegate?.didFinishPlayingAudio(self, with: currentUrl!) + // disable adjust speaker on set + needAdjust = false + speaker = isSpeaker ? .loud : .soft + // enable adjust speaker on set + needAdjust = true } } diff --git a/Nynja/AudioRecorder.swift b/Nynja/AudioRecorder.swift index 9484e57ce..ff3faff32 100644 --- a/Nynja/AudioRecorder.swift +++ b/Nynja/AudioRecorder.swift @@ -89,7 +89,7 @@ final class AudioRecorder: NSObject { default: break } - LogService.log(topic: .audioSystem, text: "AVSession error: \(errorCode)") + LogService.log(topic: .audioSystem) { return "AVSession error: \(errorCode)" } return false } } @@ -113,7 +113,7 @@ final class AudioRecorder: NSObject { audioRecorder?.record() audioSessionManager.startPlayback() } catch { - LogService.log(topic: .audioSystem, text: error.localizedDescription) + LogService.log(topic: .audioSystem) { return error.localizedDescription } } } diff --git a/Nynja/AuthHandler.swift b/Nynja/AuthHandler.swift index 94fd99f55..c5ec8f5f2 100644 --- a/Nynja/AuthHandler.swift +++ b/Nynja/AuthHandler.swift @@ -29,7 +29,7 @@ class AuthHandler: BaseHandler { delegate?.processDelete(auth: auth) } if auth.type?.string == "update" { - LogService.log(topic: .MQTT, text: "Recived new token: \(auth.token ?? "")") + LogService.log(topic: .MQTT) { return "Recived new token: \(auth.token ?? "")" } if let token = auth.token { StorageService.sharedInstance.token = token } diff --git a/Nynja/BadgeNumberService.swift b/Nynja/BadgeNumberService.swift index c816a2066..1d913c53c 100644 --- a/Nynja/BadgeNumberService.swift +++ b/Nynja/BadgeNumberService.swift @@ -22,11 +22,10 @@ class BadgeNumberService: BadgeNumberServiceProtocol, StorageSubscriber { // MARK: - Init & deinit private init() { - initCounters() subscribe() } - private func initCounters() { + func initCounters() { var badgeNumber: Int64 = 0 conversationsProvider @@ -113,6 +112,8 @@ class BadgeNumberService: BadgeNumberServiceProtocol, StorageSubscriber { badgeNumber += currentValue - prevValue } + badgeNumber = max(badgeNumber, 0) + subscribers.forEach { $0.handler(badgeNumber) } } diff --git a/Nynja/ChatBaseFactory.swift b/Nynja/ChatBaseFactory.swift index 5f95bba6e..c3ed69701 100644 --- a/Nynja/ChatBaseFactory.swift +++ b/Nynja/ChatBaseFactory.swift @@ -10,13 +10,17 @@ class ChatBaseFactory: ChatItemsFactory { var isActionsEnabled: Bool = true - override required init() { + init(isActionsEnabled: Bool) { super.init() + self.isActionsEnabled = isActionsEnabled } - init(isActionsEnabled: Bool) { + override required init() { super.init() - self.isActionsEnabled = isActionsEnabled + } + + override var initionalScrollStates : IndexPath? { + return IndexPath(indexes: [5]) } // MARK: - First lvl diff --git a/Nynja/ChatService.swift b/Nynja/ChatService.swift deleted file mode 100644 index b6e279d3f..000000000 --- a/Nynja/ChatService.swift +++ /dev/null @@ -1,164 +0,0 @@ -// -// ChatService.swift -// Nynja -// -// Created by Volodymyr Hryhoriev on 3/6/18. -// Copyright © 2018 TecSynt Solutions. All rights reserved. -// - -final class ChatService { - enum ReaderOwner { - case contact(String) - case room(String) - } - - private static let storageService = StorageService.sharedInstance - - // MARK: - Fetch - /// FIXME: Need to rewrite, because `reader` field inside `Contact` model is array. - static func fetchReader(for owner: ReaderOwner) -> Int64? { - var reader: Int64? - - switch owner { - case .contact(let phoneId): - reader = ContactDAO.fetchReader(for: phoneId) - case .room(let roomId): - reader = RoomDAO.fetchReader(for: roomId) - } - - return reader - } - - static func fetchChatModel(from message: Message) -> ChatModel? { - if let p = message.feed_id as? p2p, let target = p.opponentId, let contact = ContactDAO.findContactBy(phoneId: target) { - return contact - } else if let m = message.feed_id as? muc, let name = m.name, let room = RoomDAO.findRoom(by: name) { - return room - } - return nil - } - - // MARK: - Update - static func updateReader(from message: Message) { - guard let reader = message.id else { return } - - if let phoneId = message.p2pFeed?.opponentId { - ContactDAO.updateReader(reader, phoneId: phoneId) - } else if let roomId = message.mucFeed?.name { - RoomDAO.updateReader(reader, roomId: roomId) - } - } - - @discardableResult - static func updateLastMessage(_ message: Message, shouldChangeUnread: Bool = true, shouldUpdate: ((Int64) -> Bool)? = nil) -> Bool { - guard let chatModel = ChatService.fetchChatModel(from: message) else { return false } - - if let shouldUpdate = shouldUpdate, let lastId = chatModel.last_msg?.id, !shouldUpdate(lastId) { - return false - } - - chatModel.last_msg = message - - let shouldIncrementUnread = (!message.isOwn || message.isCursor) && !message.isEdited && shouldChangeUnread - if shouldIncrementUnread { - let counter = chatModel.unreadCount + 1 - chatModel.unread = counter - } - - if let room = chatModel as? Room, - room.hasMentions || message.hasMentions, - let roomId = message.mucFeed?.name, - case let .updated(mentions) = RoomDAO.updatedMentions(with: message, roomId: roomId) { - - room.mentions = mentions as [AnyObject]? - } - - do { - try storageService.perform(action: .save, with: chatModel) - return true - } catch { - return false - } - } - - // MARK: - Remove message - static func removeMessages(_ messages: [Message]) { - messages.forEach { removeMessage($0, shouldUpdateChat: false) } - } - - static func removeMessage(_ message: Message, shouldUpdateChat: Bool = true) { - if MessageDAO.removeMessage(using: message) { - guard let id = message.link else { - return - } - - if let action = MessageActionDAO.fetchMessageAction(by: id) { - try? StorageService.sharedInstance.perform(action: .delete, with: action) - } - - if let fetchType = self.fetchType(from: message), - let lastMessage = MessageDAO.fetchLastMessage(of: fetchType) { - - ChatService.updateLastMessage(lastMessage, shouldChangeUnread: false) { (lastMessageId) -> Bool in - return id == lastMessageId - } - } - } - } - - private static func fetchType(from message: Message) -> FetchType? { - if let p2p = message.p2pFeed, let from = p2p.from, let to = p2p.to { - return .p2p(from: from, to: to) - } else if let muc = message.mucFeed, let name = muc.name { - return .muc(name: name) - } - - return nil - } - - // MARK: - Clear History - static func clearHistory(_ message: Message) { - do { - if let muc = message.feed_id as? muc, let name = muc.name { - try MessageDAO.clearHistory(FetchType.muc(name: name)) - updateChatModelAfterClear(with: message) - } else if let p2p = message.feed_id as? p2p, let from = p2p.from, let to = p2p.to { - try MessageDAO.clearHistory(FetchType.p2p(from: from, to: to)) - updateChatModelAfterClear(with: message) - } - } catch {} - } - - static func clearMessages(before message: Message) { - do { - guard let messageServerId = message.id else { - return - } - if let muc = message.feed_id as? muc, let name = muc.name { - try MessageDAO.removeMessages(.muc(name: name), before: messageServerId) - - } else if let p2p = message.feed_id as? p2p, let from = p2p.from, let to = p2p.to { - try MessageDAO.removeMessages(.p2p(from: from, to: to), before: messageServerId) - } - - } catch {} - } - - private static func updateChatModelAfterClear(with message: Message) { - do { - try storageService.perform(action: .save, with: message) - - if let chatModel = fetchChatModel(from: message) { - chatModel.unread = unreadAfterClear(for: message) - chatModel.last_msg = message - - try storageService.perform(action: .save, with: chatModel) - } - } catch {} - } - - private static func unreadAfterClear(for message: Message) -> Int64 { - return message.mucFeed != nil ? 1 : 0 - } - -} diff --git a/Nynja/ChatService/ChatService.swift b/Nynja/ChatService/ChatService.swift new file mode 100644 index 000000000..4e9442626 --- /dev/null +++ b/Nynja/ChatService/ChatService.swift @@ -0,0 +1,249 @@ +// +// ChatService.swift +// Nynja +// +// Created by Volodymyr Hryhoriev on 3/6/18. +// Copyright © 2018 TecSynt Solutions. All rights reserved. +// + +enum ReaderOwner { + case contact(String) + case room(String) +} + +enum ReaderKind: Int { + case other = 0 + case own = 1 +} + +final class ChatService { + + private static let messageParser = MessageParser(dependencies: .init(storageService: storageService)) + private static let storageService = StorageService.sharedInstance + + // MARK: - Fetch + static func fetchReader(for owner: ReaderOwner, kind: ReaderKind) -> MessageServerId? { + let reader: Int64? + + switch owner { + case .contact(let phoneId): + reader = ContactDAO.fetchReader(for: phoneId, kind: kind) + case .room(let roomId): + reader = RoomDAO.fetchReader(for: roomId, kind: kind) + } + + return reader + } + + static func fetchChatModel(from message: Message) -> ChatModel? { + if let p = message.feed_id as? p2p, let target = p.opponentId, let contact = ContactDAO.findContactBy(phoneId: target) { + return contact + } else if let m = message.feed_id as? muc, let name = m.name, let room = RoomDAO.findRoom(by: name) { + return room + } + return nil + } + + // MARK: - Update + + static func updateReader(from message: Message, kind: ReaderKind) { + guard let reader = message.id else { + return + } + + if let phoneId = message.p2pFeed?.opponentId { + ContactDAO.updateReader(reader, phoneId: phoneId, kind: kind) + } else if let roomId = message.mucFeed?.name { + RoomDAO.updateReader(reader, roomId: roomId, kind: kind) + } + } + + @discardableResult + static func updateLastMessage(_ message: Message, + chat: ChatModel? = nil, + shouldChangeUnread: Bool = true, + shouldUpdate: ((MessageServerId) -> Bool)? = nil) -> Bool { + + guard let chatModel = chat ?? ChatService.fetchChatModel(from: message) else { + return false + } + + if let shouldUpdate = shouldUpdate, let lastId = chatModel.last_msg?.id, !shouldUpdate(lastId) { + return false + } + + chatModel.last_msg = nil + if let serverId = message.id, let lastMessageId = MessageDAO.fetchMessagePrimaryKey(with: serverId) { + chatModel.lastMessageId = lastMessageId + } + + let shouldIncrementUnread = (!message.isOwn || message.isCursor) && !message.isEdited && shouldChangeUnread + if shouldIncrementUnread { + let counter = chatModel.unreadCount + 1 + chatModel.unread = counter + } + + if let room = chatModel as? Room, + room.hasMentions || message.hasMentions, + let roomId = message.mucFeed?.name, + case let .updated(mentions) = RoomDAO.updatedMentions(with: message, roomId: roomId) { + + room.mentions = mentions as [AnyObject]? + } + + do { + try storageService.perform(action: .save, with: chatModel) + return true + } catch { + return false + } + } + + + static func updateUnreadCounter(for chat: ChatModel) { + if let contact = chat as? Contact { + ContactDAO.updateColumns([.unread], contact: contact) + } else if let room = chat as? Room { + RoomDAO.updateColumns([.unread], room: room) + } + } + + + // MARK: - Update message + + static func update(message: Message) { + let transcriptions = messageParser.parse(message, to: .transcription) + guard !transcriptions.isEmpty, let id = message.msg_id else { + return + } + guard let convert = ConvertMessageDAO.fetchConvertMessage(by: id) else { + return + } + + try? storageService.perform(action: .delete, with: convert) + } + + + // MARK: - Reset + + static func resetUnreadCount(from message: Message) { + if let oponnentId = message.p2pFeed?.opponentId { + let contact = Contact() + contact.phone_id = oponnentId + contact.unread = 0 + + ContactDAO.updateColumns([.unread], contact: contact) + } else if let roomId = message.mucFeed?.name { + let room = Room() + room.id = roomId + room.unread = 0 + + RoomDAO.updateColumns([.unread], room: room) + } + } + + + // MARK: - Remove message + + static func removeMessages(_ messages: [Message]) { + messages.forEach { removeMessage($0, shouldUpdateChat: false) } + } + + static func removeMessage(_ message: Message, shouldUpdateChat: Bool = true) { + guard MessageDAO.removeMessage(using: message), + let id = message.link else { + return + } + + if let action = MessageActionDAO.fetchMessageAction(by: id) { + try? StorageService.sharedInstance.perform(action: .delete, with: action) + } + + guard let chat = fetchChatModel(from: message) else { + return + } + + updateUnreadCounterAfterRemove(for: chat, serverId: id) + updateLastMessageAfterRemove(for: chat, message: message) + } + + private static func updateUnreadCounterAfterRemove(for chat: ChatModel, serverId: MessageServerId) { + guard let selfReader = chat.selfReader, + serverId > selfReader, + chat.unreadCount > 0 else { + return + } + + chat.unread = chat.unreadCount - 1 + updateUnreadCounter(for: chat) + } + + private static func updateLastMessageAfterRemove(for chat: ChatModel, message: Message) { + guard let fetchType = self.fetchType(from: message), + let lastMessage = MessageDAO.fetchLastMessage(of: fetchType) else { + return + } + + ChatService.updateLastMessage(lastMessage, chat: chat, shouldChangeUnread: false) { (lastMessageId) -> Bool in + return message.link == lastMessageId + } + } + + private static func fetchType(from message: Message) -> FetchType? { + if let p2p = message.p2pFeed, let from = p2p.from, let to = p2p.to { + return .p2p(from: from, to: to) + } else if let muc = message.mucFeed, let name = muc.name { + return .muc(name: name) + } + + return nil + } + + // MARK: - Clear History + static func clearHistory(_ message: Message) { + do { + message.isTrusted = true + if let muc = message.feed_id as? muc, let name = muc.name { + try MessageDAO.clearHistory(FetchType.muc(name: name)) + updateChatModelAfterClear(with: message) + } else if let p2p = message.feed_id as? p2p, let from = p2p.from, let to = p2p.to { + try MessageDAO.clearHistory(FetchType.p2p(from: from, to: to)) + updateChatModelAfterClear(with: message) + } + } catch {} + } + + static func clearMessages(before message: Message) { + do { + guard let messageServerId = message.id else { + return + } + message.isTrusted = true + if let muc = message.feed_id as? muc, let name = muc.name { + try MessageDAO.clearMessages(.muc(name: name), before: messageServerId) + + } else if let p2p = message.feed_id as? p2p, let from = p2p.from, let to = p2p.to { + try MessageDAO.clearMessages(.p2p(from: from, to: to), before: messageServerId) + } + + } catch {} + } + + private static func updateChatModelAfterClear(with message: Message) { + do { + try storageService.perform(action: .save, with: message) + + if let chatModel = fetchChatModel(from: message) { + chatModel.unread = unreadAfterClear(for: message) + chatModel.last_msg = message + + try storageService.perform(action: .save, with: chatModel) + } + } catch {} + } + + private static func unreadAfterClear(for message: Message) -> Int64 { + return message.mucFeed != nil ? 1 : 0 + } + +} diff --git a/Nynja/CircleMenuControl/Core/CircleMenu.swift b/Nynja/CircleMenuControl/Core/CircleMenu.swift index 7dc1e98d7..0bff28ebf 100644 --- a/Nynja/CircleMenuControl/Core/CircleMenu.swift +++ b/Nynja/CircleMenuControl/Core/CircleMenu.swift @@ -15,7 +15,9 @@ class CircleMenu: UIControl { private var container: UIView! private var sectors = [Sector]() private(set) var currentSectorIndex: Int? - private let separatorPadding: CGFloat = 30 + private var defaultOffset: CGFloat = CGFloat(8.adjustedByWidth) + private var separatorPadding: CGFloat { return centerIconSide/2 + defaultOffset } + private let centerIconSide: CGFloat = CGFloat(84.adjustedByWidth) private var menuNavigationStack = [CircleMenuSet]() { didSet { @@ -114,9 +116,9 @@ class CircleMenu: UIControl { self.addSubview(self.container) // Add Central image - let centerImage = UIImageView(frame: CGRect(origin: .zero, size: CGSize(width: 58, height: 58))) + let centerImage = UIImageView(frame: CGRect(origin: .zero, size: CGSize(width: centerIconSide, height: centerIconSide))) centerImage.image = UIImage(named: "wheel") - centerImage.center = CGPoint(x: self.container.center.x, y: self.container.center.y + 5) + centerImage.center = CGPoint(x: self.container.center.x, y: self.container.center.y + defaultOffset) self.container.addSubview(centerImage) } diff --git a/Nynja/CircleMenuControl/Core/Sector/SectionView.swift b/Nynja/CircleMenuControl/Core/Sector/SectionView.swift index 761af4af6..e0928faed 100644 --- a/Nynja/CircleMenuControl/Core/Sector/SectionView.swift +++ b/Nynja/CircleMenuControl/Core/Sector/SectionView.swift @@ -8,9 +8,9 @@ import UIKit -fileprivate let iconSide: CGFloat = 34 -fileprivate let padding: CGFloat = 8 -fileprivate let labelHeight: CGFloat = 22 +fileprivate let iconSide: CGFloat = CGFloat(34.adjustedByWidth) +fileprivate let padding: CGFloat = CGFloat(8.adjustedByWidth) +fileprivate let labelHeight: CGFloat = CGFloat(20.adjustedByWidth) fileprivate let height = iconSide + padding + labelHeight @@ -44,13 +44,11 @@ class SectionView: UIView { view.transform = CGAffineTransform(rotationAngle: self.angle) let shiftCenterY: CGFloat = 10.0 - let width: CGFloat = 2*height self.addSubview(view) view.snp.makeConstraints({ (make) in make.centerX.equalToSuperview() make.centerY.equalToSuperview().offset(-shiftCenterY) - make.width.equalTo(width) make.height.equalTo(height) }) return view @@ -107,10 +105,10 @@ class SectionView: UIView { private func createTitleLabel(with model: CircleMenuItem) -> UILabel { let label = UILabel(frame: .zero) - + label.numberOfLines = 1 label.textAlignment = .center - let font = UIFont(name: Constants.fonts.medium, size: 12) + let font = UIFont.makeFont(with: Constants.fonts.medium, height: labelHeight) label.font = font label.text = model.title label.textColor = model.isEnabled ? Colors.titleLabel.enabled : Colors.titleLabel.disabled diff --git a/Nynja/ConnectionSubscriberService.swift b/Nynja/ConnectionSubscriberService.swift index b413ca9ce..9aa73f9d3 100644 --- a/Nynja/ConnectionSubscriberService.swift +++ b/Nynja/ConnectionSubscriberService.swift @@ -24,7 +24,7 @@ final class ConnectionSubscriberService { // MARK: - MQTTServiceDelegate extension ConnectionSubscriberService: MQTTServiceDelegate { - func didConnect(_ mqttService: MQTTService) { - taskHandlers.forEach { $0.performTask() } + func mqttServiceDidConnect(_ mqttService: MQTTService) { + taskHandlers.forEach { $0.performTask() } } } diff --git a/Nynja/ContactDAO.swift b/Nynja/ContactDAO.swift index b3ae0d77c..738c269fb 100644 --- a/Nynja/ContactDAO.swift +++ b/Nynja/ContactDAO.swift @@ -143,14 +143,21 @@ class ContactDAO: ContactDAOProtocol { } // MARK: -- Reader - static func fetchReader(for phoneId: String) -> Int64? { - return dbManager.fetch { db in + static func fetchReader(for phoneId: String, kind: ReaderKind) -> Int64? { + let readersString = dbManager.fetch{ db in return try DBContact .filter(Column(ContactTable.Column.phoneId.title) == phoneId) .select([Column(ContactTable.Column.reader.title)]) - .asRequest(of: Int64.self) + .asRequest(of: String.self) .fetchOne(db) } + + guard let readers = readersString?.splitByComma(Int64.self), + readers.count == 2 else { + return nil + } + + return readers[kind.rawValue] } // MARK: - Update @@ -160,19 +167,22 @@ class ContactDAO: ContactDAOProtocol { } // MARK: -- Fields - static func updateReader(_ reader: Int64, phoneId: String) { + + static func updateReader(_ reader: Int64, phoneId: String, kind: ReaderKind) { guard let contact = fetchContact(by: phoneId), - let oldReader = contact.readerToArray() else { + var oldReader = contact.readerToArray() else { return } - - let newReader: [AnyObject] = [ - reader as AnyObject, - (oldReader.last ?? 0) as AnyObject - ] + + for i in oldReader.count..<2 { + oldReader.append(0) + } + + var newReader = oldReader.map { $0 as AnyObject} + newReader[kind.rawValue] = reader as AnyObject contact.reader = newReader.joinedByComma() - + try? dbManager.perform(action: .updateColumns([ContactTable.Column.reader.title]), with: contact) } diff --git a/Nynja/ContactDAOProtocol.swift b/Nynja/ContactDAOProtocol.swift index b834d0de7..f97c190fe 100644 --- a/Nynja/ContactDAOProtocol.swift +++ b/Nynja/ContactDAOProtocol.swift @@ -26,12 +26,12 @@ protocol ContactDAOProtocol: DAOProtocol { static func fetchContacts(with statuses: [Contact.Status]) -> [Contact] // MARK: -- Reader - static func fetchReader(for phoneId: String) -> Int64? + static func fetchReader(for phoneId: String, kind: ReaderKind) -> Int64? // MARK: - Update static func updateColumns(_ columns: Set, contact: Contact) // MARK: -- Fields - static func updateReader(_ reader: Int64, phoneId: String) + static func updateReader(_ reader: Int64, phoneId: String, kind: ReaderKind) } diff --git a/Nynja/ConversationsProvider.swift b/Nynja/ConversationsProvider.swift index e48f72e49..b5acc08a0 100644 --- a/Nynja/ConversationsProvider.swift +++ b/Nynja/ConversationsProvider.swift @@ -61,9 +61,9 @@ class ConversationsProvider: ConversationsProviding { } - // MARK: - Private + // MARK: - Comparator - private func comparator(lhs: ChatModel, rhs: ChatModel) -> Bool { + func comparator(lhs: ChatModel, rhs: ChatModel) -> Bool { let created1 = lhs.last_msg?.created as? Int64 ?? 0 let created2 = rhs.last_msg?.created as? Int64 ?? 0 return created1 > created2 diff --git a/Nynja/ConversationsProviding.swift b/Nynja/ConversationsProviding.swift index d89ffa97d..a4f130a74 100644 --- a/Nynja/ConversationsProviding.swift +++ b/Nynja/ConversationsProviding.swift @@ -16,4 +16,5 @@ protocol ConversationsProviding { func fetchAllConversations() -> [ChatModel] + func comparator(lhs: ChatModel, rhs: ChatModel) -> Bool } diff --git a/Nynja/ConvertMessage/ConvertMessageDAO.swift b/Nynja/ConvertMessage/ConvertMessageDAO.swift new file mode 100644 index 000000000..4597aa67d --- /dev/null +++ b/Nynja/ConvertMessage/ConvertMessageDAO.swift @@ -0,0 +1,27 @@ +// +// ConvertMessageDAO.swift +// Nynja +// +// Created by Andrey Reznik on 23.08.2018. +// Copyright © 2018 TecSynt Solutions. All rights reserved. +// + +final class ConvertMessageDAO: ConvertMessageDAOProtocol { + + // MARK: - Fetch + // MARK: -- Action + static func fetchConvertMessage(by id: String) -> DBConvertMessage? { + return dbManager.fetch { db in + return try DBConvertMessage.convertMessage(db, messageId: id) + } + } + + + // MARK: - Actions + static func fetchConvertMessages() -> [DBConvertMessage] { + return dbManager.fetch { db in + return try DBConvertMessage.fetchAll(db) + } + } + +} diff --git a/Nynja/ConvertMessage/ConvertMessageDAOProtocol.swift b/Nynja/ConvertMessage/ConvertMessageDAOProtocol.swift new file mode 100644 index 000000000..9c43cfe71 --- /dev/null +++ b/Nynja/ConvertMessage/ConvertMessageDAOProtocol.swift @@ -0,0 +1,18 @@ +// +// ConvertMessageDAOProtocol.swift +// Nynja +// +// Created by Andrey Reznik on 23.08.2018. +// Copyright © 2018 TecSynt Solutions. All rights reserved. +// + +protocol ConvertMessageDAOProtocol: DAOProtocol { + + // MARK: - Fetch + // MARK: -- Action + static func fetchConvertMessage(by id: String) -> DBConvertMessage? + + // MARK: - Actions + static func fetchConvertMessages() -> [DBConvertMessage] + +} diff --git a/Nynja/DB/Extensions/DatabaseExtension.swift b/Nynja/DB/Extensions/DatabaseExtension.swift index cfe6429ec..702c67a1b 100644 --- a/Nynja/DB/Extensions/DatabaseExtension.swift +++ b/Nynja/DB/Extensions/DatabaseExtension.swift @@ -12,16 +12,38 @@ extension Database { func hasColumns(_ columns: Set, tableName: String) throws -> Bool { let rows = try Row.fetchAll(self, "PRAGMA table_info('\(tableName)')") - let tableColumns = Set(rows.flatMap { $0["name"] }) + let tableColumns = Set(rows.compactMap { $0["name"] }) return !columns.intersection(tableColumns).isEmpty } + func hasColumns(_ columns: Set, in table: T.Type) throws -> Bool { + return try hasColumns(columns, tableName: T.name) + } + func clearTables(_ tableNames: [String]) throws { try tableNames.forEach { name in - try self.execute("delete from \(name)") - } - - try? self.execute("delete from sqlite_sequence") + try execute("delete from \(name)") + } + try? execute("delete from sqlite_sequence") + } + + func tableExists(_ table: T.Type) throws -> Bool { + return try tableExists(T.name) + } + + func create(_ table: T.Type, body: (TableDefinition) -> Void) throws { + try create(table: T.name, body: body) } + func rename(table: T.Type, to newName: String) throws { + try rename(table: T.name, to: newName) + } + + func alter(table: T.Type, body: (TableAlteration) -> Void) throws { + try alter(table: T.name, body: body) + } + + func drop(table: T.Type) throws { + try drop(table: T.name) + } } diff --git a/Nynja/DB/Models/Base/DBModelProtocol.swift b/Nynja/DB/Models/Base/DBModelProtocol.swift index 9865dea72..10f019eb5 100644 --- a/Nynja/DB/Models/Base/DBModelProtocol.swift +++ b/Nynja/DB/Models/Base/DBModelProtocol.swift @@ -14,16 +14,17 @@ protocol DBModelProtocol: Persistable, RowConvertible { @discardableResult func deleteAggregate(_ db: Database) throws -> Bool - } extension DBModelProtocol { - func saveAggregate(_ db: Database) throws {} + func saveAggregate(_ db: Database) throws { + try save(db) + } @discardableResult func deleteAggregate(_ db: Database) throws -> Bool { + try delete(db) return false } - } diff --git a/Nynja/DB/Models/DBContact.swift b/Nynja/DB/Models/DBContact.swift index 8e4754eca..3750523e5 100644 --- a/Nynja/DB/Models/DBContact.swift +++ b/Nynja/DB/Models/DBContact.swift @@ -47,7 +47,10 @@ class DBContact: Record, DBModelProtocol { if let message = contact.message { self.message = DBMessage(message: message) self.messageId = message.id + } else { + self.messageId = contact.lastMessageId } + self.rosterId = rosterId self.services = (contact.services ?? []).compactMap { DBService(service: $0, @@ -187,7 +190,6 @@ class DBContact: Record, DBModelProtocol { } func construct(_ db: Database) throws { - if let messageId = self.messageId { self.message = try DBMessage.message(db, id: messageId) } diff --git a/Nynja/DB/Models/DBContact.swift.orig b/Nynja/DB/Models/DBContact.swift.orig deleted file mode 100644 index fbd922083..000000000 --- a/Nynja/DB/Models/DBContact.swift.orig +++ /dev/null @@ -1,171 +0,0 @@ -// -// DBContact.swift -// Nynja -// -// Created by Volodymyr Hryhoriev on 11/22/17. -// Copyright © 2017 TecSynt Solutions. All rights reserved. -// - -import GRDB - -class DBContact: Record, DBModelProtocol { - - var phoneId: String - var avatar: String? - var names: String - var surnames: String? - var nick: String? - var email: String? - var voxId: String - var reader: Int64 - var unread: Int64 - var update: Int64 - var created: Int64 - var presence: String? - var status: String? - - var messageId: Int64? - var rosterId: Int64? - - var message: DBMessage? - var features: [DBFeature] = [] - - init?(contact: Contact, rosterId: Int64?) { -<<<<<<< HEAD - guard let phoneId = contact.phone_id else { return nil } -======= - guard let phoneId = contact.phone_id else { - return nil - } ->>>>>>> developer - - self.phoneId = phoneId - self.avatar = contact.avatar - self.names = contact.names ?? "" - self.surnames = contact.surnames - self.nick = contact.nick - self.email = contact.email - self.voxId = contact.voxID - self.reader = contact.reader ?? 0 - self.unread = contact.unread ?? 0 - self.update = contact.update ?? 0 - self.created = contact.created ?? 0 - self.presence = contact.presence?.string - self.status = (contact.status as? StringAtom)?.string - - if let message = contact.message { - self.message = DBMessage(message: message) - self.messageId = message.id - } - self.rosterId = rosterId - - self.features = (contact.settings ?? []).flatMap { DBFeature(feature: $0, targetId: phoneId, targetType: DBFeature.TargetType.contact) } - - - super.init() - } - - // MARK: - Record - override static var databaseTableName: String { - return ContactTable.name - } - - required init(row: Row) { - phoneId = row[ContactTable.Column.phoneId.title] - avatar = row[ContactTable.Column.avatar.title] - names = row[ContactTable.Column.names.title] - surnames = row[ContactTable.Column.surnames.title] - nick = row[ContactTable.Column.nick.title] - email = row[ContactTable.Column.email.title] - voxId = row[ContactTable.Column.voxId.title] - reader = row[ContactTable.Column.reader.title] - unread = row[ContactTable.Column.unread.title] - update = row[ContactTable.Column.update.title] - created = row[ContactTable.Column.created.title] - presence = row[ContactTable.Column.presence.title] - status = row[ContactTable.Column.status.title] - - messageId = row[ContactTable.Column.messageId.title] - rosterId = row[ContactTable.Column.rosterId.title] - - super.init() - } - - override func encode(to container: inout PersistenceContainer) { - container[ContactTable.Column.phoneId.title] = phoneId - container[ContactTable.Column.avatar.title] = avatar - container[ContactTable.Column.names.title] = names - container[ContactTable.Column.surnames.title] = surnames - container[ContactTable.Column.nick.title] = nick - container[ContactTable.Column.email.title] = email - container[ContactTable.Column.voxId.title] = voxId - container[ContactTable.Column.reader.title] = reader - container[ContactTable.Column.unread.title] = unread - container[ContactTable.Column.update.title] = update - container[ContactTable.Column.created.title] = created - container[ContactTable.Column.presence.title] = presence - container[ContactTable.Column.status.title] = status - - container[ContactTable.Column.messageId.title] = messageId - container[ContactTable.Column.rosterId.title] = rosterId - } - - // MARK: - Query, Modification - func saveAggregate(_ db: Database) throws { - try message?.saveAggregate(db) - self.messageId = message?.id - - try self.save(db) - try features.forEach { try $0.save(db) } - } - - @discardableResult - func deleteAggregate(_ db: Database) throws -> Bool { - try DBContact.request(targetId: self.phoneId).deleteAll(db) - return try self.delete(db) - } - - static func contact(from db: Database, rowId: Int64) throws -> DBContact? { - let contact = try DBContact.filter(Column.rowID == rowId).fetchOne(db) - try contact?.construct(db) - return contact - } - - static func contact(from db: Database, id: String) throws -> DBContact? { - let idColumn = Column(ContactTable.Column.phoneId.title) - let contact = try DBContact.filter(idColumn == id).fetchOne(db) - try contact?.construct(db) - return contact - } - - static func contacts(from db: Database, rosterId: Int64, status: String? = nil) throws -> [DBContact] { - let rosterIdColumn = Column(ContactTable.Column.rosterId.title) - - var request = DBContact.filter(rosterIdColumn == rosterId) - if let status = status { - let statusColumn = Column(ContactTable.Column.status.title) - request = request.filter(statusColumn == status) - } - let contacts = try request.fetchAll(db) - - try contacts.forEach { try $0.construct(db) } - return contacts - } - - func construct(_ db: Database) throws { - if let messageId = self.messageId { - self.message = try DBMessage.message(db, id: messageId) - } - self.features = (try? DBContact.request(targetId: self.phoneId).fetchAll(db)) ?? [] - } - - // MARK: - Request - static private func request(targetId: String) -> QueryInterfaceRequest { - return DBFeature.request(targetId: targetId, targetType: DBFeature.TargetType.contact) - } - - static func request(phoneId: String) -> QueryInterfaceRequest { - return DBContact.filter(Column(ContactTable.Column.phoneId.title) == phoneId) - } -} - diff --git a/Nynja/DB/Models/DBConvertMessage.swift b/Nynja/DB/Models/DBConvertMessage.swift new file mode 100644 index 000000000..46002c48f --- /dev/null +++ b/Nynja/DB/Models/DBConvertMessage.swift @@ -0,0 +1,98 @@ +// +// DBConvertMessage.swift +// Nynja +// +// Created by Andrey Reznik on 23.08.2018. +// Copyright © 2018 TecSynt Solutions. All rights reserved. +// + +import GRDBCipher + +class DBConvertMessage: Record, DBModelProtocol { + + var messageId: String + var type: Int + var process: Int + var value: String + var language: String + + init(messageId: String, type: Int, process: Int, value: String, language: String) { + self.messageId = messageId + self.type = type + self.process = process + self.value = value + self.language = language + + super.init() + } + + convenience init(messageId: String, type: Type, process: Process, value: String, language: String) { + self.init(messageId: messageId, + type: type.rawValue, + process: process.rawValue, + value: value, + language: language) + } + + // MARK: - Record + + override static var databaseTableName: String { + return ConvertMessageTable.name + } + + required init(row: Row) { + messageId = row[ConvertMessageTable.Column.messageId.title] + type = row[ConvertMessageTable.Column.type.title] + process = row[ConvertMessageTable.Column.process.title] + value = row[ConvertMessageTable.Column.value.title] + language = row[ConvertMessageTable.Column.language.title] + super.init() + } + + override func encode(to container: inout PersistenceContainer) { + container[ConvertMessageTable.Column.messageId.title] = messageId + container[ConvertMessageTable.Column.type.title] = type + container[ConvertMessageTable.Column.process.title] = process + container[ConvertMessageTable.Column.value.title] = value + container[ConvertMessageTable.Column.language.title] = language + } + + + //MARK: - DBModelProtocol + func saveAggregate(_ db: Database) throws { + try self.save(db) + } + + @discardableResult + func deleteAggregate(_ db: Database) throws -> Bool { + return try self.delete(db) + } + + + //MARK: - Fetching + static func convertMessage(_ db: Database, messageId: String) throws -> DBConvertMessage? { + let сolumn = Column(ConvertMessageTable.Column.messageId.title) + + guard let convert = try DBConvertMessage.filter(сolumn == messageId).fetchOne(db) else { + return nil + } + + return convert + } +} + +extension DBConvertMessage { + + enum `Type`: Int { + case translate + case transcribe + } + + enum Process: Int { + case convert + case upload + case transcribing + case transcribeProcessing + case send + } +} diff --git a/Nynja/DB/Models/DBJob.swift b/Nynja/DB/Models/DBJob.swift index 5de9b5a9c..2ed65af4a 100644 --- a/Nynja/DB/Models/DBJob.swift +++ b/Nynja/DB/Models/DBJob.swift @@ -17,11 +17,11 @@ class DBJob: Record, DBModelProtocol { } var id: Int64? - var serverId: Int64? + var serverId: MessageServerId? var container: String? var feedId: String? - var prev: Int64? - var next: Int64? + var prev: MessageServerId? + var next: MessageServerId? var time: Int64? var status: String? var messages : [DBJobMessage] = [] diff --git a/Nynja/DB/Models/DBLink.swift b/Nynja/DB/Models/DBLink.swift index d2644d9e6..a10c2009f 100644 --- a/Nynja/DB/Models/DBLink.swift +++ b/Nynja/DB/Models/DBLink.swift @@ -49,7 +49,6 @@ final class DBLink: Codable { let roomIdColumn = Column(LinkTable.Column.roomId.title) return DBLink.filter(roomIdColumn == roomId) } - } diff --git a/Nynja/DB/Models/DBMessage.swift b/Nynja/DB/Models/DBMessage.swift index e936ee596..7301113c6 100644 --- a/Nynja/DB/Models/DBMessage.swift +++ b/Nynja/DB/Models/DBMessage.swift @@ -10,7 +10,7 @@ import GRDBCipher private let tableName = MessageTable.name -class DBMessage: Record, DBModelProtocol { +final class DBMessage: Record, DBModelProtocol { var id: Int64? var container: String? @@ -24,10 +24,11 @@ class DBMessage: Record, DBModelProtocol { var to: String? var created: Int64? var type: String? - var editMessage: Int64? + var link: Int64? var repliedBy: String? var mentioned: String? var status: String? + var isTrusted: Bool? var files: [DBDesc] = [] @@ -44,8 +45,9 @@ class DBMessage: Record, DBModelProtocol { self.to = message.to self.created = message.created as? Int64 self.type = message.types.joinedByCommaIfNotEmpty() - self.editMessage = message.link + self.link = message.link self.status = message.statusString + self.isTrusted = message.isTrusted self.files = (message.files ?? []).compactMap { DBDesc(desc: $0, targetId: nil, targetType: .message) } @@ -81,10 +83,11 @@ class DBMessage: Record, DBModelProtocol { self.to = row[MessageTable.Column.to.title] self.created = row[MessageTable.Column.created.title] self.type = row[MessageTable.Column.type.title] - self.editMessage = row[MessageTable.Column.editMessage.title] + self.link = row[MessageTable.Column.editMessage.title] self.repliedBy = row[MessageTable.Column.repliedBy.title] self.mentioned = row[MessageTable.Column.mentioned.title] self.status = row[MessageTable.Column.status.title] + self.isTrusted = row[MessageTable.Column.trusted.title] super.init() } @@ -102,10 +105,13 @@ class DBMessage: Record, DBModelProtocol { container[MessageTable.Column.to.title] = self.to container[MessageTable.Column.created.title] = self.created container[MessageTable.Column.type.title] = self.type - container[MessageTable.Column.editMessage.title] = self.editMessage + container[MessageTable.Column.editMessage.title] = self.link container[MessageTable.Column.repliedBy.title] = self.repliedBy container[MessageTable.Column.mentioned.title] = self.mentioned container[MessageTable.Column.status.title] = self.status + if let isTrusted = isTrusted { + container[MessageTable.Column.trusted.title] = isTrusted + } } override func didInsert(with rowID: Int64, for column: String?) { @@ -181,21 +187,14 @@ class DBMessage: Record, DBModelProtocol { return message } - static func penultimateMessage(_ db: Database, ofType fetchType: FetchType) throws -> DBMessage? { - guard let lastMessage = try lastMessage(db, ofType: fetchType), - let lastMessageServerId = lastMessage.serverId else { - return nil - } - - let condition = conditionBefore(messageServerId: lastMessageServerId) - - var sql: String + static func lastMessage(_ db: Database, ofType fetchType: FetchType) throws -> DBMessage? { + let sql: String switch fetchType { case let .p2p(from, to): - sql = sqlP2p(from: from, to: to, conditions: condition, ordered: .desc) + sql = sqlP2p(from: from, to: to, ordered: .desc, orderColumn: .created) case let .muc(mucName): - sql = sqlMuc(name: mucName, conditions: condition, ordered: .desc) + sql = sqlMuc(name: mucName, ordered: .desc, orderColumn: .created) } let message = try SQLRequest(sql).asRequest(of: DBMessage.self).fetchOne(db) @@ -203,14 +202,16 @@ class DBMessage: Record, DBModelProtocol { return message } - static func lastMessage(_ db: Database, ofType fetchType: FetchType) throws -> DBMessage? { - var sql: String + static func lastOponnentMessage(_ db: Database, ofType fetchType: FetchType, phoneId: String) throws -> DBMessage? { + let condition = "and \(tableName).[\(MessageTable.Column.from.title)] <> '\(phoneId)'" + + let sql: String switch fetchType { case let .p2p(from, to): - sql = sqlP2p(from: from, to: to, ordered: .desc) + sql = sqlP2p(from: from, to: to, conditions: condition, ordered: .desc, orderColumn: .created) case let .muc(mucName): - sql = sqlMuc(name: mucName, ordered: .desc) + sql = sqlMuc(name: mucName, conditions: condition, ordered: .desc, orderColumn: .created) } let message = try SQLRequest(sql).asRequest(of: DBMessage.self).fetchOne(db) @@ -244,9 +245,9 @@ class DBMessage: Record, DBModelProtocol { switch fetchType { case .p2p(let from, let to): - request = requestP2pMessages(from: from, to: to, conditions: conditions) + request = requestP2pMessages(from: from, to: to, conditions: conditions, orderColumn: .created) case .muc(let name): - request = requestMucMessages(name: name, conditions: conditions) + request = requestMucMessages(name: name, conditions: conditions, orderColumn: .created) } let messages = try request.fetchAll(db) @@ -296,6 +297,24 @@ class DBMessage: Record, DBModelProtocol { return try DBMessage.filter(predicate).select(localIdColumn).asRequest(of: String.self).fetchOne(db) } + // MARK: - Cursor + + static func fetchCursor(_ db: Database, + fetchType: FetchType, + orderedBy order: TableOrder, + orderColumn: MessageTable.Column) throws -> RecordCursor { + var request: AnyTypedRequest + + switch fetchType { + case let .p2p(from, to): + request = requestP2pMessages(from: from, to: to, orderedBy: order, orderColumn: orderColumn, skippedStatuses: nil) + case let .muc(name): + request = requestMucMessages(name: name, orderedBy: order, orderColumn: orderColumn, skippedStatuses: nil) + } + + return try request.fetchCursor(db) + } + // MARK: - Delete @discardableResult func deleteAggregate(_ db: Database) throws -> Bool { @@ -316,7 +335,7 @@ class DBMessage: Record, DBModelProtocol { return try DBDesc.descs(targetId: targetId, targetType: .message, db: db) } - static private func requestUnsentMessages(from: String) -> AnyTypedRequest { + private static func requestUnsentMessages(from: String) -> AnyTypedRequest { let messageTable = tableName let sql = """ @@ -324,20 +343,28 @@ class DBMessage: Record, DBModelProtocol { from \(messageTable) where \(messageTable).[\(MessageTable.Column.from.title)] = '\(from)' and \(messageTable).[\(MessageTable.Column.serverId.title)] is null - and \(messageTable).\(MessageTable.Column.feedType.title) = \(FeedType.p2p.rawValue) - \(orderedBy(.asc)) + \(orderedBy(.asc, column: .created)) """ return SQLRequest(sql).asRequest(of: DBMessage.self) } - static private func requestP2pMessages(from: String, to: String, conditions: String = "") -> AnyTypedRequest { - let sql = sqlP2p(from: from, to: to, conditions: conditions, ordered: .asc) + private static func requestP2pMessages(from: String, + to: String, + conditions: String = "", + orderedBy order: TableOrder = .asc, + orderColumn: MessageTable.Column, + skippedStatuses: String? = skippedStatuses) -> AnyTypedRequest { + let sql = sqlP2p(from: from, to: to, conditions: conditions, ordered: order, orderColumn: orderColumn, skippedStatuses: skippedStatuses) return SQLRequest(sql).asRequest(of: DBMessage.self) } - static private func requestMucMessages(name: String, conditions: String = "") -> AnyTypedRequest { - let sql = sqlMuc(name: name, conditions: conditions, ordered: .asc) + private static func requestMucMessages(name: String, + conditions: String = "", + orderedBy order: TableOrder = .asc, + orderColumn: MessageTable.Column, + skippedStatuses: String? = skippedStatuses) -> AnyTypedRequest { + let sql = sqlMuc(name: name, conditions: conditions, ordered: order, orderColumn: orderColumn, skippedStatuses: skippedStatuses) return SQLRequest(sql).asRequest(of: DBMessage.self) } @@ -353,11 +380,16 @@ class DBMessage: Record, DBModelProtocol { return "and \(tableName).\(MessageTable.Column.serverId.title) < \(messageServerId)" } - static private func orderedBy(_ ordered: Ordered) -> String { - return "order by \(tableName).[\(MessageTable.Column.created.title)] \(ordered.rawValue)" + static private func orderedBy(_ ordered: TableOrder, column: MessageTable.Column) -> String { + return "order by \(tableName).[\(column.title)] \(ordered.rawValue)" } - static private func sqlP2p(from: String, to: String, conditions: String = "", ordered: Ordered) -> String { + static private func sqlP2p(from: String, + to: String, + conditions: String = "", + ordered: TableOrder, + orderColumn: MessageTable.Column, + skippedStatuses: String? = skippedStatuses) -> String { let messageTable = tableName let p2pTable = P2pTable.name @@ -368,15 +400,19 @@ class DBMessage: Record, DBModelProtocol { where \(p2pTable).[\(P2pTable.Column.from.title)] = '\(from)' and \(p2pTable).[\(P2pTable.Column.to.title)] = '\(to)' and \(messageTable).\(MessageTable.Column.feedType.title) = \(FeedType.p2p.rawValue) - and \(skippedStatuses) %@ - \(orderedBy(ordered)) + %@ + \(orderedBy(ordered, column: orderColumn)) """ - return String(format: sql, conditions) + return String(format: sql, skippedStatuses.flatMap { "and \($0)" } ?? "", conditions) } - static private func sqlMuc(name: String, conditions: String = "", ordered: Ordered) -> String { + static private func sqlMuc(name: String, + conditions: String = "", + ordered: TableOrder, + orderColumn: MessageTable.Column, + skippedStatuses: String? = skippedStatuses) -> String { let messageTable = tableName let mucTable = MucTable.name @@ -386,20 +422,11 @@ class DBMessage: Record, DBModelProtocol { left join \(messageTable) on \(mucTable).id = \(messageTable).feedId where \(mucTable).[\(MucTable.Column.name.title)] = '\(name)' and \(messageTable).\(MessageTable.Column.feedType.title) = \(FeedType.muc.rawValue) - and \(skippedStatuses) %@ - \(orderedBy(ordered)) + %@ + \(orderedBy(ordered, column: orderColumn)) """ - return String(format: sql, conditions) - } -} - -private extension DBMessage { - - enum Ordered: String { - case asc - case desc + return String(format: sql, skippedStatuses.flatMap { "and \($0)" } ?? "", conditions) } - } diff --git a/Nynja/DB/Models/DBMessageAction.swift b/Nynja/DB/Models/DBMessageAction.swift index 6e4d3d964..fa3756603 100644 --- a/Nynja/DB/Models/DBMessageAction.swift +++ b/Nynja/DB/Models/DBMessageAction.swift @@ -8,14 +8,18 @@ import GRDBCipher -class DBMessageAction: Record, DBModelProtocol { +final class DBMessageAction: Record, DBModelProtocol { - var messageId: Int64 + enum Action: String { + case delete = "delete" + } + + var messageId: MessageServerId var seenBy: Int64 var phoneId: String - var action: String + var action: Action - init(messageId: Int64, seenBy: Int64, phoneId: String, action: String) { + init(messageId: MessageServerId, seenBy: Int64, phoneId: String, action: Action) { self.messageId = messageId self.seenBy = seenBy self.phoneId = phoneId @@ -34,7 +38,7 @@ class DBMessageAction: Record, DBModelProtocol { messageId = row[MessageActionTable.Column.messageId.title] seenBy = row[MessageActionTable.Column.seenBy.title] phoneId = row[MessageActionTable.Column.phoneId.title] - action = row[MessageActionTable.Column.action.title] + action = Action(rawValue: row[MessageActionTable.Column.action.title])! super.init() } @@ -43,7 +47,7 @@ class DBMessageAction: Record, DBModelProtocol { container[MessageActionTable.Column.messageId.title] = messageId container[MessageActionTable.Column.seenBy.title] = seenBy container[MessageActionTable.Column.phoneId.title] = phoneId - container[MessageActionTable.Column.action.title] = action + container[MessageActionTable.Column.action.title] = action.rawValue } diff --git a/Nynja/DB/Models/DBMessageEditAction.swift b/Nynja/DB/Models/DBMessageEditAction.swift index 3a077769a..ee2ff754d 100644 --- a/Nynja/DB/Models/DBMessageEditAction.swift +++ b/Nynja/DB/Models/DBMessageEditAction.swift @@ -10,13 +10,13 @@ import GRDBCipher final class DBMessageEditAction: Record, DBModelProtocol { - var messageId: Int64 + var messageId: MessageServerId var payload: String var mentioned: String? // MARK: - Init - init(messageId: Int64, payload: String, mentioned: String?) { + init(messageId: MessageServerId, payload: String, mentioned: String?) { self.messageId = messageId self.payload = payload self.mentioned = mentioned @@ -48,18 +48,18 @@ final class DBMessageEditAction: Record, DBModelProtocol { // MARK: - DBModelProtocol func saveAggregate(_ db: Database) throws { - try self.save(db) + try save(db) } @discardableResult func deleteAggregate(_ db: Database) throws -> Bool { - return try self.delete(db) + return try delete(db) } // MARK: - Fetching - static func action(_ db: Database, messageId: Int64) throws -> DBMessageEditAction? { + static func action(_ db: Database, messageId: MessageServerId) throws -> DBMessageEditAction? { let messageIdColumn = Column(MessageEditActionTable.Column.messageId.title) let message = try DBMessageEditAction.filter(messageIdColumn == messageId).fetchOne(db) diff --git a/Nynja/DB/Models/DBRoom.swift b/Nynja/DB/Models/DBRoom.swift index 6c3c69d04..e8a26a904 100644 --- a/Nynja/DB/Models/DBRoom.swift +++ b/Nynja/DB/Models/DBRoom.swift @@ -58,7 +58,10 @@ class DBRoom: Record, DBModelProtocol { if let message = room.message { self.message = DBMessage(message: message) self.messageId = room.message?.id + } else { + self.messageId = room.lastMessageId } + self.rosterId = rosterId // Featuers @@ -123,7 +126,10 @@ class DBRoom: Record, DBModelProtocol { func saveAggregate(_ db: Database, withMembers: Bool) throws { try message?.saveAggregate(db) - messageId = message?.id + + if message != nil || messageId == nil { + messageId = message?.id + } try save(db) diff --git a/Nynja/DB/Models/DBRoom.swift.orig b/Nynja/DB/Models/DBRoom.swift.orig deleted file mode 100644 index bfdc52be5..000000000 --- a/Nynja/DB/Models/DBRoom.swift.orig +++ /dev/null @@ -1,210 +0,0 @@ -// -// DBRoom.swift -// Nynja -// -// Created by Volodymyr Hryhoriev on 11/24/17. -// Copyright © 2017 TecSynt Solutions. All rights reserved. -// - -import GRDB - -class DBRoom: Record, DBModelProtocol { - - var id: String - var name: String - var description: String? - var type: String? - var tos: String? - var tosUpdate: Int64 - var unread: Int64 - var update: Int64 - var reader: Int64? - var created: Int64 - var status: String? - - var rosterId: Int64? - var messageId: Int64? - - var message: DBMessage? - - var members: [DBMember] = [] - var admins: [DBMember] = [] - var features: [DBFeature] = [] - var files: [DBDesc] = [] - - init?(room: Room, rosterId: Int64?) { - guard let id = room.id else { return nil } - - self.id = id - self.name = room.name ?? "" - self.description = room.description - self.type = (room.type as? StringAtom)?.string - self.tos = room.tos - self.tosUpdate = room.tos_update ?? 0 - self.unread = room.unread ?? 0 - self.reader = (room.readers as? [Int64])?.first - self.update = room.update ?? 0 - self.created = room.created ?? 0 - self.status = (room.status as? StringAtom)?.string - - // Members - admins = (room.admins ?? []).flatMap( { DBMember(member: $0) } ) - members = (room.members ?? []).flatMap( { DBMember(member: $0) } ) - - // Message - if let message = room.message { - self.message = DBMessage(message: message) - self.messageId = room.message?.id - } - self.rosterId = rosterId - - // Featuers & Files - self.features = (room.settings as? [Feature] ?? []).flatMap( { DBFeature(feature: $0, targetId: id, targetType: .room) } ) - self.files = (room.data ?? []).flatMap( { DBDesc(desc: $0, targetId: id, targetType: .room) } ) - - super.init() - } - - // MARK: - Record - override static var databaseTableName: String { - return RoomTable.name - } - - required init(row: Row) { - id = row[RoomTable.Column.id.title] - name = row[RoomTable.Column.name.title] - description = row[RoomTable.Column.description.title] - type = row[RoomTable.Column.type.title] - tos = row[RoomTable.Column.tos.title] - tosUpdate = row[RoomTable.Column.tosUpdate.title] - unread = row[RoomTable.Column.unread.title] - reader = row[RoomTable.Column.reader.title] - update = row[RoomTable.Column.update.title] - created = row[RoomTable.Column.created.title] - status = row[RoomTable.Column.status.title] - - rosterId = row[RoomTable.Column.rosterId.title] - messageId = row[RoomTable.Column.messageId.title] - - super.init() - } - - override func encode(to container: inout PersistenceContainer) { - container[RoomTable.Column.id.title] = id - container[RoomTable.Column.name.title] = name - container[RoomTable.Column.description.title] = description - container[RoomTable.Column.type.title] = type - container[RoomTable.Column.tos.title] = tos - container[RoomTable.Column.tosUpdate.title] = tosUpdate - container[RoomTable.Column.unread.title] = unread - container[RoomTable.Column.reader.title] = reader - container[RoomTable.Column.update.title] = update - container[RoomTable.Column.created.title] = created - container[RoomTable.Column.status.title] = status - - container[RoomTable.Column.rosterId.title] = rosterId - container[RoomTable.Column.messageId.title] = messageId - } - - // MARK: - Query, Modification - func saveAggregate(_ db: Database) throws { - try message?.saveAggregate(db) - messageId = message?.id - - try save(db) - -<<<<<<< HEAD - try admins.forEach { - try $0.saveAggregate(db) - try DBRoomMember(roomId: self.id, memberId: $0.id, isAdmin: true).save(db) - } - - try members.forEach { - try $0.saveAggregate(db) - try DBRoomMember(roomId: self.id, memberId: $0.id).save(db) -======= - if withMembers { - try admins.forEach { - try $0.saveAggregate(db) - try DBRoomMember(roomId: self.id, memberId: $0.id, isAdmin: $0.isAdmin).save(db) - } - - try members.forEach { - try $0.saveAggregate(db) - try DBRoomMember(roomId: self.id, memberId: $0.id, isAdmin: $0.isAdmin).save(db) - } ->>>>>>> developer - } - - - try DBDesc.deleteAll(db, targetId: self.id, targetType: .room) - try files.forEach { - try $0.saveAggregate(db) - } - - try DBFeature.deleteAll(db, targetId: self.id, targetType: .room) - try features.forEach { try $0.save(db) } - } - - static func rooms(from db: Database, rosterId: Int64) throws -> [DBRoom] { - let rosterIdColumn = Column(ContactTable.Column.rosterId.title) - - let rooms = try DBRoom.filter(rosterIdColumn == rosterId).fetchAll(db) - - try rooms.forEach { room in - // TODO: can we fetch contact and mesage using one query? - try room.construct(db) - } - - return rooms - } - - static func room(from db: Database, rowId: Int64) throws -> DBRoom? { - let room = try DBRoom.filter(Column.rowID == rowId).fetchOne(db) - try room?.construct(db) - return room - } - - static func room(from db: Database, id: String, fullModel: Bool = true) throws -> DBRoom? { - let idColumn = Column(RoomTable.Column.id.title) - let room = try DBRoom.filter(idColumn == id).fetchOne(db) - if fullModel { - try room?.construct(db) - } - return room - } - - - private func construct(_ db: Database) throws { - if let messageId = self.messageId { - self.message = try DBMessage.message(db, id: messageId) - } - - self.admins = try DBMember.members(from: db, roomId: self.id, isAdmin: true) - self.members = try DBMember.members(from: db, roomId: self.id) - - self.features = (try? DBRoom.requestFeature(targetId: self.id).fetchAll(db)) ?? [] - self.files = (try? DBRoom.requestDesc(targetId: self.id).fetchAll(db)) ?? [] - } - - @discardableResult - func deleteAggregate(_ db: Database) throws -> Bool { - try DBRoom.requestFeature(targetId: self.id).deleteAll(db) - try DBRoom.requestDesc(targetId: self.id).deleteAll(db) - return try self.delete(db) - } - - static func fetchDesc(_ db: Database, roomId: String) -> [DBDesc] { - return (try? DBRoom.requestDesc(targetId: roomId).fetchAll(db)) ?? [] - } - - - // MARK: - Make Requests - static private func requestFeature(targetId: String) -> QueryInterfaceRequest { - return DBFeature.request(targetId: targetId, targetType: DBFeature.TargetType.contact) - } - - static private func requestDesc(targetId: String) -> QueryInterfaceRequest { - return DBDesc.request(targetId: targetId, targetType: DBDesc.TargetType.room) - } -} diff --git a/Nynja/DB/Models/DBStar.swift b/Nynja/DB/Models/DBStar.swift index db1f2ebe0..5fb1f16ca 100644 --- a/Nynja/DB/Models/DBStar.swift +++ b/Nynja/DB/Models/DBStar.swift @@ -9,7 +9,7 @@ import Foundation import GRDBCipher -class DBStar: Record, DBModelProtocol { +final class DBStar: Record, DBModelProtocol { var id: Int64? var clientId: String? var rosterId: Int64? @@ -21,7 +21,9 @@ class DBStar: Record, DBModelProtocol { // MARK: - Mapping init?(star: Star) { self.id = star.id - self.clientId = star.client_id ?? IdBuilder(format: .starClientId).build() + self.clientId = star.client_id ?? IdBuilder(format: .starClientId) + .addValueForComponent(star.message!.id!, .key) + .build() self.rosterId = star.roster_id self.status = StringAtom.string(star.status) if let msg = star.message { @@ -77,22 +79,36 @@ class DBStar: Record, DBModelProtocol { return star } + static func star(_ db: Database, clientId: String) throws -> DBStar? { + let clientIdColumn = Column(StarTable.Column.clientId.title) + let star = try DBStar.filter(clientIdColumn == clientId).fetchOne(db) + try star?.construct(db) + return star + } + static func stars(from db: Database, rosterId: Int64) throws -> [DBStar] { let rosterIdColumn = Column(StarTable.Column.rosterId.title) + let statusColumn = Column(StarTable.Column.status.title) - let stars = try DBStar.filter(rosterIdColumn == rosterId).fetchAll(db) + let filter: SQLExpressible = rosterIdColumn == rosterId && statusColumn !== Star.Status.remove.rawValue + let stars = try DBStar.filter(filter).fetchAll(db) try stars.forEach { try $0.construct(db) } return stars } - - static func stars(from db: Database) throws -> [DBStar] { - let stars = try DBStar.fetchAll(db) + + static func undeliveredStars(from db: Database, rosterId: Int64) throws -> [DBStar] { + let rosterIdColumn = Column(StarTable.Column.rosterId.title) + let starIdColumn = Column(StarTable.Column.id.title) + + let filter: SQLExpressible = rosterIdColumn == rosterId && starIdColumn == nil + let stars = try DBStar.filter(filter).fetchAll(db) + try stars.forEach { try $0.construct(db) } return stars } - func construct(_ db: Database) throws { + private func construct(_ db: Database) throws { if let messageId = self.messageID { message = try DBStarMessage.message(db, id: messageId) } @@ -103,5 +119,4 @@ class DBStar: Record, DBModelProtocol { try self.message?.deleteAggregate(db) return try self.delete(db) } - } diff --git a/Nynja/DB/Models/DBStarAction.swift b/Nynja/DB/Models/DBStarAction.swift new file mode 100644 index 000000000..9e669edbb --- /dev/null +++ b/Nynja/DB/Models/DBStarAction.swift @@ -0,0 +1,45 @@ +// +// DBStarAction.swift +// Nynja +// +// Created by Anton Poltoratskyi on 29.08.2018. +// Copyright © 2018 TecSynt Solutions. All rights reserved. +// + +import GRDBCipher + +final class DBStarAction: Record, DBModelProtocol { + enum Action: String { + case delete = "delete" + } + + let starLocalId: String + let action: Action + + init(starLocalId: String, action: Action) { + self.starLocalId = starLocalId + self.action = action + super.init() + } + + required init(row: Row) { + starLocalId = row[StarActionTable.Column.starId.title] + action = Action(rawValue: row[StarActionTable.Column.action.title])! + super.init() + } + + override func encode(to container: inout PersistenceContainer) { + container[StarActionTable.Column.starId.title] = starLocalId + container[StarActionTable.Column.action.title] = action.rawValue + } + + override static var databaseTableName: String { + return StarActionTable.name + } + + static func starAction(_ db: Database, starLocalId: String) throws -> DBStarAction? { + let starIdColumn = Column(StarActionTable.Column.starId.title) + let filter: SQLExpressible = starIdColumn == starLocalId + return try DBStarAction.filter(filter).fetchOne(db) + } +} diff --git a/Nynja/DB/Models/DBStarMessage.swift b/Nynja/DB/Models/DBStarMessage.swift index 90d9bb181..c3143042d 100644 --- a/Nynja/DB/Models/DBStarMessage.swift +++ b/Nynja/DB/Models/DBStarMessage.swift @@ -9,7 +9,7 @@ import Foundation import GRDBCipher -class DBStarMessage: Record, DBModelProtocol { +final class DBStarMessage: Record, DBModelProtocol { var id: Int64? var container: String? diff --git a/Nynja/DB/Tables/Base/Table.swift b/Nynja/DB/Tables/Base/Table.swift index d37700328..b798936cf 100644 --- a/Nynja/DB/Tables/Base/Table.swift +++ b/Nynja/DB/Tables/Base/Table.swift @@ -9,9 +9,31 @@ import GRDBCipher protocol Table { - static var name: String { get } - static func create(in db: Database) throws +} + +extension Table { + + static func createIfNotExists(in db: Database) throws { + if try !db.tableExists(self) { + try create(in: db) + } + } + + static func drop(in db: Database) throws { + try db.drop(table: self) + } + + static func alter(in db: Database, body: (TableAlteration) -> Void) throws { + try db.alter(table: self, body: body) + } + + static func rename(to newName: String, in db: Database) throws { + try db.rename(table: self, to: newName) + } + static func hasColumns(_ columns: Set, in db: Database) throws -> Bool { + return try db.hasColumns(columns, in: self) + } } diff --git a/Nynja/DB/Tables/Base/TableOrder.swift b/Nynja/DB/Tables/Base/TableOrder.swift new file mode 100644 index 000000000..ca83a8d20 --- /dev/null +++ b/Nynja/DB/Tables/Base/TableOrder.swift @@ -0,0 +1,12 @@ +// +// TableOrder.swift +// Nynja +// +// Created by Anton Poltoratskyi on 17.08.2018. +// Copyright © 2018 TecSynt Solutions. All rights reserved. +// + +enum TableOrder: String { + case asc + case desc +} diff --git a/Nynja/DB/Tables/ChatCheckpointTable.swift b/Nynja/DB/Tables/ChatCheckpointTable.swift index bdbb03a2d..a8cdc27e4 100644 --- a/Nynja/DB/Tables/ChatCheckpointTable.swift +++ b/Nynja/DB/Tables/ChatCheckpointTable.swift @@ -8,14 +8,14 @@ import GRDBCipher -class ChatCheckpointTable : Table { +final class ChatCheckpointTable : Table { class var name: String { return "chat_checkpoint" } static func create(in db: Database) throws { - try db.create(table: ChatCheckpointTable.name) { (t) in + try db.create(self) { t in t.column(Column.feedId, .integer).notNull() t.column(Column.feedType, .integer).notNull() t.column(Column.serverId, .integer) @@ -37,6 +37,5 @@ extension ChatCheckpointTable { case messageId case topOffset } - } diff --git a/Nynja/DB/Tables/ContactTable.swift b/Nynja/DB/Tables/ContactTable.swift index 9285b90ee..7c2280d77 100644 --- a/Nynja/DB/Tables/ContactTable.swift +++ b/Nynja/DB/Tables/ContactTable.swift @@ -8,14 +8,14 @@ import GRDBCipher -class ContactTable: Table { +final class ContactTable: Table { class var name: String { return "contact" } static func create(in db: Database) throws { - try db.create(table: ContactTable.name) { t in + try db.create(self) { t in t.column(Column.phoneId, .text).notNull().primaryKey() t.column(Column.avatar, .text) t.column(Column.names, .text) @@ -32,7 +32,6 @@ class ContactTable: Table { t.column(Column.rosterId, .integer).references(RosterTable.name, onDelete: .cascade) } } - } extension ContactTable { @@ -52,5 +51,4 @@ extension ContactTable { case messageId case rosterId } - } diff --git a/Nynja/DB/Tables/ConvertMessageTable.swift b/Nynja/DB/Tables/ConvertMessageTable.swift new file mode 100644 index 000000000..5fb93c51d --- /dev/null +++ b/Nynja/DB/Tables/ConvertMessageTable.swift @@ -0,0 +1,40 @@ +// +// ConvertMessageTable.swift +// Nynja +// +// Created by Andrey Reznik on 23.08.2018. +// Copyright © 2018 TecSynt Solutions. All rights reserved. +// + +import GRDBCipher + +class ConvertMessageTable: Table { + + class var name: String { + return "convert_message" + } + + static func create(in db: Database) throws { + try db.create(self) { t in + t.column(Column.messageId, .text) + t.column(Column.type, .integer) + t.column(Column.process, .integer) + t.column(Column.value, .text) + t.column(Column.language, .text) + t.primaryKey([Column.messageId.title, Column.type.title], onConflict: .replace) + } + } +} + +// MARK: Column +extension ConvertMessageTable { + + enum Column: Int, Describable { + case messageId + case type + case process + case value + case language + } + +} diff --git a/Nynja/DB/Tables/DescTable.swift b/Nynja/DB/Tables/DescTable.swift index 97ab32582..79b491860 100644 --- a/Nynja/DB/Tables/DescTable.swift +++ b/Nynja/DB/Tables/DescTable.swift @@ -15,11 +15,7 @@ final class DescTable: Table { } static func create(in db: Database) throws { - try create(in: db, name: DescTable.name) - } - - static func create(in db: Database, name: String) throws { - try db.create(table: name) { t in + try db.create(self) { t in t.primaryKey([Column.serverId.title, Column.targetType.title], onConflict: nil) t.column(Column.serverId, .text).notNull() t.column(Column.mime, .text) diff --git a/Nynja/DB/Tables/FeatureTable.swift b/Nynja/DB/Tables/FeatureTable.swift index 0bae0441b..6892e1d5d 100644 --- a/Nynja/DB/Tables/FeatureTable.swift +++ b/Nynja/DB/Tables/FeatureTable.swift @@ -8,14 +8,14 @@ import GRDBCipher -class FeatureTable: Table { +final class FeatureTable: Table { class var name: String { return "feature" } static func create(in db: Database) throws { - try db.create(table: FeatureTable.name, body: { (t) in + try db.create(self) { t in t.primaryKey([Column.id.title, Column.targetId.title, Column.targetType.title], onConflict: nil) t.column(Column.id, .text).notNull() t.column(Column.key, .text).notNull() @@ -23,9 +23,8 @@ class FeatureTable: Table { t.column(Column.group, .text) t.column(Column.targetId, .text).notNull() t.column(Column.targetType, .text).notNull() - }) + } } - } // MARK: - Column @@ -39,5 +38,4 @@ extension FeatureTable { case targetId case targetType } - } diff --git a/Nynja/DB/Tables/JobMessageTable.swift b/Nynja/DB/Tables/JobMessageTable.swift index 4a7bae3bf..cbd6bdb48 100644 --- a/Nynja/DB/Tables/JobMessageTable.swift +++ b/Nynja/DB/Tables/JobMessageTable.swift @@ -6,18 +6,16 @@ // Copyright © 2018 TecSynt Solutions. All rights reserved. // -import Foundation - import GRDBCipher -class JobMessageTable: Table { +final class JobMessageTable: Table { class var name: String { return "job_message" } static func create(in db: Database) throws { - try db.create(table: JobMessageTable.name, body: { (t) in + try db.create(self) { t in t.column(JobMessageTable.Column.id, .integer).primaryKey(onConflict: nil, autoincrement: true) t.column(JobMessageTable.Column.container, .text) t.column(JobMessageTable.Column.feedId, .integer) @@ -35,9 +33,8 @@ class JobMessageTable: Table { t.column(JobMessageTable.Column.mentioned, .text) t.column(JobMessageTable.Column.status, .text) t.column(JobMessageTable.Column.jobId, .integer) - }) + } } - } // MARK: - Column @@ -62,5 +59,4 @@ extension JobMessageTable { case status case jobId } - } diff --git a/Nynja/DB/Tables/JobTable.swift b/Nynja/DB/Tables/JobTable.swift index adbab29f7..ce966f7f0 100644 --- a/Nynja/DB/Tables/JobTable.swift +++ b/Nynja/DB/Tables/JobTable.swift @@ -6,18 +6,16 @@ // Copyright © 2018 TecSynt Solutions. All rights reserved. // -import Foundation - import GRDBCipher -class JobTable: Table { +final class JobTable: Table { class var name: String { return "job" } static func create(in db: Database) throws { - try db.create(table: JobTable.name, body: { (t) in + try db.create(self) { t in t.column(JobTable.Column.id, .integer).primaryKey(onConflict: nil, autoincrement: true) t.column(JobTable.Column.serverId, .integer) t.column(JobTable.Column.container, .text) @@ -27,9 +25,8 @@ class JobTable: Table { t.column(JobTable.Column.time, .integer) t.column(JobTable.Column.status, .text) t.column(JobTable.Column.type, .integer).notNull().defaults(to: DBJob.JobType.schedule.rawValue) - }) + } } - } // MARK: - Column @@ -46,5 +43,4 @@ extension JobTable { case status case type } - } diff --git a/Nynja/DB/Tables/LinkTable.swift b/Nynja/DB/Tables/LinkTable.swift index 36884f8bd..474ab23f3 100644 --- a/Nynja/DB/Tables/LinkTable.swift +++ b/Nynja/DB/Tables/LinkTable.swift @@ -10,12 +10,12 @@ import GRDBCipher final class LinkTable: Table { - static var name: String { + class var name: String { return "link" } static func create(in db: Database) throws { - try db.create(table: LinkTable.name) { table in + try db.create(self) { table in table.column(Column.id, .text).notNull().primaryKey() table.column(Column.name, .text).notNull() table.column(Column.roomId, .text).notNull().references(RoomTable.name, onDelete: .cascade) @@ -23,7 +23,6 @@ final class LinkTable: Table { table.column(Column.status, .text) } } - } @@ -38,5 +37,4 @@ extension LinkTable { case created case status } - } diff --git a/Nynja/DB/Tables/MemberTable.swift b/Nynja/DB/Tables/MemberTable.swift index 29ba576d8..3a3184c1d 100644 --- a/Nynja/DB/Tables/MemberTable.swift +++ b/Nynja/DB/Tables/MemberTable.swift @@ -8,14 +8,14 @@ import GRDBCipher -class MemberTable: Table { +final class MemberTable: Table { class var name: String { return "member" } static func create(in db: Database) throws { - try db.create(table: MemberTable.name, body: { (t) in + try db.create(self) { t in t.column(Column.id, .integer).primaryKey() t.column(Column.container, .text) t.column(Column.feedId, .integer) @@ -31,9 +31,8 @@ class MemberTable: Table { t.column(Column.update, .integer) t.column(Column.presence, .text) t.column(Column.status, .text) - }) + } } - } // MARK: Column diff --git a/Nynja/DB/Tables/MessageActionTable.swift b/Nynja/DB/Tables/MessageActionTable.swift index 816b7e561..ed0bd81cf 100644 --- a/Nynja/DB/Tables/MessageActionTable.swift +++ b/Nynja/DB/Tables/MessageActionTable.swift @@ -8,21 +8,20 @@ import GRDBCipher -class MessageActionTable: Table { +final class MessageActionTable: Table { class var name: String { return "message_action" } static func create(in db: Database) throws { - try db.create(table: MessageActionTable.name, body: { (t) in + try db.create(self) { t in t.column(Column.messageId, .integer).primaryKey(onConflict: .replace, autoincrement: false) t.column(Column.seenBy, .integer) - t.column(Column.phoneId, .text).defaults(to: "") - t.column(Column.action, .text).defaults(to: false) - }) + t.column(Column.phoneId, .text) + t.column(Column.action, .text) + } } - } // MARK: Column @@ -34,5 +33,4 @@ extension MessageActionTable { case phoneId case action } - } diff --git a/Nynja/DB/Tables/MessageEditActionTable.swift b/Nynja/DB/Tables/MessageEditActionTable.swift index cd7361f2d..d469c112c 100644 --- a/Nynja/DB/Tables/MessageEditActionTable.swift +++ b/Nynja/DB/Tables/MessageEditActionTable.swift @@ -15,12 +15,10 @@ final class MessageEditActionTable: Table { } static func create(in db: Database) throws { - try db.create(table: MessageEditActionTable.name) { t in - t.column(Column.messageId, .integer) + try db.create(self) { t in + t.column(Column.messageId, .integer).primaryKey(onConflict: .replace, autoincrement: false) t.column(Column.payload, .text) - t.column(Column.mentioned, .text) - t.primaryKey([Column.messageId.title], onConflict: .replace) } } } diff --git a/Nynja/DB/Tables/MessageLinkTable.swift b/Nynja/DB/Tables/MessageLinkTable.swift index edf9a19cf..d540ea7c0 100644 --- a/Nynja/DB/Tables/MessageLinkTable.swift +++ b/Nynja/DB/Tables/MessageLinkTable.swift @@ -8,21 +8,20 @@ import GRDBCipher -class MessageLinkTable: Table { +final class MessageLinkTable: Table { class var name: String { return "message_link" } static func create(in db: Database) throws { - try db.create(table: MessageLinkTable.name, body: { (t) in + try db.create(self) { t in t.primaryKey([Column.value.title, Column.feedId.title, Column.feedType.title], onConflict: nil) t.column(Column.value, .text) t.column(Column.feedId, .text) t.column(Column.feedType, .integer) - }) + } } - } // MARK: - Column @@ -34,5 +33,4 @@ extension MessageLinkTable { case feedType case feedId } - } diff --git a/Nynja/DB/Tables/MessageTable.swift b/Nynja/DB/Tables/MessageTable.swift index 93cdfcf3e..c65e1fd62 100644 --- a/Nynja/DB/Tables/MessageTable.swift +++ b/Nynja/DB/Tables/MessageTable.swift @@ -15,7 +15,7 @@ final class MessageTable: Table { } static func create(in db: Database) throws { - try db.create(table: MessageTable.name, body: { (t) in + try db.create(self) { t in t.column(Column.id, .integer).primaryKey(onConflict: nil, autoincrement: true) t.column(Column.container, .text) t.column(Column.feedId, .integer) @@ -32,7 +32,8 @@ final class MessageTable: Table { t.column(Column.repliedBy, .text) t.column(Column.mentioned, .text) t.column(Column.status, .text) - }) + t.column(Column.trusted, .boolean) + } } } @@ -56,5 +57,6 @@ extension MessageTable { case repliedBy case mentioned case status + case trusted } } diff --git a/Nynja/DB/Tables/MucTable.swift b/Nynja/DB/Tables/MucTable.swift index 0640a0803..e4974192a 100644 --- a/Nynja/DB/Tables/MucTable.swift +++ b/Nynja/DB/Tables/MucTable.swift @@ -8,19 +8,18 @@ import GRDBCipher -class MucTable: Table { +final class MucTable: Table { class var name: String { return "muc" } static func create(in db: Database) throws { - try db.create(table: MucTable.name, body: { (t) in + try db.create(self) { t in t.column(Column.id, .integer).notNull().primaryKey(onConflict: nil, autoincrement: true) t.column(Column.name, .text).notNull().unique(onConflict: .replace) - }) + } } - } // MARK: - Column @@ -30,5 +29,4 @@ extension MucTable { case id case name } - } diff --git a/Nynja/DB/Tables/P2pTable.swift b/Nynja/DB/Tables/P2pTable.swift index 7b1d3f8dd..2b56c7267 100644 --- a/Nynja/DB/Tables/P2pTable.swift +++ b/Nynja/DB/Tables/P2pTable.swift @@ -8,22 +8,21 @@ import GRDBCipher -class P2pTable: Table { +final class P2pTable: Table { class var name: String { return "p2p" } static func create(in db: Database) throws { - try db.create(table: P2pTable.name, body: { (t) in + try db.create(self) { t in t.column(Column.id, .integer).notNull().primaryKey(onConflict: nil, autoincrement: true) t.column(Column.from, .text).notNull t.column(Column.to, .text).notNull t.uniqueKey([Column.from.title, Column.to.title], onConflict: .replace) - }) + } } - } // MARK: - Column @@ -34,5 +33,4 @@ extension P2pTable { case from case to } - } diff --git a/Nynja/DB/Tables/ProfileTable.swift b/Nynja/DB/Tables/ProfileTable.swift index bedd2e103..44507c5f3 100644 --- a/Nynja/DB/Tables/ProfileTable.swift +++ b/Nynja/DB/Tables/ProfileTable.swift @@ -8,14 +8,14 @@ import GRDBCipher -class ProfileTable: Table { +final class ProfileTable: Table { class var name: String { return "profile" } static func create(in db: Database) throws { - try db.create(table: ProfileTable.name) { (t) in + try db.create(self) { t in t.column(Column.phone, .text).notNull().primaryKey() t.column(Column.update, .integer).notNull().defaults(to: 0) t.column(Column.balance, .integer).notNull().defaults(to: 0) @@ -23,7 +23,6 @@ class ProfileTable: Table { t.column(Column.status, .text) // Note: perhaps enum } } - } extension ProfileTable { @@ -35,5 +34,4 @@ extension ProfileTable { case presence case status } - } diff --git a/Nynja/DB/Tables/RecentStickerTable.swift b/Nynja/DB/Tables/RecentStickerTable.swift index 37e230659..6f7476799 100644 --- a/Nynja/DB/Tables/RecentStickerTable.swift +++ b/Nynja/DB/Tables/RecentStickerTable.swift @@ -20,7 +20,7 @@ final class RecentStickerTable: Table { } static func create(in db: Database) throws { - try db.create(table: RecentStickerTable.name) { t in + try db.create(self) { t in t.column(Column.id, .integer).primaryKey(onConflict: .replace, autoincrement: true) t.column(Column.stickerId, .text) } diff --git a/Nynja/DB/Tables/RoomMemberTable.swift b/Nynja/DB/Tables/RoomMemberTable.swift index 9c27d5533..f3a41b8b2 100644 --- a/Nynja/DB/Tables/RoomMemberTable.swift +++ b/Nynja/DB/Tables/RoomMemberTable.swift @@ -8,14 +8,14 @@ import GRDBCipher -class RoomMemberTable: Table { +final class RoomMemberTable: Table { class var name: String { return "room_member" } static func create(in db: Database) throws { - try db.create(table: RoomMemberTable.name) { t in + try db.create(self) { t in t.column(Column.roomId, .text) t.column(Column.memberId, .integer) t.column(Column.isAdmin, .boolean).defaults(to: false) @@ -26,7 +26,6 @@ class RoomMemberTable: Table { t.foreignKey([Column.memberId.title], references: MemberTable.name, onDelete: .cascade) } } - } // MARK: Column @@ -37,5 +36,4 @@ extension RoomMemberTable { case memberId /// phoneID case isAdmin } - } diff --git a/Nynja/DB/Tables/RoomTable.swift b/Nynja/DB/Tables/RoomTable.swift index 8cea93bff..ac54bcc4e 100644 --- a/Nynja/DB/Tables/RoomTable.swift +++ b/Nynja/DB/Tables/RoomTable.swift @@ -8,14 +8,14 @@ import GRDBCipher -class RoomTable: Table { +final class RoomTable: Table { class var name: String { return "room" } static func create(in db: Database) throws { - try db.create(table: RoomTable.name) { t in + try db.create(self) { t in t.column(Column.id, .text).primaryKey() t.column(Column.name, .text).notNull() t.column(Column.description, .text) @@ -35,7 +35,6 @@ class RoomTable: Table { t.column(Column.rosterId, .integer).references(RosterTable.name) } } - } extension RoomTable { diff --git a/Nynja/DB/Tables/RosterTable.swift b/Nynja/DB/Tables/RosterTable.swift index b1bb8fa09..a70d1032d 100644 --- a/Nynja/DB/Tables/RosterTable.swift +++ b/Nynja/DB/Tables/RosterTable.swift @@ -8,14 +8,14 @@ import GRDBCipher -class RosterTable: Table { +final class RosterTable: Table { class var name: String { return "roster" } static func create(in db: Database) throws { - try db.create(table: RosterTable.name, body: { (t) in + try db.create(self) { t in t.column(Column.id, .integer).notNull().primaryKey() t.column(Column.names, .text).notNull() t.column(Column.surnames, .text) @@ -26,9 +26,8 @@ class RosterTable: Table { t.column(Column.update, .integer).notNull().defaults(to: 0) t.column(Column.status, .text) t.column(Column.profileId, .text).references(ProfileTable.name, onDelete: .cascade) - }) + } } - } extension RosterTable { @@ -45,5 +44,4 @@ extension RosterTable { case status case profileId } - } diff --git a/Nynja/DB/Tables/ServiceTable.swift b/Nynja/DB/Tables/ServiceTable.swift index 8747e150c..df5dd7993 100644 --- a/Nynja/DB/Tables/ServiceTable.swift +++ b/Nynja/DB/Tables/ServiceTable.swift @@ -8,14 +8,14 @@ import GRDBCipher -class ServiceTable: Table { +final class ServiceTable: Table { class var name: String { return "service" } static func create(in db: Database) throws { - try db.create(table: ServiceTable.name) { (t) in + try db.create(self) { t in t.primaryKey([Column.id.title, Column.targetId.title, Column.targetType.title], onConflict: nil) t.column(Column.id, .text).notNull() t.column(Column.type, .text) @@ -28,7 +28,6 @@ class ServiceTable: Table { t.column(Column.targetType, .text).notNull() } } - } extension ServiceTable { @@ -44,6 +43,4 @@ extension ServiceTable { case targetId case targetType } - } - diff --git a/Nynja/DB/Tables/StarActionTable.swift b/Nynja/DB/Tables/StarActionTable.swift new file mode 100644 index 000000000..e90649168 --- /dev/null +++ b/Nynja/DB/Tables/StarActionTable.swift @@ -0,0 +1,36 @@ +// +// StarActionTable.swift +// Nynja +// +// Created by Anton Poltoratskyi on 29.08.2018. +// Copyright © 2018 TecSynt Solutions. All rights reserved. +// + +import GRDBCipher + +final class StarActionTable: Table { + + class var name: String { + return "star_action" + } + + static func create(in db: Database) throws { + try db.create(self) { t in + t.column(Column.starId, .text) + .notNull() + .primaryKey(onConflict: .replace, autoincrement: false) + .references(StarTable.name, column: StarTable.Column.clientId.title, onDelete: .cascade) + + t.column(Column.action, .text).notNull() + } + } +} + +// MARK: Column +extension StarActionTable { + + enum Column: Int, Describable { + case starId + case action + } +} diff --git a/Nynja/DB/Tables/StarMessageTable.swift b/Nynja/DB/Tables/StarMessageTable.swift index 9346e58e1..696f8bdad 100644 --- a/Nynja/DB/Tables/StarMessageTable.swift +++ b/Nynja/DB/Tables/StarMessageTable.swift @@ -6,18 +6,16 @@ // Copyright © 2018 TecSynt Solutions. All rights reserved. // -import Foundation - import GRDBCipher -class StarMessageTable: Table { +final class StarMessageTable: Table { class var name: String { return "star_message" } static func create(in db: Database) throws { - try db.create(table: StarMessageTable.name, body: { (t) in + try db.create(self) { t in t.column(Column.id, .integer).primaryKey(onConflict: nil, autoincrement: true) t.column(Column.container, .text) t.column(Column.feedId, .integer) @@ -34,9 +32,8 @@ class StarMessageTable: Table { t.column(Column.repliedBy, .text) t.column(Column.mentioned, .text) t.column(Column.status, .text) - }) + } } - } // MARK: - Column @@ -66,5 +63,4 @@ extension StarMessageTable { case senderAvatar // -- } - } diff --git a/Nynja/DB/Tables/StarTable.swift b/Nynja/DB/Tables/StarTable.swift index 78be85c40..1768cacb6 100644 --- a/Nynja/DB/Tables/StarTable.swift +++ b/Nynja/DB/Tables/StarTable.swift @@ -8,22 +8,21 @@ import GRDBCipher -class StarTable: Table { +final class StarTable: Table { class var name: String { return "star" } static func create(in db: Database) throws { - try db.create(table: StarTable.name, body: { (t) in + try db.create(self) { t in t.column(Column.id, .integer) t.column(Column.clientId, .text).notNull().primaryKey() t.column(Column.status, .text) t.column(Column.rosterId, .integer).references(RosterTable.name, onDelete: .cascade) t.column(Column.messageId, .integer).references(StarMessageTable.name, onDelete: .cascade) - }) + } } - } // MARK: - Column @@ -36,5 +35,4 @@ extension StarTable { case rosterId case messageId } - } diff --git a/Nynja/DB/Tables/StickerPackTable.swift b/Nynja/DB/Tables/StickerPackTable.swift index 6feb58172..4f0d1b8a2 100644 --- a/Nynja/DB/Tables/StickerPackTable.swift +++ b/Nynja/DB/Tables/StickerPackTable.swift @@ -26,7 +26,7 @@ final class StickerPackTable: Table { } static func create(in db: Database) throws { - try db.create(table: StickerPackTable.name) { t in + try db.create(self) { t in t.column(Column.id, .integer).primaryKey(onConflict: .replace) t.column(Column.name, .text) t.column(Column.keywords, .text) diff --git a/Nynja/DB/Tables/SyncFileTable.swift b/Nynja/DB/Tables/SyncFileTable.swift index 51110a351..9257a6f3c 100644 --- a/Nynja/DB/Tables/SyncFileTable.swift +++ b/Nynja/DB/Tables/SyncFileTable.swift @@ -8,21 +8,20 @@ import GRDBCipher -class SyncFileTable: Table { +final class SyncFileTable: Table { class var name: String { return "sync_file" } static func create(in db: Database) throws { - try db.create(table: SyncFileTable.name) { (t) in + try db.create(self) { t in t.column(Column.id, .integer) .primaryKey(onConflict: .rollback, autoincrement: true) t.column(Column.serverLink, .text) t.column(Column.localLink, .text) } } - } extension SyncFileTable { @@ -32,6 +31,5 @@ extension SyncFileTable { case serverLink case localLink } - } diff --git a/Nynja/DB/Tables/TagTable.swift b/Nynja/DB/Tables/TagTable.swift index fd3bff1a1..7c4cfacd0 100644 --- a/Nynja/DB/Tables/TagTable.swift +++ b/Nynja/DB/Tables/TagTable.swift @@ -8,14 +8,14 @@ import GRDBCipher -class TagTable: Table { +final class TagTable: Table { class var name: String { return "tag" } static func create(in db: Database) throws { - try db.create(table: TagTable.name, body: { (t) in + try db.create(self) { t in t.column(Column.id, .integer).notNull().primaryKey(onConflict: nil, autoincrement: true) t.column(Column.name, .text) t.column(Column.color, .text) @@ -23,9 +23,8 @@ class TagTable: Table { t.column(Column.rosterId, .integer).references(RosterTable.name) t.column(Column.starId, .integer).references(StarTable.name) - }) + } } - } // MARK: - Column diff --git a/Nynja/DBManagerProtocol.swift b/Nynja/DBManagerProtocol.swift index c967463bc..cce9695e9 100644 --- a/Nynja/DBManagerProtocol.swift +++ b/Nynja/DBManagerProtocol.swift @@ -17,9 +17,13 @@ enum DatabaseAction { protocol DBManagerProtocol { - // MAKR: - Fethching + // MARK: - Fethching func fetch(_ closure: (Database) throws -> T?) -> T? func fetch(_ closure: (Database) throws -> [T]) -> [T] + func rowExists(in table: T.Type, where condition: String, arguments: StatementArguments?) -> Bool + + // MARK: - Writing + func write(_ closure: (Database) throws -> Void) throws // MARK: - Perform Actions func perform(action: DatabaseAction, with model: DBModelConvertible) throws diff --git a/Nynja/DatabaseManager.swift b/Nynja/DatabaseManager.swift index 98d1d6532..9794ea67b 100644 --- a/Nynja/DatabaseManager.swift +++ b/Nynja/DatabaseManager.swift @@ -30,7 +30,7 @@ final class DatabaseManager: DBManagerProtocol { try block(dbPool) } } catch let error { - LogService.log(topic: .db, text: error.localizedDescription) + LogService.log(topic: .db) { return error.localizedDescription } throw error } } @@ -54,26 +54,16 @@ final class DatabaseManager: DBManagerProtocol { // MARK: - Setup DB - func setupDatabase(with name: String, oldPassphrase: String?, newPassphrase: String, application: UIApplication) { + func setupDatabase(with name: String, encryptionMode mode: EncryptionMode, application: UIApplication) { do { let name = "\(name).sqlite" - let newConfiguration = makeConfiguration(with: newPassphrase) - let isFirstEncryption = try encryptOldDatabase(with: newConfiguration, newName: name) + let isFirstEncryption = try encryptOldDatabase(with: mode, newName: name) if let path = fileManagerService.getPathOfFile(folder: folderName, name: name) { - let passphrase = isFirstEncryption ? newPassphrase : oldPassphrase - let configuration = makeConfiguration(with: passphrase) - - dbPool = try DatabasePool(path: path, configuration: configuration) - try performMigration() - - if !isFirstEncryption { - try dbPool?.change(passphrase: newPassphrase) - } + try setupExistedDatabase(at: path, mode: mode, isFirstEncryption: isFirstEncryption) } else if let path = fileManagerService.createFile(folder: folderName, name: name) { - dbPool = try DatabasePool(path: path, configuration: newConfiguration) - createTables() + try setupNewDatabase(at: path, mode: mode) } else { throw DatabaseError.hasNoDBFile } @@ -81,21 +71,41 @@ final class DatabaseManager: DBManagerProtocol { dbPool?.add(transactionObserver: DBObserver.default) dbPool?.setupMemoryManagement(in: application) } catch let error { - LogService.log(topic: .db, text: error.localizedDescription) + LogService.log(topic: .db) { return error.localizedDescription } + } + } + + private func setupExistedDatabase(at path: String, mode: EncryptionMode, isFirstEncryption: Bool) throws { + let passphrase = mode.passphraseForFirstEncryption(isFirstEncryption) + let configuration = makeConfiguration(with: passphrase) + + dbPool = try DatabasePool(path: path, configuration: configuration) + try performMigration() + + if !isFirstEncryption, let newPassphrase = mode.newPassphrase { + try dbPool?.change(passphrase: newPassphrase) } } + private func setupNewDatabase(at path: String, mode: EncryptionMode) throws { + let configuration = makeConfiguration(with: mode.newPassphrase) + + dbPool = try DatabasePool(path: path, configuration: configuration) + createTables() + } + private func makeConfiguration(with passphrase: String?) -> Configuration { var configuration = Configuration() configuration.passphrase = passphrase configuration.trace = { info in - let log = "DB Path: \(self.dbPool?.path ?? "")" + "\n" + info - LogService.log(topic: .db, text: log) + LogService.log(topic: .db) { return "DB Path: \(self.dbPool?.path ?? "")" + "\n" + info } } return configuration } - private func encryptOldDatabase(with configuration: Configuration, newName name: String) throws -> Bool { + private func encryptOldDatabase(with mode: EncryptionMode, newName name: String) throws -> Bool { + let configuration = makeConfiguration(with: mode.newPassphrase) + let oldFileName = "mainDB.sqlite" guard let oldPath = fileManagerService.getPathOfFile(folder: folderName, name: oldFileName), @@ -154,6 +164,7 @@ final class DatabaseManager: DBManagerProtocol { func clear() { dbPool = nil try? fileManagerService.removeFiles(in: folderName, contains: ".sqlite") + LogService.log(topic: .db) { return "Clear database" } } // MARK: - Fetching @@ -178,6 +189,26 @@ final class DatabaseManager: DBManagerProtocol { return models } + func rowExists(in table: T.Type, where condition: String, arguments: StatementArguments?) -> Bool { + let sql = """ + SELECT EXISTS(SELECT 1 FROM \(T.name) WHERE \(condition)) + """ + + return fetch { db in + return try SQLRequest(sql, arguments: arguments).asRequest(of: Bool.self).fetchOne(db) + } ?? false + } + + + // MARK: - Writing + + func write(_ closure: (Database) throws -> Void) throws { + try writeInTransaction { db in + try closure(db) + return .commit + } + } + // MARK: - Perform Actions with DB @@ -213,7 +244,6 @@ final class DatabaseManager: DBManagerProtocol { return .commit } } - } @@ -255,7 +285,49 @@ extension DatabaseManager { MessageEditActionTable.self, RecentStickerTable.self, - StickerPackTable.self + StickerPackTable.self, + + ConvertMessageTable.self, + StarActionTable.self ] +} + + +// MARK: - EncryptionMode + +extension DatabaseManager { + + enum EncryptionMode { + case reencrypt(usingOld: String?, new: String) + case none + + var shouldEncrypt: Bool { + if case .none = self { + return false + } + + return true + } + + var newPassphrase: String? { + if case .reencrypt(usingOld: _, new: let new) = self { + return new + } + + return nil + } + + var oldPassphrase: String? { + if case .reencrypt(usingOld: let old, new: _) = self { + return old + } + + return nil + } + + func passphraseForFirstEncryption(_ isFirstEncryption: Bool) -> String? { + return isFirstEncryption ? newPassphrase : oldPassphrase + } + } } diff --git a/Nynja/Debug/DebugLogs.swift b/Nynja/Debug/DebugLogs.swift index acfaaa580..dd8ea8f2d 100644 --- a/Nynja/Debug/DebugLogs.swift +++ b/Nynja/Debug/DebugLogs.swift @@ -9,5 +9,5 @@ import Foundation func deinited(_ object: AnyObject) { - debugPrint("\(type(of: object)) deinitied") + LogService.log(topic: .arc) { "\(type(of: object)) deinited" } } diff --git a/Nynja/DefaultMessageProcessingManager.swift b/Nynja/DefaultMessageProcessingManager.swift index 798c25608..1b8d31f2e 100644 --- a/Nynja/DefaultMessageProcessingManager.swift +++ b/Nynja/DefaultMessageProcessingManager.swift @@ -30,12 +30,6 @@ class DefaultMessagesProcessingManager: DefaultMessagesProcessingManagerInterfac return queue }() - lazy var downloadOperationQueue: OperationQueue = { - let queue = OperationQueue() - queue.maxConcurrentOperationCount = 1 - return queue - }() - static var shared = DefaultMessagesProcessingManager() private let typesWithAttachment = [SendMessageType.audio, .image, .file, .video] @@ -70,6 +64,13 @@ extension DefaultMessagesProcessingManager { return nil } + + func isValidForUploading(_ message: Message) -> Bool { + guard let typeString = message.mainFile?.mime, let type = SendMessageType(rawValue: typeString) else { + return false + } + return typesWithAttachment.contains(type) + } func downloadMessage(_ message: Message) { guard let url = message.mainUrl, @@ -94,8 +95,7 @@ extension DefaultMessagesProcessingManager { return } - let downloadOperation = DownloadOperation(msg: message, url: url, delegate: self, listener: self) - downloadOperationQueue.addOperation(downloadOperation) + TransferManager.shared.download(url: url, listener: self) } func cancelProcessing(_ message: Message) { @@ -105,21 +105,12 @@ extension DefaultMessagesProcessingManager { return } - downloadOperationQueue.operations - .compactMap { $0 as? DownloadOperation } - .filter { $0.url == url } - .forEach { $0.cancel() } - uploadOperationQueue.operations .compactMap { $0 as? UploadOperation } .filter { $0.url == url } .forEach { $0.cancel() } - if message.id != nil { - TransferManager.shared.pauseRequestForUrl(url) - } else { - TransferManager.shared.cancelRequestForUrl(url) - } + TransferManager.shared.cancelRequestForUrl(url) } func progressFor(_ urls: [URL]) -> Dictionary { @@ -219,7 +210,7 @@ private extension DefaultMessagesProcessingManager { let progress = TransferManager.shared.progressForUrl(url) - if progress.status != .notStarted { + if [.atProgress, .done].contains(progress.status) { return progress } diff --git a/Nynja/ExtendedStarHandler.swift b/Nynja/ExtendedStarHandler.swift index 95e84ddfb..0322062f2 100644 --- a/Nynja/ExtendedStarHandler.swift +++ b/Nynja/ExtendedStarHandler.swift @@ -15,6 +15,10 @@ final class ExtendedStarHandler: BaseHandler { let stars = extendedStars.compactMap { extendedStar -> DBStar? in guard let rosterId = StorageService.sharedInstance.rosterId else { return nil } + + if let star = extendedStar.star, StarActionDAO.containsDeleteAction(for: star) { + star.starStatus = .remove + } return DBStar(extendedStar: extendedStar, rosterId: rosterId) } diff --git a/Nynja/Extensions/BERT/StringAtomExtension.swift b/Nynja/Extensions/BERT/StringAtomExtension.swift index 70a6c88b7..2298c30b6 100644 --- a/Nynja/Extensions/BERT/StringAtomExtension.swift +++ b/Nynja/Extensions/BERT/StringAtomExtension.swift @@ -19,5 +19,4 @@ extension StringAtom { return nil } } - } diff --git a/Nynja/Extensions/CollectionsExtensions.swift b/Nynja/Extensions/CollectionsExtensions.swift index a09f52aba..038ceac38 100644 --- a/Nynja/Extensions/CollectionsExtensions.swift +++ b/Nynja/Extensions/CollectionsExtensions.swift @@ -9,7 +9,7 @@ import Foundation extension Collection { - subscript (safe index: Index) -> Element? { + subscript(safe index: Index) -> Element? { return indices.contains(index) ? self[index] : nil } } diff --git a/Nynja/Extensions/Date+Extension.swift b/Nynja/Extensions/Date+Extension.swift index 788e7af2a..d0509965f 100644 --- a/Nynja/Extensions/Date+Extension.swift +++ b/Nynja/Extensions/Date+Extension.swift @@ -61,4 +61,8 @@ extension Date { return Calendar.current.startOfDay(for: self) } + + func isDayEqual(to date: Date) -> Bool { + return Calendar.current.compare(self, to: date, toGranularity: .day) == .orderedSame + } } diff --git a/Nynja/Extensions/JobExtension+BERT.swift b/Nynja/Extensions/JobExtension+BERT.swift index 7ba3bef58..7f46dd6e9 100644 --- a/Nynja/Extensions/JobExtension+BERT.swift +++ b/Nynja/Extensions/JobExtension+BERT.swift @@ -13,6 +13,11 @@ extension Job { func getBert() -> BertObject { let _topic = BertAtom(fromString: "Job") let _id = Bert.getBin(self.id) + let _container: BertObject = self.container + .flatMap { ($0 as? StringAtom)?.string } + .map { BertAtom(fromString: $0) } + ?? BertNil() + let _feed_id = self.feed_id?.getBert() ?? BertNil() let _prev = Bert.getBin(self.prev) let _next = Bert.getBin(self.next) @@ -37,7 +42,7 @@ extension Job { } let statusString = (self.status as? StringAtom)?.string ?? (self.status as? String) let _status = BertAtom(fromString: statusString ?? "") - let model = BertTuple(fromElements: [_topic, _id, BertNil(), _feed_id, _next, _prev, BertNil(), BertNil(), _time, _data, BertNil(), _settings, _status]) + let model = BertTuple(fromElements: [_topic, _id, _container, _feed_id, _next, _prev, BertNil(), BertNil(), _time, _data, BertNil(), _settings, _status]) return model } diff --git a/Nynja/Extensions/Models/Desc/Desc+Construct.swift b/Nynja/Extensions/Models/Desc/Desc+Construct.swift new file mode 100644 index 000000000..47a8f266d --- /dev/null +++ b/Nynja/Extensions/Models/Desc/Desc+Construct.swift @@ -0,0 +1,20 @@ +// +// Desc+Construct.swift +// Nynja +// +// Created by Anton Poltoratskyi on 22.08.2018. +// Copyright © 2018 TecSynt Solutions. All rights reserved. +// + +extension Desc { + + convenience init(desc: Desc) { + self.init() + + self.id = desc.id + self.mime = desc.mime + self.payload = desc.payload + self.parentid = desc.parentid + self.data = desc.data + } +} diff --git a/Nynja/Extensions/Models/Desc/Desc+Messages.swift b/Nynja/Extensions/Models/Desc/Desc+Messages.swift index 861cc44b0..d0329ed6b 100644 --- a/Nynja/Extensions/Models/Desc/Desc+Messages.swift +++ b/Nynja/Extensions/Models/Desc/Desc+Messages.swift @@ -200,3 +200,9 @@ extension Desc { return (origin: originalText, translate: translatedText) } } + +extension Desc { + var filename: String? { + return value(forKey: "FILENAME") + } +} diff --git a/Nynja/Extensions/Models/Desc/DescExtension.swift b/Nynja/Extensions/Models/Desc/DescExtension.swift index 9e8a57c8a..7d7c725d5 100644 --- a/Nynja/Extensions/Models/Desc/DescExtension.swift +++ b/Nynja/Extensions/Models/Desc/DescExtension.swift @@ -24,15 +24,6 @@ enum DescMime { // MARK: - Factory methods extension Desc { - convenience init(desc: Desc) { - self.init() - - self.id = desc.id - self.mime = desc.mime - self.payload = desc.payload - self.parentid = desc.parentid - self.data = desc.data - } convenience init(mime: DescMime) { self.init() @@ -66,8 +57,9 @@ extension Desc { // MARK: - Clone func cloned() -> Desc { - let clone = Desc(desc: self) + let clone = Desc.init(desc: self) clone.id = IdBuilder(format: .defaultId).build() + clone.data = self.data?.map() { Feature(clone: $0) } return clone } } diff --git a/Nynja/Extensions/Models/Feature/FeatureExtension.swift b/Nynja/Extensions/Models/Feature/FeatureExtension.swift index c12d21acd..0f7588448 100644 --- a/Nynja/Extensions/Models/Feature/FeatureExtension.swift +++ b/Nynja/Extensions/Models/Feature/FeatureExtension.swift @@ -253,6 +253,19 @@ extension Feature { return result.isEmpty ? nil : result } + convenience init(clone: Feature) { + self.init() + let key = clone.key ?? "" + let builder = IdBuilder.init(format: .featureId) + .addValueForComponent(key, .key) + let id = builder.build() + + self.id = id + self.key = key + self.value = clone.value + self.group = clone.group + } + static func fileDataFeature(forKey key: T, value: String) -> Feature where T.RawValue == String { return rawFileDataFeature(forKey: key.rawValue, value: value) diff --git a/Nynja/Extensions/Models/JobExtension.swift b/Nynja/Extensions/Models/JobExtension.swift index 0b2d13f9c..e2f507e9c 100644 --- a/Nynja/Extensions/Models/JobExtension.swift +++ b/Nynja/Extensions/Models/JobExtension.swift @@ -14,8 +14,14 @@ extension Job : CellModel { extension Job { - convenience init(phoneId: String, messages: [Message], timestamp: Int64?, features: [Feature]?, status: String = "init") { + convenience init(phoneId: String, + container: String?, + messages: [Message], + timestamp: Int64?, + features: [Feature]?, + status: String = "init") { self.init() + self.container = container.flatMap { StringAtom(string: $0) } let a = act() a.name = "publish" a.data = phoneId as AnyObject diff --git a/Nynja/Extensions/Models/MemberExtension.swift b/Nynja/Extensions/Models/MemberExtension.swift index 333516781..5529417fd 100644 --- a/Nynja/Extensions/Models/MemberExtension.swift +++ b/Nynja/Extensions/Models/MemberExtension.swift @@ -50,6 +50,10 @@ extension Member { } } + var isRemoved: Bool { + return memberStatus.isRemoved + } + var fullName: String? { if let name = self.names, let surname = self.surnames { if surname != "" { diff --git a/Nynja/Extensions/Models/Message/Message+DB.swift b/Nynja/Extensions/Models/Message/Message+DB.swift index 16afc63ee..891a54a70 100644 --- a/Nynja/Extensions/Models/Message/Message+DB.swift +++ b/Nynja/Extensions/Models/Message/Message+DB.swift @@ -22,7 +22,7 @@ extension Message { self.to = message.to self.created = message.created as AnyObject? self.types = Set(message.type?.components(separatedBy: Constants.commaSeparator) ?? []) - self.link = message.editMessage + self.link = message.link self.repliedby = message.repliedBy?.splitIntegerIdentifiers() self.mentioned = message.mentioned?.splitIntegerIdentifiers() @@ -38,6 +38,7 @@ extension Message { self.feed_id = muc(muc: feedMuc) } + self.isTrusted = message.isTrusted } convenience init(message: DBStarMessage) { @@ -109,7 +110,11 @@ extension Message { extension Message: DBModelConvertible { - var databaseModel: DBModelProtocol? { + var dbMessage: DBMessage? { return DBMessage(message: self) } + + var databaseModel: DBModelProtocol? { + return dbMessage + } } diff --git a/Nynja/Extensions/Models/P2P+DB.swift b/Nynja/Extensions/Models/P2P+DB.swift index 5142b4788..c00f1c089 100644 --- a/Nynja/Extensions/Models/P2P+DB.swift +++ b/Nynja/Extensions/Models/P2P+DB.swift @@ -11,5 +11,4 @@ extension p2p { convenience init(p2p: DBP2p) { self.init(firstId: p2p.from, secondId: p2p.to) } - } diff --git a/Nynja/Extensions/Models/P2P+Opponent.swift b/Nynja/Extensions/Models/P2P+Opponent.swift new file mode 100644 index 000000000..36a845772 --- /dev/null +++ b/Nynja/Extensions/Models/P2P+Opponent.swift @@ -0,0 +1,21 @@ +// +// P2P+Opponent.swift +// Nynja +// +// Created by Anton Poltoratskyi on 22.08.2018. +// Copyright © 2018 TecSynt Solutions. All rights reserved. +// + +extension p2p { + var opponentId: String? { + guard let phoneId = StorageService.sharedInstance.phoneId else { return nil } + + if phoneId == to { + return from + } else if phoneId == from { + return to + } + + return nil + } +} diff --git a/Nynja/Extensions/Models/P2pExtension.swift b/Nynja/Extensions/Models/P2pExtension.swift index 44c4fd620..4f121876d 100644 --- a/Nynja/Extensions/Models/P2pExtension.swift +++ b/Nynja/Extensions/Models/P2pExtension.swift @@ -21,19 +21,6 @@ extension p2p { convenience init(p2p: p2p) { self.init(firstId: p2p.from, secondId: p2p.to) } - - var opponentId: String? { - guard let phoneId = StorageService.sharedInstance.phoneId else { return nil } - - if phoneId == to { - return from - } else if phoneId == from { - return to - } - - return nil - } - } extension p2p: Equatable { diff --git a/Nynja/Extensions/Models/Room/Room+DB.swift b/Nynja/Extensions/Models/Room/Room+DB.swift index 2fd38437f..7bb5c5631 100644 --- a/Nynja/Extensions/Models/Room/Room+DB.swift +++ b/Nynja/Extensions/Models/Room/Room+DB.swift @@ -42,7 +42,11 @@ extension Room: CellModel { extension Room: DBModelConvertible { - var databaseModel: DBModelProtocol? { + var dbRoom: DBRoom? { return DBRoom(room: self, rosterId: StorageService.sharedInstance.rosterId) } + + var databaseModel: DBModelProtocol? { + return dbRoom + } } diff --git a/Nynja/Extensions/Models/StarExtension.swift b/Nynja/Extensions/Models/StarExtension.swift index 82f5217db..a0f63c181 100644 --- a/Nynja/Extensions/Models/StarExtension.swift +++ b/Nynja/Extensions/Models/StarExtension.swift @@ -106,6 +106,10 @@ extension Star { } return timestamp } + + var isDelivered: Bool { + return id != nil + } } // MARK: - DBModelConvertible @@ -124,6 +128,6 @@ extension Star: DBModelConvertible { self.message = Message(message: msg) } self.roster_id = star.rosterId - self.status = star.status as AnyObject + self.status = StringAtom(string: star.status) } } diff --git a/Nynja/Extensions/Range+Extension.swift b/Nynja/Extensions/Range+Extension.swift index 410f59ea3..632ae77ca 100644 --- a/Nynja/Extensions/Range+Extension.swift +++ b/Nynja/Extensions/Range+Extension.swift @@ -79,3 +79,27 @@ extension Range where Bound == Int { return upperBound == range.lowerBound } } + +extension Range where Bound == Int { + + func intersects(_ range: Range) -> Bool { + return contains(range.lowerBound) + || contains(range.upperBound - 1) + || range.contains(lowerBound) + || range.contains(upperBound - 1) + } + + func intersects(_ range: CountableRange) -> Bool { + return contains(range.lowerBound) + || contains(range.upperBound - 1) + || range.contains(lowerBound) + || range.contains(upperBound - 1) + } + + func intersects(_ range: CountableClosedRange) -> Bool { + return contains(range.lowerBound) + || contains(range.upperBound) + || range.contains(lowerBound) + || range.contains(upperBound - 1) + } +} diff --git a/Nynja/Extensions/SwiftLibrary/Collection/CollectionExtension.swift b/Nynja/Extensions/SwiftLibrary/Collection/BidirectionalCollection.swift similarity index 50% rename from Nynja/Extensions/SwiftLibrary/Collection/CollectionExtension.swift rename to Nynja/Extensions/SwiftLibrary/Collection/BidirectionalCollection.swift index 002a07f02..d57951285 100644 --- a/Nynja/Extensions/SwiftLibrary/Collection/CollectionExtension.swift +++ b/Nynja/Extensions/SwiftLibrary/Collection/BidirectionalCollection.swift @@ -1,37 +1,12 @@ // -// CollectionExtension.swift +// BidirectionalCollection.swift // Nynja // -// Created by Anton Poltoratskyi on 13.08.2018. +// Created by Anton Poltoratskyi on 04.09.2018. // Copyright © 2018 TecSynt Solutions. All rights reserved. // -import Foundation - -extension Collection { - - func joinedByComma() -> String { - return myJoined(separator: Constants.commaSeparator) - } - - func joinedByCommaIfNotEmpty() -> String? { - return myJoinedIfNotEmpty(separator: Constants.commaSeparator) - } - - func myJoined(separator: String = ",") -> String { - return self.map { String(describing: $0) }.joined(separator: separator) - } - - func myJoinedIfNotEmpty(separator: String = ",") -> String? { - if self.isEmpty { - return nil - } else { - return self.myJoined(separator: separator) - } - } -} - -extension BidirectionalCollection where Self: RandomAccessCollection { +extension BidirectionalCollection { func lastIndex(where predicate: (Iterator.Element) -> Bool) -> Index? { guard !isEmpty else { @@ -40,7 +15,7 @@ extension BidirectionalCollection where Self: RandomAccessCollection { var result: Index? var currentIndex = endIndex - + while currentIndex != startIndex { let index = self.index(before: currentIndex) let element = self[index] diff --git a/Nynja/Extensions/SwiftLibrary/Collection/Collection.swift b/Nynja/Extensions/SwiftLibrary/Collection/Collection.swift new file mode 100644 index 000000000..f7e0c58f9 --- /dev/null +++ b/Nynja/Extensions/SwiftLibrary/Collection/Collection.swift @@ -0,0 +1,32 @@ +// +// Collection.swift +// Nynja +// +// Created by Anton Poltoratskyi on 13.08.2018. +// Copyright © 2018 TecSynt Solutions. All rights reserved. +// + +import Foundation + +extension Collection { + + func joinedByComma() -> String { + return myJoined(separator: Constants.commaSeparator) + } + + func joinedByCommaIfNotEmpty() -> String? { + return myJoinedIfNotEmpty(separator: Constants.commaSeparator) + } + + func myJoined(separator: String = ",") -> String { + return self.map { String(describing: $0) }.joined(separator: separator) + } + + func myJoinedIfNotEmpty(separator: String = ",") -> String? { + if self.isEmpty { + return nil + } else { + return self.myJoined(separator: separator) + } + } +} diff --git a/Nynja/Extensions/SwiftLibrary/String/String+Search.swift b/Nynja/Extensions/SwiftLibrary/String/String+Search.swift index b17ec6e7b..644219f76 100644 --- a/Nynja/Extensions/SwiftLibrary/String/String+Search.swift +++ b/Nynja/Extensions/SwiftLibrary/String/String+Search.swift @@ -18,6 +18,16 @@ extension String { func contains(substring: String, options: String.CompareOptions) -> Bool { return range(of: substring, options: options) != nil } + + func starts(with possiblePrefix: String, options: String.CompareOptions) -> Bool { + guard !possiblePrefix.isEmpty else { + return true + } + guard let range = self.range(of: possiblePrefix, options: options) else { + return false + } + return range.lowerBound == startIndex + } } extension Array where Element == String { diff --git a/Nynja/FeedDAO.swift b/Nynja/FeedDAO.swift index 6088c127d..5cc9a517b 100644 --- a/Nynja/FeedDAO.swift +++ b/Nynja/FeedDAO.swift @@ -16,10 +16,24 @@ class FeedDAO: FeedDAOProtocol { } } + static func fetchP2P(from: String, to: String) -> FeedProtocol? { + return dbManager.fetch { db in + return try DBP2p.p2p(db, firstId: from, secondId: to) + } + } + static func fetchMuc(for name: String) -> FeedProtocol? { return dbManager.fetch { db in return try DBMuc.muc(db, name: name) } } + static func fetchFeedId(for fetchType: FetchType) -> Int64? { + switch fetchType { + case let .muc(name): + return fetchMuc(for: name)?.id + case let .p2p(from, to): + return fetchP2P(from: from, to: to)?.id + } + } } diff --git a/Nynja/FileManager.swift b/Nynja/FileManager.swift index 92952c118..cdec76d9a 100644 --- a/Nynja/FileManager.swift +++ b/Nynja/FileManager.swift @@ -68,6 +68,16 @@ class FileManagerService { return fromPath } + func listFilesFrom(_ folder: String) -> [String]? + { + let fileMngr = FileManager.default + let path = documentDirectory.appending("/\(folder)/") + + // List all contents of directory and return as [String] OR nil if failed + return try? fileMngr.contentsOfDirectory(atPath:path) + } + + func overrideFile(folder: String, name: String) -> String? { var path = documentDirectory.appending("/\(folder)") try? fileManager.createDirectory(atPath: path, withIntermediateDirectories: false, attributes: nil) @@ -103,7 +113,7 @@ class FileManagerService { let attr = try fileManager.attributesOfItem(atPath: path) return attr[FileAttributeKey.size] as? Int64 } catch { - LogService.log(topic: .fileSystem, text: "error: \(error.localizedDescription)") + LogService.log(topic: .fileSystem) { return "error: \(error.localizedDescription)" } return nil } } diff --git a/Nynja/HistoryRequestModelTypeProtocol.swift b/Nynja/HistoryRequestModelTypeRepresentable.swift similarity index 69% rename from Nynja/HistoryRequestModelTypeProtocol.swift rename to Nynja/HistoryRequestModelTypeRepresentable.swift index 99c88972c..0db9b3ccc 100644 --- a/Nynja/HistoryRequestModelTypeProtocol.swift +++ b/Nynja/HistoryRequestModelTypeRepresentable.swift @@ -1,24 +1,23 @@ // -// HistoryRequestModelTypeProtocol.swift +// HistoryRequestModelTypeRepresentable.swift // Nynja // // Created by Volodymyr Hryhoriev on 8/3/18. // Copyright © 2018 TecSynt Solutions. All rights reserved. // -protocol HistoryRequestModelTypeProtocol { +protocol HistoryRequestModelTypeRepresentable: class { var historyRequestModelType: HistoryRequestModel.RequestInput.HistoryType? { get } } -extension Contact: HistoryRequestModelTypeProtocol { +extension Contact: HistoryRequestModelTypeRepresentable { var historyRequestModelType: HistoryRequestModel.RequestInput.HistoryType? { return phone_id.flatMap { .p2p(opponentId: $0) } } } -extension Room: HistoryRequestModelTypeProtocol { +extension Room: HistoryRequestModelTypeRepresentable { var historyRequestModelType: HistoryRequestModel.RequestInput.HistoryType? { return id.flatMap { .muc(mucId: $0) } } } - diff --git a/Nynja/HomeItemsFactory.swift b/Nynja/HomeItemsFactory.swift index 2a1f99f3b..e46843c23 100644 --- a/Nynja/HomeItemsFactory.swift +++ b/Nynja/HomeItemsFactory.swift @@ -83,5 +83,5 @@ class HomeItemsFactory: WCBaseItemsFactory { item.state = .selected return item } - + } diff --git a/Nynja/IdBuilder/IdBuilder.swift b/Nynja/IdBuilder/IdBuilder.swift index 4aac6e129..fe267dbc5 100644 --- a/Nynja/IdBuilder/IdBuilder.swift +++ b/Nynja/IdBuilder/IdBuilder.swift @@ -136,7 +136,7 @@ extension IdBuilder { case .defaultIdWithKey: return [.deviceId, .phoneId, .key, .timestamp, .uuid] case .featureId: - return [.deviceId, .phoneId, .key, .uuid] + return [.deviceId, .phoneId, .key, .uuid, .timestamp] case .starClientId: return [.phoneId, .key] case .resourceId: diff --git a/Nynja/KeychainService/KeychainService.swift b/Nynja/KeychainService/KeychainService.swift index f69c13ada..50c48a0ae 100644 --- a/Nynja/KeychainService/KeychainService.swift +++ b/Nynja/KeychainService/KeychainService.swift @@ -94,6 +94,10 @@ final class KeychainService { let isSuccess = deleteResult == errSecSuccess || deleteResult == errSecItemNotFound return partial && isSuccess } + + defer { + LogService.log(topic: .keychain) { return "Wipe keychain" } + } } } diff --git a/Nynja/KeychainService/QueryFactory/QueryFactory.swift b/Nynja/KeychainService/QueryFactory/QueryFactory.swift index 232db52bf..e243a0c99 100644 --- a/Nynja/KeychainService/QueryFactory/QueryFactory.swift +++ b/Nynja/KeychainService/QueryFactory/QueryFactory.swift @@ -59,6 +59,7 @@ private extension QueryFactory { return [ KeychainKeys.class: genericPassword, KeychainKeys.Attributes.label: label, + KeychainKeys.Attributes.accessible: KeychainKeyValues.Accessible.afterFirstUnlockThisDeviceOnly, KeychainKeys.Attributes.account: account ] } @@ -67,6 +68,7 @@ private extension QueryFactory { return [ KeychainKeys.class: certificate, KeychainKeys.Attributes.label: label, + KeychainKeys.Attributes.accessible: KeychainKeyValues.Accessible.afterFirstUnlockThisDeviceOnly ] } } @@ -85,6 +87,7 @@ private extension QueryFactory { enum Attributes { static let label = kSecAttrLabel as String static let account = kSecAttrAccount as String + static let accessible = kSecAttrAccessible as String } } @@ -94,5 +97,9 @@ private extension QueryFactory { static let genericPassword = kSecClassGenericPassword as String static let certificate = kSecClassCertificate } + + enum Accessible { + static let afterFirstUnlockThisDeviceOnly = kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly as String + } } } diff --git a/Nynja/Library/MessageFactory/MessageFactory.swift b/Nynja/Library/MessageFactory/MessageFactory.swift index 1d51c088b..ef4b71c75 100644 --- a/Nynja/Library/MessageFactory/MessageFactory.swift +++ b/Nynja/Library/MessageFactory/MessageFactory.swift @@ -9,7 +9,7 @@ import AVFoundation import CoreLocation -protocol MessageFactoryProtocol { +protocol MessageFactoryProtocol: class { func makeTextMessage(inputText: InputTextMessage, contact: Contact?, room: Room?) -> Message func makeTextMessageEdited(message: Message, newInputText: InputTextMessage) -> Message @@ -58,27 +58,24 @@ extension MessageFactoryProtocol { } } -final class MessageFactory: MessageFactoryProtocol, SetInjectable { +final class MessageFactory: MessageFactoryProtocol, InitializeInjectable { + + private let storageService: StorageService + private let payloadBuilder: MessagePayloadBuilderInput - private var storageService: StorageService! - private var payloadBuilder: MessagePayloadBuilderInput! -} - -// MARK: - Injectable -extension MessageFactory { struct Dependencies { let storageService: StorageService let payloadBuilder: MessagePayloadBuilderInput } - func inject(dependencies: MessageFactory.Dependencies) { + init(dependencies: Dependencies) { storageService = dependencies.storageService payloadBuilder = dependencies.payloadBuilder } -} - -// MARK: - MessageFactoryProtocol -extension MessageFactory { + + + // MARK: - MessageFactoryProtocol + func makePaymentMessage(inputText: String, contact: Contact) -> Message { let descMimes = [DescMime.payment(mime: .payment, payload: inputText)] diff --git a/Nynja/Library/UI/BaseVC/BaseVC.swift b/Nynja/Library/UI/BaseVC/BaseVC.swift index 67ac063a3..e7e8ec703 100644 --- a/Nynja/Library/UI/BaseVC/BaseVC.swift +++ b/Nynja/Library/UI/BaseVC/BaseVC.swift @@ -223,7 +223,7 @@ class BaseVC: UIViewController, UIGestureRecognizerDelegate, BaseViewProtocol, U } } - func showSpinner(color: UIColor? = Constants.colors.red.getColor()) { + func showSpinner(color: UIColor?) { guard !isSpinnerShown else { return } @@ -235,6 +235,10 @@ class BaseVC: UIViewController, UIGestureRecognizerDelegate, BaseViewProtocol, U isSpinnerShown = true } + + func showSpinner() { + showSpinner(color: Constants.colors.red.getColor()) + } func hideSpinner() { guard isSpinnerShown else { diff --git a/Nynja/Library/UI/BaseVC/BaseVC.swift.orig b/Nynja/Library/UI/BaseVC/BaseVC.swift.orig deleted file mode 100644 index 5fd0d2cab..000000000 --- a/Nynja/Library/UI/BaseVC/BaseVC.swift.orig +++ /dev/null @@ -1,204 +0,0 @@ -// -// BaseVC.swift -// Nynja -// -// Created by Anton Makarov on 22.06.2017. -// Copyright © 2017 TecSynt Solutions. All rights reserved. -// - -import Foundation -import UIKit - -let viewShoudEndEditing = Notification.Name("viewShoudEndEditing") - -class BaseVC: UIViewController, UIGestureRecognizerDelegate, BaseViewProtocol, UserSettingsRespondable { - - var _presenter: BasePresenterProtocol? - - private var spinner = UIActivityIndicatorView() - - weak var inputBarDelegate: BaseInputViewProtocol? - - var screenTitle: String? { - didSet { - if oldValue != screenTitle { - adjustNavigationView() - } - } - } - - private var currentTheme: Theme = .default { - didSet { - if currentTheme != oldValue { - setNeedsStatusBarAppearanceUpdate() - } - } - } - - override var preferredStatusBarStyle: UIStatusBarStyle { - return currentTheme.statusBarStyle - } - - // MARK: Views - lazy var navigationView: NavigationView = { - let navView = NavigationView() - navView.clipsToBounds = true - - self.view.addSubview(navView) - navView.snp.makeConstraints({ (make) in - self.adjustVerticalInset(.top, make: make) - make.left.right.equalToSuperview() - make.height.equalTo(0) - }) - - return navView - }() - - lazy var backImage: UIImageView = { - let img = UIImageView() - img.isUserInteractionEnabled = true - setupBackground(for: img) - self.view.addSubview(img) - - img.snp.makeConstraints({ (make) in - make.top.left.right.bottom.equalTo(self.view) - }) - return img - }() - - // MARK: View lifecycle - override func viewDidLoad() { - super.viewDidLoad() - backImage.isHidden = false - - spinner.hidesWhenStopped = true - spinner.translatesAutoresizingMaskIntoConstraints = false - view.addSubview(spinner) - spinner.activityIndicatorViewStyle = .whiteLarge - - spinner.snp.makeConstraints({ (make) in - make.center.equalTo(self.view) - }) - - let tapRecognizer = UITapGestureRecognizer(target: self, action: #selector(self.tapOnScreen)) - tapRecognizer.cancelsTouchesInView = false - tapRecognizer.delegate = self - self.view.addGestureRecognizer(tapRecognizer) - - screenTitle = nil - - self.initialize() -<<<<<<< HEAD - _presenter?.loadData() - registerForUserInterfaceSettingsNotifications() -======= - - registerForUserSettingsUpdates(options: .ui) ->>>>>>> developer - } - - override func viewWillAppear(_ animated: Bool) { - NotificationCenter.default.addObserver(self,selector: #selector(self.keyboardNotification),name: NSNotification.Name.UIKeyboardWillChangeFrame,object: nil) - self.view.bringSubview(toFront: navigationView) - _presenter?.screenBecomeActive() - } - - override func viewDidAppear(_ animated: Bool) { - super.viewDidAppear(animated) - _presenter?.screenFinishedDisplaying() - _presenter?.viewAppeared() - } - - override func viewWillDisappear(_ animated: Bool) { - super.viewWillDisappear(animated) - NotificationCenter.default.removeObserver(self) - _presenter?.screenWillHide() - } - - - // MARK: Gestures & Notifications - func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldReceive touch: UITouch) -> Bool { - if let flag = touch.view?.isKind(of: UIControl.self), flag == true { - return false - } - return true - } - - @objc func tapOnScreen() { - let notification = Notification(name: viewShoudEndEditing) - NotificationCenter.default.post(notification) - } - - @objc func keyboardNotification(notification: NSNotification) { - if let userInfo = notification.userInfo as? [String:Any] { - if let endFrame = (userInfo["UIKeyboardFrameEndUserInfoKey"] as? NSValue)?.cgRectValue { - let duration: TimeInterval = (userInfo["UIKeyboardAnimationDurationUserInfoKey"] as? NSNumber)?.doubleValue ?? 0 - let animationCurveRawNSN = userInfo["UIKeyboardAnimationCurveUserInfoKey"] as? NSNumber - let animationCurveRaw = animationCurveRawNSN?.uintValue ?? UIViewAnimationOptions.curveEaseOut.rawValue - let animationCurve:UIViewAnimationOptions = UIViewAnimationOptions(rawValue: animationCurveRaw) - self.keyboardNotified(endFrame: endFrame) - - UIView.animate(withDuration: duration, delay: TimeInterval(0), options: animationCurve, animations: { - self.view.layoutIfNeeded() - }, completion: nil) - } - } - } - - // MARK: BaseVC - func initialize() { - } - - deinit { - unregisterForUserSettingsUpdates() - } - - func keyboardNotified(endFrame: CGRect) {} - - func isKeyboardGoingToHide(_ endFrame: CGRect) -> Bool { - return endFrame.origin.y >= UIScreen.main.bounds.size.height - } - - // MARK: Spinner - func showSpinner(color: UIColor? = Constants.colors.red.getColor()) { - view.bringSubview(toFront: spinner) - spinner.color = color - spinner.startAnimating() - view.isUserInteractionEnabled = false - } - - func hideSpinner() { - spinner.stopAnimating() - view.isUserInteractionEnabled = true - } - - // MARK: Separator - func showSeparator() { - navigationView.showSeparator() - } - - func hideSeparator() { - navigationView.hideSeparator() - } - - // MARK: - UserSettingsRespondable - - func userSettingsDidChange(_ newSettings: UserSettings) { - setupBackground(for: backImage, theme: newSettings.theme) - currentTheme = newSettings.theme - } - - private func setupBackground(for imageView: UIImageView, theme: Theme = .default) { - imageView.image = UIImage(named: theme.backgroundName) - } - - // MARK: - Private methods - private func adjustNavigationView() { - navigationView.titleLabel.text = screenTitle - - let height = screenTitle != nil ? NavigationView.Constraints.height : 0 - navigationView.snp.updateConstraints { (make) in - make.height.equalTo(height) - } - } -} diff --git a/Nynja/Library/UI/BaseVC/LoadingInteractive.swift b/Nynja/Library/UI/BaseVC/LoadingInteractive.swift new file mode 100644 index 000000000..23e6d7fd1 --- /dev/null +++ b/Nynja/Library/UI/BaseVC/LoadingInteractive.swift @@ -0,0 +1,17 @@ +// +// LoadingInteractive.swift +// Nynja +// +// Created by Anton Poltoratskyi on 04.09.2018. +// Copyright © 2018 TecSynt Solutions. All rights reserved. +// + +protocol LoadingInteractiveView: class { + func showSpinner() + func hideSpinner() +} + +protocol LoadingInteractive: class { + func showHUD() + func hideHUD() +} diff --git a/Nynja/Library/UI/BottomActions/ActionsView/ActionsView.swift b/Nynja/Library/UI/BottomActions/ActionsView/ActionsView.swift index 6a14180b5..cdbdef67b 100644 --- a/Nynja/Library/UI/BottomActions/ActionsView/ActionsView.swift +++ b/Nynja/Library/UI/BottomActions/ActionsView/ActionsView.swift @@ -177,7 +177,7 @@ class ActionsView: UIView { button.setTitle(title, for: .normal) } - button.titleLabel?.font = UIFont.init(fontName: Constants.fonts.medium, height: Constraints.buttons.labelHeight) + button.titleLabel?.font = UIFont.makeFont(with: Constants.fonts.medium, height: Constraints.buttons.labelHeight) button.addTarget(self, action: #selector(actionButtonTapped(_:)), for: .touchUpInside) diff --git a/Nynja/Library/UI/Buttons/NynjaButton/BaseNynjaButton.swift b/Nynja/Library/UI/Buttons/NynjaButton/BaseNynjaButton.swift index 108a27827..14e87ce72 100644 --- a/Nynja/Library/UI/Buttons/NynjaButton/BaseNynjaButton.swift +++ b/Nynja/Library/UI/Buttons/NynjaButton/BaseNynjaButton.swift @@ -44,7 +44,7 @@ class BaseNynjaButton: UIButton { convenience init(frame: CGRect = .zero, fontName: String, labelHeight: CGFloat = BaseNynjaButton.defaultLabelHeight) { self.init(frame: frame) - self.titleLabel?.font = UIFont(fontName: fontName, height: labelHeight) + self.titleLabel?.font = UIFont.makeFont(with: fontName, height: labelHeight) } required init?(coder aDecoder: NSCoder) { @@ -61,7 +61,7 @@ class BaseNynjaButton: UIButton { self.setTitleColor(Constants.colors.white.getColor(), for: .normal) self.setTitleColor(Constants.colors.whiteDisabled.getColor(), for: .disabled) - self.titleLabel?.font = UIFont(fontName: Constants.fonts.medium, height: BaseNynjaButton.defaultLabelHeight) + self.titleLabel?.font = UIFont.makeFont(with: Constants.fonts.medium, height: BaseNynjaButton.defaultLabelHeight) } } diff --git a/Nynja/Library/UI/ContextMenu/NynjaContextMenuItemsFactory+Design.swift b/Nynja/Library/UI/ContextMenu/NynjaContextMenuItemsFactory+Design.swift index 7ff1bd286..52cf5b34c 100644 --- a/Nynja/Library/UI/ContextMenu/NynjaContextMenuItemsFactory+Design.swift +++ b/Nynja/Library/UI/ContextMenu/NynjaContextMenuItemsFactory+Design.swift @@ -11,7 +11,7 @@ import NynjaUIKit extension NynjaContextMenuItemsFactory { static var defaultDesign: NynjaContextMenu.Design { - let font = UIFont(fontName: Constants.fonts.regular, height: 17)! + let font = UIFont.makeFont(with: Constants.fonts.regular, height: 17)! return NynjaContextMenu.Design(backgroundColor: Constants.colors.contextMenu.background.default.getColor(), highlightedColor: Constants.colors.contextMenu.background.highlighted.getColor(), diff --git a/Nynja/Library/UI/ContextMenu/NynjaContextMenuItemsFactory+Messages.swift b/Nynja/Library/UI/ContextMenu/NynjaContextMenuItemsFactory+Messages.swift index b9106e671..fb8024b24 100644 --- a/Nynja/Library/UI/ContextMenu/NynjaContextMenuItemsFactory+Messages.swift +++ b/Nynja/Library/UI/ContextMenu/NynjaContextMenuItemsFactory+Messages.swift @@ -87,6 +87,8 @@ extension NynjaContextMenuItemsFactory { model.isTranslated ? untranslate(with: .double) : translate(with: .double), copy(), shouldAddEdit(for: model) ? edit() : placeholder()] + ), + ContextMenuRow(items: [marketplace(with: .full)] ) ] } diff --git a/Nynja/Library/UI/Extensions/String+Split.swift b/Nynja/Library/UI/Extensions/String+Split.swift new file mode 100644 index 000000000..b4b2a3f14 --- /dev/null +++ b/Nynja/Library/UI/Extensions/String+Split.swift @@ -0,0 +1,44 @@ +// +// String+Split.swift +// Nynja +// +// Created by Anton Poltoratskyi on 22.08.2018. +// Copyright © 2018 TecSynt Solutions. All rights reserved. +// + +protocol StringInitializable { + init?(_ string: String) +} + +extension Int64: StringInitializable { } + +extension String { + func splitByComma(_ type: T.Type) -> [T]? { + return components(separatedBy: ",").compactMap(T.init) + } + + func splitByComma(transform: (String) -> T?) -> [T]? { + return components(separatedBy: ",").compactMap(transform) + } + + func splitByComma() -> [String]? { + return components(separatedBy: ",").compactMap { $0 } + } + + func splitIntegerIdentifiers() -> [AnyObject]? { + return splitByComma(Int64.self) as [AnyObject]? + } + + func split(by length: Int) -> [String] { + var startIndex = self.startIndex + var results = [Substring]() + + while startIndex < self.endIndex { + let endIndex = self.index(startIndex, offsetBy: length, limitedBy: self.endIndex) ?? self.endIndex + results.append(self[startIndex.. CGFloat { - guard let font = UIFont(fontName: fontName, height: height) else { + guard let font = UIFont.makeFont(with: fontName, height: height) else { return 0 } @@ -279,7 +279,6 @@ extension String { } } - // MARK: - Coding/Decoding extension String { @@ -334,41 +333,17 @@ extension String { } -//MARK: - Split -protocol StringInitializable { - init?(_ string: String) -} -extension Int64: StringInitializable {} +// MARK: - Trim extension String { - func splitByComma(_ type: T.Type) -> [T]? { - return components(separatedBy: Constants.commaSeparator).compactMap(T.init) - } - - func splitByComma(transform: (String) -> T?) -> [T]? { - return components(separatedBy: Constants.commaSeparator).compactMap(transform) - } - func splitByComma() -> [String]? { - return components(separatedBy: Constants.commaSeparator).compactMap { $0 } + mutating func trim() { + self = trimmed() } - func splitIntegerIdentifiers() -> [AnyObject]? { - return splitByComma(Int64.self) as [AnyObject]? + func trimmed() -> String { + return self.trimmingCharacters(in: .whitespacesAndNewlines) } - func split(by length: Int) -> [String] { - var startIndex = self.startIndex - var results = [Substring]() - - while startIndex < self.endIndex { - let endIndex = self.index(startIndex, offsetBy: length, limitedBy: self.endIndex) ?? self.endIndex - results.append(self[startIndex.. UIFont? { + let values = FontInitialValues(fontName: fontName, height: height, width: width) + + if let font = UIFont.fontsCache[values] { + return font + } else { + let font = UIFont(fontName: fontName, width: width, height: height) + + if let font = font { + UIFont.fontsCache[values] = font + } + + return font + } + } +} +private extension UIFont { convenience init?(fontName: String, width: CGFloat = 100, height: CGFloat) { let size = "A".getFontSize(fontName: fontName, width: width, height: height) self.init(name: fontName, size: size) } - } + diff --git a/Nynja/Library/UI/Extensions/UI/UIImageView/UIImageView+SetImage.swift b/Nynja/Library/UI/Extensions/UI/UIImageView/UIImageView+SetImage.swift index c101dfcb1..b46d8dc14 100644 --- a/Nynja/Library/UI/Extensions/UI/UIImageView/UIImageView+SetImage.swift +++ b/Nynja/Library/UI/Extensions/UI/UIImageView/UIImageView+SetImage.swift @@ -47,46 +47,4 @@ extension UIImageView { objc_setAssociatedObject(self, &AssociatedKeys.imageURL, newValue as Any, .OBJC_ASSOCIATION_RETAIN_NONATOMIC) } } - - /// FIXME: will be removed later - func setRemoteImage(url: URL?, remoteDestination: RemoteStorageDestination = .default, completion: ImageCompletion? = nil) { - self.imageURL = url - var placeholderIdentifier = "placeholder_image" - var loadedIdentifier = "loaded_image" - - func markAsPlaceholderContent() { - self.accessibilityIdentifier = placeholderIdentifier - } - func markAsLoadedContent() { - self.accessibilityIdentifier = loadedIdentifier - } - - markAsPlaceholderContent() - - guard let url = url else { - return - } - - DispatchQueue.global(qos: .userInitiated).async { - SyncFileManager.sharedInstance.downloader = AmazonManager.shared - SyncFileManager.sharedInstance.getFileLink(url: url.absoluteString, from: remoteDestination) { furl, _, _ in - guard let fileURL = furl else { - return - } - do { - guard try fileURL.checkResourceIsReachable() else { - return - } - dispatchAsyncMain { - guard url == self.imageURL else { - return - } - self.image = UIImage(fileUrl: fileURL) - markAsLoadedContent() - completion?(url, self.image) - } - } catch { } - } - } - } } diff --git a/Nynja/Library/UI/Extensions/URLExtensions.swift b/Nynja/Library/UI/Extensions/URLExtensions.swift index d18603f0f..b94d59f2e 100644 --- a/Nynja/Library/UI/Extensions/URLExtensions.swift +++ b/Nynja/Library/UI/Extensions/URLExtensions.swift @@ -22,4 +22,12 @@ extension URL { var isLocalURL: Bool { return isFileURL || absoluteString.contains("/var/mobile/Containers/Data/Application") } + + var thumbUrl: URL? { + let fileName = String(self.lastPathComponent) + guard let path = FileManagerService.sharedInstance.getPathOfFile(folder: FileManagerService.Folders.thumb.rawValue, name: fileName) else { + return nil + } + return URL(fileURLWithPath: path) + } } diff --git a/Nynja/Library/UI/ImagePreviewTransitionController/ImagePreviewTransitionController.swift b/Nynja/Library/UI/ImagePreviewTransitionController/ImagePreviewTransitionController.swift index 0da38122b..f97a4ebc4 100644 --- a/Nynja/Library/UI/ImagePreviewTransitionController/ImagePreviewTransitionController.swift +++ b/Nynja/Library/UI/ImagePreviewTransitionController/ImagePreviewTransitionController.swift @@ -40,6 +40,9 @@ UIViewControllerAnimatedTransitioning, UIViewControllerInteractiveTransitioning weak private var dismissalTransitionContext: UIViewControllerContextTransitioning? weak private var imageView: UIImageView? weak private var overlayView: UIView? + + private var startingCornerRadius: CGFloat = 0 + private var endingCornerRadius: CGFloat = 0 private lazy var fadeView: UIView = { let fadeView = UIView() @@ -99,7 +102,8 @@ UIViewControllerAnimatedTransitioning, UIViewControllerInteractiveTransitioning } let referenceViewCopy = referenceView.imageViewCopy - + startingCornerRadius = referenceViewCopy.layer.cornerRadius + fadeView.alpha = 0 fadeView.frame = transitionContext.finalFrame(for: to) transitionContext.containerView.addSubview(fadeView) @@ -136,10 +140,26 @@ UIViewControllerAnimatedTransitioning, UIViewControllerInteractiveTransitioning let scaledSize = CGSize(width: aspectRatioAdjustedSize.width * scale, height: aspectRatioAdjustedSize.height * scale) - let scaleAnimations = { () in + let scaleAnimations = { [weak self] () in + guard let `self` = self else { + return + } + referenceViewCopy.transform = .identity referenceViewCopy.frame.size = scaledSize referenceViewCopy.center = to.view.center + + if #available(iOS 11.0, *) { + } else { + let animation = CABasicAnimation(keyPath: "cornerRadius") + animation.timingFunction = CAMediaTimingFunction(name: kCAMediaTimingFunctionEaseInEaseOut) + animation.fromValue = NSNumber(value: Float(referenceViewCopy.layer.cornerRadius)) + animation.toValue = NSNumber(value: Float(self.endingCornerRadius)) + animation.duration = self.transitionDuration(using: transitionContext) + referenceViewCopy.layer.add(animation, forKey: "cornerRadius") + } + + referenceViewCopy.layer.cornerRadius = self.endingCornerRadius } let scaleCompletion = { [weak self] (_ finished: Bool) in @@ -229,6 +249,9 @@ UIViewControllerAnimatedTransitioning, UIViewControllerInteractiveTransitioning guard let `self` = self else { return } + + imageView.layer.cornerRadius = self.startingCornerRadius + imageView.layer.masksToBounds = true if self.canPerformContextualDismissal() { guard let referenceView = self.transitionInfo.endingView else { diff --git a/Nynja/Library/UI/ItemSelector/ItemSelectorCell.swift b/Nynja/Library/UI/ItemSelector/ItemSelectorCell.swift index 3951c92ee..ab63f6bd0 100644 --- a/Nynja/Library/UI/ItemSelector/ItemSelectorCell.swift +++ b/Nynja/Library/UI/ItemSelector/ItemSelectorCell.swift @@ -34,7 +34,6 @@ class ItemSelectorCell : UICollectionViewCell { func setup(title:String, borderWidth:CGFloat) { titleLabel.text = title - titleLabel.accessibilityValue = title layer.borderWidth = borderWidth layer.borderColor = Design.borderColor.cgColor layer.cornerRadius = frame.height / 2 diff --git a/Nynja/Library/UI/ItemSelector/ItemsSelector.swift b/Nynja/Library/UI/ItemSelector/ItemsSelector.swift index 8f6022bea..cd67651e9 100644 --- a/Nynja/Library/UI/ItemSelector/ItemsSelector.swift +++ b/Nynja/Library/UI/ItemSelector/ItemsSelector.swift @@ -9,12 +9,14 @@ import Foundation class SelectorItemModel { - var title:String - var associated:Any? + let title: String + let associated: Any? + let accessibilityIdentifier: String - init (title:String, associated:Any? = nil) { + init(title: String, associated: Any? = nil, identifier: String) { self.title = title self.associated = associated + self.accessibilityIdentifier = identifier } } @@ -75,10 +77,10 @@ class ItemsSelector : UIView, UICollectionViewDataSource, UICollectionViewDelega func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell { let cell = collectionView.dequeueReusableCell(withReuseIdentifier: Constants.cellId, for: indexPath) - if let selectorCell = cell as? ItemSelectorCell, - let item = items?[indexPath.item] { - selectorCell.setup(title: item.title, - borderWidth: Constants.borderWidth) + + if let selectorCell = cell as? ItemSelectorCell, let item = items?[indexPath.item] { + selectorCell.accessibilityIdentifier = item.accessibilityIdentifier + selectorCell.setup(title: item.title, borderWidth: Constants.borderWidth) } return cell diff --git a/Nynja/Library/UI/Lists/EmptyStateView/CollectionState.swift b/Nynja/Library/UI/Lists/EmptyStateView/CollectionState.swift index a74669561..c44578197 100644 --- a/Nynja/Library/UI/Lists/EmptyStateView/CollectionState.swift +++ b/Nynja/Library/UI/Lists/EmptyStateView/CollectionState.swift @@ -7,6 +7,6 @@ // enum CollectionState { - case empty(EmptyStateViewModel) + case empty(EmptyStateViewModel?) case filled(data: Input) } diff --git a/Nynja/Library/UI/Lists/EmptyStateView/EmptyStateView.swift b/Nynja/Library/UI/Lists/EmptyStateView/EmptyStateView.swift index c95dca9eb..424540feb 100644 --- a/Nynja/Library/UI/Lists/EmptyStateView/EmptyStateView.swift +++ b/Nynja/Library/UI/Lists/EmptyStateView/EmptyStateView.swift @@ -11,10 +11,6 @@ import SnapKit class EmptyStateView: BaseView { typealias ActionHandler = () -> Void - override var activatedViews: [UIView] { - return [actionButton] - } - var actionHandler: ActionHandler? private var heightConstraint: Constraint? @@ -60,9 +56,9 @@ class EmptyStateView: BaseView { self.addSubview(button) button.snp.makeConstraints { make in - heightConstraint = make.height.equalTo(Constraints.ActionButton.height).constraint + heightConstraint = make.height.equalTo(0).constraint topInsetConstraint = make.top.equalTo(descriptionLabel.snp.bottom) - .offset(Constraints.ActionButton.topInset).constraint + .offset(0).constraint make.left.right.equalToSuperview() make.bottom.equalToSuperview() @@ -74,6 +70,11 @@ class EmptyStateView: BaseView { // MARK: - Setup + override func baseSetup() { + super.baseSetup() + actionButton.isHidden = true + } + func setup(with model: EmptyStateViewModel) { imageView.image = model.image descriptionLabel.text = model.descriptionText diff --git a/Nynja/Library/UI/Lists/EmptyStateView/EmptyStateViewModel.swift b/Nynja/Library/UI/Lists/EmptyStateView/EmptyStateViewModel.swift index 0c8e7407e..beeea6062 100644 --- a/Nynja/Library/UI/Lists/EmptyStateView/EmptyStateViewModel.swift +++ b/Nynja/Library/UI/Lists/EmptyStateView/EmptyStateViewModel.swift @@ -11,6 +11,12 @@ struct EmptyStateViewModel { let descriptionText: String let actionViewModel: ActionViewModel? + + init(image: UIImage?, descriptionText: String, actionViewModel: ActionViewModel? = nil) { + self.image = image + self.descriptionText = descriptionText + self.actionViewModel = actionViewModel + } } diff --git a/Nynja/Library/UI/Lists/TableView/Cells/ChatListMessageCell/Cell/ChatListMessageAccessoryView.swift b/Nynja/Library/UI/Lists/TableView/Cells/ChatListMessageCell/Cell/ChatListMessageAccessoryView.swift index e1dd97224..9975d84e9 100644 --- a/Nynja/Library/UI/Lists/TableView/Cells/ChatListMessageCell/Cell/ChatListMessageAccessoryView.swift +++ b/Nynja/Library/UI/Lists/TableView/Cells/ChatListMessageCell/Cell/ChatListMessageAccessoryView.swift @@ -37,7 +37,7 @@ final class ChatListMessageAccessoryView: BaseView { let topInset = Constraints.countView.topInset.adjustedByWidth let fontHeight = Constraints.countView.fontHeight.adjustedByWidth - let font = UIFont(fontName: Constants.fonts.regular, height: fontHeight)! + let font = UIFont.makeFont(with: Constants.fonts.regular, height: fontHeight)! let textColor = Constants.colors.white.getColor() let inset = Constraints.countView.horizontalContentInset.adjustedByWidth diff --git a/Nynja/Library/UI/Lists/TableView/Cells/ChatListMessageCell/Cell/ChatListMessageContentView.swift b/Nynja/Library/UI/Lists/TableView/Cells/ChatListMessageCell/Cell/ChatListMessageContentView.swift index 51de81fb3..5386bfd93 100644 --- a/Nynja/Library/UI/Lists/TableView/Cells/ChatListMessageCell/Cell/ChatListMessageContentView.swift +++ b/Nynja/Library/UI/Lists/TableView/Cells/ChatListMessageCell/Cell/ChatListMessageContentView.swift @@ -11,10 +11,10 @@ import SnapKit final class ChatListMessageContentView: BaseView { - private static let contentFont = UIFont(fontName: Constants.fonts.regular, + private static let contentFont = UIFont.makeFont(with: Constants.fonts.regular, height: Constraints.contentLabel.height.adjustedByWidth)! - private static let contentBoldFont = UIFont(fontName: Constants.fonts.bold, + private static let contentBoldFont = UIFont.makeFont(with: Constants.fonts.bold, height: Constraints.contentLabel.height.adjustedByWidth)! private static let contentLabelTextColor = Constants.colors.darkGray diff --git a/Nynja/Library/UI/Lists/TableView/Cells/ChatListMessageCell/Cell/CounterView.swift b/Nynja/Library/UI/Lists/TableView/Cells/ChatListMessageCell/Cell/CounterView.swift index 86d83ed6d..61e433330 100644 --- a/Nynja/Library/UI/Lists/TableView/Cells/ChatListMessageCell/Cell/CounterView.swift +++ b/Nynja/Library/UI/Lists/TableView/Cells/ChatListMessageCell/Cell/CounterView.swift @@ -24,7 +24,7 @@ final class CounterView: UIView { } } - var font: UIFont = UIFont(fontName: Constants.fonts.regular, height: CGFloat(17.0.adjustedByWidth))! { + var font: UIFont = UIFont.makeFont(with: Constants.fonts.regular, height: CGFloat(17.0.adjustedByWidth))! { didSet { countLabel.font = font } diff --git a/Nynja/Library/UI/Lists/TableView/DataSource/EmptyStateTableViewDS.swift b/Nynja/Library/UI/Lists/TableView/DataSource/EmptyStateTableViewDS.swift index cb82bb55a..226a5acef 100644 --- a/Nynja/Library/UI/Lists/TableView/DataSource/EmptyStateTableViewDS.swift +++ b/Nynja/Library/UI/Lists/TableView/DataSource/EmptyStateTableViewDS.swift @@ -20,7 +20,9 @@ class EmptyStateTableViewDS: TableViewDataSourceProxy { return view }() - let emptyStateView: EmptyStateView = EmptyStateView() + private let emptyStateView: EmptyStateView = EmptyStateView() + + var emptyStateViewModel: EmptyStateViewModel? override func numberOfSections(in tableView: UITableView) -> Int { let sections = super.numberOfSections(in: tableView) @@ -42,13 +44,17 @@ class EmptyStateTableViewDS: TableViewDataSourceProxy { } private func toggleBackgroundViewIfNeeded(in tableView: UITableView, shouldShow: Bool) { - let isShown = tableView.backgroundView != nil - - guard isShown != shouldShow else { + guard shouldShow else { + tableView.backgroundView = nil return } - tableView.backgroundView = shouldShow ? backgroundView : nil + if let viewModel = emptyStateViewModel { + emptyStateView.setup(with: viewModel) + tableView.backgroundView = backgroundView + } else { + tableView.backgroundView = nil + } } } diff --git a/Nynja/Library/UI/MentionCounter/MentionCounterView.swift b/Nynja/Library/UI/MentionCounter/MentionCounterView.swift index 378325b36..a1621d187 100644 --- a/Nynja/Library/UI/MentionCounter/MentionCounterView.swift +++ b/Nynja/Library/UI/MentionCounter/MentionCounterView.swift @@ -46,7 +46,7 @@ final class MentionCounterView: BaseView { let bottomInset = Constraints.counterView.bottomInset.adjustedByWidth let fontHeight = Constraints.counterView.fontHeight.adjustedByWidth - let font = UIFont(fontName: Constants.fonts.regular, height: fontHeight)! + let font = UIFont.makeFont(with: Constants.fonts.regular, height: fontHeight)! let textColor = Constants.colors.red.getColor() let inset = Constraints.counterView.horizontalContentInset.adjustedByWidth diff --git a/Nynja/Library/UI/ReturnToCallContentView.swift b/Nynja/Library/UI/ReturnToCallContentView.swift index 540dbb750..1c481d68c 100644 --- a/Nynja/Library/UI/ReturnToCallContentView.swift +++ b/Nynja/Library/UI/ReturnToCallContentView.swift @@ -13,9 +13,11 @@ extension ReturnToCallContentView { static let leftPadding = 16.0.adjustedByWidth static let rightPadding = 16.0.adjustedByWidth static let bottomPadding = 2.0.adjustedByWidth + static let imageSize = 36.0.adjustedByWidth + static let offset = 8.0.adjustedByWidth struct Call { - static let height = 16.0.adjustedByWidth + static let height = 36.0.adjustedByWidth } struct Return { static let leftPadding = 16.0.adjustedByWidth @@ -28,7 +30,7 @@ class ReturnToCallContentView: UIView { var timer:Timer? weak var nynCall: NYNCall? - + lazy var img: UIImageView = { let view = UIImageView() @@ -48,6 +50,22 @@ class ReturnToCallContentView: UIView { lbl.text = "Return_to_call".localized lbl.font = UIFont(name: Constants.fonts.regular, size: 12) lbl.textColor = Constants.colors.white.getColor() + lbl.backgroundColor = .clear + self.addSubview(lbl) + + lbl.snp.makeConstraints({ (make) in + make.left.equalTo(self.img.snp.right).offset(Constraints.Return.leftPadding) + make.bottom.equalTo(labelName.snp.top) + }) + return lbl + }() + + lazy var labelName: UILabel = { + let lbl = UILabel() + lbl.text = "".localized + lbl.font = UIFont(name: Constants.fonts.regular, size: 12) + lbl.textColor = Constants.colors.white.getColor() + lbl.backgroundColor = .clear self.addSubview(lbl) lbl.snp.makeConstraints({ (make) in @@ -64,46 +82,102 @@ class ReturnToCallContentView: UIView { lbl.font = UIFont(name: Constants.fonts.regular, size: 12) lbl.adjustsFontSizeToFitWidth = true lbl.textColor = Constants.colors.white.getColor() + lbl.backgroundColor = .clear lbl.setContentHuggingPriority(.required, for: .horizontal) lbl.setContentCompressionResistancePriority(.required, for: .horizontal) self.addSubview(lbl) lbl.snp.makeConstraints({ (make) in - make.right.equalToSuperview().offset(-Constraints.Return.rightPadding) - make.left.equalTo(self.hing.snp.right) + make.right.equalTo(self.contactImage.snp.left).offset(-Constraints.offset) + make.left.equalTo(self.labelName.snp.right).offset(Constraints.offset) make.bottom.equalTo(-Constraints.bottomPadding) }) return lbl }() + lazy var contactImage: UIImageView = { + let img = UIImageView() + img.isUserInteractionEnabled = true + img.contentMode = .scaleAspectFill + img.backgroundColor = .clear + self.addSubview(img) + + img.snp.makeConstraints({ (make) in + make.right.equalToSuperview().offset(-Constraints.Return.rightPadding) + make.bottom.equalTo(-Constraints.bottomPadding) + make.width.height.equalTo(Constraints.imageSize) + }) + return img + }() + + func updateImageConstraintWithSize(imgSize: Float) { + + contactImage.snp.updateConstraints { (make) in + make.width.height.equalTo(imgSize) + } + + updateConstraints() + } + func setup(call: NYNCall) { self.nynCall = call -// self.startTimer() + //self.startTimer() self.backgroundColor = Constants.colors.greenForReturnToCallColor.getColor() img.isHidden = false hing.isHidden = false + time.isHidden = true + time.text = "" + + if call.isConference() { + contactImage.isHidden = true + contactImage.image = nil + if let room = RoomDAO.findRoom(by: call.externalInfo), let rn = room.name { + labelName.isHidden = false + labelName.text = rn + } else { + labelName.isHidden = true + labelName.text = "" + } + updateImageConstraintWithSize(imgSize: 0) + } else { + if let ctc = ContactDAO.findContactBy(phoneId: call.callee) { + labelName.isHidden = false + contactImage.isHidden = false + contactImage.setImage(url: ctc.avatarUrl, placeHolder: UIImage(named: "ava_placeholder")) + updateImageConstraintWithSize(imgSize: Float(Constraints.imageSize)) + contactImage.layer.cornerRadius = CGFloat(Constraints.imageSize/2) + contactImage.clipsToBounds = true + labelName.text = ctc.fullName ?? "" + } else { + labelName.isHidden = true + contactImage.isHidden = true + contactImage.image = nil + updateImageConstraintWithSize(imgSize: 0) + labelName.text = "" + } + } } - //TODO: ASK ANGEL -// func startTimer() { -// timer = Timer.scheduledTimer(timeInterval: 1, target: self, selector: #selector(runTimedCode), userInfo: nil, repeats: true) -// } - -// @objc func runTimedCode() { -// if let call = self.call { -// let durationInt = Int(call.duration()) -// let minutes = durationInt / 60 -// let seconds = durationInt % 60 -// -// if let status = call.callStatus { -// if status == .ongoing { -// self.time.text = String.localizedStringWithFormat("%02u:%02u", minutes,seconds) -// } else { -// self.time.text = status.rawValue -// } -// } -// } -// } + // func startTimer() { + // timer = Timer.scheduledTimer(timeInterval: 1, target: self, selector: #selector(runTimedCode), userInfo: nil, repeats: true) + // } + // + // @objc func runTimedCode() { + // if let call = self.nynCall { + // let durationInt = Int(call.duration()) + // let minutes = durationInt / 60 + // let seconds = durationInt % 60 + // + // if let status = call.callStatus { + // if status == .ongoing { + // self.time.text = String.localizedStringWithFormat("%02u:%02u", minutes,seconds) + // } else { + // self.time.text = status.rawValue + // } + // } + // } + // } } + diff --git a/Nynja/Library/UI/ReturnToCallView.swift b/Nynja/Library/UI/ReturnToCallView.swift index 1e19333c5..c8f0fd6ac 100644 --- a/Nynja/Library/UI/ReturnToCallView.swift +++ b/Nynja/Library/UI/ReturnToCallView.swift @@ -34,6 +34,6 @@ class ReturnToCallView: UIView { extension ReturnToCallView { struct Constraints { - static let height = 24.0.adjustedByWidth + static let height = 44.0.adjustedByWidth } } diff --git a/Nynja/Library/UI/TextInput/InputBar/InputBar.swift b/Nynja/Library/UI/TextInput/InputBar/InputBar.swift index 30081ef1e..464964320 100644 --- a/Nynja/Library/UI/TextInput/InputBar/InputBar.swift +++ b/Nynja/Library/UI/TextInput/InputBar/InputBar.swift @@ -19,7 +19,7 @@ final class InputBar: UIView { typealias SendTypingHandler = (TypingModelType) -> Void typealias ChangeHandler = (UITextView) -> Void - typealias TextRangeReplaceHandler = (UITextView, NSRange, _ replacementText: String) -> Void + typealias TextRangeReplaceHandler = (UITextView, NSRange, _ replacementText: String) -> Bool typealias ChangesHeightHandler = (CGFloat) -> Void @@ -69,6 +69,18 @@ final class InputBar: UIView { } } + // TODO: replace private defaultTextAttributes with the next public API: + // + // var textColor: UIColor? + // + // var mentionTextColor: UIColor? + // + // var textFont: UIFont? + // + private var defaultTextAttributes: TextAttributes { + return TextInputContent.defaultTextAttributes + } + private func shouldSetupDisplayMode(_ displayMode: DisplayMode, oldValue: DisplayMode) -> Bool { if case .action = displayMode { return true @@ -142,7 +154,7 @@ final class InputBar: UIView { let height = Constraints.Main.RightButton.height let button = ScheduleButton(frame: CGRect(x: 0, y: 0, width: width, height: height)) - button.titleLabel?.font = UIFont(fontName: Constants.fonts.medium, height: 16) + button.titleLabel?.font = UIFont.makeFont(with: Constants.fonts.medium, height: 16) button.setTitleColor(.white, for: .normal) let leftInset = 5.0.adjustedByWidth @@ -270,6 +282,7 @@ final class InputBar: UIView { private func makeTextContent(with text: ContentType.Text?) -> InputContentView { let content = TextInputContent() + content.defaultTextAttributes = defaultTextAttributes content.inputType = lastSelectedInputType content.inputChangeHandler = inputTypeChangeHandler @@ -353,7 +366,7 @@ final class InputBar: UIView { private func makeChannelAction(with info: DisplayMode.ActionInfo) -> InputContentView { let button = UIButton() - let font = UIFont(fontName: Constants.fonts.medium, height: Constraints.Main.ChannelActionButton.labelHeight) + let font = UIFont.makeFont(with: Constants.fonts.medium, height: Constraints.Main.ChannelActionButton.labelHeight) button.titleLabel?.font = font button.setTitleColor(Constants.colors.white.getColor(), for: .normal) @@ -708,8 +721,7 @@ extension InputBar: ALTextInputBarDelegate { } func textView(textView: ALTextView, shouldChangeTextInRange range: NSRange, replacementText text: String) -> Bool { - textRangeReplaceHandler?(textView, range, text) - return true + return textRangeReplaceHandler?(textView, range, text) ?? true } func textViewDidChangeSelection(textView: ALTextView) { diff --git a/Nynja/Library/UI/TextInput/InputBar/InputContent/TextInputContent.swift b/Nynja/Library/UI/TextInput/InputBar/InputContent/TextInputContent.swift index 85b81bb65..096f6cd09 100644 --- a/Nynja/Library/UI/TextInput/InputBar/InputContent/TextInputContent.swift +++ b/Nynja/Library/UI/TextInput/InputBar/InputContent/TextInputContent.swift @@ -120,14 +120,14 @@ extension TextInputContent { static let defaultFont = UIFont(name: Constants.fonts.regular, size: 18)! - static var defaultTextAttributes: [NSAttributedStringKey: Any] { + static var defaultTextAttributes: TextAttributes { return [ NSAttributedStringKey.foregroundColor: defaultTextColor, NSAttributedStringKey.font: defaultFont ] } - static var mentionAttributes: [NSAttributedStringKey: Any] { + static var mentionAttributes: TextAttributes { return [ NSAttributedStringKey.foregroundColor: Constants.colors.blue.getColor(), NSAttributedStringKey.font: defaultFont diff --git a/Nynja/Library/UI/TextInput/InputField/CodeField.swift b/Nynja/Library/UI/TextInput/InputField/CodeField.swift index 4cc830cf0..8ba6afdd9 100644 --- a/Nynja/Library/UI/TextInput/InputField/CodeField.swift +++ b/Nynja/Library/UI/TextInput/InputField/CodeField.swift @@ -33,7 +33,7 @@ class CodeField: BaseInputView, UITextFieldDelegate { tf.textAlignment = .center let height = UIScreen.main.bounds.height * 0.028 - tf.font = UIFont(fontName: Constants.fonts.medium, height: height) + tf.font = UIFont.makeFont(with: Constants.fonts.medium, height: height) tf.textColor = Constants.colors.white.getColor() tf.delegate = self diff --git a/Nynja/Library/UI/TextInput/InputField/NynjaSearchField.swift b/Nynja/Library/UI/TextInput/InputField/NynjaSearchField.swift index e9403011a..0275a4237 100644 --- a/Nynja/Library/UI/TextInput/InputField/NynjaSearchField.swift +++ b/Nynja/Library/UI/TextInput/InputField/NynjaSearchField.swift @@ -30,6 +30,11 @@ class NynjaSearchField: ImagePlaceholderField { placeholerImage = #imageLiteral(resourceName: "ic_participants_search") textField.addTarget(self, action: #selector(filterEdited(_:)), for: .editingChanged) + + textField.returnKeyType = .done + returnHandler = { [weak self] in + self?.textField.resignFirstResponder() + } setupTestingKeys() } diff --git a/Nynja/Library/UI/TextInput/InputField/TextInputBar/ALTextInputBar.swift b/Nynja/Library/UI/TextInput/InputField/TextInputBar/ALTextInputBar.swift index 0ff79a32b..1a6b4e24d 100755 --- a/Nynja/Library/UI/TextInput/InputField/TextInputBar/ALTextInputBar.swift +++ b/Nynja/Library/UI/TextInput/InputField/TextInputBar/ALTextInputBar.swift @@ -31,6 +31,8 @@ extension TextInputProtocol { } } +public typealias TextAttributes = [NSAttributedStringKey: Any] + open class ALTextInputBar: UIView, ALTextViewDelegate { public weak var delegate: ALTextInputBarDelegate? @@ -86,14 +88,22 @@ open class ALTextInputBar: UIView, ALTextViewDelegate { /// The horizontal spacing between subviews public var horizontalSpacing: CGFloat = 5 + public var defaultTextAttributes: TextAttributes? + /// Convenience set and retrieve the text view text public var text: String! { get { return textView.text } - set(newValue) { - textView.text = newValue - textView.delegate?.textViewDidChange?(textView) + set { + if let defaultTextAttributes = defaultTextAttributes { + let string = newValue ?? "" + textView.textColor = defaultTextAttributes[.foregroundColor] as? UIColor + attributedText = NSAttributedString(string: string, attributes: defaultTextAttributes) + } else { + textView.text = newValue + textView.delegate?.textViewDidChange?(textView) + } } } @@ -102,7 +112,7 @@ open class ALTextInputBar: UIView, ALTextViewDelegate { get { return textView.attributedText } - set(newValue) { + set { textView.attributedText = newValue textView.delegate?.textViewDidChange?(textView) } diff --git a/Nynja/Library/UI/TextInput/Material/Config/NynjaMTIConfig.swift b/Nynja/Library/UI/TextInput/Material/Config/NynjaMTIConfig.swift index 3f7558993..30cdbb6d6 100644 --- a/Nynja/Library/UI/TextInput/Material/Config/NynjaMTIConfig.swift +++ b/Nynja/Library/UI/TextInput/Material/Config/NynjaMTIConfig.swift @@ -6,7 +6,7 @@ // Copyright © 2018 TecSynt Solutions. All rights reserved. // -fileprivate let defaultFont = UIFont(fontName: Constants.fonts.regular, height: CGFloat(22).adjustedByWidth)! +fileprivate let defaultFont = UIFont.makeFont(with: Constants.fonts.regular, height: CGFloat(22).adjustedByWidth)! fileprivate let defaultColor = Constants.colors.red.getColor() fileprivate let darkGrayColor = Constants.colors.darkGray.getColor() @@ -19,7 +19,7 @@ struct NynjaMTIConfig: MTIConfigProtocol { let keyboardType: UIKeyboardType = .default let placeholderFont: UIFont = defaultFont - let placeholderCollapsedFontSize: CGFloat = UIFont(fontName: Constants.fonts.regular, + let placeholderCollapsedFontSize: CGFloat = UIFont.makeFont(with: Constants.fonts.regular, height: CGFloat(17).adjustedByWidth)!.pointSize let placeholderColor: UIColor = darkGrayColor @@ -28,7 +28,7 @@ struct NynjaMTIConfig: MTIConfigProtocol { var activeSeparatorColor: UIColor = defaultColor var inactiveSeparatorColor: UIColor = darkGrayColor - let infoFont: UIFont? = UIFont(fontName: Constants.fonts.medium, height: CGFloat(17).adjustedByWidth) + let infoFont: UIFont? = UIFont.makeFont(with: Constants.fonts.medium, height: CGFloat(17).adjustedByWidth) let warningColor: UIColor = defaultColor let successColor: UIColor = Constants.colors.greenForReturnToCallColor.getColor() diff --git a/Nynja/Library/UI/TextInput/Material/MaterialTextField.swift b/Nynja/Library/UI/TextInput/Material/MaterialTextField.swift index 22015ccb3..5b41b0ee6 100644 --- a/Nynja/Library/UI/TextInput/Material/MaterialTextField.swift +++ b/Nynja/Library/UI/TextInput/Material/MaterialTextField.swift @@ -33,10 +33,14 @@ class MaterialTextField: MaterialTextContainer { didSet { textField.keyboardType = keyboardType } } + var prohibitedOptions: ProhibitedOptions = .none { + didSet { textField.prohibitedOptions = prohibitedOptions } + } + // MARK: - Views - private lazy var textField: UITextField = { - let textField = UITextField() + private lazy var textField: TextField = { + let textField = TextField() textField.font = font textField.backgroundColor = .clear diff --git a/Nynja/Library/UI/WheelContainer/Wheel/ItemModels/WheelItemModel.swift b/Nynja/Library/UI/WheelContainer/Wheel/ItemModels/WheelItemModel.swift index 252404080..e095da730 100644 --- a/Nynja/Library/UI/WheelContainer/Wheel/ItemModels/WheelItemModel.swift +++ b/Nynja/Library/UI/WheelContainer/Wheel/ItemModels/WheelItemModel.swift @@ -137,7 +137,7 @@ class WheelItemModel: WheelItemModelDelegate { var subitems: ItemModels = [] /// Action which is called when user tap on the item - private var action: AnyAction? = nil + var action: AnyAction? = nil // MARK: - Init init() {} diff --git a/Nynja/Library/UI/WheelContainer/Wheel/WheelPreviews/Subviews/MediaInfoView.swift b/Nynja/Library/UI/WheelContainer/Wheel/WheelPreviews/Subviews/MediaInfoView.swift index 15cc6b0c5..0ff2e3cc9 100644 --- a/Nynja/Library/UI/WheelContainer/Wheel/WheelPreviews/Subviews/MediaInfoView.swift +++ b/Nynja/Library/UI/WheelContainer/Wheel/WheelPreviews/Subviews/MediaInfoView.swift @@ -54,7 +54,7 @@ extension MediaInfoView { let label = UILabel() view.addSubview(label) - label.font = UIFont.init(name: Constants.fonts.notoSansRegular, size: CGFloat(Layout.SizeInBytesLabel.fontSize)) + label.font = UIFont(name: Constants.fonts.notoSansRegular, size: CGFloat(Layout.SizeInBytesLabel.fontSize)) label.textColor = UIColor.white label.textAlignment = .center @@ -63,7 +63,7 @@ extension MediaInfoView { make.left.right.equalToSuperview().inset(Layout.SizeInBytesLabel.leftAndRightInset) if !isHaveBottom { - make.top.equalToSuperview().inset(Layout.SizeInBytesLabel.topAndBottomInset) + make.bottom.equalToSuperview().inset(Layout.SizeInBytesLabel.topAndBottomInset) } } @@ -74,7 +74,7 @@ extension MediaInfoView { let label = UILabel() view.addSubview(label) - label.font = UIFont.init(name: Constants.fonts.notoSansRegular, size: CGFloat(Layout.ResolutionLabel.fontSize)) + label.font = UIFont(name: Constants.fonts.notoSansRegular, size: CGFloat(Layout.ResolutionLabel.fontSize)) label.textColor = UIColor.white label.textAlignment = .center diff --git a/Nynja/Library/UI/WheelContainer/Wheel/WheelPreviews/WheelImageFullItemPreview.swift b/Nynja/Library/UI/WheelContainer/Wheel/WheelPreviews/WheelImageFullItemPreview.swift index 42ca39a79..8a28d72ec 100644 --- a/Nynja/Library/UI/WheelContainer/Wheel/WheelPreviews/WheelImageFullItemPreview.swift +++ b/Nynja/Library/UI/WheelContainer/Wheel/WheelPreviews/WheelImageFullItemPreview.swift @@ -105,7 +105,32 @@ private extension WheelImageFullItemPreview { imageInfoView = nil } - let data = UIImageJPEGRepresentation(image, 1.0) + var data: Data? + + if type == .image { + data = UIImageJPEGRepresentation(image, 1.0) + } else if type == .video { + let group = DispatchGroup() + let options = PHVideoRequestOptions() + options.isNetworkAccessAllowed = true + options.version = .original + + PHImageManager.default().requestAVAsset( + forVideo: asset, + options: options, + resultHandler: { (asset, audioMix, info) in + guard let urlAsset = asset as? AVURLAsset else { + return + } + + data = try? Data(contentsOf: urlAsset.url) + + group.leave() + }) + + group.enter() + group.wait() + } imageInfoView = makeImageInfoView(on: self, resolution: image.size, diff --git a/Nynja/Library/UI/WheelContainer/WheelContainer.swift b/Nynja/Library/UI/WheelContainer/WheelContainer.swift index 3ac8560e2..78b6ed25f 100644 --- a/Nynja/Library/UI/WheelContainer/WheelContainer.swift +++ b/Nynja/Library/UI/WheelContainer/WheelContainer.swift @@ -144,17 +144,18 @@ class WheelContainer: UIView, UserSettingsRespondable { return } - - // Check that index of item is not out of bounds -// guard itemIndex < wheel.items.count, itemIndex >= 0 else { -// return -// } - let lvl = isFull ? indexPath.count - 1 : indexPath.level - guard itemIndex < dataSource!.wheelContainer(self, numberOfItemsInLevel: lvl), itemIndex >= 0 else { + let level = isFull ? indexPath.count - 1 : indexPath.level + guard itemIndex < dataSource!.wheelContainer(self, numberOfItemsInLevel: level), itemIndex >= 0 else { return } wheel.scrollToItem(at: itemIndex, animated: animated) + + if level < currentScrollingPath.count { + currentScrollingPath[level] = itemIndex + } else { + currentScrollingPath.append(itemIndex) + } } /// Selects item at full or simple index path @@ -312,12 +313,8 @@ class WheelContainer: UIView, UserSettingsRespondable { } func scroll(to index: Int, at level: Int = 0) { - guard wheels.indices.contains(level), level < currentScrollingPath.count else { - return - } - let wheel = wheels[level] - self.currentScrollingPath[level] = index - scrollWheel(wheel, at: level, to: self.currentScrollingPath) + let indexPath = IndexPath(level: level, item: index) + scrollToItem(at: indexPath, isFull: false, animated: false) } private func resetStateIfNeeded() { diff --git a/Nynja/LogService/LogService.swift b/Nynja/LogService/LogService.swift index a8c9eba98..fc96a8b5a 100644 --- a/Nynja/LogService/LogService.swift +++ b/Nynja/LogService/LogService.swift @@ -11,7 +11,7 @@ import os.log class LogService { - static var enabled = true + static var enable:[LogServiceTopic] = LogService.allValues enum LogServiceTopic: String { case wallet = "Wallet" @@ -30,33 +30,44 @@ class LogService { case QRCode = "QRCode" case passphrase = "Passphrase" case push = "Push notification" + case userDefaults = "User Defaults" + case arc = "ARC" } - static let allValues: [LogServiceTopic] = [.wallet, .keychain, .fileSystem, .db, .audioSystem, .MQTT, .amazon, .callSystem, .locationSystem, .system, .network, .galery, .videoConverter, .QRCode] + static let allValues: [LogServiceTopic] = [.userDefaults, .wallet, .keychain, .fileSystem, .db, + .audioSystem, .MQTT, .amazon, .callSystem, .locationSystem, + .system, .network, .galery, .videoConverter, .QRCode, + .passphrase, .arc] static var allValuesStrings: String { - get { - return allValues.reduce("") { (result, topic) -> String in - return "\(result)\(topic.rawValue),\n" - } + return allValues.reduce("") { (result, topic) -> String in + return "\(result)\(topic.rawValue),\n" } } - static func log(topic: LogServiceTopic, text: String) { - #if DEBUG - if !enabled { - return - } - var i = 0 - text.split(by: 30000).forEach { (part) in - let log = OSLog(subsystem: Bundle.main.bundleIdentifier, category: topic.rawValue) - if i != 0 { - os_log("%{public}@", log: log, type: .default, ">>>>> \(i):\n \(part)") - } else { - os_log("%{public}@", log: log, type: .default, "\(part)") - } - i += 1 - } + static func log(topic: LogServiceTopic, block: () -> String) { + #if !RELEASE + if !enable.contains(topic) { return } + LogService.executeLogs(topic: topic, text: block(), thread: Thread.current.debugDescription) #endif } + + private static func executeLogs(topic: LogServiceTopic, text: String, thread: String) { + LogWriter.shared?.writeLog(topic: topic.rawValue, description: text, thread: thread) + printToLog(topic: topic, text: text, thread: thread) + } + + private static func printToLog(topic: LogServiceTopic, text: String, thread: String) { + var i = 0 + let text = "\(thread), \(text)" + text.split(by: 30000).forEach { (part) in + let log = OSLog(subsystem: Bundle.main.bundleIdentifier, category: topic.rawValue) + if i != 0 { + os_log("%{public}@", log: log, type: .default, ">>>>> \(i):\n \(part)") + } else { + os_log("%{public}@", log: log, type: .default, "\(part)") + } + i += 1 + } + } } diff --git a/Nynja/LogService/LogWriter.swift b/Nynja/LogService/LogWriter.swift new file mode 100644 index 000000000..1a1453996 --- /dev/null +++ b/Nynja/LogService/LogWriter.swift @@ -0,0 +1,76 @@ +// +// LogWriter.swift +// Nynja +// +// Created by Anton M on 15.08.2018. +// Copyright © 2018 TecSynt Solutions. All rights reserved. +// + +import Foundation + +class LogWriter { + + static let shared: LogWriter? = LogWriter() + + private var handle: FileHandle! + + private init?() { + let fileName = LogWriter.getCurrentDateAndTime() + ".log" + FileManagerService.sharedInstance.createDirectory(dirName: "Logs") + + guard let path = FileManagerService.sharedInstance.createFile(folder: "Logs", name: fileName), + FileManagerService.sharedInstance.fileManager.createFile(atPath: path, contents: nil, attributes: nil), + let url = URL(string: path) else { return nil } + + do { handle = try FileHandle(forUpdating: url) } + catch { + return nil + } + } + + private static func getCurrentDateAndTime() -> String { + let formatter = DateFormatter() + formatter.dateFormat = "yyyy_MM_dd_HH_mm_ss" + return formatter.string(from: Date()) + } + + func writeLog(topic: String, description: String, thread: String) { + guard let _log = Log(topic: topic, description: description, thread: thread), let data = _log.data else { return } + handle.seekToEndOfFile() + handle.write(data) + } + + func closeFile() { + handle.closeFile() + } + + private struct Log { + private var _bytes: [UInt8] + + init?(topic: String, description: String, thread: String) { + let title = BertAtom(fromString: "Log") + let _topic = topic.getBin() + let _description = description.getBin() + let timestamp = Int64(Date().timeIntervalSince1970.seconds) + let _timeStamp = BertNumber(fromInt64: timestamp) + let _thread = thread.getBin() + let bert = BertTuple(fromElements: [title, _topic, _description, _timeStamp, _thread]) + do { + let bytes = try Bert.encode(object: bert) + _bytes = [UInt8](repeating: 0, count: bytes.length) + bytes.getBytes(&_bytes, length: bytes.length) + } catch { + return nil + } + } + + var data: Data? { + get { + guard let bytesString = _bytes.toBase64() else { return nil } + return (bytesString + ";").data(using: String.Encoding.ascii) + } + } + } +} + + diff --git a/Nynja/MessageActionDAO.swift b/Nynja/MessageActionDAO.swift index b592ff643..32f2cb49a 100644 --- a/Nynja/MessageActionDAO.swift +++ b/Nynja/MessageActionDAO.swift @@ -6,16 +6,26 @@ // Copyright © 2018 TecSynt Solutions. All rights reserved. // -class MessageActionDAO: MessageActionDAOProtocol { +import GRDBCipher + +final class MessageActionDAO: MessageActionDAOProtocol { // MARK: - Fetch // MARK: -- Action - static func fetchMessageAction(by id: Int64) -> DBMessageAction? { + static func fetchMessageAction(by messageServerId: MessageServerId) -> DBMessageAction? { return dbManager.fetch { db in - return try DBMessageAction.action(db, messageId: id) + return try DBMessageAction.action(db, messageId: messageServerId) } } + static func containsDeleteAction(for messageServerId: MessageServerId) -> Bool { + return dbManager.rowExists( + in: MessageActionTable.self, + where: "\(MessageActionTable.Column.messageId) = ? AND \(MessageActionTable.Column.action) = ?", + arguments: [messageServerId, DBMessageAction.Action.delete.rawValue] + ) + } + // MARK: - Actions static func fetchMessageActions() -> [DBMessageAction] { return dbManager.fetch { db in @@ -23,4 +33,7 @@ class MessageActionDAO: MessageActionDAOProtocol { } } + static func delete(_ actions: [DBMessageAction]) throws { + try dbManager.perform(action: .delete, with: actions) + } } diff --git a/Nynja/MessageActionDAOProtocol.swift b/Nynja/MessageActionDAOProtocol.swift index 774a9cfd5..ffd7f7ea8 100644 --- a/Nynja/MessageActionDAOProtocol.swift +++ b/Nynja/MessageActionDAOProtocol.swift @@ -10,9 +10,10 @@ protocol MessageActionDAOProtocol: DAOProtocol { // MARK: - Fetch // MARK: -- Action - static func fetchMessageAction(by id: Int64) -> DBMessageAction? + static func fetchMessageAction(by messageServerId: MessageServerId) -> DBMessageAction? + static func containsDeleteAction(for messageServerId: MessageServerId) -> Bool // MARK: - Actions static func fetchMessageActions() -> [DBMessageAction] - + static func delete(_ actions: [DBMessageAction]) throws } diff --git a/Nynja/MessageBackgroundTaskHandler.swift b/Nynja/MessageBackgroundTaskHandler.swift index dc43d4607..ebf4b7c82 100644 --- a/Nynja/MessageBackgroundTaskHandler.swift +++ b/Nynja/MessageBackgroundTaskHandler.swift @@ -10,8 +10,12 @@ import Foundation final class MessageBackgroundTaskHandler: BackgroundTaskHandler { + // MARK: - Dependencies + private let mqttService: MQTTService + private let storageService: StorageService + private let processingManager: DefaultMessagesProcessingManager private let messageFactory: MessageFactory @@ -20,74 +24,138 @@ final class MessageBackgroundTaskHandler: BackgroundTaskHandler { private let messageEditService: MessageEditServiceProtocol + private let transcribeService: TranscribeService + + + // MARK: - Init + init() { - self.mqttService = MQTTService.sharedInstance - self.processingManager = DefaultMessagesProcessingManager.shared + mqttService = MQTTService.sharedInstance + storageService = StorageService.sharedInstance + processingManager = DefaultMessagesProcessingManager.shared - let messageFactory = MessageFactory() - messageFactory.inject(dependencies: + messageFactory = MessageFactory(dependencies: .init(storageService: StorageService.sharedInstance, payloadBuilder: MessagePayloadBuilder()) ) - self.messageFactory = messageFactory - - let sendingService = MessageSendingService() - sendingService.inject(dependencies: + sendingService = MessageSendingService(dependencies: .init(mqttService: MQTTService.sharedInstance, storageService: StorageService.sharedInstance, processingManager: DefaultMessagesProcessingManager.shared) ) - self.sendingService = sendingService - - self.messageEditService = MessageEditService(dependencies: + messageEditService = MessageEditService(dependencies: .init(storageService: StorageService.sharedInstance) ) + + transcribeService = TranscribeService.shared } + + // MARK: - BackgroundTaskHandler + func performTask() { - uploadMessages() - resendDeleteMessageActions() - resentForwardMessageJobs() - resentEditMessageActions() - } - - private func uploadMessages() { - guard let user = StorageService.sharedInstance.phoneId else { + guard let phoneId = storageService.phoneId, let rosterId = storageService.rosterId else { return } - let messages = MessageDAO.fetchUnsentMessages(from: user) - messages.forEach { processingManager.uploadMessage($0) } - } - - private func resendDeleteMessageActions() { - MessageActionDAO.fetchMessageActions().forEach { - if $0.action == "delete" { deleteMessage($0) } + + // Need to separate messages: + // - can be sent without uploading (can be delivered in jobs) + // - requires uploading before send + var undeliveredMessages = [Message]() + var uploadTargets = [Message]() + + for message in MessageDAO.fetchUnsentMessages(from: phoneId) { + if processingManager.isValidForUploading(message) { + uploadTargets.append(message) + } else { + undeliveredMessages.append(message) + } + } + + // Delete + + let deletedMessages = MessageActionDAO.fetchMessageActions().compactMap { action -> Message? in + guard case .delete = action.action else { + return nil + } + return deletedMessage(for: action) + } + undeliveredMessages.append(contentsOf: deletedMessages) + + // Edit + + let editedMessages = messageEditService.getEditMessagesAndActions().map { action, message in + messageFactory.makeTextMessageEdited(message: message, action: action) } + undeliveredMessages.append(contentsOf: editedMessages) + + upload(uploadTargets) + send(undeliveredMessages, phoneId: phoneId) + + resendUndeliveredForwardMessages() + resendUndeliveredTranscribes() + resendStars(rosterId: rosterId) } - private func deleteMessage(_ action: DBMessageAction) { - guard let message = MessageDAO.fetchMessage(serverId: action.messageId) else { return } + + // MARK: - Messages + + private func deletedMessage(for action: DBMessageAction) -> Message? { + guard let message = MessageDAO.fetchMessage(serverId: action.messageId) else { + return nil + } - var messageForDelete: Message + let messageForDelete: Message if action.phoneId.isEmpty { messageForDelete = messageFactory.makeMessageForDelete(message: message, seenBy: [action.seenBy] as [AnyObject]) } else { let seenBy = action.seenBy == -1 ? -1 as AnyObject : action.phoneId as AnyObject - messageForDelete = messageFactory.makeMessageForDelete(message: message, seenBy: [seenBy] ) + messageForDelete = messageFactory.makeMessageForDelete(message: message, seenBy: [seenBy] ) } - mqttService.sendMessage(message: messageForDelete) + return messageForDelete } - private func resentForwardMessageJobs() { - JobDAO.fetchJobs(of: .forward).forEach { MQTTService.sharedInstance.sendJob($0) } + private func send(_ messages: [Message], phoneId: String) { + guard !messages.isEmpty else { + return + } + // TODO: move to factory + let job = Job(phoneId: phoneId, container: "chain", messages: messages, timestamp: nil, features: nil) + mqttService.sendJob(job) } - - private func resentEditMessageActions() { - messageEditService.getEditMessagesAndActions().forEach { (action, message) in - let editedMessage = messageFactory.makeTextMessageEdited(message: message, action: action) - sendingService.sendMessage(editedMessage) + + private func upload(_ messages: [Message]) { + for message in messages { + processingManager.uploadMessage(message) } } + + // MARK: Forward + + private func resendUndeliveredForwardMessages() { + JobDAO.fetchJobs(of: .forward).forEach { mqttService.sendJob($0) } + } + + // MARK: Transcribe + + private func resendUndeliveredTranscribes() { + ConvertMessageDAO.fetchConvertMessages().forEach { transcribeService.transcribe($0) } + } + + + // MARK: - Stars + + private func resendStars(rosterId: Int64) { + let deletedStars: [Star] = StarActionDAO.fetchStarActions().compactMap { starAction in + guard case .delete = starAction.action else { + return nil + } + return StarDAO.fetchStarBy(clientId: starAction.starLocalId).map { Star(star: $0) } + } + deletedStars.forEach { mqttService.sendStar(star: $0) } + + StarDAO.fetchUndeliveredStars(rosterId: rosterId).forEach { mqttService.sendStar(star: $0) } + } } diff --git a/Nynja/MessageDAO.swift b/Nynja/MessageDAO.swift index 67b936cbb..4c96dbd33 100644 --- a/Nynja/MessageDAO.swift +++ b/Nynja/MessageDAO.swift @@ -15,8 +15,23 @@ class MessageDAO: MessageDAOProtocol { try dbManager.perform(action: .save, with: messages) } + // MARK: - Update + static func updateColumns(_ columns: Set, message: Message) { + let columns = Set(columns.map { $0.title }) + try? dbManager.perform(action: .updateColumns(columns), with: message) + } + // MARK: - Fetch // MARK: -- Message + + static func messageExists(serverId: MessageServerId) throws -> Bool { + return dbManager.rowExists( + in: MessageTable.self, + where: "\(MessageTable.Column.serverId) = ?", + arguments: [serverId] + ) + } + static func fetchMessage(by rowId: Int64) -> Message? { return fetchMessage { db in return try DBMessage.message(from: db, rowId: rowId) @@ -29,13 +44,13 @@ class MessageDAO: MessageDAOProtocol { } } - static func fetchMessage(serverId: Int64) -> Message? { + static func fetchMessage(serverId: MessageServerId) -> Message? { return fetchMessage { db in return try DBMessage.message(db, serverId: serverId) } } - static func fetchMessage(localId: String) -> Message? { + static func fetchMessage(localId: MessageLocalId) -> Message? { return fetchMessage { db in return try DBMessage.message(db, localId: localId) } @@ -47,9 +62,9 @@ class MessageDAO: MessageDAOProtocol { } } - static func fetchPenultimateMessage(of type: FetchType) -> Message? { + static func fetchLastOponnentMessage(of type: FetchType, phoneId: String) -> Message? { return fetchMessage { db in - return try DBMessage.penultimateMessage(db, ofType: type) + return try DBMessage.lastOponnentMessage(db, ofType: type, phoneId: phoneId) } } @@ -81,7 +96,7 @@ class MessageDAO: MessageDAOProtocol { return fetchMessages(.p2p(from: ids[0], to: ids[1])) } - static func fetchMessages(to: String, serversIds: Set) -> [Message] { + static func fetchMessages(to: String, serversIds: Set) -> [Message] { guard let from = StorageService.sharedInstance.phoneId else { return [] } let ids = [from, to].sorted() @@ -93,7 +108,7 @@ class MessageDAO: MessageDAOProtocol { return fetchMessages(.muc(name: name)) } - static func fetchMessages(name: String, serversIds: Set) -> [Message] { + static func fetchMessages(name: String, serversIds: Set) -> [Message] { return fetchMessages(.muc(name: name), serverIds: serversIds) } @@ -105,7 +120,7 @@ class MessageDAO: MessageDAOProtocol { return messages.map { Message(message: $0) } } - static func fetchMessages(_ type: FetchType, serverIds: Set) -> [Message] { + static func fetchMessages(_ type: FetchType, serverIds: Set) -> [Message] { let messages = dbManager.fetch { db in return try DBMessage.messages(db, fetchType: type, serverIds: serverIds) } @@ -113,15 +128,53 @@ class MessageDAO: MessageDAOProtocol { return messages.map { Message(message: $0) } } - /// Remove all messages with 'serverId' < messageServerId. - static func removeMessages(_ type: FetchType, before messageServerId: Int64) throws { - let messages = dbManager.fetch { db in - return try DBMessage.messages(db, fetchType: type, before: messageServerId) + // MARK: - Cursor + + static func dropFirst(for fetchType: FetchType, closure: (DBMessage) -> Void) throws { + dbManager.fetch { db in + let cursor = try DBMessage.fetchCursor(db, fetchType: fetchType, orderedBy: .desc, orderColumn: .serverId).dropFirst() + + while let message = try cursor.next() { + closure(message) + } + } + } + + // MARK: - Trusted + + static func trustMessages(before serverId: MessageServerId, in fetchType: FetchType) throws { + guard let feedId = FeedDAO.fetchFeedId(for: fetchType) else { + return + } + + let sql = """ + UPDATE \(MessageTable.name) + SET \(MessageTable.Column.trusted) = ? + WHERE \(MessageTable.Column.serverId) <= ? AND \(MessageTable.Column.feedId) = ? + """ + + try dbManager.write { db in + try db.execute(sql, arguments: [true, serverId, feedId]) } - try dbManager.perform(action: .delete, with: messages) } + static func trustIfNextMessageExists(before message: Message) throws { + var isTrusted: Bool? + + if message.next == nil { + isTrusted = true + } else if let serverId = message.next, try MessageDAO.messageExists(serverId: serverId) { + isTrusted = true + } + + if let isTrusted = isTrusted { + message.isTrusted = isTrusted + } + } + + // MARK: - Clear History + static func clearHistory(_ type: FetchType) throws { let messages = dbManager.fetch { db in return try DBMessage.messages(db, fetchType: type) @@ -130,6 +183,14 @@ class MessageDAO: MessageDAOProtocol { try dbManager.perform(action: .delete, with: messages) } + /// Remove all messages with 'serverId' < messageServerId. + static func clearMessages(_ type: FetchType, before messageServerId: Int64) throws { + let messages = dbManager.fetch { db in + return try DBMessage.messages(db, fetchType: type, before: messageServerId) + } + try dbManager.perform(action: .delete, with: messages) + } + // MARK: - Remove static func removeMessage(using message: Message) -> Bool { @@ -153,6 +214,14 @@ class MessageDAO: MessageDAOProtocol { return shouldMarkMessageAsDelete } + //MARK: - Primary Key + static func fetchMessagePrimaryKey(with serverId: Int64) -> Int64? { + let dbMessage = dbManager.fetch { db in + return try DBMessage.message(db, serverId: serverId) + } + return dbMessage?.id + } + private static func shouldMarkMessageAsDelete(_ message: Message) -> Bool { let stringIds = message.seenby as? [String] ?? [] let intIds = message.seenby as? [Int64] ?? [] @@ -177,13 +246,13 @@ class MessageDAO: MessageDAOProtocol { return false } - private static func findLocalId(serverId: Int64) -> String? { + private static func findLocalId(serverId: MessageServerId) -> String? { let sql = """ select \(MessageTable.Column.localId) from \(MessageTable.name) where \(MessageTable.Column.serverId) = '\(serverId)' """ - let request = SQLRequest(sql).asRequest(of: String.self) + let request = SQLRequest(sql).asRequest(of: MessageLocalId.self) return dbManager.fetch { db in return try request.fetchOne(db) diff --git a/Nynja/MessageDAOProtocol.swift b/Nynja/MessageDAOProtocol.swift index 0db692d17..b703ed2ad 100644 --- a/Nynja/MessageDAOProtocol.swift +++ b/Nynja/MessageDAOProtocol.swift @@ -16,6 +16,9 @@ protocol MessageDAOProtocol: DAOProtocol { // MARK: - Save static func saveMessages(_ messages: [Message]) throws + // MARK: - Update + static func updateColumns(_ columns: Set, message: Message) + // MARK: - Fetch // MARK: -- Message static func fetchMessage(by rowId: Int64) -> Message? @@ -24,7 +27,7 @@ protocol MessageDAOProtocol: DAOProtocol { static func fetchMessage(localId: String) -> Message? static func fetchLastMessage(of type: FetchType) -> Message? - static func fetchPenultimateMessage(of type: FetchType) -> Message? + static func fetchLastOponnentMessage(of type: FetchType, phoneId: String) -> Message? // MARK: -- Unsent static func fetchUnsentMessages(from: String) -> [Message] @@ -39,10 +42,22 @@ protocol MessageDAOProtocol: DAOProtocol { static func fetchMessages(_ type: FetchType) -> [Message] static func fetchMessages(_ type: FetchType, serverIds: Set) -> [Message] + // MARK: - Cursor + static func dropFirst(for fetchType: FetchType, closure: (DBMessage) -> Void) throws + + // MARK: - Trusted + + static func trustMessages(before serverId: MessageServerId, in fetchType: FetchType) throws + static func trustIfNextMessageExists(before message: Message) throws + // MARK: - Clear History static func clearHistory(_ type: FetchType) throws + static func clearMessages(_ type: FetchType, before messageServerId: Int64) throws // MARK: - Remove static func removeMessage(using message: Message) -> Bool + + //MARK: - Primary Key + static func fetchMessagePrimaryKey(with serverId: Int64) -> Int64? } diff --git a/Nynja/MessageEditActionDAO.swift b/Nynja/MessageEditActionDAO.swift index ba1e92717..d816eb1c3 100644 --- a/Nynja/MessageEditActionDAO.swift +++ b/Nynja/MessageEditActionDAO.swift @@ -6,21 +6,30 @@ // Copyright © 2018 TecSynt Solutions. All rights reserved. // -class MessageEditActionDAO: MessageEditActionDAOProtocol { +import GRDBCipher + +final class MessageEditActionDAO: MessageEditActionDAOProtocol { // MARK: - Fetch // MARK: -- Action - static func fetchMessageEditAction(by id: Int64) -> DBMessageEditAction? { + static func fetchMessageEditAction(by id: MessageServerId) -> DBMessageEditAction? { return dbManager.fetch { db in return try DBMessageEditAction.action(db, messageId: id) } } + static func containsEditAction(for messageServerId: MessageServerId) -> Bool { + return dbManager.rowExists( + in: MessageEditActionTable.self, + where: "\(MessageEditActionTable.Column.messageId) = ?", + arguments: [messageServerId] + ) + } + // MARK: - Actions static func fetchMessageEditActions() -> [DBMessageEditAction] { return dbManager.fetch { db in return try DBMessageEditAction.fetchAll(db) } } - } diff --git a/Nynja/MessageEditActionDAOProtocol.swift b/Nynja/MessageEditActionDAOProtocol.swift index d883998ef..acdd4193d 100644 --- a/Nynja/MessageEditActionDAOProtocol.swift +++ b/Nynja/MessageEditActionDAOProtocol.swift @@ -10,7 +10,9 @@ protocol MessageEditActionDAOProtocol: DAOProtocol { // MARK: - Fetch // MARK: -- Action - static func fetchMessageEditAction(by id: Int64) -> DBMessageEditAction? + static func fetchMessageEditAction(by id: MessageServerId) -> DBMessageEditAction? + + static func containsEditAction(for messageServerId: MessageServerId) -> Bool // MARK: - Actions static func fetchMessageEditActions() -> [DBMessageEditAction] diff --git a/Nynja/MessageEditService/MessageEditService.swift b/Nynja/MessageEditService/MessageEditService.swift index 72dc69fd9..3482fa82b 100644 --- a/Nynja/MessageEditService/MessageEditService.swift +++ b/Nynja/MessageEditService/MessageEditService.swift @@ -28,14 +28,14 @@ final class MessageEditService: MessageEditServiceProtocol, InitializeInjectable let storageService: StorageService } - init(dependencies: MessageEditService.Dependencies) { + init(dependencies: Dependencies) { storageService = dependencies.storageService } // MARK: - MessageEditServiceProtocol func getMergeActionForMessage(_ message: Message) -> EditMessageMergeAction { - guard let id = message.id, MessageEditActionDAO.fetchMessageEditAction(by: id) != nil else { + guard let id = message.id, MessageEditActionDAO.containsEditAction(for: id) else { return .override } return .skip diff --git a/Nynja/MessagesProcessingManager.swift b/Nynja/MessagesProcessingManager.swift index 15fffa5f0..f26059f16 100644 --- a/Nynja/MessagesProcessingManager.swift +++ b/Nynja/MessagesProcessingManager.swift @@ -15,6 +15,8 @@ protocol MessageProcessingDelegate : class { protocol MessageProcessingManagerInterface: class { var delegate: MessageProcessingDelegate? {get set} + func isValidForUploading(_ message: Message) -> Bool + @discardableResult func uploadMessage(_ message:Message) -> ProgressModel? func downloadMessage(_ message:Message) diff --git a/Nynja/MigrationManager.swift b/Nynja/MigrationManager.swift index 238d326c1..643ebe939 100644 --- a/Nynja/MigrationManager.swift +++ b/Nynja/MigrationManager.swift @@ -9,6 +9,7 @@ import GRDBCipher enum Migration: Int, Describable { + case createConvertMessageTable case createRecentStickersTable case createStickerPackTable case updateDescSticker @@ -21,6 +22,8 @@ enum Migration: Int, Describable { case renameChannelFeatureKeys case updateMessageIdForeignKeyOnContactRoomTables case primaryKeyMessageAction + case addTrustedColumnForMessage + case createStarActionTable static var allTitles: [String] = { var i = 0 @@ -36,7 +39,7 @@ enum Migration: Int, Describable { }() } -class MigrationManager { +final class MigrationManager { private var migrator: DatabaseMigrator @@ -54,24 +57,26 @@ class MigrationManager { try migrator.migrate(poolOrQueue) } } catch let error { - LogService.log(topic: .db, text: error.localizedDescription) + LogService.log(topic: .db) { return error.localizedDescription } } } // MARK: - Private methods private func registerMigrations() { - migrator.registerMigration(Migration.createRecentStickersTable.title) { db in - guard try !db.tableExists(RecentStickerTable.name) else { return } - try RecentStickerTable.create(in: db) + migrator.registerMigration(.createConvertMessageTable) { db in + try ConvertMessageTable.createIfNotExists(in: db) } - migrator.registerMigration(Migration.createStickerPackTable.title) { db in - guard try !db.tableExists(StickerPackTable.name) else { return } - try StickerPackTable.create(in: db) + migrator.registerMigration(.createRecentStickersTable) { db in + try RecentStickerTable.createIfNotExists(in: db) } - migrator.registerMigration(Migration.updateDescSticker.title) { db in - try db.rename(table: DescTable.name, to: "OldDesc") + migrator.registerMigration(.createStickerPackTable) { db in + try StickerPackTable.createIfNotExists(in: db) + } + + migrator.registerMigration(.updateDescSticker) { db in + try DescTable.rename(to: "OldDesc", in: db) try DescTable.create(in: db) let rows = try Row.fetchCursor(db, "Select * from OldDesc") @@ -89,9 +94,10 @@ class MigrationManager { try db.drop(table: "OldDesc") } - migrator.registerMigration(Migration.updateContactReader.title) { db in - try db.rename(table: ContactTable.name, to: "OldContact") + migrator.registerMigration(.updateContactReader) { db in + try ContactTable.rename(to: "OldContact", in: db) try ContactTable.create(in: db) + let rows = try Row.fetchCursor(db, "Select * from OldContact") while let row = try rows.next() { let newContact = DBContact(oldReaderRow: row) @@ -100,12 +106,11 @@ class MigrationManager { try db.drop(table: "OldContact") } - migrator.registerMigration(Migration.removeVoxIdEmailContact.title) { (db) in + migrator.registerMigration(.removeVoxIdEmailContact) { (db) in let emailNameColumn = "email" let voxIdNameColumn = "voxId" - let tableName = ContactTable.name - if try db.hasColumns([emailNameColumn, voxIdNameColumn], tableName: tableName) { + if try ContactTable.hasColumns([emailNameColumn, voxIdNameColumn], in: db) { let contacts = try DBContact.fetchAll(db) try contacts.forEach { try $0.construct(db) @@ -115,7 +120,7 @@ class MigrationManager { try $0.deleteAggregate(db) } - try db.drop(table: tableName) + try ContactTable.drop(in: db) try ContactTable.create(in: db) try contacts.forEach { @@ -124,12 +129,11 @@ class MigrationManager { } } - migrator.registerMigration(Migration.removeVoxIdEmailMember.title) { (db) in + migrator.registerMigration(.removeVoxIdEmailMember) { (db) in let emailNameColumn = "email" let voxIdNameColumn = "voxId" - let tableName = MemberTable.name - if try db.hasColumns([emailNameColumn, voxIdNameColumn], tableName: tableName) { + if try MemberTable.hasColumns([emailNameColumn, voxIdNameColumn], in: db) { let roomMembers = try DBRoomMember.fetchAll(db) let members = try DBMember.all(from: db) @@ -138,7 +142,7 @@ class MigrationManager { try $0.deleteAggregate(db) } - try db.drop(table: tableName) + try MemberTable.drop(in: db) try MemberTable.create(in: db) try members.forEach { @@ -151,15 +155,15 @@ class MigrationManager { } } - migrator.registerMigration(Migration.serviceTableWalletMigration.title) { db in - let tableName = ServiceTable.name + migrator.registerMigration(.serviceTableWalletMigration) { db in let targetIdColumn = ServiceTable.Column.targetId.title let targetTypeColumn = ServiceTable.Column.targetType.title - guard try !db.hasColumns([targetIdColumn, targetTypeColumn], tableName: tableName) else { return } + guard try !ServiceTable.hasColumns([targetIdColumn, targetTypeColumn], in: db) else { return } let serviceOldKey = "oldService" - try db.rename(table: tableName, to: serviceOldKey) + + try ServiceTable.rename(to: serviceOldKey, in: db) try ServiceTable.create(in: db) let rows = try Row.fetchCursor(db, "Select * from \(serviceOldKey)") @@ -180,7 +184,7 @@ class MigrationManager { try db.drop(table: serviceOldKey) } - migrator.registerMigration(Migration.renameLinksToMessageLink.title) { db in + migrator.registerMigration(.renameLinksToMessageLink) { db in let oldName = "links" let newName = MessageLinkTable.name @@ -194,26 +198,24 @@ class MigrationManager { } } - migrator.registerMigration(Migration.createLinkForRoom.title) { db in - if try !db.tableExists(LinkTable.name) { - try LinkTable.create(in: db) - } + migrator.registerMigration(.createLinkForRoom) { db in + try LinkTable.createIfNotExists(in: db) } - migrator.registerMigration(Migration.renameChannelFeatureKeys.title) { db in + migrator.registerMigration(.renameChannelFeatureKeys) { db in try self.replaceFeatureKey("SubscribersCount", newKey: .subscribersCount, db: db) try self.replaceFeatureKey("AdminsCount", newKey: .adminsCount, db: db) } - migrator.registerMigration(Migration.updateMessageIdForeignKeyOnContactRoomTables.title) { db in + migrator.registerMigration(.updateMessageIdForeignKeyOnContactRoomTables) { db in let rooms = try DBRoom.fetchAll(db) let contacts = try DBContact.fetchAll(db) let roomMembers = try DBRoomMember.fetchAll(db) let links = try DBLink.fetchAll(db) - try db.drop(table: LinkTable.name) - try db.drop(table: RoomTable.name) - try db.drop(table: ContactTable.name) + try LinkTable.drop(in: db) + try RoomTable.drop(in: db) + try ContactTable.drop(in: db) try RoomTable.create(in: db) try ContactTable.create(in: db) @@ -225,29 +227,32 @@ class MigrationManager { try roomMembers.forEach { try $0.save(db) } } - migrator.registerMigration(Migration.primaryKeyMessageAction.title) { db in + migrator.registerMigration(.primaryKeyMessageAction) { db in let actions = try DBMessageAction.fetchAll(db) - try db.drop(table: MessageActionTable.name) + try MessageActionTable.drop(in: db) try MessageActionTable.create(in: db) try actions.forEach { try $0.saveAggregate(db) } } - } - - private func recreateDescTable(_ db: Database, closure: ((DBDesc) -> Void)? = nil) throws { - // NOTE: I think that in such situation it is better to use cursor, but we have constraints, - // that 'databaseTableName' is calculating property and we can't mock it :( - let descs = try DBDesc.fetchAll(db) - try db.drop(table: DescTable.name) - try DescTable.create(in: db) - try descs.forEach({ (desc) in - closure?(desc) - try desc.saveAggregate(db) - }) - } - - private func createTableIfNotExists(_ db: Database, type: T.Type) throws { - if try !db.tableExists(type.name) { - try type.create(in: db) + + migrator.registerMigration(.addTrustedColumnForMessage) { db in + let column = MessageTable.Column.trusted.title + guard try !MessageTable.hasColumns([column], in: db) else { + return + } + try MessageTable.alter(in: db) { t in + t.add(column: column, .boolean) + } + + let sql = """ + UPDATE \(MessageTable.name) + SET \(column) = ? + """ + + try db.execute(sql, arguments: [true]) + } + + migrator.registerMigration(.createStarActionTable) { db in + try StarActionTable.createIfNotExists(in: db) } } @@ -261,5 +266,12 @@ class MigrationManager { try db.execute(sqlString) } - +} + +// MARK: - DatabaseMigrator + +private extension DatabaseMigrator { + mutating func registerMigration(_ migration: Migration, migrate: @escaping (Database) throws -> Void) { + registerMigration(migration.title, migrate: migrate) + } } diff --git a/Nynja/Models/ChatModel.swift b/Nynja/Models/ChatModel.swift index bb50bb25c..43b7a4d9c 100644 --- a/Nynja/Models/ChatModel.swift +++ b/Nynja/Models/ChatModel.swift @@ -6,4 +6,4 @@ // Copyright © 2018 TecSynt Solutions. All rights reserved. // -typealias ChatModel = BaseChatModel & DBModelConvertible & HistoryRequestModelTypeProtocol +typealias ChatModel = BaseChatModel & DBModelConvertible & HistoryRequestModelTypeRepresentable diff --git a/Nynja/Modules/AddContactByUsername/AddContactByUsernameProtocols.swift b/Nynja/Modules/AddContactByUsername/AddContactByUsernameProtocols.swift index d869875e8..ee8ce74a0 100644 --- a/Nynja/Modules/AddContactByUsername/AddContactByUsernameProtocols.swift +++ b/Nynja/Modules/AddContactByUsername/AddContactByUsernameProtocols.swift @@ -18,10 +18,9 @@ protocol AddContactByUsernameWireFrameProtocol: class { func showContact(contact: Contact) func showMyProfile() - } -protocol AddContactByUsernameViewProtocol: class { +protocol AddContactByUsernameViewProtocol: LoadingInteractiveView { var presenter: AddContactByUsernamePresenterProtocol! { get set } @@ -30,9 +29,9 @@ protocol AddContactByUsernameViewProtocol: class { */ } -protocol AddContactByUsernamePresenterProtocol: AnyObject, BasePresenterProtocol { +protocol AddContactByUsernamePresenterProtocol: BasePresenterProtocol { - var view: AddContactByUsernameViewProtocol! { get set } +// var view: AddContactByUsernameViewProtocol! { get set } var interactor: AddContactByUsernameInteractorInputProtocol! { get set } var wireFrame: AddContactByUsernameWireFrameProtocol! { get set } @@ -43,7 +42,7 @@ protocol AddContactByUsernamePresenterProtocol: AnyObject, BasePresenterProtocol func search(_ name:String) } -protocol AddContactByUsernameInteractorOutputProtocol: class { +protocol AddContactByUsernameInteractorOutputProtocol: LoadingInteractive { /** * Add here your methods for communication INTERACTOR -> PRESENTER diff --git a/Nynja/Modules/AddContactByUsername/Interactor/AddContactByUsernameInteractor.swift b/Nynja/Modules/AddContactByUsername/Interactor/AddContactByUsernameInteractor.swift index fd6ec4332..21d47a38e 100644 --- a/Nynja/Modules/AddContactByUsername/Interactor/AddContactByUsernameInteractor.swift +++ b/Nynja/Modules/AddContactByUsername/Interactor/AddContactByUsernameInteractor.swift @@ -6,33 +6,100 @@ // Copyright © 2018 Michael Katkov. All rights reserved. // -class AddContactByUsernameInteractor: AddContactByUsernameInteractorInputProtocol, IoHandlerDelegate { +final class AddContactByUsernameInteractor: AddContactByUsernameInteractorInputProtocol, MQTTServiceDelegate, IoHandlerDelegate { weak var presenter: AddContactByUsernameInteractorOutputProtocol! + private let mqttService: MQTTService + + private var searchAction: (() -> Void)? + + + // MARK: - Init + init() { + mqttService = MQTTService.sharedInstance + mqttService.addSubscriber(self) IoHandler.delegate = self } + deinit { + mqttService.removeSubscriber(self) + } + + + // MARK: - AddContactByUsernameInteractorInputProtocol + func getContactByUserName(username: String) { + searchAction = nil + if let contact = ContactDAO.findContactBy(username: username) { if contact.phoneId == StorageService.sharedInstance.phoneId { - self.presenter.getMyProfile() + presenter.getMyProfile() } else { - self.presenter.getContactSuccess(contact: contact) + presenter.getContactSuccess(contact: contact) } } else { - MQTTService.sharedInstance.searchContactByUsername(nick: username) + searchAction = { [weak self] in + guard let `self` = self, self.mqttService.isConnectedSuccess else { + return + } + self.mqttService.searchContactByUsername(nick: username) + } + startRemoteSearch() } } + // MARK: - IoHandlerDelegate + func getContactByUsernameSucces(contact: Contact) { - self.presenter.getContactSuccess(contact: contact) + finishSearch() + + // TODO: need to implement case insensitive search in database and save valid local status here + if StringAtom.string(contact.status) == nil, let phoneId = contact.phone_id { + let status = ContactDAO.fetchContact(by: phoneId)?.status + contact.status = StringAtom(string: status) + } + + presenter.getContactSuccess(contact: contact) } func contactByUsernameNotFound() { - self.presenter.getContactFailed() + finishSearch() + presenter.getContactFailed() } + + // MARK: - Utils + + private func startRemoteSearch() { + guard let searchAction = searchAction else { + return + } + if mqttService.isConnectedSuccess { + presenter.showHUD() + } + searchAction() + } + + private func finishSearch() { + searchAction = nil + presenter.hideHUD() + } + + + // MARK: - MQTTServiceDelegate + + func mqttServiceDidConnect(_ mqttService: MQTTService) { + DispatchQueue.main.async { + self.startRemoteSearch() + } + } + + func mqttServiceDidDisconnect(_ mqttService: MQTTService) { + DispatchQueue.main.async { + self.presenter.hideHUD() + } + } } diff --git a/Nynja/Modules/AddContactByUsername/Presenter/AddContactByUsernamePresenter.swift b/Nynja/Modules/AddContactByUsername/Presenter/AddContactByUsernamePresenter.swift index 55bc750c4..c7e1307d1 100644 --- a/Nynja/Modules/AddContactByUsername/Presenter/AddContactByUsernamePresenter.swift +++ b/Nynja/Modules/AddContactByUsername/Presenter/AddContactByUsernamePresenter.swift @@ -6,7 +6,7 @@ // Copyright © 2018 Michael Katkov. All rights reserved. // -class AddContactByUsernamePresenter: BasePresenter, AddContactByUsernamePresenterProtocol, AddContactByUsernameInteractorOutputProtocol { +final class AddContactByUsernamePresenter: BasePresenter, AddContactByUsernamePresenterProtocol, AddContactByUsernameInteractorOutputProtocol { override var itemsFactory: WCItemsFactory? { return ByUsernameItemsFactory() @@ -29,7 +29,14 @@ class AddContactByUsernamePresenter: BasePresenter, AddContactByUsernamePresente } func getMyProfile() { - self.wireFrame.showMyProfile() + wireFrame.showMyProfile() + } + + func showHUD() { + view.showSpinner() + } + + func hideHUD() { + view.hideSpinner() } - } diff --git a/Nynja/Modules/AddContactByUsername/View/AddContactByUsernameViewController.swift b/Nynja/Modules/AddContactByUsername/View/AddContactByUsernameViewController.swift index 8d55eb7e8..413183ce3 100644 --- a/Nynja/Modules/AddContactByUsername/View/AddContactByUsernameViewController.swift +++ b/Nynja/Modules/AddContactByUsername/View/AddContactByUsernameViewController.swift @@ -8,7 +8,7 @@ import UIKit -class AddContactByUsernameViewController: BaseVC, AddContactByUsernameViewProtocol { +final class AddContactByUsernameViewController: BaseVC, AddContactByUsernameViewProtocol { var presenter: AddContactByUsernamePresenterProtocol! { didSet { @@ -77,13 +77,14 @@ class AddContactByUsernameViewController: BaseVC, AddContactByUsernameViewProtoc // MARK: - Actions - @objc func searchTapped() { + @objc private func searchTapped() { if valid() { self.view.endEditing(true) self.presenter.search(userNameField.input.text ?? "") } } + // MARK: - Utils private func valid() -> Bool { diff --git a/Nynja/Modules/AddContactByUsername/WireFrame/AddContactByUsernameWireframe.swift b/Nynja/Modules/AddContactByUsername/WireFrame/AddContactByUsernameWireframe.swift index 0756b36e5..dc3f7fab6 100644 --- a/Nynja/Modules/AddContactByUsername/WireFrame/AddContactByUsernameWireframe.swift +++ b/Nynja/Modules/AddContactByUsername/WireFrame/AddContactByUsernameWireframe.swift @@ -8,7 +8,7 @@ import UIKit -class AddContactByUsernameWireFrame: AddContactByUsernameWireFrameProtocol { +final class AddContactByUsernameWireFrame: AddContactByUsernameWireFrameProtocol { weak var navigation : UINavigationController? weak var main: MainWireFrame? diff --git a/Nynja/Modules/AddContactViaPhone/AddContactViaPhoneProtocols.swift b/Nynja/Modules/AddContactViaPhone/AddContactViaPhoneProtocols.swift index c9ae16501..44f91475b 100644 --- a/Nynja/Modules/AddContactViaPhone/AddContactViaPhoneProtocols.swift +++ b/Nynja/Modules/AddContactViaPhone/AddContactViaPhoneProtocols.swift @@ -21,7 +21,7 @@ protocol AddContactViaPhoneWireFrameProtocol: class { func showSelectCountry(_ selectCountryDelegate: SelectCountryDelegate) } -protocol AddContactViaPhoneViewProtocol: class { +protocol AddContactViaPhoneViewProtocol: LoadingInteractiveView { var presenter: AddContactViaPhonePresenterProtocol! { get set } @@ -43,7 +43,7 @@ protocol AddContactViaPhonePresenterProtocol: BasePresenterProtocol { func showSelectCountry(_ selectCountryDelegate: SelectCountryDelegate) } -protocol AddContactViaPhoneInteractorOutputProtocol: class { +protocol AddContactViaPhoneInteractorOutputProtocol: LoadingInteractive { /** * Add here your methods for communication INTERACTOR -> PRESENTER diff --git a/Nynja/Modules/AddContactViaPhone/Interactor/AddContactViaPhoneInteractor.swift b/Nynja/Modules/AddContactViaPhone/Interactor/AddContactViaPhoneInteractor.swift index a6bec84fa..b3e89801c 100644 --- a/Nynja/Modules/AddContactViaPhone/Interactor/AddContactViaPhoneInteractor.swift +++ b/Nynja/Modules/AddContactViaPhone/Interactor/AddContactViaPhoneInteractor.swift @@ -6,33 +6,58 @@ // Copyright © 2017 TecSynt Solutions. All rights reserved. // -class AddContactViaPhoneInteractor: AddContactViaPhoneInteractorInputProtocol, IoHandlerDelegate, SetInjectable { +final class AddContactViaPhoneInteractor: AddContactViaPhoneInteractorInputProtocol, IoHandlerDelegate, MQTTServiceDelegate, SetInjectable { weak var presenter: AddContactViaPhoneInteractorOutputProtocol! private var mqttService: MQTTService! private var storageService: StorageService! + private var searchAction: (() -> Void)? + + private var currentNumber: String? + + + // MARK: - Init + init() { IoHandler.delegate = self } - var currentNumer = "" - var status = "" + deinit { + mqttService.removeSubscriber(self) + } + + + // MARK: - AddContactViaPhoneInteractorInputProtocol func getContactByPhone(number: String) { - currentNumer = number + currentNumber = number + searchAction = nil if storageService.phone == number { presenter.getMyProfile() } else if let contact = ContactDAO.findContactBy(phone: number) { presenter.getContactByPhoneSuccess(contact: contact) } else { - mqttService.tryFindContact(number: number) + searchAction = { [weak self] in + guard let `self` = self, self.mqttService.isConnectedSuccess else { + return + } + self.mqttService.tryFindContact(number: number) + } + startRemoteSearch() } } + + + // MARK: - IoHandlerDelegate func getContactSuccess(contact: Contact) { - self.presenter.getContactByPhoneSuccess(contact: contact) + guard let currentNumber = currentNumber, contact.phoneNumber == currentNumber else { + return + } + finishSearch() + presenter.getContactByPhoneSuccess(contact: contact) } func contactsFound(contacts: [Contact]) { @@ -41,12 +66,42 @@ class AddContactViaPhoneInteractor: AddContactViaPhoneInteractorInputProtocol, I } } - func profileNotFound() { - self.presenter.getContactByPhoneFailed() + func contactNotFound() { + finishSearch() + presenter.getContactByPhoneFailed() } - func contactNotFound() { - profileNotFound() + + // MARK: - Utils + + private func startRemoteSearch() { + guard let searchAction = searchAction else { + return + } + if mqttService.isConnectedSuccess { + presenter.showHUD() + } + searchAction() + } + + private func finishSearch() { + searchAction = nil + presenter.hideHUD() + } + + + // MARK: - MQTTServiceDelegate + + func mqttServiceDidConnect(_ mqttService: MQTTService) { + DispatchQueue.main.async { + self.startRemoteSearch() + } + } + + func mqttServiceDidDisconnect(_ mqttService: MQTTService) { + DispatchQueue.main.async { + self.presenter.hideHUD() + } } } @@ -63,5 +118,6 @@ extension AddContactViaPhoneInteractor { presenter = dependencies.presenter mqttService = dependencies.mqttService storageService = dependencies.storageService + mqttService.addSubscriber(self) } } diff --git a/Nynja/Modules/AddContactViaPhone/Presenter/AddContactViaPhonePresenter.swift b/Nynja/Modules/AddContactViaPhone/Presenter/AddContactViaPhonePresenter.swift index b1b4cbe4f..35bdb4b0e 100644 --- a/Nynja/Modules/AddContactViaPhone/Presenter/AddContactViaPhonePresenter.swift +++ b/Nynja/Modules/AddContactViaPhone/Presenter/AddContactViaPhonePresenter.swift @@ -6,7 +6,7 @@ // Copyright © 2017 TecSynt Solutions. All rights reserved. // -class AddContactViaPhonePresenter: BasePresenter, AddContactViaPhonePresenterProtocol, AddContactViaPhoneInteractorOutputProtocol { +final class AddContactViaPhonePresenter: BasePresenter, AddContactViaPhonePresenterProtocol, AddContactViaPhoneInteractorOutputProtocol { override var itemsFactory: WCItemsFactory? { return ByNumberItemsFactory() @@ -35,4 +35,12 @@ class AddContactViaPhonePresenter: BasePresenter, AddContactViaPhonePresenterPro func showSelectCountry(_ selectCountryDelegate: SelectCountryDelegate) { self.wireFrame.showSelectCountry(selectCountryDelegate) } + + func showHUD() { + view.showSpinner() + } + + func hideHUD() { + view.hideSpinner() + } } diff --git a/Nynja/Modules/AddContactViaPhone/View/AddContactViaPhoneViewController.swift b/Nynja/Modules/AddContactViaPhone/View/AddContactViaPhoneViewController.swift index 873ac2cf9..567f8fde2 100644 --- a/Nynja/Modules/AddContactViaPhone/View/AddContactViaPhoneViewController.swift +++ b/Nynja/Modules/AddContactViaPhone/View/AddContactViaPhoneViewController.swift @@ -9,7 +9,7 @@ import UIKit import libPhoneNumber_iOS -class AddContactViaPhoneViewController: BaseVC, AddContactViaPhoneViewProtocol { +final class AddContactViaPhoneViewController: BaseVC, AddContactViaPhoneViewProtocol { var presenter: AddContactViaPhonePresenterProtocol! { didSet { @@ -42,7 +42,7 @@ class AddContactViaPhoneViewController: BaseVC, AddContactViaPhoneViewProtocol { return lbl }() - private let inputFont: UIFont = UIFont(fontName: Constants.fonts.medium, height: 22)! + private let inputFont: UIFont = UIFont.makeFont(with: Constants.fonts.medium, height: 22)! private lazy var countryField: CountryField = { let topOffset = 16.adjustedByWidth diff --git a/Nynja/Modules/AddContactViaPhone/WireFrame/AddContactViaPhoneWireframe.swift b/Nynja/Modules/AddContactViaPhone/WireFrame/AddContactViaPhoneWireframe.swift index dca040466..1869f810b 100644 --- a/Nynja/Modules/AddContactViaPhone/WireFrame/AddContactViaPhoneWireframe.swift +++ b/Nynja/Modules/AddContactViaPhone/WireFrame/AddContactViaPhoneWireframe.swift @@ -8,7 +8,7 @@ import UIKit -class AddContactViaPhoneWireFrame: AddContactViaPhoneWireFrameProtocol { +final class AddContactViaPhoneWireFrame: AddContactViaPhoneWireFrameProtocol { weak var navigation : UINavigationController? weak var main: MainWireFrame? diff --git a/Nynja/Modules/AddParticipants/AddParticipantsProtocols.swift b/Nynja/Modules/AddParticipants/AddParticipantsProtocols.swift index 98cd7d640..9cdfd4b04 100644 --- a/Nynja/Modules/AddParticipants/AddParticipantsProtocols.swift +++ b/Nynja/Modules/AddParticipants/AddParticipantsProtocols.swift @@ -20,7 +20,7 @@ extension EditParticipantsDelegate { protocol AddParticipantsWireFrameProtocol: class { - func presentAddParticipants(navigation: UINavigationController, main: MainWireFrame?, selectedContacts: [Contact]?, delegate: EditParticipantsDelegate?, mode : ParticipantsModeType, members: [Member]?, room: Room?) + func presentAddParticipants(navigation: UINavigationController, main: MainWireFrame?, selectedContacts: [Contact]?, delegate: EditParticipantsDelegate?, mode : ParticipantsModeType, members: [Member]?, room: Room?, membersToAddLimit: UInt) /** * Add here your methods for communication PRESENTER -> WIREFRAME @@ -47,6 +47,7 @@ protocol AddParticipantsPresenterProtocol: BasePresenterProtocol, NavigationProt var wireFrame: AddParticipantsWireFrameProtocol! { get set } var participantsMode : ParticipantsModeType! { get } + var maxParticipantsToAdd : UInt { get } /** * Add here your methods for communication VIEW -> PRESENTER @@ -64,7 +65,9 @@ protocol AddParticipantsInteractorInputProtocol: BaseInteractorProtocol { var members: [Member]? { get set } var participantsMode : ParticipantsModeType { get } - + var maxParticipantsToAdd : UInt { get } + var allocateId : String? { get } + /** * Add here your methods for communication PRESENTER -> INTERACTOR */ @@ -74,6 +77,7 @@ protocol AddParticipantsInteractorInputProtocol: BaseInteractorProtocol { func fetchParticipants(`for` mode: ParticipantsModeType) func createRoom(name: String, avatar: UIImage?, members: [Member]) func getMySelf() -> Contact? + func allocateConference() } protocol AddParticipantsInteractorOutputProtocol: class { diff --git a/Nynja/Modules/AddParticipants/Interactor/AddParticipantsInteractor.swift b/Nynja/Modules/AddParticipants/Interactor/AddParticipantsInteractor.swift index cf58a6a04..7b222043c 100644 --- a/Nynja/Modules/AddParticipants/Interactor/AddParticipantsInteractor.swift +++ b/Nynja/Modules/AddParticipants/Interactor/AddParticipantsInteractor.swift @@ -7,18 +7,23 @@ // -class AddParticipantsInteractor: BaseInteractor, AddParticipantsInteractorInputProtocol { +class AddParticipantsInteractor: BaseInteractor, AddParticipantsInteractorInputProtocol, NynjaCommunicatorServiceDelegate { weak var presenter: AddParticipantsInteractorOutputProtocol! var selectedContacts: [Contact]? var members: [Member]? var room: Room? - + var participants: [Participant] = [] var participantsMode: ParticipantsModeType + var maxParticipantsToAdd: UInt = UInt.max + var allocateId: String? private let localId = IdBuilder(format: .defaultId).build() + private let storageService = StorageService.sharedInstance + private let mqttService = MQTTService.sharedInstance + private let callService = NynjaCommunicatorService.sharedInstance required init(mode: ParticipantsModeType) { self.participantsMode = mode @@ -39,10 +44,21 @@ class AddParticipantsInteractor: BaseInteractor, AddParticipantsInteractorInputP } func getMySelf() -> Contact? { - guard let id = StorageService.sharedInstance.rosterId, let roster = RosterDAO.findRosterBy(id: id) else { return nil } + guard let id = storageService.rosterId, let roster = RosterDAO.findRosterBy(id: id) else { return nil } return roster.myContact } + func allocateConference() { + + guard nil == self.allocateId else {return} + + self.allocateId = UUID().uuidString + + callService.allocateConference(requestId: self.allocateId!, + subject: room?.name ?? "Unnamed", + roomId: room?.id ?? localId) + } + override func loadData() { super.loadData() fetchParticipants(for: participantsMode) @@ -53,7 +69,7 @@ class AddParticipantsInteractor: BaseInteractor, AddParticipantsInteractorInputP } func sendRoom(with name: String, avatar: String?, members: [Member]) { - MQTTService.sharedInstance.createRoom(id: localId, name: name, avatar: avatar, members: members) + mqttService.createRoom(id: localId, name: name, avatar: avatar, members: members) } //MARK: - AddParticipantsInteractorInputProtocol @@ -109,7 +125,7 @@ class AddParticipantsInteractor: BaseInteractor, AddParticipantsInteractorInputP private func makeParticipant(with contact: Contact) -> Participant { let participant = Participant(contact: contact) participant.alias = members?.first(where: { $0.phone_id == contact.phone_id })?.alias - participant.isEnable = contact.phoneId != StorageService.sharedInstance.phoneId + participant.isEnable = contact.phoneId != storageService.phoneId participant.isSelected = (selectedContacts?.contains(where: { (contact) -> Bool in return contact.phoneId == participant.contact.phoneId })) ?? false @@ -145,9 +161,33 @@ class AddParticipantsInteractor: BaseInteractor, AddParticipantsInteractorInputP if let dbRoom = changes.first?.entity as? DBRoom, dbRoom.id == localId { let room = Room(room: dbRoom) presenter?.created(room: room) + if let callId = allocateId, let subject = room.name { + callService.updateConferenceInfo(callId: callId, subject: subject) + } } default: break } } + + // MARK - NynjaCommunicatorServiceDelegate + func didAllocateConference(requestId: String, call: NYNCall?, error: Error?) { + + if let aId = self.allocateId, aId.elementsEqual(requestId) { + + if let c = call { + // Success + self.allocateId = c.callId + maxParticipantsToAdd = c.participantsCountLimit + } else { + // Error + self.allocateId = nil + } + } + } + + func didEndConference(requestId: String, error: Error?) { + + } + } diff --git a/Nynja/Modules/AddParticipants/Presenter/AddParticipantsPresenter.swift b/Nynja/Modules/AddParticipants/Presenter/AddParticipantsPresenter.swift index 0d190c0ea..3e53f80dd 100644 --- a/Nynja/Modules/AddParticipants/Presenter/AddParticipantsPresenter.swift +++ b/Nynja/Modules/AddParticipants/Presenter/AddParticipantsPresenter.swift @@ -20,6 +20,17 @@ class AddParticipantsPresenter: BasePresenter, AddParticipantsPresenterProtocol, } } + override func screenLoaded() { + super.screenLoaded() + + switch self.participantsMode! { + case .createGroupCall, .createConferenceCall: + interactor.allocateConference() + default: + break + } + } + weak var view: AddParticipantsViewProtocol! var interactor: AddParticipantsInteractorInputProtocol! { didSet { @@ -31,7 +42,11 @@ class AddParticipantsPresenter: BasePresenter, AddParticipantsPresenterProtocol, var participantsMode: ParticipantsModeType! { return interactor.participantsMode } - + + var maxParticipantsToAdd: UInt { + return interactor.maxParticipantsToAdd + } + func createGroup(with contacts: [Contact]) { var members = [Member]() for i in contacts { @@ -105,13 +120,13 @@ class AddParticipantsPresenter: BasePresenter, AddParticipantsPresenterProtocol, if contacts.isEmpty { wireFrame.hide(with: .cancel) } else { - wireFrame.hide(with: .createGroupCall(contacts: contacts, room: room)) + wireFrame.hide(with: .createGroupCall(callId: interactor.allocateId, contacts: contacts, room: room)) } case .createConferenceCall: if contacts.isEmpty { wireFrame.hide(with: .cancel) } else { - wireFrame.hide(with: .createConferenceCall(contacts: contacts, room:roomToCreate)) + wireFrame.hide(with: .createConferenceCall(callId: interactor.allocateId, contacts: contacts, room:roomToCreate)) } case .updateGroupCall: if contacts.isEmpty { diff --git a/Nynja/Modules/AddParticipants/View/AddParticipantsViewController.swift b/Nynja/Modules/AddParticipants/View/AddParticipantsViewController.swift index f13014285..c2c48ad07 100644 --- a/Nynja/Modules/AddParticipants/View/AddParticipantsViewController.swift +++ b/Nynja/Modules/AddParticipants/View/AddParticipantsViewController.swift @@ -157,10 +157,15 @@ class AddParticipantsViewController: BaseVC, AddParticipantsViewProtocol { }() var scrollBar: ScrollBar! + + private lazy var swipeBackHelper: SwipeBackHelper = { + return SwipeBackHelper(with: self) + }() // MARK: - BaseVC override func initialize() { super.initialize() + swipeBackHelper.addGesture() baseSetup() var title:String = "" @@ -173,17 +178,17 @@ class AddParticipantsViewController: BaseVC, AddParticipantsViewProtocol { title = "add_members_to_call".localized.uppercased() backBtnImage = .closeImage navHandler = presenter - self.selectAllButtonVisible = true + //TODO: re-enable self.selectAllButtonVisible=true here when necessary } else if presenter.participantsMode == .updateGroupCall { title = "add_members_to_call".localized.uppercased() backBtnImage = .closeImage navHandler = presenter - self.selectAllButtonVisible = true + //TODO: re-enable self.selectAllButtonVisible=true here when necessary } else if presenter.participantsMode == .createConferenceCall { title = "add_members_to_call".localized.uppercased() backBtnImage = .closeImage navHandler = presenter - self.selectAllButtonVisible = true + //TODO: re-enable self.selectAllButtonVisible=true here when necessary } else if presenter.participantsMode == .admins { title = "admins".localized.uppercased() } else { @@ -289,7 +294,7 @@ class AddParticipantsViewController: BaseVC, AddParticipantsViewProtocol { //TODO: IOS-87 self.view.endEditing(true) if (presenter.participantsMode == .create || presenter.participantsMode == .createConferenceCall || presenter.participantsMode == .createGroupCall || - presenter.participantsMode == .updateGroupCall || presenter.participantsMode == .createConferenceCall) + presenter.participantsMode == .updateGroupCall) && participantsDataSource.selectedParticipants.isEmpty { AlertManager.sharedInstance.showAlertOk(message: "please_choose_at_least_one_member".localized) return @@ -298,6 +303,20 @@ class AddParticipantsViewController: BaseVC, AddParticipantsViewProtocol { AlertManager.sharedInstance.showAlertOk(message: "you_cannot_remove_all".localized) return } + + if presenter.participantsMode == .updateGroupCall { + if presenter.maxParticipantsToAdd == 0 { + AlertManager.sharedInstance.showAlertOk(message: "call_no_available_slots".localized) + return + } + + if participantsDataSource.selectedParticipants.count > presenter.maxParticipantsToAdd { + let message = String(format: "call_available_slots_count".localized, presenter.maxParticipantsToAdd) + AlertManager.sharedInstance.showAlertOk(message: message) + return + } + } + let contacts = self.participantsDataSource.selectedParticipants.map({ (participant) -> Contact in return participant.contact }) @@ -316,33 +335,6 @@ class AddParticipantsViewController: BaseVC, AddParticipantsViewProtocol { presenter.hide(with: [Contact]()) } - @objc private func selectAllButtonAction(sender: UIButton!) { - - sender.isSelected = !sender.isSelected - - participantsDataSource.selectedParticipants.removeAll() - - for letter in participantsDataSource.sortedLetters { - - if let participants = participantsDataSource.groupedParticipants[letter] { - - for part in participants { - - part.isSelected = sender.isSelected - } - - if sender.isSelected { - - participantsDataSource.selectedParticipants.append(contentsOf: participants) - - } - } - } - - avatarsView.reloadData() - tableView.reloadData() - } - @objc private func selectAllTapped(sender: UIButton!) { sender.isSelected = !sender.isSelected @@ -452,6 +444,29 @@ extension AddParticipantsViewController: ParticipantsActionsDelegate { updateDoneButtonState() } + func canSelectParticipant(_ participant: Participant) -> Bool { + if participant.isSelected { + return true + } + + var fCanSelect: Bool = true + + switch presenter.participantsMode { + case .createGroupCall: + fCanSelect = participantsDataSource.selectedParticipants.count < (presenter.maxParticipantsToAdd - 1) + case .createConferenceCall: + fCanSelect = participantsDataSource.selectedParticipants.count < (presenter.maxParticipantsToAdd - 1) + default: + fCanSelect = true + } + + if false == fCanSelect { + AlertManager.sharedInstance.showAlertOk(message: "call_maximum_participants_reached".localized) + } + + return fCanSelect + } + private func deselectParticipant(_ participant: Participant) { if let index = participantsDataSource.selectedParticipants.index(of: participant) { participantsDataSource.selectedParticipants.remove(at: index) diff --git a/Nynja/Modules/AddParticipants/View/ParticipantsActionsDelegate.swift b/Nynja/Modules/AddParticipants/View/ParticipantsActionsDelegate.swift index 919323494..c6b169cb0 100644 --- a/Nynja/Modules/AddParticipants/View/ParticipantsActionsDelegate.swift +++ b/Nynja/Modules/AddParticipants/View/ParticipantsActionsDelegate.swift @@ -9,8 +9,10 @@ protocol ParticipantsActionsDelegate: class { func avatarTapped(_ participant: Participant) func participantTapped(_ participant: Participant) + func canSelectParticipant(_ participant: Participant) -> Bool } extension ParticipantsActionsDelegate { func avatarTapped(_ participant: Participant) {} + func canSelectParticipant(_ participant: Participant) -> Bool { return true} } diff --git a/Nynja/Modules/AddParticipants/View/TableView/ParticipantsDelegate.swift b/Nynja/Modules/AddParticipants/View/TableView/ParticipantsDelegate.swift index 81a0f577b..238c80f7e 100644 --- a/Nynja/Modules/AddParticipants/View/TableView/ParticipantsDelegate.swift +++ b/Nynja/Modules/AddParticipants/View/TableView/ParticipantsDelegate.swift @@ -26,6 +26,18 @@ class ParticipantsDelegate: NSObject, UITableViewDelegate, UICollectionViewDeleg actionsDelegate?.participantTapped(participant) } } + + func tableView(_ tableView: UITableView, willSelectRowAt indexPath: IndexPath) -> IndexPath? { + if let cell = tableView.cellForRow(at: indexPath) as? ParticipantsContactCell, + let participant = cell.participant + { + if actionsDelegate?.canSelectParticipant(participant) ?? true { + return indexPath + } + } + + return nil + } // MARK: Header func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? { diff --git a/Nynja/Modules/AddParticipants/WireFrame/AddParticipantsWireframe.swift b/Nynja/Modules/AddParticipants/WireFrame/AddParticipantsWireframe.swift index 45bd5c4f8..a22c95270 100644 --- a/Nynja/Modules/AddParticipants/WireFrame/AddParticipantsWireframe.swift +++ b/Nynja/Modules/AddParticipants/WireFrame/AddParticipantsWireframe.swift @@ -23,8 +23,8 @@ enum ParticipantsResult { case delete(contacts: [Contact]) case admins(contacts: [Contact]) case cancel - case createGroupCall(contacts: [Contact], room: Room?) - case createConferenceCall(contacts: [Contact], room: Room?) + case createGroupCall(callId: String?, contacts: [Contact], room: Room?) + case createConferenceCall(callId: String?, contacts: [Contact], room: Room?) case updateGroupCall(contacts: [Contact]) } @@ -36,7 +36,7 @@ class AddParticipantsWireFrame: AddParticipantsWireFrameProtocol { var isEdit = false - func presentAddParticipants(navigation: UINavigationController, main: MainWireFrame?, selectedContacts: [Contact]?, delegate: EditParticipantsDelegate?, mode : ParticipantsModeType = .create, members: [Member]? = nil, room: Room? = nil) { + func presentAddParticipants(navigation: UINavigationController, main: MainWireFrame?, selectedContacts: [Contact]?, delegate: EditParticipantsDelegate?, mode : ParticipantsModeType = .create, members: [Member]? = nil, room: Room? = nil, membersToAddLimit: UInt = UInt.max) { self.delegate = delegate let view = AddParticipantsViewController() let presenter = AddParticipantsPresenter() @@ -49,7 +49,11 @@ class AddParticipantsWireFrame: AddParticipantsWireFrameProtocol { interactor.selectedContacts = selectedContacts interactor.members = members interactor.room = room + interactor.maxParticipantsToAdd = membersToAddLimit + if mode == .createGroupCall || mode == .createConferenceCall { + NynjaCommunicatorService.sharedInstance.delegates.addDelegate(interactor) + } presenter.room = room // Connecting diff --git a/Nynja/Modules/AssigningInterpreter/View/AsigningInterpreterLayout.swift b/Nynja/Modules/AssigningInterpreter/View/AsigningInterpreterLayout.swift index 195b7817a..fcbb4a4f4 100644 --- a/Nynja/Modules/AssigningInterpreter/View/AsigningInterpreterLayout.swift +++ b/Nynja/Modules/AssigningInterpreter/View/AsigningInterpreterLayout.swift @@ -20,13 +20,13 @@ extension AssigningInterpreterViewController { struct Texts { struct bottomLabel { static let height = CGFloat(33.adjustedByWidth) - static let font = UIFont(fontName: Constants.fonts.medium, height: height) + static let font = UIFont.makeFont(with: Constants.fonts.medium, height: height) static let color = Constants.colors.red.getColor() } struct descriptionLabel { static let height = CGFloat(22.adjustedByWidth) - static let font = UIFont(fontName: Constants.fonts.medium, height: height) + static let font = UIFont.makeFont(with: Constants.fonts.medium, height: height) static let color = Constants.colors.darkGray.getColor() } } diff --git a/Nynja/Modules/Auth/Interactor/AuthInteractor.swift b/Nynja/Modules/Auth/Interactor/AuthInteractor.swift index ded814722..b9c4dc62e 100644 --- a/Nynja/Modules/Auth/Interactor/AuthInteractor.swift +++ b/Nynja/Modules/Auth/Interactor/AuthInteractor.swift @@ -49,7 +49,7 @@ class AuthInteractor: BaseInteractor, AuthInteractorInputProtocol, IoHandlerDele } func getSMSCode(result: ((String) -> ())?) { - #if DEBUG + #if !RELEASE let path = "http://\(MQTTService.sharedInstance.host.url):8888/sessions?phone=\(phone)" if let url = URL(string: path) { var req = URLRequest(url: url) @@ -134,8 +134,10 @@ class AuthInteractor: BaseInteractor, AuthInteractorInputProtocol, IoHandlerDele } // MARK: - MQTTServiceDelegate - func wrongServerVersion() { - AlertManager.sharedInstance.showAlertOk(message: "wrongVersion".localized) + func mqttServiceDidReceiveWrongServerVersion() { + DispatchQueue.main.async { + AlertManager.sharedInstance.showAlertOk(message: "wrongVersion".localized) + } } // MARK: - StorageSubscriber diff --git a/Nynja/Modules/Auth/View/LoginView.swift b/Nynja/Modules/Auth/View/LoginView.swift index f78c8619f..dbbd05b57 100644 --- a/Nynja/Modules/Auth/View/LoginView.swift +++ b/Nynja/Modules/Auth/View/LoginView.swift @@ -86,7 +86,7 @@ class LoginView: UIView, UserSettingsRespondable { return cf }() - let inputFont: UIFont = UIFont(fontName: Constants.fonts.medium, height: 22)! + let inputFont: UIFont = UIFont.makeFont(with: Constants.fonts.medium, height: 22)! lazy var phoneField: PhoneField = { let height = Constraints.countryField.height.adjustedByHeight diff --git a/Nynja/Modules/Call/CallInProgressProtocols.swift b/Nynja/Modules/Call/CallInProgressProtocols.swift index 28a07f462..9abde5aa4 100644 --- a/Nynja/Modules/Call/CallInProgressProtocols.swift +++ b/Nynja/Modules/Call/CallInProgressProtocols.swift @@ -31,15 +31,16 @@ protocol CallInProgressViewProtocol: class { */ func setupUI() func updateTime(text: String) - func remoteVideoStreamStopped() func changeUIToIncall() func update(participants: [NYNCallParticipant]) - func updateCallBy(status: CallStatus) - func callFailed() func askEndOrLeave() func didAddRemoteVideoStream() + func didRemoveRemoteVideoStream() func didStartLocalCapturer() func didStopLocalCapturer() + func callConnecting() + func callRinging() + func callConnected() } protocol CallInProgressPresenterProtocol: class { @@ -65,15 +66,21 @@ protocol CallInProgressPresenterProtocol: class { func toggleMicrophone() func isMuted()->Bool func switchCamera() - func viewShowed() func rejectCall() func updateCallParticipants() func showMenuWith(groupCollectionCell: GroupCollectionViewCell) func endCall() func hangupCall() func addRemoteVideoRenderer(inView view: UIView) + func removeRemoteVideoRenderer(inView view: UIView) func attachLocalVideoPreview(inView view: UIView) func dettachLocalVideoPreview(inView view: UIView) + func cameraAction() + func isCameraRunning()->Bool + func hasRemoteVideo()->Bool + func unmuteCamera() + func updateCallStatus() + func fetchCallParticipants() } protocol CallInProgressInteractorOutputProtocol: class { @@ -83,15 +90,17 @@ protocol CallInProgressInteractorOutputProtocol: class { */ func callClosed() + func callConnecting() + func callRinging() func callConnected(withVideo: Bool) func setRingingWithoutVideo() func setRingingStatus() func remoteVideoStreamStopped() func updateTime(text: String) func update(participants: [NYNCallParticipant]) - func callFailed() func askEndOrLeave() func didAddRemoteVideoStream() + func didRemoveRemoteVideoStream() func didStartLocalCapturer() func didStopLocalCapturer() } @@ -118,12 +127,18 @@ protocol CallInProgressInteractorInputProtocol: class { func endCall() func hangupCall() func addRemoteVideoRenderer(inView view: UIView) + func removeRemoteVideoRenderer(inView view: UIView) func attachLocalVideoPreview(inView view: UIView) func detachLocalVideoPreview(inView view: UIView) + func cameraAction() + func isCameraRunning()->Bool + func messageAction() + func hasRemoteVideo()->Bool + func unmuteCamera() + func updateCallStatus() + func fetchCallParticipants() } protocol ManageCallInProgressParticipantsProtocol: class { func remove(participant: NYNCallParticipant?) - func mute(participant: NYNCallParticipant?) - func unmute(participant: NYNCallParticipant?) } diff --git a/Nynja/Modules/Call/CallScreens/CallInProgress/Interactor/CallInProgressInteractor.swift b/Nynja/Modules/Call/CallScreens/CallInProgress/Interactor/CallInProgressInteractor.swift index 31ec6174b..e30b75554 100644 --- a/Nynja/Modules/Call/CallScreens/CallInProgress/Interactor/CallInProgressInteractor.swift +++ b/Nynja/Modules/Call/CallScreens/CallInProgress/Interactor/CallInProgressInteractor.swift @@ -11,11 +11,32 @@ class CallInProgressInteractor: CallInProgressInteractorInputProtocol, NynjaCall weak var presenter: CallInProgressInteractorOutputProtocol! - var callService = NynjaCommunicatorService.sharedInstance var timer: Timer? + private let callService = NynjaCommunicatorService.sharedInstance init() { callService.callDelegate = self + + NotificationCenter.default.addObserver(self, + selector: #selector(applicationDidBecomeActive), + name: NSNotification.Name.UIApplicationDidBecomeActive, + object: nil) + + NotificationCenter.default.addObserver(self, + selector: #selector(applicationWillResignActive), + name: NSNotification.Name.UIApplicationWillResignActive, + object: nil) + + } + + deinit { + NotificationCenter.default.removeObserver(self, + name: NSNotification.Name.UIApplicationWillResignActive, + object: nil) + + NotificationCenter.default.removeObserver(self, + name: NSNotification.Name.UIApplicationDidBecomeActive, + object: nil) } var room: Room? { @@ -32,27 +53,21 @@ class CallInProgressInteractor: CallInProgressInteractorInputProtocol, NynjaCall let processingManager = DefaultMessagesProcessingManager.shared private(set) lazy var messageFactory: MessageFactoryProtocol = { - let factory = MessageFactory() - let dependencies = MessageFactory.Dependencies( storageService: storageService, payloadBuilder: payloadBuilder ) - factory.inject(dependencies: dependencies) - return factory + return MessageFactory(dependencies: dependencies) }() private(set) lazy var messageSendingService: MessageSendingServiceProtocol = { - let service = MessageSendingService() let dependencies = MessageSendingService.Dependencies( mqttService: mqttService, storageService: storageService, processingManager: processingManager ) - service.inject(dependencies: dependencies) - - return service + return MessageSendingService(dependencies: dependencies) }() func acceptCall(withVideo: Bool) { @@ -123,6 +138,10 @@ class CallInProgressInteractor: CallInProgressInteractorInputProtocol, NynjaCall } } + func messageAction() { + stopTimer() + self.nynCall?.muteCamera() + } func startRinging() { presenter.setRingingStatus() @@ -157,6 +176,7 @@ class CallInProgressInteractor: CallInProgressInteractorInputProtocol, NynjaCall } func startTimer() { + stopTimer() timer = Timer.scheduledTimer(timeInterval: 1, target: self, selector: #selector(runTimedCode), userInfo: nil, repeats: true) } @@ -177,6 +197,10 @@ class CallInProgressInteractor: CallInProgressInteractorInputProtocol, NynjaCall self.presenter.didAddRemoteVideoStream() } + func didRemoveVideoStreamForCall(call: NYNCall) { + self.presenter.didRemoveRemoteVideoStream() + } + func didStartLocalCapturerForCall(call: NYNCall) { self.presenter.didStartLocalCapturer() } @@ -189,6 +213,10 @@ class CallInProgressInteractor: CallInProgressInteractorInputProtocol, NynjaCall callService.attachRemoteVideoRenderer(inView: view) } + func removeRemoteVideoRenderer(inView view: UIView) { + callService.detachRemoteVideoRenderer(inView: view) + } + func attachLocalVideoPreview(inView view: UIView) { callService.attachLocalVideoPreview(inView: view) } @@ -197,6 +225,79 @@ class CallInProgressInteractor: CallInProgressInteractorInputProtocol, NynjaCall callService.detachLocalVideoPreview(inView: view) } + func cameraAction() { + LogService.log(topic: .callSystem) { return "call interactor cameraAction" } + + if let call = self.nynCall { + if call.isCameraRunning() { + LogService.log(topic: .callSystem) { return "call interactor cameraAction, call will stop camera"} + call.stopCamera() + } else { + LogService.log(topic: .callSystem) { return "call interactor cameraAction, call will start camera"} + call.startCamera() + } + } else { + LogService.log(topic: .callSystem) { return "call interactor cameraAction, no current call"} + } + } + + func isCameraRunning() -> Bool { + if let call = self.nynCall { + return call.isCameraRunning() + } + + return false + } + + func hasRemoteVideo() -> Bool { + if let call = self.nynCall { + return call.hasRemoteVideo() + } + + return false + } + + func unmuteCamera() { + self.nynCall?.unmuteCamera() + } + + func updateCallStatus() { + if let c = self.nynCall { + + switch c.callState + { + case NYNCallState.new: + break + case NYNCallState.readyToStart: + break; + case NYNCallState.ringing: + self.presenter.callRinging() + break + case NYNCallState.connecting: + self.presenter.callConnecting() + break + case NYNCallState.connected: + self.presenter.callConnected(withVideo: false) + self.startTimer() + break + case NYNCallState.failed: + break + case NYNCallState.disconnected: + break + case NYNCallState.closed: + break + case NYNCallState.count: + break + } + } + } + + func fetchCallParticipants() { + if let ncall = self.nynCall { + self.presenter.update(participants: ncall.participants) + } + } + func stopTimer () { if timer != nil { @@ -227,16 +328,17 @@ class CallInProgressInteractor: CallInProgressInteractorInputProtocol, NynjaCall case NYNCallState.readyToStart: self.readyToStart(call: ncall) break; - case NYNCallState.establishing: + case NYNCallState.ringing: + self.presenter.callRinging() break case NYNCallState.connecting: + self.presenter.callConnecting() break case NYNCallState.connected: self.presenter.callConnected(withVideo: false) self.startTimer() break case NYNCallState.failed: - self.presenter.callFailed() break case NYNCallState.disconnected: break @@ -286,5 +388,14 @@ class CallInProgressInteractor: CallInProgressInteractorInputProtocol, NynjaCall callService.removeConferenceMember(conferenceId: ncall.callId, memberId: memberId) } } + + @objc func applicationDidBecomeActive() { + self.nynCall?.unmuteCamera() + } + + @objc func applicationWillResignActive() { + self.nynCall?.muteCamera() + } + } diff --git a/Nynja/Modules/Call/CallScreens/CallInProgress/Presenter/CallInProgressPresenter.swift b/Nynja/Modules/Call/CallScreens/CallInProgress/Presenter/CallInProgressPresenter.swift index 8f9b6b1d1..48eb5e0e0 100644 --- a/Nynja/Modules/Call/CallScreens/CallInProgress/Presenter/CallInProgressPresenter.swift +++ b/Nynja/Modules/Call/CallScreens/CallInProgress/Presenter/CallInProgressPresenter.swift @@ -54,10 +54,12 @@ class CallInProgressPresenter: CallInProgressPresenterProtocol, CallInProgressIn func messageAcion(with roomId:String, isVideo: Bool) { guard let room = RoomDAO.findRoom(by: roomId) else {return} + interactor.messageAction() wireFrame.messageActionWith(room:room, isVideo:isVideo) } func messageAcion(with contact:Contact, isVideo: Bool) { + interactor.messageAction() wireFrame.messageAction(isVideo: isVideo, contact: contact) } @@ -83,19 +85,16 @@ class CallInProgressPresenter: CallInProgressPresenterProtocol, CallInProgressIn wireFrame.callClosed() } - func callConnected(withVideo: Bool) { - if withVideo { - self.view.setupUI() - } else { - self.view.setupUI() - } - - self.view.updateCallBy(status:.callInProgress) + func callConnecting() { + self.view.callConnecting() } - func callFailed() { - - self.view.callFailed() + func callRinging() { + self.view.callRinging() + } + + func callConnected(withVideo: Bool) { + self.view.callConnected() } func askEndOrLeave() { @@ -106,6 +105,10 @@ class CallInProgressPresenter: CallInProgressPresenterProtocol, CallInProgressIn self.view.didAddRemoteVideoStream() } + func didRemoveRemoteVideoStream() { + self.view.didRemoveRemoteVideoStream() + } + func didStartLocalCapturer() { self.view.didStartLocalCapturer() } @@ -118,6 +121,10 @@ class CallInProgressPresenter: CallInProgressPresenterProtocol, CallInProgressIn self.interactor.addRemoteVideoRenderer(inView: view) } + func removeRemoteVideoRenderer(inView view: UIView) { + self.interactor.removeRemoteVideoRenderer(inView: view) + } + func attachLocalVideoPreview(inView view: UIView) { self.interactor.attachLocalVideoPreview(inView: view) } @@ -125,29 +132,47 @@ class CallInProgressPresenter: CallInProgressPresenterProtocol, CallInProgressIn func dettachLocalVideoPreview(inView view: UIView) { self.interactor.detachLocalVideoPreview(inView: view) } + + func cameraAction() { + self.interactor.cameraAction() + } + + func isCameraRunning() -> Bool { + return self.interactor.isCameraRunning() + } + + func hasRemoteVideo()->Bool { + return self.interactor.hasRemoteVideo() + } + + func unmuteCamera() { + self.interactor.unmuteCamera() + } + + func updateCallStatus() { + self.interactor.updateCallStatus() + } + + func fetchCallParticipants() { + self.interactor.fetchCallParticipants() + } func remoteVideoStreamStopped() { - //self.view.remoteVideoStreamStopped() self.view.setupUI() - self.view.updateCallBy(status:.callInProgress) } - func setRingingWithoutVideo() { self.view.setupUI() - self.view.updateCallBy(status:.callStarting) } func setRingingStatus() { self.view.setupUI() - self.view.updateCallBy(status:.callOutgoingAudio) } func willShow() { self.view.setupUI() } - func updateTime(text: String) { self.view.updateTime(text: text) } @@ -157,9 +182,6 @@ class CallInProgressPresenter: CallInProgressPresenterProtocol, CallInProgressIn self.view.update(participants: participants) } - func viewShowed() { - } - // MARK: EditParticpantsDelegate func updateGroupCall(contacts: [Contact]) { @@ -186,13 +208,4 @@ class CallInProgressPresenter: CallInProgressPresenterProtocol, CallInProgressIn func remove(participant: NYNCallParticipant?) { interactor.removeCallMember(memberId: (participant?.memberId)!) } - - func mute(participant: NYNCallParticipant?) { - - } - - func unmute(participant: NYNCallParticipant?) { - - } - } diff --git a/Nynja/Modules/Call/CallScreens/CallInProgress/View/CallInProgressNavigationController.swift b/Nynja/Modules/Call/CallScreens/CallInProgress/View/CallInProgressNavigationController.swift deleted file mode 100644 index a3599bbac..000000000 --- a/Nynja/Modules/Call/CallScreens/CallInProgress/View/CallInProgressNavigationController.swift +++ /dev/null @@ -1,35 +0,0 @@ -// -// CallInProgressNavigationController.swift -// Nynja -// -// Created by Bozhko Terziev on 6.07.18. -// Copyright © 2018 TecSynt Solutions. All rights reserved. -// - -import UIKit - -class CallInProgressNavigationController: UINavigationController { - - override func viewDidLoad() { - super.viewDidLoad() - - // Do any additional setup after loading the view. - } - - override func didReceiveMemoryWarning() { - super.didReceiveMemoryWarning() - // Dispose of any resources that can be recreated. - } - - - /* - // MARK: - Navigation - - // In a storyboard-based application, you will often want to do a little preparation before navigation - override func prepare(for segue: UIStoryboardSegue, sender: Any?) { - // Get the new view controller using segue.destinationViewController. - // Pass the selected object to the new view controller. - } - */ - -} diff --git a/Nynja/Modules/Call/CallScreens/CallInProgress/View/CallInProgressViewController.swift b/Nynja/Modules/Call/CallScreens/CallInProgress/View/CallInProgressViewController.swift index 1688f4407..2137d2d60 100644 --- a/Nynja/Modules/Call/CallScreens/CallInProgress/View/CallInProgressViewController.swift +++ b/Nynja/Modules/Call/CallScreens/CallInProgress/View/CallInProgressViewController.swift @@ -57,6 +57,7 @@ class CallInProgressViewController: BaseVC, CallInProgressViewProtocol, BottomCa var initialized: Bool = false var needLocalRenderer: Bool = false var needRemoteRenderer: Bool = false + var willAppearAtLeastOnce: Bool = false private struct ConstraintConstants { @@ -69,8 +70,15 @@ class CallInProgressViewController: BaseVC, CallInProgressViewProtocol, BottomCa static let labelsFontSize: CGFloat = 11.0 static let minSectionHeight: CGFloat = 83.0 + + static let gradientHeight: CGFloat = 144.0 static let declineButtonSize:CGFloat = 68.0 + + static let myVideoViewWidth:CGFloat = 72.0 + static let myVideoViewHeight:CGFloat = 115.0 + static let myVideoViewLeftInset:CGFloat = 16.0 + static let myVideoViewBottomInset:CGFloat = 20.0 } lazy var expectedRowsInCollectionView: Int = { @@ -102,6 +110,20 @@ class CallInProgressViewController: BaseVC, CallInProgressViewProtocol, BottomCa return sections }() + + lazy var backgroundGradientView: UIView = { + + let view = GradientView(colors: [Constants.colors.callGradientStart.getColor(), + Constants.colors.callGradientEnd.getColor()]) + self.view.addSubview(view) + view.snp.makeConstraints({ (make) in + + make.top.left.right.equalTo(self.view) + make.height.equalTo(ConstraintConstants.gradientHeight.adjustedByHeight) + }) + + return view + }() lazy var backgroundImage: UIImageView = { @@ -140,10 +162,10 @@ class CallInProgressViewController: BaseVC, CallInProgressViewProtocol, BottomCa self.view.addSubview(view) view.snp.makeConstraints({ (make) in - make.left.equalTo(self.view).offset(1.5*offset) - make.bottom.equalTo(self.otherVideoView.snp.bottom).offset(-1.5*offset) - make.width.equalTo(72.adjustedByWidth) - make.height.equalTo(115.adjustedByHeight) + make.left.equalTo(self.view).offset(ConstraintConstants.myVideoViewLeftInset.adjustedByWidth) + make.bottom.equalTo(self.otherVideoView.snp.bottom).offset(-ConstraintConstants.myVideoViewBottomInset.adjustedByWidth) + make.width.equalTo(ConstraintConstants.myVideoViewWidth.adjustedByWidth) + make.height.equalTo(ConstraintConstants.myVideoViewHeight.adjustedByWidth) }) return view }() @@ -166,7 +188,6 @@ class CallInProgressViewController: BaseVC, CallInProgressViewProtocol, BottomCa return btn }() - lazy var collectionView: UICollectionView = { let layout = UICollectionViewFlowLayout() layout.scrollDirection = .vertical @@ -327,7 +348,8 @@ class CallInProgressViewController: BaseVC, CallInProgressViewProtocol, BottomCa lazy var cameraButton: UIButton = { let button = UIButton() - button.setImage(#imageLiteral(resourceName: "ic_video_on_voice_call"), for: .normal) + let image = self.isCameraRunning() ? #imageLiteral(resourceName: "ic_video_on_voice_call") : #imageLiteral(resourceName: "ic_video_off_voice_call") + button.setImage(image, for: .normal) button.addTarget(self, action: #selector(onCameraButtonPressed), for: .touchUpInside) button.isEnabled = (.oneToOneVideo == self.callInProgressMode) self.bottomView.addSubview(button) @@ -577,15 +599,19 @@ class CallInProgressViewController: BaseVC, CallInProgressViewProtocol, BottomCa self.otherVideoView.isHidden = true self.switchCameraButton.isHidden = true self.backgroundImage.isHidden = false + self.backgroundGradientView.isHidden = false self.backgroundImage.setImage(url: self.contact?.avatarUrl, placeHolder: UIImage(named: "ava_placeholder")) self.view.bringSubview(toFront: self.backgroundImage) + self.view.insertSubview(self.backgroundGradientView, aboveSubview: self.backgroundImage) case .oneToOneVideo: self.switchCameraButton.isHidden = true self.myVideoView.isHidden = true - self.otherVideoView.isHidden = false + self.otherVideoView.isHidden = true self.backgroundImage.isHidden = false + self.backgroundGradientView.isHidden = false self.backgroundImage.setImage(url: self.contact?.avatarUrl, placeHolder: UIImage(named: "ava_placeholder")) self.view.bringSubview(toFront: self.backgroundImage) + self.view.insertSubview(self.backgroundGradientView, aboveSubview: self.backgroundImage) self.view.bringSubview(toFront: self.otherVideoView) self.view.bringSubview(toFront: self.myVideoView) self.view.bringSubview(toFront: self.switchCameraButton) @@ -595,6 +621,7 @@ class CallInProgressViewController: BaseVC, CallInProgressViewProtocol, BottomCa self.otherVideoView.isHidden = true self.switchCameraButton.isHidden = true self.backgroundImage.isHidden = true + self.backgroundGradientView.isHidden = true self.view.bringSubview(toFront: self.collectionView) } @@ -629,22 +656,6 @@ class CallInProgressViewController: BaseVC, CallInProgressViewProtocol, BottomCa updateTitle() } - func updateCallBy(status: CallStatus) { - - } - - func callFailed() { - } - - func remoteVideoStreamStopped() { - -// backgtoundVideoImage.isHidden = false -// backgtoundVideoImage.image = backgtoundImage.image -// -// partnerVideoView.isHidden = true - } - - @objc func resizeButtonAction() { presenter.messageAcion(with: roomId, isVideo: true) } @@ -654,8 +665,6 @@ class CallInProgressViewController: BaseVC, CallInProgressViewProtocol, BottomCa // Note: need to prevent weird animation which appears when incoming/outcoming call appears. self.view.layoutIfNeeded() - - self.presenter.viewShowed() if self.needLocalRenderer { self.didStartLocalCapturer() @@ -666,7 +675,31 @@ class CallInProgressViewController: BaseVC, CallInProgressViewProtocol, BottomCa self.didAddRemoteVideoStream() self.needRemoteRenderer = false } + + if false == willAppearAtLeastOnce { + // Appear for the first time. + + if self.callInProgressMode == .groupAudio { + presenter.fetchCallParticipants() + } + + // show / hide local/remote renderers + + if presenter.hasRemoteVideo() { + handleDidAddRemoteVideoStream() + } + + if presenter.isCameraRunning() { + handleDidStartLocalCapturer() + presenter.unmuteCamera() + } + + presenter.updateCallStatus() + } + + willAppearAtLeastOnce = true } + override func initialize() { super.initialize() @@ -781,6 +814,11 @@ class CallInProgressViewController: BaseVC, CallInProgressViewProtocol, BottomCa @objc func messageButtonPressed() { + if .oneToOneVideo == self.callInProgressMode { + self.presenter.dettachLocalVideoPreview(inView: self.myVideoView) + self.presenter.removeRemoteVideoRenderer(inView: self.otherVideoView) + } + if let contact = self.contact { presenter.messageAcion(with: contact, isVideo: false) } else { @@ -809,13 +847,8 @@ class CallInProgressViewController: BaseVC, CallInProgressViewProtocol, BottomCa } func acceptAudioButtonPressed() { -// if self.callMode == .incamingVideoCall || self.callMode == .incamingVideoGroupCall { -// presenter.acceptCall(withVideo: false) -// } else { -// presenter.disableVideo() -// } } - + @objc func switchCameraButtonPressed() { presenter.switchCamera() } @@ -831,7 +864,6 @@ class CallInProgressViewController: BaseVC, CallInProgressViewProtocol, BottomCa func changeUIToIncall() { - self.updateCallBy(status: .callInProgress) self.setupUI() } @@ -862,11 +894,8 @@ class CallInProgressViewController: BaseVC, CallInProgressViewProtocol, BottomCa @objc func onCameraButtonPressed() { - if cameraButton.imageView?.image == #imageLiteral(resourceName: "ic_video_on_voice_call") { - cameraButton.setImage(#imageLiteral(resourceName: "ic_video_off_voice_call"), for: .normal) - } else { - cameraButton.setImage(#imageLiteral(resourceName: "ic_video_on_voice_call"), for: .normal) - } + LogService.log(topic: .callSystem) { return "onCameraButtonPressed"} + self.presenter.cameraAction() } @objc func onMoreButtonPressed() { @@ -949,6 +978,14 @@ class CallInProgressViewController: BaseVC, CallInProgressViewProtocol, BottomCa } } + func didRemoveRemoteVideoStream() { + if self.initialized { + handleDidRemoveRemoteVideoStream() + } else { + self.needRemoteRenderer = false + } + } + func didStartLocalCapturer() { if self.initialized { handleDidStartLocalCapturer() @@ -964,22 +1001,39 @@ class CallInProgressViewController: BaseVC, CallInProgressViewProtocol, BottomCa self.needLocalRenderer = false } } + + func callConnecting() { + statusLabel.text = "call_connecting".localized + } + + func callRinging() { + statusLabel.text = "call_ringing".localized + } + func callConnected() { + updateTitle() + } + private func handleDidAddRemoteVideoStream() { self.otherVideoView.isHidden = false - self.switchCameraButton.isHidden = false self.backgroundImage.isHidden = true - self.backgroundImage.image = nil - self.view.sendSubview(toBack: self.backgroundImage) - + self.presenter.addRemoteVideoRenderer(inView: self.otherVideoView); } + private func handleDidRemoveRemoteVideoStream() { + self.otherVideoView.isHidden = true + self.backgroundImage.isHidden = false + } + private func handleDidStartLocalCapturer() { self.myVideoView.isHidden = false self.view.insertSubview(self.myVideoView, aboveSubview: self.otherVideoView) + self.switchCameraButton.isHidden = false + + self.cameraButton.setImage( #imageLiteral(resourceName: "ic_video_on_voice_call") , for: .normal) self.presenter.attachLocalVideoPreview(inView: self.myVideoView); } @@ -988,9 +1042,18 @@ class CallInProgressViewController: BaseVC, CallInProgressViewProtocol, BottomCa self.myVideoView.isHidden = true self.view.sendSubview(toBack: self.myVideoView) - + self.switchCameraButton.isHidden = true + + self.cameraButton.setImage( #imageLiteral(resourceName: "ic_video_off_voice_call") , for: .normal) + self.presenter.dettachLocalVideoPreview(inView: self.myVideoView); } - + private func isCameraRunning() -> Bool { + if .oneToOneVideo != self.callInProgressMode { + return false + } + + return presenter.isCameraRunning() + } } diff --git a/Nynja/Modules/Call/CallScreens/CallInProgress/WireFrame/CallInProgressWireframe.swift b/Nynja/Modules/Call/CallScreens/CallInProgress/WireFrame/CallInProgressWireframe.swift index d1303d33b..157a8d11d 100644 --- a/Nynja/Modules/Call/CallScreens/CallInProgress/WireFrame/CallInProgressWireframe.swift +++ b/Nynja/Modules/Call/CallScreens/CallInProgress/WireFrame/CallInProgressWireframe.swift @@ -27,7 +27,6 @@ class CallInProgressWireframe: CallInProgressWireFrameProtocol { view.callInProgressMode = callInProgressMode self.view = view - main?.callInProgressVC = view presenter.callInProgressType = callInProgressMode presenter.contact = contact @@ -54,7 +53,6 @@ class CallInProgressWireframe: CallInProgressWireFrameProtocol { self.nynCall = call self.view = view self.mainWF = main - main?.callInProgressVC = view presenter.callInProgressType = callInProgressMode presenter.contact = contact @@ -84,7 +82,6 @@ class CallInProgressWireframe: CallInProgressWireFrameProtocol { self.nynCall = call self.view = view self.mainWF = main - main?.callInProgressVC = view presenter.callInProgressType = callInProgressMode // Connecting @@ -129,7 +126,9 @@ class CallInProgressWireframe: CallInProgressWireFrameProtocol { } } - AddParticipantsWireFrame().presentAddParticipants(navigation: navigation!, main: mainWF, selectedContacts: nil, delegate: external, mode: .updateGroupCall, members: members, room:self.view?.presenter.interactor.room) + let membersToAddLimit: UInt = (nynCall?.participantsCountLimit ?? UInt.max) - (nynCall?.participantsCount ?? 0) + + AddParticipantsWireFrame().presentAddParticipants(navigation: navigation!, main: mainWF, selectedContacts: nil, delegate: external, mode: .updateGroupCall, members: members, room:self.view?.presenter.interactor.room, membersToAddLimit: membersToAddLimit) } func showMenuWith(participant: NYNCallParticipant?, delegate: ManageCallInProgressParticipantsProtocol) { @@ -155,7 +154,7 @@ class CallInProgressWireframe: CallInProgressWireFrameProtocol { } func callClosed() { - LogService.log(topic: .callSystem, text: "call view popped to root") + LogService.log(topic: .callSystem) { return "call view popped to root" } navigation?.popViewController(animated: true) } diff --git a/Nynja/Modules/Call/View/BottomCallView.swift b/Nynja/Modules/Call/View/BottomCallView.swift index 702a4685d..2cd20f74d 100644 --- a/Nynja/Modules/Call/View/BottomCallView.swift +++ b/Nynja/Modules/Call/View/BottomCallView.swift @@ -620,6 +620,8 @@ class BottomCallView: UIView, SpeakerDelegate { AudioManager.sharedInstance.speaker = .soft case .soft: AudioManager.sharedInstance.speaker = .loud + default:() + AudioManager.sharedInstance.speaker = .soft } } diff --git a/Nynja/Modules/Channel/NewChannel/Interactor/NewChannelInteractor.swift b/Nynja/Modules/Channel/NewChannel/Interactor/NewChannelInteractor.swift index a0d5d7a88..943525191 100644 --- a/Nynja/Modules/Channel/NewChannel/Interactor/NewChannelInteractor.swift +++ b/Nynja/Modules/Channel/NewChannel/Interactor/NewChannelInteractor.swift @@ -187,18 +187,22 @@ extension NewChannelInteractor { extension NewChannelInteractor { - func didConnect(_ mqttService: MQTTService) { - presenter?.didConnectToServer() - - guard let action = createChannelAction else { - return + func mqttServiceDidConnect(_ mqttService: MQTTService) { + DispatchQueue.main.async { + self.presenter?.didConnectToServer() + + guard let action = self.createChannelAction else { + return + } + self.presenter?.showHUD() + action() } - presenter?.showHUD() - action() } - func didDisconnect(_ mqttService: MQTTService) { - presenter?.hideHUD() + func mqttServiceDidDisconnect(_ mqttService: MQTTService) { + DispatchQueue.main.async { + self.presenter?.hideHUD() + } } } diff --git a/Nynja/Modules/Channel/SubscribersSelector/Presenter/SubscribersSelectorPresenter.swift b/Nynja/Modules/Channel/SubscribersSelector/Presenter/SubscribersSelectorPresenter.swift index 1823ac3a3..c6c0313ae 100644 --- a/Nynja/Modules/Channel/SubscribersSelector/Presenter/SubscribersSelectorPresenter.swift +++ b/Nynja/Modules/Channel/SubscribersSelector/Presenter/SubscribersSelectorPresenter.swift @@ -294,7 +294,7 @@ private extension SubscribersSelectorPresenter { case pluralSubscribers = "channel_plural_subscribers" case allSubscribers = "channel_all_subscribers" case noSubscibers = "channel_no_subscribers_to_select" - case noSearchResult = "channel_no_search_result" + case noSearchResult = "no_search_result" } } diff --git a/Nynja/Modules/Channel/SubscribersSelector/View/SubscribersSelectorViewController.swift b/Nynja/Modules/Channel/SubscribersSelector/View/SubscribersSelectorViewController.swift index 4d5c17aa3..21c3ae7c3 100644 --- a/Nynja/Modules/Channel/SubscribersSelector/View/SubscribersSelectorViewController.swift +++ b/Nynja/Modules/Channel/SubscribersSelector/View/SubscribersSelectorViewController.swift @@ -267,7 +267,7 @@ final class SubscribersSelectorViewController: BaseVC, SubscribersSelectorViewPr func updateSubscribersCollection(with state: CollectionState) { switch state { case let .empty(viewModel): - emptyStateDS.emptyStateView.setup(with: viewModel) + emptyStateDS.emptyStateViewModel = viewModel tableDataSource.subscribers = [:] case let .filled(data: data): updateAvatars(data.selected) diff --git a/Nynja/Modules/ChannelsList/Presenter/ChannelsListPresenter.swift b/Nynja/Modules/ChannelsList/Presenter/ChannelsListPresenter.swift index 0ebc20f0a..dfd686a2d 100644 --- a/Nynja/Modules/ChannelsList/Presenter/ChannelsListPresenter.swift +++ b/Nynja/Modules/ChannelsList/Presenter/ChannelsListPresenter.swift @@ -133,7 +133,7 @@ extension ChannelsListPresenter { case noChannels = "channel_list_is_empty" case createChannel = "channel_list_create_channel" - case noSearchResult = "channel_list_no_search_result" + case noSearchResult = "no_search_result" } } diff --git a/Nynja/Modules/ChannelsList/View/ChannelsListViewController.swift b/Nynja/Modules/ChannelsList/View/ChannelsListViewController.swift index 350f9730d..84497e8b1 100644 --- a/Nynja/Modules/ChannelsList/View/ChannelsListViewController.swift +++ b/Nynja/Modules/ChannelsList/View/ChannelsListViewController.swift @@ -31,8 +31,6 @@ final class ChannelsListViewController: BaseVC, ChannelsListViewProtocol, SetInj lazy var tableView: UITableView = { let tv = UITableView.default - tv.tableFooterView = UIView() - self.view.addSubview(tv) tv.snp.makeConstraints({ (make) in make.left.right.equalToSuperview() @@ -60,9 +58,11 @@ final class ChannelsListViewController: BaseVC, ChannelsListViewProtocol, SetInj private lazy var searchField: NynjaSearchField = { let searchField = NynjaSearchField() + searchField.searchTextChangeHandler = { [weak self] searchQuery in self?.presenter.filter(with: searchQuery) } + return searchField }() @@ -104,7 +104,7 @@ final class ChannelsListViewController: BaseVC, ChannelsListViewProtocol, SetInj switch state { case let .empty(viewModel): - emptyStateDS.emptyStateView.setup(with: viewModel) + emptyStateDS.emptyStateViewModel = viewModel channels = [] if displayMode == .default { diff --git a/Nynja/Modules/ChatsList/ChatsListProtocols.swift b/Nynja/Modules/ChatsList/ChatsListProtocols.swift index 6817a62fc..c5aafbdb5 100644 --- a/Nynja/Modules/ChatsList/ChatsListProtocols.swift +++ b/Nynja/Modules/ChatsList/ChatsListProtocols.swift @@ -25,7 +25,7 @@ protocol ChatsListViewProtocol: class { /** * Add here your methods for communication PRESENTER -> VIEW */ - func setup(result: [Contact]) + func setup(with state: CollectionState<[Contact]>, displayMode: CollectionDisplayMode) } protocol ChatsListPresenterProtocol: BasePresenterProtocol { @@ -46,7 +46,8 @@ protocol ChatsListInteractorOutputProtocol: class { /** * Add here your methods for communication INTERACTOR -> PRESENTER */ - func getChatListSuccess(result: [Contact]) + func didFetch(chats: [Contact]) + func didFilter(chats: [Contact]) } protocol ChatsListInteractorInputProtocol: BaseInteractorProtocol { @@ -56,6 +57,5 @@ protocol ChatsListInteractorInputProtocol: BaseInteractorProtocol { /** * Add here your methods for communication PRESENTER -> INTERACTOR */ - init(conversationsProvider: ConversationsProviding) func filter(with text: String) } diff --git a/Nynja/Modules/ChatsList/Interactor/ChatsListInteractor.swift b/Nynja/Modules/ChatsList/Interactor/ChatsListInteractor.swift index 6f3a533ff..c40b768de 100644 --- a/Nynja/Modules/ChatsList/Interactor/ChatsListInteractor.swift +++ b/Nynja/Modules/ChatsList/Interactor/ChatsListInteractor.swift @@ -6,12 +6,29 @@ // Copyright © 2017 TecSynt Solutions. All rights reserved. // -class ChatsListInteractor: BaseInteractor, ChatsListInteractorInputProtocol { +class ChatsListInteractor: BaseInteractor, ChatsListInteractorInputProtocol, InitializeInjectable { weak var presenter: ChatsListInteractorOutputProtocol! + private let storageService: StorageService private let conversationsProvider: ConversationsProviding + private var chats: [Contact] = [] + private var searchText: String = "" + + + // MARK: - InitializeInjectable + + struct Dependencies { + let storageService: StorageService + let conversationsProvider: ConversationsProviding + } + + required init(dependencies: Dependencies) { + storageService = dependencies.storageService + conversationsProvider = dependencies.conversationsProvider + } + // MARK: - BaseInteractor @@ -21,40 +38,65 @@ class ChatsListInteractor: BaseInteractor, ChatsListInteractorInputProtocol { override func loadData() { super.loadData() - - getChatList() + fetchChats() } // MARK: - ChatsListInteractorInputProtocol - required init(conversationsProvider: ConversationsProviding) { - self.conversationsProvider = conversationsProvider + func filter(with text: String) { + searchText = text.trimmed() + applyFilter(with: searchText) } + - private func getChatList(_ filter: String? = nil) { - var chats = conversationsProvider.fetchChats() - - if let filterText = filter, !filterText.isEmpty { - chats = chats.filter { contact in - let fullname = contact.fullName ?? "" - return filterText.isIn(string: fullname, options: .caseInsensitive) + // MARK: - StorageSubscriber + + override func update(with changes: [StorageChange], type: SubscribeType) { + if case .contact = type { + if let dbContact = changes.first?.entity as? DBContact { + let contact = Contact(contact: dbContact) + chats = updateChatsList(with: contact) + applyFilter(with: searchText) } } - - self.presenter.getChatListSuccess(result: chats) } - func filter(with text: String) { - self.getChatList(text) + + //MARK: - Private + + private func fetchChats() { + chats = conversationsProvider.fetchChats() + presenter.didFetch(chats: chats) } - - // MARK: - StorageSubscriber + private func applyFilter(with text: String) { + if !text.isEmpty { + let filteredChats = chats.filter { contact in + guard let fullname = contact.fullName else { + return false + } + return text.isIn(string: fullname, options: .caseInsensitive) + } + + presenter.didFilter(chats: filteredChats) + } else { + presenter.didFetch(chats: chats) + } + } - override func update(with changes: [StorageChange], type: SubscribeType) { - if case .contact = type { - getChatList() + private func updateChatsList(with contact: Contact) -> [Contact] { + guard let phoneId = contact.phone_id, + phoneId != storageService.phoneId else { + return chats + } + + var newChats = chats + if let index = chats.index(where: { $0.phoneId == phoneId }) { + newChats[index] = contact + } else { + newChats.append(contact) } + return newChats.sorted(by: conversationsProvider.comparator) } } diff --git a/Nynja/Modules/ChatsList/Presenter/ChatsListPresenter.swift b/Nynja/Modules/ChatsList/Presenter/ChatsListPresenter.swift index e75acc382..fb884190a 100644 --- a/Nynja/Modules/ChatsList/Presenter/ChatsListPresenter.swift +++ b/Nynja/Modules/ChatsList/Presenter/ChatsListPresenter.swift @@ -14,30 +14,56 @@ class ChatsListPresenter: BasePresenter, ChatsListPresenterProtocol, ChatsListIn return P2pChatsItemsFactory() } - - //MARK: - ChatsListPresenterProtocol - weak var view: ChatsListViewProtocol! + var wireFrame: ChatsListWireFrameProtocol! var interactor: ChatsListInteractorInputProtocol! { didSet { _interactor = interactor } } - var wireFrame: ChatsListWireFrameProtocol! + + + // MARK: - ChatsListPresenterProtocol func filter(with text: String) { self.interactor.filter(with: text) } - //MARK: -- Actions - func tappedContact(contact: Contact) { wireFrame.showChatWith(contact: contact) } - //MARK: - ChatsListInteractorOutputProtocol - func getChatListSuccess(result: [Contact]) { - view.setup(result: result) + // MARK: - ChatsListInteractorOutputProtocol + + func didFetch(chats: [Contact]) { + updateCollectionState(for: .default, chats: chats) + + } + + func didFilter(chats: [Contact]) { + updateCollectionState(for: .search, chats: chats) + } + + // MARK: - Private + + private func updateCollectionState(for displayMode: CollectionDisplayMode, chats: [Contact]) { + let state = makeCollectionState(for: displayMode, chats: chats) + view.setup(with: state, displayMode: displayMode) } + + private func makeCollectionState(for displayMode: CollectionDisplayMode, + chats: [Contact]) -> CollectionState<[Contact]> { + if chats.isEmpty { + guard displayMode == .search else { + return .empty(nil) + } + + let emptyStateViewModel = EmptyStateViewModel(image: #imageLiteral(resourceName: "ic_search_empty"), descriptionText: "no_search_result".localized) + return .empty(emptyStateViewModel) + } else { + return .filled(data: chats) + } + } + } diff --git a/Nynja/Modules/ChatsList/View/ChatsListViewController.swift b/Nynja/Modules/ChatsList/View/ChatsListViewController.swift index 67e35be4b..29506c794 100644 --- a/Nynja/Modules/ChatsList/View/ChatsListViewController.swift +++ b/Nynja/Modules/ChatsList/View/ChatsListViewController.swift @@ -16,50 +16,34 @@ class ChatsListViewController: BaseVC, ChatsListViewProtocol, TextInputProtocol, } } - private lazy var dataSource = ChatListTableDS(payloadParser: MessagePayloadParser(), delegate: self) - - - //MARK: - Subviews + override var activatedViews: [UIView] { + return [controlContainerView] + } lazy var swipeBackHelper: SwipeBackHelper = { return SwipeBackHelper(with: self) }() + private var emptyStateDS: EmptyStateTableViewDS! + private var dataSource: ChatListTableDS! + + // MARK: - Subviews + lazy var tableView: UITableView = { - let tv = UITableView(frame: CGRect.zero) - tv.delegate = self - tv.backgroundColor = UIColor.clear + let tv = UITableView.default + tv.clipsToBounds = true - tv.dataSource = self.dataSource - tv.delegate = self - tv.separatorStyle = .none tv.keyboardDismissMode = .interactive - tv.register(viewModel: ChatListMessageCellModel.self) - tv.rowHeight = ChatListMessageCellModel.Cell.Constraints.height - tv.estimatedRowHeight = tv.rowHeight - tv.tableFooterView = UIView() - self.view.addSubview(tv) + self.view.addSubview(tv) tv.snp.makeConstraints({ (make) in + make.top.equalTo(navigationView.snp.bottom) make.left.right.equalToSuperview() - adjustVerticalInset(.top, make: make, offset: NavigationView.calculatedHeight) }) + return tv }() - - //MARK: - BaseVC - - override func initialize() { - super.initialize() - - screenTitle = "chats_list_title".localized.uppercased() - tableView.isHidden = false - self.navigationView.isSeparatorVisible = true - swipeBackHelper.addGesture() - self.controlContainerView.isHidden = false - } - private lazy var controlContainerView: NynjaControlContainerView = { let bottomOffset = Constraints.controlContainerView.bottomOffset let containerView = NynjaControlContainerView(contentView: searchField) @@ -67,8 +51,8 @@ class ChatsListViewController: BaseVC, ChatsListViewProtocol, TextInputProtocol, view.addSubview(containerView) containerView.snp.makeConstraints { maker in maker.left.right.equalToSuperview() - maker.top.equalTo(self.tableView.snp.bottom) - maker.bottom.equalTo(self.view.keyboardLayoutGuide.snp.top).offset(-bottomOffset) + maker.top.equalTo(tableView.snp.bottom) + maker.bottom.equalTo(view.keyboardLayoutGuide.snp.top).offset(-bottomOffset) } containerView.addGradientView() @@ -82,19 +66,57 @@ class ChatsListViewController: BaseVC, ChatsListViewProtocol, TextInputProtocol, let text = searchQuery ?? "" self?.presenter.filter(with: text) } - searchField.returnHandler = { [weak self] in - self?.view.endEditing(true) - } return searchField }() - //MARK: - ChatsListViewProtocol + // MARK: - BaseVC + + override func initialize() { + super.initialize() + setupUI() + } - func setup(result: [Contact]) { - dataSource.chatList = result + + // MARK: - ChatsListViewProtocol + + func setup(with state: CollectionState<[Contact]>, displayMode: CollectionDisplayMode) { + var contacts: [Contact] = [] + + switch state { + case let .empty(viewModel): + emptyStateDS.emptyStateViewModel = viewModel + case let .filled(data: data): + contacts = data + } + + dataSource.chatList = contacts tableView.reloadData() } + + // MARK: - UI Setup + + private func setupUI() { + screenTitle = "chats_list_title".localized.uppercased() + self.navigationView.isSeparatorVisible = true + + swipeBackHelper.addGesture() + self.controlContainerView.isHidden = false + + setupTableView() + } + + private func setupTableView() { + tableView.register(viewModel: ChatListMessageCellModel.self) + tableView.rowHeight = ChatListMessageCellModel.Cell.Constraints.height + tableView.estimatedRowHeight = tableView.rowHeight + + dataSource = ChatListTableDS(payloadParser: MessagePayloadParser(), delegate: self) + emptyStateDS = EmptyStateTableViewDS(dataSource: dataSource) + tableView.dataSource = emptyStateDS + + tableView.delegate = self + } } // MARK: - UITableViewDelegate @@ -106,6 +128,7 @@ extension ChatsListViewController: UITableViewDelegate { let contact = dataSource.chatList[index] presenter.tappedContact(contact: contact) } + } // MARK: - ChatListMessageCellModelDelegate diff --git a/Nynja/Modules/ChatsList/WireFrame/ChatsListWireframe.swift b/Nynja/Modules/ChatsList/WireFrame/ChatsListWireframe.swift index 7968ed8d9..b55a56e88 100644 --- a/Nynja/Modules/ChatsList/WireFrame/ChatsListWireframe.swift +++ b/Nynja/Modules/ChatsList/WireFrame/ChatsListWireframe.swift @@ -14,13 +14,13 @@ class ChatsListWireFrame: ChatsListWireFrameProtocol { func presentChatsList(navigation: UINavigationController, main: MainWireFrame?, animated: Bool) { - // Dependencies - let conversationsProvider = ConversationsProvider() - // Componenets let view = ChatsListViewController() let presenter = ChatsListPresenter() - let interactor = ChatsListInteractor(conversationsProvider: conversationsProvider) + + let interactor = ChatsListInteractor( + dependencies: .init(storageService: StorageService.sharedInstance, + conversationsProvider: ConversationsProvider())) self.main = main diff --git a/Nynja/Modules/Contacts/Interactor/ContactsInteractor.swift b/Nynja/Modules/Contacts/Interactor/ContactsInteractor.swift index 9bff9f478..ced32e0d3 100644 --- a/Nynja/Modules/Contacts/Interactor/ContactsInteractor.swift +++ b/Nynja/Modules/Contacts/Interactor/ContactsInteractor.swift @@ -56,7 +56,7 @@ class ContactsInteractor: BaseInteractor, ContactsInteractorInputProtocol, IoHan } if !text.isEmpty { - filteredContacts = contacts.filter { contact in + filteredContacts = filteredContacts.filter { contact in let strings = [contact.fullName, contact.nick].compactMap { $0 } return text.isIn(strings: strings, options: .caseInsensitive) } diff --git a/Nynja/Modules/Contacts/View/TableView/Cell/ContactCell.swift b/Nynja/Modules/Contacts/View/TableView/Cell/ContactCell.swift index 4e95ccfc3..a6c72d2ed 100644 --- a/Nynja/Modules/Contacts/View/TableView/Cell/ContactCell.swift +++ b/Nynja/Modules/Contacts/View/TableView/Cell/ContactCell.swift @@ -78,7 +78,7 @@ class ContactCell: UITableViewCell { let button = UIButton() self.addSubview(button) - button.titleLabel?.font = UIFont(fontName: Constants.fonts.medium, height: CGFloat(Constraints.addButton.labelHeight)) + button.titleLabel?.font = UIFont.makeFont(with: Constants.fonts.medium, height: CGFloat(Constraints.addButton.labelHeight)) button.titleLabel?.baselineAdjustment = .alignCenters button.titleLabel?.adjustsFontSizeToFitWidth = true diff --git a/Nynja/Modules/Contacts/View/ViewController/ContactsViewController.swift b/Nynja/Modules/Contacts/View/ViewController/ContactsViewController.swift index 07409b034..b43c0e49a 100644 --- a/Nynja/Modules/Contacts/View/ViewController/ContactsViewController.swift +++ b/Nynja/Modules/Contacts/View/ViewController/ContactsViewController.swift @@ -81,9 +81,6 @@ class ContactsViewController: BaseVC, ContactsViewProtocol, ContactCellDelegate, let text = searchQuery ?? "" self?.presenter.filter(with: text) } - searchField.returnHandler = { [weak self] in - self?.view.endEditing(true) - } return searchField }() diff --git a/Nynja/Modules/CreateGroup/Interactor/CreateGroupInteractor.swift b/Nynja/Modules/CreateGroup/Interactor/CreateGroupInteractor.swift index ef479e969..1f98f786c 100644 --- a/Nynja/Modules/CreateGroup/Interactor/CreateGroupInteractor.swift +++ b/Nynja/Modules/CreateGroup/Interactor/CreateGroupInteractor.swift @@ -74,15 +74,19 @@ class CreateGroupInteractor: BaseInteractor, CreateGroupInteractorInputProtocol extension CreateGroupInteractor: MQTTServiceDelegate { - func didConnect(_ mqttService: MQTTService) { - guard let action = sendRoomAction else { - return + func mqttServiceDidConnect(_ mqttService: MQTTService) { + DispatchQueue.main.async { + guard let action = self.sendRoomAction else { + return + } + self.presenter?.showHUD() + action() } - presenter?.showHUD() - action() } - func didDisconnect(_ mqttService: MQTTService) { - presenter?.hideHUD() + func mqttServiceDidDisconnect(_ mqttService: MQTTService) { + DispatchQueue.main.async { + self.presenter?.hideHUD() + } } } diff --git a/Nynja/Modules/Favorites/Interactor/FavoritesInteractor.swift b/Nynja/Modules/Favorites/Interactor/FavoritesInteractor.swift index ed685bc7e..64fc9d2ac 100644 --- a/Nynja/Modules/Favorites/Interactor/FavoritesInteractor.swift +++ b/Nynja/Modules/Favorites/Interactor/FavoritesInteractor.swift @@ -10,39 +10,53 @@ class FavoritesInteractor: BaseInteractor, FavoritesInteractorInputProtocol { weak var presenter: FavoritesInteractorOutputProtocol! + private let storageService: StorageService = StorageService.sharedInstance + override var subscribes: [SubscribeType]? { return [.star(nil)] } + + // MARK: - FavoritesInteractorInputProtocol + func loadStars() { MQTTService.sharedInstance.getFavorites() } func fetchStars() { - guard let rosterId = StorageService.sharedInstance.rosterId else { return } - - let stars = StarDAO.fetchStars(rosterId: rosterId).values.sorted(by: self.starsComparator) - self.presenter.getStarsSuccess(stars: stars) + guard let rosterId = storageService.rosterId else { + return + } + let stars = StarDAO.fetchStars(rosterId: rosterId).values.sorted { $0.timestamp > $1.timestamp } + presenter.getStarsSuccess(stars: stars) } func deleteStar(_ star: Star) { - star.status = StringAtom(string: "remove") + star.starStatus = .remove + + if let starLocalId = star.client_id { + if star.isDelivered { + let action = DBStarAction(starLocalId: starLocalId, action: .delete) + try? storageService.perform(action: .save, with: star) + try? storageService.perform(action: .save, with: action) + } else { + try? storageService.perform(action: .delete, with: star) + } + } + MQTTService.sharedInstance.sendStar(star: star) } func chatModel(from message: Message) -> ChatModel? { return ChatService.fetchChatModel(from: message) } - - private func starsComparator(lhs: Star, rhs: Star) -> Bool { - return lhs.timestamp > rhs.timestamp - } + // MARK: - StorageSubscriber + override func update(with changes: [StorageChange], type: SubscribeType) { if case .star = type { fetchStars() } } - } diff --git a/Nynja/Modules/Favorites/View/FavoritesViewController.swift b/Nynja/Modules/Favorites/View/FavoritesViewController.swift index 07c3d23e5..040204246 100644 --- a/Nynja/Modules/Favorites/View/FavoritesViewController.swift +++ b/Nynja/Modules/Favorites/View/FavoritesViewController.swift @@ -61,7 +61,10 @@ class FavoritesViewController: BaseVC, FavoritesViewProtocol, ItemSelectorDelega }) case .locations: filtered = self.allStars.filter({ (star) -> Bool in - return star.message?.files?.last?.mime == SendMessageType.location.rawValue + let mime = star.message?.files?.last?.mime + let isLocation = mime == SendMessageType.location.rawValue + let isPlace = mime == SendMessageType.place.rawValue + return isPlace || isLocation }) case .links: filtered = self.allStars @@ -125,23 +128,32 @@ class FavoritesViewController: BaseVC, FavoritesViewProtocol, ItemSelectorDelega selector.delegate = self selector.items = [SelectorItemModel(title: Strings.all.localized.uppercased(), - associated: Strings.all), + associated: Strings.all, + identifier: "filter_item_all"), SelectorItemModel(title: Strings.text.localized.uppercased(), - associated: Strings.text), + associated: Strings.text, + identifier: "filter_item_text"), SelectorItemModel(title: Strings.voice.localized.uppercased(), - associated: Strings.voice), + associated: Strings.voice, + identifier: "filter_item_voice"), SelectorItemModel(title: Strings.images.localized.uppercased(), - associated: Strings.images), + associated: Strings.images, + identifier: "filter_item_image"), SelectorItemModel(title: Strings.files.localized.uppercased(), - associated: Strings.files), + associated: Strings.files, + identifier: "filter_item_file"), SelectorItemModel(title: Strings.video.localized.uppercased(), - associated: Strings.video), + associated: Strings.video, + identifier: "filter_item_video"), SelectorItemModel(title: Strings.contacts.localized.uppercased(), - associated: Strings.contacts), + associated: Strings.contacts, + identifier: "filter_item_contact"), SelectorItemModel(title: Strings.locations.localized.uppercased(), - associated: Strings.locations), + associated: Strings.locations, + identifier: "filter_item_location"), SelectorItemModel(title: Strings.links.localized.uppercased(), - associated: Strings.links) + associated: Strings.links, + identifier: "filter_item_link") ] return selector diff --git a/Nynja/Modules/Flows/CameraFlow/VideoPreview/Interactor/CameraVideoPreviewInteractor.swift b/Nynja/Modules/Flows/CameraFlow/VideoPreview/Interactor/CameraVideoPreviewInteractor.swift index d35a919a2..fa2a3732e 100644 --- a/Nynja/Modules/Flows/CameraFlow/VideoPreview/Interactor/CameraVideoPreviewInteractor.swift +++ b/Nynja/Modules/Flows/CameraFlow/VideoPreview/Interactor/CameraVideoPreviewInteractor.swift @@ -178,9 +178,9 @@ private extension CameraVideoPreviewInteractor { exportSession?.exportAsynchronously { switch exportSession!.status { case .failed: - LogService.log(topic: .videoConverter, text: "export failed") + LogService.log(topic: .videoConverter) { return "export failed" } case .cancelled: - LogService.log(topic: .videoConverter, text: "export canceled") + LogService.log(topic: .videoConverter) { return "export canceled" } case .completed: dispatchAsyncMain { complete(writeUrl) diff --git a/Nynja/Modules/Flows/GalleryFlow/Gallery/Interactor/GalleryInteractor.swift b/Nynja/Modules/Flows/GalleryFlow/Gallery/Interactor/GalleryInteractor.swift index e2f8c2c46..60d1e0ffb 100644 --- a/Nynja/Modules/Flows/GalleryFlow/Gallery/Interactor/GalleryInteractor.swift +++ b/Nynja/Modules/Flows/GalleryFlow/Gallery/Interactor/GalleryInteractor.swift @@ -132,7 +132,7 @@ extension GalleryInteractor { resourceManager.fetchPhoto(item: asset, completion: { (item) in completion(item.image) }) { (error) in - LogService.log(topic: .galery, text: error.localizedDescription) + LogService.log(topic: .galery) { return error.localizedDescription } } } @@ -140,7 +140,7 @@ extension GalleryInteractor { resourceManager.fetchVideo(item: asset, completion: { (item) in completion(item.videoUrl) }) { (error) in - LogService.log(topic: .galery, text: error.localizedDescription) + LogService.log(topic: .galery) { return error.localizedDescription } } } } diff --git a/Nynja/Modules/Flows/GalleryFlow/MultiplePreview/Interactor/MultiplePreviewInteractor.swift b/Nynja/Modules/Flows/GalleryFlow/MultiplePreview/Interactor/MultiplePreviewInteractor.swift index 11eb8f35d..4de7f6b35 100644 --- a/Nynja/Modules/Flows/GalleryFlow/MultiplePreview/Interactor/MultiplePreviewInteractor.swift +++ b/Nynja/Modules/Flows/GalleryFlow/MultiplePreview/Interactor/MultiplePreviewInteractor.swift @@ -93,7 +93,7 @@ extension MultiplePreviewInteractor { resourceManager.fetchVideo(item: asset, completion: { (item) in completion(item.videoUrl) }) { (error) in - LogService.log(topic: .galery, text: error.localizedDescription) + LogService.log(topic: .galery) { return error.localizedDescription } } } } diff --git a/Nynja/Modules/ForwardSelector/ForwardSelectorProtocols.swift b/Nynja/Modules/ForwardSelector/ForwardSelectorProtocols.swift index 1f01b9931..dff1c82b5 100644 --- a/Nynja/Modules/ForwardSelector/ForwardSelectorProtocols.swift +++ b/Nynja/Modules/ForwardSelector/ForwardSelectorProtocols.swift @@ -83,7 +83,6 @@ protocol ForwardSelectorInteractorInputProtocol: BaseInteractorProtocol { func filterContacts(with text: String) func filterGroups(with text: String) - func send(for targets: ForwardTargets) func scheduleInfo(with forwardTargets: ForwardTargets) -> ScheduleInfo? } diff --git a/Nynja/Modules/ForwardSelector/Interactor/ForwardSelectorInteractor.swift b/Nynja/Modules/ForwardSelector/Interactor/ForwardSelectorInteractor.swift index 0e882fc53..898ef5ea7 100644 --- a/Nynja/Modules/ForwardSelector/Interactor/ForwardSelectorInteractor.swift +++ b/Nynja/Modules/ForwardSelector/Interactor/ForwardSelectorInteractor.swift @@ -99,19 +99,6 @@ final class ForwardSelectorInteractor: BaseInteractor, ForwardSelectorInteractor } } - func send(for targets: ForwardTargets) { - guard let info = scheduleInfo(with: targets), let phoneId = storageService.phoneId else { return } - - let messages = info.targets.messages(from: info.message, phoneId: phoneId) - mqttService.forwardMessage(phoneId: phoneId, messages: messages) { [weak self] createdJob in - guard let `self` = self, let dbJob = DBJob(job: createdJob) else { return } - - dbJob.type = .forward - - try? self.storageService.perform(action: .save, with: dbJob) - } - } - func scheduleInfo(with targets: ForwardTargets) -> ScheduleInfo? { guard let localId = messageLocalId, let message = MessageDAO.fetchMessage(localId: localId) else { return nil } return ScheduleInfo(message: message.forwardMessage, targets: targets) diff --git a/Nynja/Modules/ForwardSelector/Presenter/ForwardSelectorPresenter.swift b/Nynja/Modules/ForwardSelector/Presenter/ForwardSelectorPresenter.swift index ad0901fcc..5dbc38b41 100644 --- a/Nynja/Modules/ForwardSelector/Presenter/ForwardSelectorPresenter.swift +++ b/Nynja/Modules/ForwardSelector/Presenter/ForwardSelectorPresenter.swift @@ -76,11 +76,7 @@ final class ForwardSelectorPresenter: BasePresenter, ForwardSelectorPresenterPro func didFinishSelection() { continueIfCan { targets in - if mode == .edit { - delegate?.didSelectForwardTargets(targets) - } else { - interactor.send(for: targets) - } + delegate?.didSelectForwardTargets(targets) close() } } diff --git a/Nynja/Modules/GroupRules/View/GroupRulesViewController.swift b/Nynja/Modules/GroupRules/View/GroupRulesViewController.swift index ab08b278a..f9a3f60eb 100644 --- a/Nynja/Modules/GroupRules/View/GroupRulesViewController.swift +++ b/Nynja/Modules/GroupRules/View/GroupRulesViewController.swift @@ -54,7 +54,7 @@ class GroupRulesViewController: BaseVC, GroupRulesViewProtocol { tv.backgroundColor = UIColor.clear tv.textColor = Constants.colors.white.getColor(withAlpha: 1) - tv.font = UIFont(fontName: Constants.fonts.notoSansRegular, height: Constraints.textView.labelHeight.adjustedByWidth) + tv.font = UIFont.makeFont(with: Constants.fonts.notoSansRegular, height: Constraints.textView.labelHeight.adjustedByWidth) textViewContainer.addSubview(tv) tv.snp.makeConstraints { make in diff --git a/Nynja/Modules/GroupStorage/GroupStorageProtocols.swift b/Nynja/Modules/GroupStorage/GroupStorageProtocols.swift index 38c4780d0..973fcbff2 100644 --- a/Nynja/Modules/GroupStorage/GroupStorageProtocols.swift +++ b/Nynja/Modules/GroupStorage/GroupStorageProtocols.swift @@ -10,7 +10,7 @@ import UIKit protocol GroupStorageWireFrameProtocol: class { - func presentGroupStorage(navigation: UINavigationController, room: Room?, contact: Contact?, main: MainWireFrame?) + func presentGroupStorage(navigation: UINavigationController, chatModel: ChatModel, main: MainWireFrame?) func showMessage(localId: String, chat: ChatModel) @@ -77,9 +77,8 @@ protocol GroupStorageInteractorOutputProtocol: class { protocol GroupStorageInteractorInputProtocol: BaseInteractorProtocol { var presenter: GroupStorageInteractorOutputProtocol! { get set } - var chat: ChatModel? { get } - var room: Room? { get set } - var contact: Contact? { get set } + var chat: ChatModel! { get } + func getMessages(type: SendMessageType) -> (messages: [Message], progressModels: [URL : ProgressModel]) func startLoading(_ id: String) diff --git a/Nynja/Modules/GroupStorage/Interactor/GroupStorageInteractor.swift b/Nynja/Modules/GroupStorage/Interactor/GroupStorageInteractor.swift index 2a76339a0..3e071e051 100644 --- a/Nynja/Modules/GroupStorage/Interactor/GroupStorageInteractor.swift +++ b/Nynja/Modules/GroupStorage/Interactor/GroupStorageInteractor.swift @@ -14,69 +14,59 @@ class GroupStorageInteractor: BaseInteractor, GroupStorageInteractorInputProtoco weak var presenter: GroupStorageInteractorOutputProtocol! - var chat: ChatModel? { - if let room = self.room { - return room - } else if let contact = self.contact { - return contact - } - - return nil - } - - var room: Room? - var contact: Contact? + var chat: ChatModel! private var chatId: String? { - return contact?.phoneId ?? room?.id + if let contact = chat as? Contact { + return contact.phoneId + } + if let room = chat as? Room { + return room.id + } + return nil } private var processingManager: MessageProcessingManagerInterface - init(room: Room?, contact: Contact?) { - self.room = room - self.contact = contact + init(chatModel: ChatModel) { + self.chat = chatModel processingManager = DefaultMessagesProcessingManager.shared - super.init() - processingManager.delegate = self } func getMessages(type: SendMessageType) -> (messages: [Message], progressModels: [URL : ProgressModel]) { let emptyTuple = (messages: [Message](), progressModels:[URL : ProgressModel]()) - if let roomId = room?.id { - // FIXME: Type cheking should be embedded into sql query and removed from this interactor. But for now MessageTable.type always NULL. - let messages = MessageDAO - .fetchMessages(name: roomId) + var messages: [Message]? + if let contact = chat as? Contact { + messages = MessageDAO + .fetchMessages(to: contact.phoneId) .filter { $0.mainFile?.mime == type.rawValue } - - let progressModels = processingManager.progressFor(messages.messagesUrls) - return (messages: messages, progressModels: progressModels) - } else if let phoneId = contact?.phoneId { - let messages = MessageDAO - .fetchMessages(to: phoneId) + } + if let roomID = (chat as? Room)?.id { + messages = MessageDAO + .fetchMessages(name: roomID) .filter { $0.mainFile?.mime == type.rawValue } - + } + if let messages = messages { let progressModels = processingManager.progressFor(messages.messagesUrls) - return (messages: messages, progressModels:progressModels) + return (messages: messages, progressModels: progressModels) } - return emptyTuple } func getLinks() { var feed: FeedProtocol? = nil - if let phoneId = contact?.phoneId { - feed = FeedDAO.fetchP2p(for: phoneId) - } else if let room = self.room, let roomId = room.id { - feed = FeedDAO.fetchMuc(for: roomId) + if let contact = chat as? Contact { + feed = FeedDAO.fetchP2p(for: contact.phoneId) } - - if let f = feed, let result = MessageLinkDAO.fetchLinks(for: f) { - self.presenter?.getLinksSuccess(links: result) + if let roomID = (chat as? Room)?.id { + feed = FeedDAO.fetchMuc(for: roomID) } + guard feed != nil, + let result = MessageLinkDAO.fetchLinks(for: feed) else { return } + self.presenter?.getLinksSuccess(links: result) } // MARK: Message processing diff --git a/Nynja/Modules/GroupStorage/Presenter/GroupStorageListItems.swift b/Nynja/Modules/GroupStorage/Presenter/GroupStorageListItems.swift index d251558e5..82f7f45f5 100644 --- a/Nynja/Modules/GroupStorage/Presenter/GroupStorageListItems.swift +++ b/Nynja/Modules/GroupStorage/Presenter/GroupStorageListItems.swift @@ -19,6 +19,10 @@ class GroupStorageListItem : Equatable { var type: GroupStorageFilterOption { return .unspecified } + + var accessibilityIdentifier: String? { + return nil + } required init (message:Message) { messageLocalId = message.msg_id ?? "" @@ -41,6 +45,10 @@ class GroupFilesItem : GroupStorageListItem { var name:String? var size:Int64? + override var accessibilityIdentifier: String? { + return "storage_file_cell" + } + override var type:GroupStorageFilterOption {return .files} override func setupFrom(message: Message) { @@ -53,10 +61,21 @@ class GroupFilesItem : GroupStorageListItem { } } +class GroupAudiosItem: GroupFilesItem { + + override var accessibilityIdentifier: String? { + return "storage_audio_cell" + } +} + class GroupVideosItem : GroupStorageListItem { var duration: Int64? var thumbUrl: URL? + override var accessibilityIdentifier: String? { + return "storage_video_cell" + } + override var type:GroupStorageFilterOption {return .videos} override func setupFrom(message: Message) { @@ -75,23 +94,15 @@ class GroupVideosItem : GroupStorageListItem { class GroupImagesItem : GroupStorageListItem{ var thumbUrl:URL? - override var type:GroupStorageFilterOption {return .images} - - override func setupFrom(message: Message) { - super.setupFrom(message: message) - - thumbUrl = message.thumbUrl + override var accessibilityIdentifier: String? { + return "storage_image_cell" } -} - -class GroupLinksItem : GroupStorageListItem{ - var link:String? override var type:GroupStorageFilterOption {return .images} override func setupFrom(message: Message) { super.setupFrom(message: message) - // thumbUrl = message.thumbUrl + thumbUrl = message.thumbUrl } } diff --git a/Nynja/Modules/GroupStorage/Presenter/GroupStoragePresenter.swift b/Nynja/Modules/GroupStorage/Presenter/GroupStoragePresenter.swift index 64a98ec62..448ab7389 100644 --- a/Nynja/Modules/GroupStorage/Presenter/GroupStoragePresenter.swift +++ b/Nynja/Modules/GroupStorage/Presenter/GroupStoragePresenter.swift @@ -9,7 +9,13 @@ class GroupStoragePresenter: BasePresenter, GroupStoragePresenterProtocol, GroupStorageInteractorOutputProtocol { override var itemsFactory: WCItemsFactory? { - return GroupOptionsItemsFactory() + if (interactor.chat as? Room) != nil { + return GroupOptionsItemsFactory() + } + if (interactor.chat as? Contact) != nil { + return OtherUserProfileItemsFactory() + } + return nil } weak var view: GroupStorageViewProtocol! @@ -34,7 +40,7 @@ class GroupStoragePresenter: BasePresenter, GroupStoragePresenterProtocol, Group private let itemClassForFilter:FilterToItemClass = [GroupStorageFilterOption.files : GroupFilesItem.self, - GroupStorageFilterOption.audios : GroupFilesItem.self, + GroupStorageFilterOption.audios : GroupAudiosItem.self, GroupStorageFilterOption.videos : GroupVideosItem.self, GroupStorageFilterOption.images : GroupImagesItem.self] diff --git a/Nynja/Modules/GroupStorage/View/Collection/GroupStorageCollectionVC.swift b/Nynja/Modules/GroupStorage/View/Collection/GroupStorageCollectionVC.swift index 46609f978..bc260078d 100644 --- a/Nynja/Modules/GroupStorage/View/Collection/GroupStorageCollectionVC.swift +++ b/Nynja/Modules/GroupStorage/View/Collection/GroupStorageCollectionVC.swift @@ -48,7 +48,9 @@ class GroupStorageCollectionVC : GroupStorageListVC, UICollectionViewDelegate, U let cell = collection.dequeueReusableCell(withReuseIdentifier: CellsId.regular, for: indexPath) if let storageCell = cell as? GroupStorageCell { - let model = getItems()[indexPath.row] + let model = getItems()[indexPath.item] + + storageCell.accessibilityIdentifier = model.accessibilityIdentifier storageCell.setupWithModel(model) storageCell.delegate = self } diff --git a/Nynja/Modules/GroupStorage/View/Collection/GroupVideosCell.swift b/Nynja/Modules/GroupStorage/View/Collection/GroupVideosCell.swift index 5c5b9d268..9b630341d 100644 --- a/Nynja/Modules/GroupStorage/View/Collection/GroupVideosCell.swift +++ b/Nynja/Modules/GroupStorage/View/Collection/GroupVideosCell.swift @@ -47,7 +47,7 @@ class GroupVideosCell : GroupCollectionCell { private lazy var durationLabel: UILabel = { var label = UILabel() - label.font = UIFont(fontName: Constants.fonts.regular, + label.font = UIFont.makeFont(with: Constants.fonts.regular, height: Constraints.durationLabel.height) label.textColor = Constants.colors.white.getColor() diff --git a/Nynja/Modules/GroupStorage/View/Files/GroupFilesCell.swift b/Nynja/Modules/GroupStorage/View/Files/GroupFilesCell.swift index c2ea20d66..1df14bca8 100644 --- a/Nynja/Modules/GroupStorage/View/Files/GroupFilesCell.swift +++ b/Nynja/Modules/GroupStorage/View/Files/GroupFilesCell.swift @@ -62,7 +62,7 @@ class GroupFilesCell : UITableViewCell, GroupStorageCell { }) label.textColor = Constants.colors.white.getColor() - label.font = UIFont(fontName: Constants.fonts.regular, height: Constraints.labelsContainer.fontHeight) + label.font = UIFont.makeFont(with: Constants.fonts.regular, height: Constraints.labelsContainer.fontHeight) return label }() @@ -77,7 +77,7 @@ class GroupFilesCell : UITableViewCell, GroupStorageCell { }) label.textColor = Constants.colors.gray.getColor() - label.font = UIFont(fontName: Constants.fonts.regular, height: Constraints.labelsContainer.fontHeight) + label.font = UIFont.makeFont(with: Constants.fonts.regular, height: Constraints.labelsContainer.fontHeight) return label }() diff --git a/Nynja/Modules/GroupStorage/View/Files/GroupFilesListVC.swift b/Nynja/Modules/GroupStorage/View/Files/GroupFilesListVC.swift index 3c425fd68..ccaea2b74 100644 --- a/Nynja/Modules/GroupStorage/View/Files/GroupFilesListVC.swift +++ b/Nynja/Modules/GroupStorage/View/Files/GroupFilesListVC.swift @@ -54,8 +54,10 @@ class GroupFilesListVC : GroupStorageListVC, UITableViewDelegate, UITableViewDat let cell = tableView.dequeueReusableCell(withIdentifier: CellsId.regular) if let fileCell = cell as? GroupFilesCell { + let item = items[indexPath.row] + fileCell.accessibilityIdentifier = item.accessibilityIdentifier fileCell.delegate = self - fileCell.setupWithModel(items[indexPath.row]) + fileCell.setupWithModel(item) } return cell ?? UITableViewCell() diff --git a/Nynja/Modules/GroupStorage/View/GroupLinksListVC.swift b/Nynja/Modules/GroupStorage/View/GroupLinksListVC.swift index ca6cd0d3c..8c3ade2f3 100644 --- a/Nynja/Modules/GroupStorage/View/GroupLinksListVC.swift +++ b/Nynja/Modules/GroupStorage/View/GroupLinksListVC.swift @@ -56,9 +56,11 @@ class GroupLinksListVC : GroupStorageListVC, UITableViewDataSource, UITableViewD func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { var cell = UITableViewCell() if let c = tableView.dequeueReusableCell(withIdentifier: "cell", for: indexPath) as? LinksCell { - c.setupLink(link: self.links[indexPath.row]) - cell = c + let link = self.links[indexPath.row] + c.setupLink(link: link) + c.accessibilityIdentifier = "storage_link_cell" c.delegate = self + cell = c } return cell } diff --git a/Nynja/Modules/GroupStorage/View/GroupStorageCell.swift b/Nynja/Modules/GroupStorage/View/GroupStorageCell.swift index 83af9135d..ef2d995a9 100644 --- a/Nynja/Modules/GroupStorage/View/GroupStorageCell.swift +++ b/Nynja/Modules/GroupStorage/View/GroupStorageCell.swift @@ -25,6 +25,8 @@ protocol GroupStorageCell : class { var progressIndicator:RoundProgressIndicator? {set get} var loadButton:UIButton? {set get} + var accessibilityIdentifier: String? { get set } + func setupWithModel(_ model:GroupStorageListItem) func loadingContainerContraints() -> (ConstraintMaker) -> Void } diff --git a/Nynja/Modules/GroupStorage/View/GroupStorageViewController.swift b/Nynja/Modules/GroupStorage/View/GroupStorageViewController.swift index 910b2a571..d5a4add1e 100644 --- a/Nynja/Modules/GroupStorage/View/GroupStorageViewController.swift +++ b/Nynja/Modules/GroupStorage/View/GroupStorageViewController.swift @@ -73,15 +73,20 @@ class GroupStorageViewController: BaseVC, GroupStorageViewProtocol, ItemSelector selector.delegate = self selector.items = [SelectorItemModel(title: Strings.photosFilter.localized.uppercased(), - associated: GroupStorageFilterOption.images), + associated: GroupStorageFilterOption.images, + identifier: "filter_item_image"), SelectorItemModel(title: Strings.videosFilter.localized.uppercased(), - associated: GroupStorageFilterOption.videos), + associated: GroupStorageFilterOption.videos, + identifier: "filter_item_video"), SelectorItemModel(title: Strings.filesFilter.localized.uppercased(), - associated: GroupStorageFilterOption.files), + associated: GroupStorageFilterOption.files, + identifier: "filter_item_file"), SelectorItemModel(title: Strings.linksFilter.localized.uppercased(), - associated: GroupStorageFilterOption.links), + associated: GroupStorageFilterOption.links, + identifier: "filter_item_link"), SelectorItemModel(title: Strings.audiosFilter.localized.uppercased(), - associated: GroupStorageFilterOption.audios), + associated: GroupStorageFilterOption.audios, + identifier: "filter_item_audio"), ] return selector diff --git a/Nynja/Modules/GroupStorage/WireFrame/GroupStorageWireframe.swift b/Nynja/Modules/GroupStorage/WireFrame/GroupStorageWireframe.swift index 46ac2e96e..f6b69ef67 100644 --- a/Nynja/Modules/GroupStorage/WireFrame/GroupStorageWireframe.swift +++ b/Nynja/Modules/GroupStorage/WireFrame/GroupStorageWireframe.swift @@ -13,12 +13,12 @@ class GroupStorageWireFrame: GroupStorageWireFrameProtocol { var main:MainWireFrame? weak var navigation : UINavigationController? - func presentGroupStorage(navigation: UINavigationController, room: Room?, contact: Contact? = nil, main: MainWireFrame?) { + func presentGroupStorage(navigation: UINavigationController, chatModel: ChatModel, main: MainWireFrame?) { self.main = main let view = GroupStorageViewController() let presenter = GroupStoragePresenter() - let interactor = GroupStorageInteractor(room: room, contact: contact) + let interactor = GroupStorageInteractor(chatModel: chatModel) self.navigation = navigation diff --git a/Nynja/Modules/GroupsList/GroupsListProtocols.swift b/Nynja/Modules/GroupsList/GroupsListProtocols.swift index cc451ff9a..febeb35ca 100644 --- a/Nynja/Modules/GroupsList/GroupsListProtocols.swift +++ b/Nynja/Modules/GroupsList/GroupsListProtocols.swift @@ -17,7 +17,6 @@ protocol GroupsListWireFrameProtocol: class { */ func showGroup(_ group: Room) -// func updateMenu() } protocol GroupsListViewProtocol: class { @@ -28,7 +27,7 @@ protocol GroupsListViewProtocol: class { * Add here your methods for communication PRESENTER -> VIEW */ - func setup(groups: [Room]) + func setup(with state: CollectionState<[Room]>, displayMode: CollectionDisplayMode) } protocol GroupsListPresenterProtocol: BasePresenterProtocol { @@ -52,7 +51,8 @@ protocol GroupsListInteractorOutputProtocol: class { /** * Add here your methods for communication INTERACTOR -> PRESENTER */ - func getGroupsListSuccess(groups: [Room]) + func didFetch(groups: [Room]) + func didFilter(groups: [Room]) } protocol GroupsListInteractorInputProtocol: BaseInteractorProtocol { diff --git a/Nynja/Modules/GroupsList/Interactor/GroupsListInteractor.swift b/Nynja/Modules/GroupsList/Interactor/GroupsListInteractor.swift index 5e0fcb510..e52d77196 100644 --- a/Nynja/Modules/GroupsList/Interactor/GroupsListInteractor.swift +++ b/Nynja/Modules/GroupsList/Interactor/GroupsListInteractor.swift @@ -26,7 +26,7 @@ class GroupsListInteractor: BaseInteractor, GroupsListInteractorInputProtocol { override func loadData() { super.loadData() - fetchRooms() + fetchGroups() } @@ -37,21 +37,28 @@ class GroupsListInteractor: BaseInteractor, GroupsListInteractorInputProtocol { self.conversationsProvider = conversationsProvider } - private func fetchRooms(_ filter: String? = nil) { - var rooms = conversationsProvider.fetchGroups() - - if let filterText = filter, !filterText.isEmpty { - rooms = rooms.filter { room in - let name = room.name ?? "" - return filterText.isIn(string: name, options: .caseInsensitive) - } - } - - presenter.getGroupsListSuccess(groups: rooms) + private func fetchGroups(_ filter: String? = nil) { + let groups = conversationsProvider.fetchGroups() + presenter.didFetch(groups: groups) } func filter(with text: String) { - self.fetchRooms(text) + let text = text.trimmed() + + if !text.isEmpty { + var groups = conversationsProvider.fetchGroups() + + groups = groups.filter { group in + guard let name = group.name else { + return false + } + return text.isIn(string: name, options: .caseInsensitive) + } + + presenter.didFilter(groups: groups) + } else { + fetchGroups() + } } @@ -59,7 +66,8 @@ class GroupsListInteractor: BaseInteractor, GroupsListInteractorInputProtocol { override func update(with changes: [StorageChange], type: SubscribeType) { if case .room = type { - fetchRooms() + fetchGroups() } } + } diff --git a/Nynja/Modules/GroupsList/Presenter/GroupsListPresenter.swift b/Nynja/Modules/GroupsList/Presenter/GroupsListPresenter.swift index 849c72c03..02a7b5e24 100644 --- a/Nynja/Modules/GroupsList/Presenter/GroupsListPresenter.swift +++ b/Nynja/Modules/GroupsList/Presenter/GroupsListPresenter.swift @@ -46,7 +46,35 @@ class GroupsListPresenter: BasePresenter, GroupsListPresenterProtocol, GroupsLis // MARK: - GroupsListInteractorOutputProtocol - func getGroupsListSuccess(groups: [Room]) { - view.setup(groups: groups) + func didFetch(groups: [Room]) { + updateCollectionState(for: .default, groups: groups) } + + func didFilter(groups: [Room]) { + updateCollectionState(for: .search, groups: groups) + } + + + // MARK: - Private + + private func updateCollectionState(for displayMode: CollectionDisplayMode, groups: [Room]) { + let state = makeCollectionState(for: displayMode, groups: groups) + view.setup(with: state, displayMode: displayMode) + } + + private func makeCollectionState(for displayMode: CollectionDisplayMode, + groups: [Room]) -> CollectionState<[Room]> { + + if groups.isEmpty { + guard displayMode == .search else { + return .empty(nil) + } + + let emptyStateViewModel = EmptyStateViewModel(image: #imageLiteral(resourceName: "ic_search_empty"), descriptionText: "no_search_result".localized) + return .empty(emptyStateViewModel) + } else { + return .filled(data: groups) + } + } + } diff --git a/Nynja/Modules/GroupsList/View/GroupsListViewController.swift b/Nynja/Modules/GroupsList/View/GroupsListViewController.swift index 90dbb0d9f..0a64523a3 100644 --- a/Nynja/Modules/GroupsList/View/GroupsListViewController.swift +++ b/Nynja/Modules/GroupsList/View/GroupsListViewController.swift @@ -16,44 +16,39 @@ class GroupsListViewController: BaseVC, GroupsListViewProtocol, BackSwipable { } } - private lazy var dataSource = GroupsListTableDS(payloadParser: MessagePayloadParser(), delegate: self) + override var activatedViews: [UIView] { + return [controlContainerView] + } + + lazy var swipeBackHelper: SwipeBackHelper = { + return SwipeBackHelper(with: self) + }() + + private var emptyStateDS: EmptyStateTableViewDS! + private var dataSource: GroupsListTableDS! + // MARK: - Views + lazy var tableView: UITableView = { - let tv = UITableView(frame: CGRect.zero) + let tv = UITableView.default - tv.backgroundColor = UIColor.clear tv.clipsToBounds = true - tv.dataSource = self.dataSource - tv.delegate = self - tv.separatorStyle = .none tv.keyboardDismissMode = .interactive - tv.register(viewModel: ChatListMessageCellModel.self) - tv.rowHeight = ChatListMessageCellModel.Cell.Constraints.height - tv.tableFooterView = UIView() - self.view.addSubview(tv) + self.view.addSubview(tv) tv.snp.makeConstraints({ (make) in make.left.right.equalToSuperview() self.adjustVerticalInset(.top, make: make, offset: NavigationView.calculatedHeight) }) + return tv }() - lazy var swipeBackHelper: SwipeBackHelper = { - return SwipeBackHelper(with: self) - }() - - // MARK: - View lifecycle - override func viewWillAppear(_ animated: Bool) { - super.viewWillAppear(animated) - presenter.viewWillAppear() - } - private lazy var controlContainerView: NynjaControlContainerView = { let bottomOffset = Constraints.controlContainerView.bottomOffset let containerView = NynjaControlContainerView(contentView: searchField) - + view.addSubview(containerView) containerView.snp.makeConstraints { maker in maker.left.right.equalToSuperview() @@ -72,30 +67,65 @@ class GroupsListViewController: BaseVC, GroupsListViewProtocol, BackSwipable { let text = searchQuery ?? "" self?.presenter.filter(with: text) } - searchField.returnHandler = { [weak self] in - self?.view.endEditing(true) - } return searchField }() + // MARK: - View lifecycle + + override func viewWillAppear(_ animated: Bool) { + super.viewWillAppear(animated) + presenter.viewWillAppear() + } + + // MARK: - BaseVC + override func initialize() { super.initialize() - self.navigationView.isSeparatorVisible = true + setupUI() + } + + + // MARK: - UI Setup + + private func setupUI() { self.swipeBackHelper.addGesture() + + self.navigationView.isSeparatorVisible = true screenTitle = Strings.groupsListTitle.localized.uppercased() - tableView.isHidden = false - self.controlContainerView.isHidden = false + + setupTableView() + } + + private func setupTableView() { + tableView.register(viewModel: ChatListMessageCellModel.self) + tableView.rowHeight = ChatListMessageCellModel.Cell.Constraints.height + + dataSource = GroupsListTableDS(payloadParser: MessagePayloadParser(), delegate: self) + emptyStateDS = EmptyStateTableViewDS(dataSource: dataSource) + tableView.dataSource = emptyStateDS + + tableView.delegate = self } - // MARK: - Setup screen - func setup(groups: [Room]) { + func setup(with state: CollectionState<[Room]>, displayMode: CollectionDisplayMode) { + var groups: [Room] = [] + + switch state { + case let .empty(viewModel): + emptyStateDS.emptyStateViewModel = viewModel + case let .filled(data: data): + groups = data + } + dataSource.rooms = groups tableView.reloadData() } + // MARK: - Strings + enum Strings: String { case groupsListTitle = "groups_list_title" @@ -103,9 +133,12 @@ class GroupsListViewController: BaseVC, GroupsListViewProtocol, BackSwipable { return self.rawValue.localized } } + } + // MARK: - UITableViewDelegate + extension GroupsListViewController: UITableViewDelegate { func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { @@ -115,7 +148,9 @@ extension GroupsListViewController: UITableViewDelegate { } + // MARK: - ChatListMessageCellModelDelegate + extension GroupsListViewController: ChatListMessageCellModelDelegate { func chatListCellModel(_ cellModel: ChatListMessageCellModel, senderNameForModel model: DialogCellModel) -> String? { @@ -127,6 +162,9 @@ extension GroupsListViewController: ChatListMessageCellModelDelegate { } + +// MARK: - Layout + extension GroupsListViewController { struct Constraints { struct controlContainerView { diff --git a/Nynja/Modules/History/View/HistoryCell.swift.orig b/Nynja/Modules/History/View/HistoryCell.swift.orig deleted file mode 100644 index 0a9349b38..000000000 --- a/Nynja/Modules/History/View/HistoryCell.swift.orig +++ /dev/null @@ -1,233 +0,0 @@ -// -// HistoryCell.swift -// Nynja -// -// Created by Bohdan Paliychuk on 7/18/17. -// Copyright © 2017 TecSynt Solutions. All rights reserved. -// - -import UIKit -import libPhoneNumber_iOS - -protocol HistoryCellDelegate: class { - func acceptTupped(index: Int) - func tapped(index: Int) -} - -class HistoryCell: UITableViewCell { - - weak var delegat : HistoryCellDelegate? - var index = 0 - - // MARK: - Views - - private lazy var backgroundImageView: UIImageView = { - let scwidth = UIScreen.main.bounds.width - let imageView = UIImageView() - self.addSubview(imageView) - let lpadding = scwidth*0.045 - let top = scwidth*0.016 - imageView.image = #imageLiteral(resourceName: "history_background") - imageView.snp.makeConstraints({ (make) in - make.left.equalTo(self).offset(lpadding) - make.right.equalTo(self).offset(-lpadding) - make.top.equalTo(self).offset(top) - make.bottom.equalTo(self).offset(top) - }) - - return imageView - }() - - private lazy var userPhotoImageView: UIImageView = { - let scwidth = UIScreen.main.bounds.width - let imageWidth = scwidth*0.1 - let imageView = UIImageView(frame: CGRect(x: 0, y: 0, width: imageWidth, height: imageWidth)) - imageView.contentMode = .scaleAspectFill - self.addSubview(imageView) - let lpadding = scwidth*0.026 - imageView.roundImageView(borderWidth: 0, borderColor: Constants.colors.black.getColor()) - - imageView.snp.makeConstraints({ (make) in - make.left.equalTo(self.backgroundImageView.snp.left).offset(lpadding) - make.centerY.equalTo(self.backgroundImageView.snp.centerY) - make.width.equalTo(imageView.frame.width) - make.height.equalTo(imageView.snp.width).multipliedBy(1.0) - }) - - return imageView - }() - - private lazy var nameLabel: UILabel = { - let lbl = UILabel() - lbl.textAlignment = .left - lbl.font = UIFont(name: Constants.fonts.bold, size: 12) - lbl.textColor = Constants.colors.white.getColor() - lbl.numberOfLines = 1 - lbl.baselineAdjustment = .alignCenters - - let scwidth = UIScreen.main.bounds.width - let leadding = scwidth * 0.02 - let fontWidth = scwidth * 0.22 - let fontHeight = scwidth * 0.023 - let topOffcet = scwidth * 0.003 - - self.addSubview(lbl) - lbl.snp.makeConstraints({ (make) in - make.top.equalTo(self.userPhotoImageView.snp.top).offset(topOffcet) - make.left.equalTo(self.userPhotoImageView.snp.right).offset(leadding) - }) - return lbl - }() - - private lazy var phoneLabel: UILabel = { - let lbl = UILabel() - lbl.textAlignment = .left - lbl.font = UIFont(name: Constants.fonts.medium, size: 13) - lbl.textColor = Constants.colors.white.getColor() - lbl.numberOfLines = 1 - lbl.baselineAdjustment = .alignCenters - - let scwidth = UIScreen.main.bounds.width - let leadding = scwidth * 0.02 - let fontWidth = scwidth * 0.26 - let fontHeight = scwidth * 0.023 - let topOffcet = scwidth*0.022 - - self.addSubview(lbl) - lbl.snp.makeConstraints({ (make) in - make.bottom.equalTo(self.snp.bottom).offset(-topOffcet) - make.left.equalTo(self.userPhotoImageView.snp.right).offset(leadding) - }) - - return lbl - }() - - private lazy var addButton: UIButton = { - let btn = UIButton() - btn.titleLabel?.font = UIFont(name: Constants.fonts.medium, size: 11) - btn.titleLabel?.adjustsFontSizeToFitWidth = true - btn.titleLabel?.baselineAdjustment = .alignCenters - btn.titleLabel?.lineBreakMode = .byClipping - btn.layer.cornerRadius = 8 - btn.layer.masksToBounds = true - btn.isHidden = true - btn.addTarget(self, action: #selector(self.addTapped), for: .touchUpInside) - - let scwidth = UIScreen.main.bounds.width - let tralling = scwidth * 0.08 - let width = scwidth * 0.21 - let height = scwidth * 0.078 - let leading = 16.0.adjustedByWidth - - self.addSubview(btn) - btn.snp.makeConstraints({ (make) in - make.left.equalTo(self.nameLabel.snp.right).offset(leading) - make.left.equalTo(self.phoneLabel.snp.right).offset(leading) - - make.right.equalTo(self).offset(-tralling) - make.centerY.equalTo(self.backgroundImageView.snp.centerY) - make.width.equalTo(width) - make.height.equalTo(height) - }) - - return btn - }() - - private lazy var rightLabel: UILabel = { - let lbl = UILabel() - lbl.textAlignment = .right - lbl.font = UIFont(name: Constants.fonts.medium, size: 11) - lbl.textColor = Constants.colors.red.getColor() - lbl.numberOfLines = 1 - lbl.adjustsFontSizeToFitWidth = true - lbl.baselineAdjustment = .alignCenters - lbl.lineBreakMode = .byClipping - - let scwidth = UIScreen.main.bounds.width - let leadding = scwidth*0.109 - let fontWidth = scwidth * 0.26 - let fontHeight = scwidth * 0.023 - - self.addSubview(lbl) - lbl.isHidden = true - lbl.snp.makeConstraints({ (make) in - make.center.equalTo(self.addButton) - }) - - return lbl - }() - - - // MARK: - Init - - override init(style: UITableViewCellStyle, reuseIdentifier: String?) { - super.init(style: style, reuseIdentifier: reuseIdentifier) - baseSetup() - } - - required init?(coder aDecoder: NSCoder) { - super.init(coder: aDecoder) - baseSetup() - } - - - // MARK: - Setup - - func setup(model: Contact) { - self.userPhotoImageView.setImage(url: model.avatarUrl, placeHolder: UIImage(named: "ava_placeholder")) - - nameLabel.text = model.fullName - phoneLabel.text = model.plusPhoneNumber?.stringAsPhone() -<<<<<<< HEAD - setupAddButton(status: model.originalStatus) - self.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.tapped))) -======= - setupAddButton(status: model.contactStatus) ->>>>>>> developer - } - - private func baseSetup() { - backgroundImageView.isHidden = false - self.backgroundColor = .clear - self.selectionStyle = .none - self.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.tapped))) - } - -<<<<<<< HEAD - func setupAddButton(status: Contact.Status?) { -======= - private func setupAddButton(status: ContactStatus?) { ->>>>>>> developer - guard let status = status else { return } - - switch status { - case .friend, .ban, .banned: - rightLabel.isHidden = false - rightLabel.text = "contact_added".localized - rightLabel.textColor = Constants.colors.darkGray.getColor() - addButton.isHidden = true - case .request: - rightLabel.isHidden = false - rightLabel.text = "contact_requested".localized - rightLabel.textColor = Constants.colors.red.getColor() - addButton.isHidden = true - case .authorization: - addButton.isHidden = false - addButton.backgroundColor = Constants.colors.red.getColor() - addButton.isUserInteractionEnabled = true - addButton.setTitle("contact_accept".localized, for: .normal) - break - } - } - - // MARK: - Actions - - @objc private func tapped() { - self.delegat?.tapped(index: self.index) - } - - @objc private func addTapped() { - self.delegat?.acceptTupped(index: self.index) - } - -} diff --git a/Nynja/Modules/History/View/HistoryViewController.swift b/Nynja/Modules/History/View/HistoryViewController.swift index 2ac46f127..8c2b2e611 100644 --- a/Nynja/Modules/History/View/HistoryViewController.swift +++ b/Nynja/Modules/History/View/HistoryViewController.swift @@ -70,9 +70,6 @@ class HistoryViewController: BaseVC, HistoryViewProtocol, HistoryTableDSDelegate let text = searchQuery ?? "" self?.presenter.filter(with: text) } - searchField.returnHandler = { [weak self] in - self?.view.endEditing(true) - } return searchField }() diff --git a/Nynja/Modules/ImagePreview/View/ImagePreviewViewController.swift b/Nynja/Modules/ImagePreview/View/ImagePreviewViewController.swift index 3d0d07086..1a80535b9 100644 --- a/Nynja/Modules/ImagePreview/View/ImagePreviewViewController.swift +++ b/Nynja/Modules/ImagePreview/View/ImagePreviewViewController.swift @@ -23,8 +23,18 @@ UIViewControllerTransitioningDelegate, SetInjectable { private var maxSize = CGSize.zero private var initialScale: CGFloat = 0 + + private enum ScaleModificator { + static let min: CGFloat = 1 + static let middle: CGFloat = 1.75 + static let max: CGFloat = 3 + + static let allValues: [CGFloat] = [ScaleModificator.min, ScaleModificator.middle, ScaleModificator.max] + } private var panGesture: UIPanGestureRecognizer? + + private var doubleTapGesture: UITapGestureRecognizer? fileprivate var isForcingNonInteractiveDismissal = false @@ -97,6 +107,11 @@ UIViewControllerTransitioningDelegate, SetInjectable { let pan = UIPanGestureRecognizer(target: self, action: #selector(handlePanGesture(_:))) panGesture = pan view.addGestureRecognizer(pan) + + let doubleTap = UITapGestureRecognizer(target: self, action: #selector(handleDoubleTapGesture(_:))) + doubleTap.numberOfTapsRequired = 2 + doubleTapGesture = doubleTap + view.addGestureRecognizer(doubleTap) transitioningDelegate = self } @@ -113,6 +128,7 @@ UIViewControllerTransitioningDelegate, SetInjectable { if canceled { self.isForcingNonInteractiveDismissal = false self.panGesture?.isEnabled = true + self.doubleTapGesture?.isEnabled = true } completion?() @@ -151,6 +167,15 @@ extension ImagePreviewViewController { transitionController?.handlePanGesture(panGesture) } + + @objc func handleDoubleTapGesture(_ doubleTapGesture: UITapGestureRecognizer) { + let nextScale = ScaleModificator.allValues + .map { $0 * initialScale } + .filter { $0 > scrollView.zoomScale } + .first + + scrollView.setZoomScale(nextScale ?? initialScale, animated: true) + } } // MARK: - UIScrollViewDelegate @@ -255,6 +280,9 @@ extension ImagePreviewViewController: ImagePreviewTransitionHostVCProtocol { private extension ImagePreviewViewController { func newInset(scrollView: UIScrollView) -> UIEdgeInsets { + if #available(iOS 12.0, *) { + return UIEdgeInsets(top: 0, left: 0, bottom: 0, right: 0) + } var yOffset = imageView.frame.origin.y * scrollView.zoomScale var xOffset = imageView.frame.origin.x * scrollView.zoomScale @@ -298,7 +326,7 @@ private extension ImagePreviewViewController { scrollView.contentSize = imageView.bounds.size scrollView.minimumZoomScale = minScale scrollView.zoomScale = minScale - scrollView.maximumZoomScale = minScale * 3 + scrollView.maximumZoomScale = minScale * ScaleModificator.max view.layoutIfNeeded() diff --git a/Nynja/Modules/Interpretation/View/AlertTextFieldViewController/AlertTextFieldViewController.swift b/Nynja/Modules/Interpretation/View/AlertTextFieldViewController/AlertTextFieldViewController.swift index b374e1ff4..892159616 100644 --- a/Nynja/Modules/Interpretation/View/AlertTextFieldViewController/AlertTextFieldViewController.swift +++ b/Nynja/Modules/Interpretation/View/AlertTextFieldViewController/AlertTextFieldViewController.swift @@ -63,6 +63,7 @@ class AlertTextFieldViewController: BaseVC { lazy var inputField: MaterialTextField = { let field = MaterialTextField() field.keyboardType = .numberPad + field.prohibitedOptions = .all self.containerView.addSubview(field) field.snp.makeConstraints { (make) in diff --git a/Nynja/Modules/Interpretation/View/AlertTextFieldViewController/AlertTextFieldViewControllerLayout.swift b/Nynja/Modules/Interpretation/View/AlertTextFieldViewController/AlertTextFieldViewControllerLayout.swift index 22b7088dd..893c342d8 100644 --- a/Nynja/Modules/Interpretation/View/AlertTextFieldViewController/AlertTextFieldViewControllerLayout.swift +++ b/Nynja/Modules/Interpretation/View/AlertTextFieldViewController/AlertTextFieldViewControllerLayout.swift @@ -44,18 +44,18 @@ extension AlertTextFieldViewController { struct Texts { struct dafaultWhite { static let height = 20.adjustedByWidth - static let font = UIFont(fontName: Constants.fonts.medium, height: CGFloat(height)) + static let font = UIFont.makeFont(with: Constants.fonts.medium, height: CGFloat(height)) static let color = Constants.colors.white.getColor() } struct titleLabel { static let height = 17.adjustedByWidth - static let font = UIFont(fontName: Constants.fonts.regular, height: CGFloat(height)) + static let font = UIFont.makeFont(with: Constants.fonts.regular, height: CGFloat(height)) static let color = Constants.colors.darkGray.getColor() } struct buttons { static let titleHeight = 22.adjustedByWidth - static let font = UIFont(fontName: Constants.fonts.medium, height: CGFloat(titleHeight)) + static let font = UIFont.makeFont(with: Constants.fonts.medium, height: CGFloat(titleHeight)) static let color = Constants.colors.red.getColor() } } diff --git a/Nynja/Modules/Interpretation/View/InterpretationLayout.swift b/Nynja/Modules/Interpretation/View/InterpretationLayout.swift index 7aa7cdbbf..8a4317924 100644 --- a/Nynja/Modules/Interpretation/View/InterpretationLayout.swift +++ b/Nynja/Modules/Interpretation/View/InterpretationLayout.swift @@ -14,12 +14,12 @@ extension InterpretationViewController { struct Text { struct defaultWhite { static let height = CGFloat(22.adjustedByWidth) - static let font = UIFont(fontName: Constants.fonts.medium, height: height) + static let font = UIFont.makeFont(with: Constants.fonts.medium, height: height) static let fontColor = Constants.colors.white.getColor() } struct defaultGray { static let height = CGFloat(20.adjustedByWidth) - static let font = UIFont(fontName: Constants.fonts.regular, height: height) + static let font = UIFont.makeFont(with: Constants.fonts.regular, height: height) static let fontColor = Constants.colors.gray.getColor() } } diff --git a/Nynja/Modules/Interpretation/View/LanguagePickerDelegate.swift b/Nynja/Modules/Interpretation/View/LanguagePickerDelegate.swift index 5fd82bb47..4546a55c6 100644 --- a/Nynja/Modules/Interpretation/View/LanguagePickerDelegate.swift +++ b/Nynja/Modules/Interpretation/View/LanguagePickerDelegate.swift @@ -72,7 +72,7 @@ class LanguagePickerDelegate : NSObject, UIPickerViewDataSource, UIPickerViewDel struct Values { static let height = CGFloat(24.adjustedByWidth) - static let font = UIFont(fontName: Constants.fonts.medium, height: height) + static let font = UIFont.makeFont(with: Constants.fonts.medium, height: height) static let fontColor = Constants.colors.white.getColor() } } diff --git a/Nynja/Modules/InterpretationType/View/TableView/Cell/InterpretationTypeCellLayout.swift b/Nynja/Modules/InterpretationType/View/TableView/Cell/InterpretationTypeCellLayout.swift index 89c87015d..bd919cc42 100644 --- a/Nynja/Modules/InterpretationType/View/TableView/Cell/InterpretationTypeCellLayout.swift +++ b/Nynja/Modules/InterpretationType/View/TableView/Cell/InterpretationTypeCellLayout.swift @@ -46,19 +46,19 @@ extension InterpretationTypeCell { struct Text { struct defaultWhite { static let height = 22.adjustedByWidth - static let font = UIFont(fontName: Constants.fonts.medium, height: CGFloat(height)) + static let font = UIFont.makeFont(with: Constants.fonts.medium, height: CGFloat(height)) static let color = Constants.colors.white.getColor() } struct defaultGray { static let height = 20.adjustedByWidth - static let font = UIFont(fontName: Constants.fonts.regular, height: CGFloat(height)) + static let font = UIFont.makeFont(with: Constants.fonts.regular, height: CGFloat(height)) static let color = Constants.colors.gray.getColor() } struct defaultRed { static let height = 22.adjustedByWidth - static let font = UIFont(fontName: Constants.fonts.medium, height: CGFloat(height)) + static let font = UIFont.makeFont(with: Constants.fonts.medium, height: CGFloat(height)) static let color = Constants.colors.red.getColor() } } diff --git a/Nynja/Modules/LanguageSettings/LanguageSelector/View/LanguageSelectorViewController.swift b/Nynja/Modules/LanguageSettings/LanguageSelector/View/LanguageSelectorViewController.swift index b612069b7..f7d0169ea 100644 --- a/Nynja/Modules/LanguageSettings/LanguageSelector/View/LanguageSelectorViewController.swift +++ b/Nynja/Modules/LanguageSettings/LanguageSelector/View/LanguageSelectorViewController.swift @@ -67,10 +67,6 @@ final class LanguageSelectorViewController: BaseVC, LanguageSelectorViewProtocol private lazy var searchField: NynjaSearchField = { let searchField = NynjaSearchField() - searchField.textField.returnKeyType = .search - searchField.returnHandler = { [weak self] in - self?.view.endEditing(true) - } searchField.searchTextChangeHandler = { [weak self] searchQuery in self?.filterEdited(searchQuery) } diff --git a/Nynja/Modules/LanguageSettings/LanguageSettings/Presenter/ChatLanguageSettingsPresenter.swift b/Nynja/Modules/LanguageSettings/LanguageSettings/Presenter/ChatLanguageSettingsPresenter.swift index 3b3fdf9a3..793b5d618 100644 --- a/Nynja/Modules/LanguageSettings/LanguageSettings/Presenter/ChatLanguageSettingsPresenter.swift +++ b/Nynja/Modules/LanguageSettings/LanguageSettings/Presenter/ChatLanguageSettingsPresenter.swift @@ -127,10 +127,8 @@ class ChatLanguageSettingsPresenter: BasePresenter, ChatLanguageSettingsPresente return } - let isNeedOffAutotranslate = language.language == LanguageSettingConstants.languageNone - sendingLang?.title = language.name - if isNeedOffAutotranslate { + if language.isNone { sectionCoordinator.languageSwitch?.isOn = false } @@ -138,7 +136,7 @@ class ChatLanguageSettingsPresenter: BasePresenter, ChatLanguageSettingsPresente self.sendingLang = selectedLanguage - if isNeedOffAutotranslate { + if language.isNone { self.sendingLangIsOn = false self.interactor.updateAutotranslateSending(false) } @@ -166,7 +164,8 @@ class ChatLanguageSettingsPresenter: BasePresenter, ChatLanguageSettingsPresente let input = LanguageSelector.Input(selectorType: .sending, selectedLang: currentLang, selectorLang: { [weak self] selected in - guard case let .lang(lang) = selected else { + guard case .lang(let language) = selected, + !language.isNone else { switchSendingLang?.isOn = false self?.view.reloadData() self?.sendingLangIsOn = false @@ -175,7 +174,7 @@ class ChatLanguageSettingsPresenter: BasePresenter, ChatLanguageSettingsPresente let isOn = true - sectionCoordinator.languagePicker?.title = lang.name + sectionCoordinator.languagePicker?.title = language.name switchSendingLang?.isOn = isOn self?.view.reloadData() @@ -183,7 +182,7 @@ class ChatLanguageSettingsPresenter: BasePresenter, ChatLanguageSettingsPresente self?.sendingLang = selected self?.sendingLangIsOn = isOn - self?.interactor.updateSendingLanguage(lang) + self?.interactor.updateSendingLanguage(language) self?.interactor.updateAutotranslateSending(isOn) }, saveLangHandler: nil, @@ -193,7 +192,9 @@ class ChatLanguageSettingsPresenter: BasePresenter, ChatLanguageSettingsPresente } let switchSendingAction: ()->Void = { [weak self, weak switchSendingLang] in - guard let currentLang = self?.sendingLang, case .lang(_) = currentLang else { + guard let currentLang = self?.sendingLang, + case .lang(let language) = currentLang, + !language.isNone else { forceSendingLangAction() return } diff --git a/Nynja/Modules/LogOutput/Interactor/LogOutputInteractor.swift b/Nynja/Modules/LogOutput/Interactor/LogOutputInteractor.swift new file mode 100644 index 000000000..3505903f4 --- /dev/null +++ b/Nynja/Modules/LogOutput/Interactor/LogOutputInteractor.swift @@ -0,0 +1,39 @@ +// +// LogOutputInteractor.swift +// Nynja +// +// Created by Anton M on 17.08.2018. +// Copyright © 2018 TecSynt Solutions. All rights reserved. +// + +import Foundation + +class LogOutputInteractor: LogOutputInteractorInputProtocol { + weak var presenter: LogOutputInteractorOutputProtocol! + + + func getData() { + if let listFiles = FileManagerService.sharedInstance.listFilesFrom("Logs") { + var result = [LogOutputDS.Files]() + for i in listFiles { + if i.contains(".log") { + result.append(LogOutputDS.Files(title: i, isSelected: false)) + } + } + result.sort { (left, right) -> Bool in + return left.title > right.title + } + self.presenter.getDataSuccess(result: result) + } + } + + func share(_ files: [LogOutputDS.Files]) { + var urls = [URL]() + for i in files { + let path = FileManagerService.sharedInstance.pathFor(folder: "Logs", name: i.title) + let url = URL(fileURLWithPath: path) + urls.append(url) + } + self.presenter.linksSuccess(result: urls) + } +} diff --git a/Nynja/Modules/LogOutput/LogOutputProtocols.swift b/Nynja/Modules/LogOutput/LogOutputProtocols.swift new file mode 100644 index 000000000..e5b569c97 --- /dev/null +++ b/Nynja/Modules/LogOutput/LogOutputProtocols.swift @@ -0,0 +1,63 @@ +// +// LogOutputProtocols.swift +// Nynja +// +// Created by Anton M on 17.08.2018. +// Copyright © 2018 TecSynt Solutions. All rights reserved. +// + +import Foundation + +protocol LogOutputWireFrameProtocol: class { + + func presentLogOutputView(navigation: UINavigationController) + + /** + * Add here your methods for communication PRESENTER -> WIREFRAME + */ + func nativeShare(_ urls: [URL]) +} + +protocol LogOutputViewProtocol: class { + + var presenter: LogOutputPresenterProtocol! { get set } + /** + * Add here your methods for communication PRESENTER -> VIEW + */ + func setup(files: [LogOutputDS.Files]) +} + +protocol LogOutputPresenterProtocol: class { + + var view: LogOutputViewProtocol! { get set } + var interactor: LogOutputInteractorInputProtocol! { get set } + var wireFrame: LogOutputWireFrameProtocol! { get set } + + func showed() + func share(_ files: [LogOutputDS.Files]) + /** + * Add here your methods for communication VIEW -> PRESENTER + */ +} + +protocol LogOutputInteractorOutputProtocol: class { + + /** + * Add here your methods for communication INTERACTOR -> PRESENTER + */ + + func getDataSuccess(result: [LogOutputDS.Files]) + func linksSuccess(result: [URL]) +} + +protocol LogOutputInteractorInputProtocol: class { + + var presenter: LogOutputInteractorOutputProtocol! { get set } + + + func getData() + func share(_ files: [LogOutputDS.Files]) + /** + * Add here your methods for communication PRESENTER -> INTERACTOR + */ +} diff --git a/Nynja/Modules/LogOutput/Presenter/LogOutputPresenter.swift b/Nynja/Modules/LogOutput/Presenter/LogOutputPresenter.swift new file mode 100644 index 000000000..65128355b --- /dev/null +++ b/Nynja/Modules/LogOutput/Presenter/LogOutputPresenter.swift @@ -0,0 +1,33 @@ +// +// LogOutputPresenter.swift +// Nynja +// +// Created by Anton M on 17.08.2018. +// Copyright © 2018 TecSynt Solutions. All rights reserved. +// + +import Foundation + +class LogOutputPresenter: LogOutputPresenterProtocol, LogOutputInteractorOutputProtocol { + weak var view: LogOutputViewProtocol! + + var interactor: LogOutputInteractorInputProtocol! + + var wireFrame: LogOutputWireFrameProtocol! + + func showed() { + self.interactor.getData() + } + + func getDataSuccess(result: [LogOutputDS.Files]) { + self.view.setup(files: result) + } + + func share(_ files: [LogOutputDS.Files]) { + self.interactor.share(files) + } + + func linksSuccess(result: [URL]) { + self.wireFrame.nativeShare(result) + } +} diff --git a/Nynja/Modules/LogOutput/View/LogOutputDS.swift b/Nynja/Modules/LogOutput/View/LogOutputDS.swift new file mode 100644 index 000000000..11d32ee3d --- /dev/null +++ b/Nynja/Modules/LogOutput/View/LogOutputDS.swift @@ -0,0 +1,36 @@ +// +// LogOutputDS.swift +// Nynja +// +// Created by Anton M on 19.08.2018. +// Copyright © 2018 TecSynt Solutions. All rights reserved. +// + +import UIKit + + + +class LogOutputDS: NSObject, UITableViewDataSource { + + var files: [Files] = [] + + func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { + return files.count + } + + func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { + if let cell = tableView.dequeueReusableCell(withIdentifier: "cell") as? LogOutputCell { + let model = files[indexPath.row] + cell.setup(model) + cell.selectionStyle = .none + return cell + } + return UITableViewCell() + } + + + struct Files { + var title: String + var isSelected: Bool + } +} diff --git a/Nynja/Modules/LogOutput/View/LogOutputView.swift b/Nynja/Modules/LogOutput/View/LogOutputView.swift new file mode 100644 index 000000000..9cc7567d2 --- /dev/null +++ b/Nynja/Modules/LogOutput/View/LogOutputView.swift @@ -0,0 +1,121 @@ +// +// LogOutputView.swift +// Nynja +// +// Created by Anton M on 17.08.2018. +// Copyright © 2018 TecSynt Solutions. All rights reserved. +// + +import Foundation + +class LogOutputViewController: UIViewController, LogOutputViewProtocol, UITableViewDelegate { + var presenter: LogOutputPresenterProtocol! + + private var datasource = LogOutputDS() + + private lazy var back: UIButton = { + let btn = UIButton() + btn.setTitle("Close", for: .normal) + btn.backgroundColor = UIColor.red + btn.addTarget(self, action: #selector(self.close), for: .touchUpInside) + self.view.addSubview(btn) + + btn.snp.makeConstraints({ (make) in + make.bottom.right.equalTo(self.view).offset(-44) + make.width.equalTo(100) + make.height.equalTo(44) + }) + return btn + }() + + private lazy var share: UIButton = { + let btn = UIButton() + btn.setTitle("Share", for: .normal) + btn.backgroundColor = UIColor.green + btn.addTarget(self, action: #selector(self.shareTapped), for: .touchUpInside) + self.view.addSubview(btn) + + btn.snp.makeConstraints({ (make) in + make.bottom.equalTo(self.view).offset(-44) + make.left.equalTo(self.view).offset(20) + make.width.equalTo(100) + make.height.equalTo(44) + }) + return btn + }() + + private lazy var selectAll: UIButton = { + let btn = UIButton() + btn.setTitle("Select All", for: .normal) + btn.setTitle("Unselect All", for: .selected) + btn.backgroundColor = UIColor.green + btn.addTarget(self, action: #selector(self.selectAllTapped), for: .touchUpInside) + self.view.addSubview(btn) + + btn.snp.makeConstraints({ (make) in + make.bottom.equalTo(self.view).offset(-44) + make.left.equalTo(self.share.snp.right).offset(5) + make.right.greaterThanOrEqualTo(self.back.snp.left).offset(-10) + make.height.equalTo(44) + }) + return btn + }() + + private lazy var tableView: UITableView = { + let tv = UITableView(frame: CGRect.zero) + + tv.backgroundColor = UIColor.clear + tv.clipsToBounds = true + tv.delegate = self + tv.separatorStyle = .none + tv.keyboardDismissMode = .interactive + tv.register(LogOutputCell.self, forCellReuseIdentifier: "cell") + tv.rowHeight = ChatListMessageCellModel.Cell.Constraints.height + tv.tableFooterView = UIView() + self.view.addSubview(tv) + + tv.snp.makeConstraints({ (make) in + make.left.right.equalToSuperview() + make.top.equalToSuperview().offset(20) + make.bottom.equalTo(self.back.snp.top).offset(-5) + }) + return tv + }() + + @objc private func close() { + self.navigationController?.popViewController(animated: true) + } + + @objc private func shareTapped() { + let selected = self.datasource.files.filter { (file) -> Bool in + return file.isSelected + } + self.presenter.share(selected) + } + + @objc private func selectAllTapped() { + self.selectAll.isSelected = !self.selectAll.isSelected + for i in 0.. String? { + let dateFormatter = DateFormatter() + dateFormatter.dateFormat = "yyyy_MM_dd_HH_mm_ss" + guard let date = dateFormatter.date(from: str.replacingOccurrences(of: ".log", with: "")) else { + return nil + } + dateFormatter.dateFormat = "dd/MM/yyyy HH:mm:ss" + return dateFormatter.string(from: date) + } +} diff --git a/Nynja/Modules/LogOutput/Wireframe/LogOutputWireFrame.swift b/Nynja/Modules/LogOutput/Wireframe/LogOutputWireFrame.swift new file mode 100644 index 000000000..b51f2bbdb --- /dev/null +++ b/Nynja/Modules/LogOutput/Wireframe/LogOutputWireFrame.swift @@ -0,0 +1,37 @@ +// +// LogOutputWireFrame.swift +// Nynja +// +// Created by Anton M on 17.08.2018. +// Copyright © 2018 TecSynt Solutions. All rights reserved. +// + +import Foundation + +class LogOutputWireFrame: LogOutputWireFrameProtocol { + + weak var navigation : UINavigationController? + + func presentLogOutputView(navigation: UINavigationController) { + let view = LogOutputViewController() + let presenter = LogOutputPresenter() + let interactor = LogOutputInteractor() + + self.navigation = navigation + + // Connecting + view.presenter = presenter + presenter.view = view + presenter.wireFrame = self + presenter.interactor = interactor + interactor.presenter = presenter +// navigation.present(view, animated: true, completion: nil) + navigation.pushViewController(view, animated: true) + } + + func nativeShare(_ urls: [URL]) { + let res = urls.map() { $0 as NSURL } + let shareVC = UIActivityViewController(activityItems: res, applicationActivities: nil) + self.navigation?.present(shareVC, animated: true, completion: nil) + } +} diff --git a/Nynja/Modules/Main/Interactor/MainInteractor.swift b/Nynja/Modules/Main/Interactor/MainInteractor.swift index 0f24383c6..71868b0be 100644 --- a/Nynja/Modules/Main/Interactor/MainInteractor.swift +++ b/Nynja/Modules/Main/Interactor/MainInteractor.swift @@ -44,12 +44,20 @@ class MainInteractor: MainInteractorInputProtocol, EditPhotoDelegate, MQTTServic NynjaCommunicatorService.sharedInstance.dialInGroup(groupname: name) } - func createGroupCall(contacts: [Contact], room: Room?) { - NynjaCommunicatorService.sharedInstance.createConference(contacts: contacts, room: room) + func createGroupCall(callId: String?, contacts: [Contact], room: Room?) { + if let cid = callId { + NynjaCommunicatorService.sharedInstance.addAndStartConference(callId: cid, contacts: contacts, room: room) + } else { + NynjaCommunicatorService.sharedInstance.createConference(contacts: contacts, room: room) + } } - func createConferenceCall(contacts: [Contact], room: Room?) { - NynjaCommunicatorService.sharedInstance.createConference(contacts: contacts, room: room) + func createConferenceCall(callId: String?, contacts: [Contact], room: Room?) { + if let cid = callId { + NynjaCommunicatorService.sharedInstance.addAndStartConference(callId: cid, contacts: contacts, room: room) + } else { + NynjaCommunicatorService.sharedInstance.createConference(contacts: contacts, room: room) + } } func updateGroupCall(contacts: [Contact]) { @@ -61,6 +69,7 @@ class MainInteractor: MainInteractorInputProtocol, EditPhotoDelegate, MQTTServic func logout() { MQTTService.sharedInstance.logout() + LogService.log(topic: .db) { return "Clear storage: logout" } cleanServices() } @@ -68,6 +77,7 @@ class MainInteractor: MainInteractorInputProtocol, EditPhotoDelegate, MQTTServic if let phone = StorageService.sharedInstance.phone, phone != "" { MQTTService.sharedInstance.deleteUser(number: phone) } + LogService.log(topic: .db) { return "Clear storage: delete account" } cleanServices() } @@ -106,13 +116,19 @@ class MainInteractor: MainInteractorInputProtocol, EditPhotoDelegate, MQTTServic } // MARK: MQTT subscribing - func didConnect(_ mqttService: MQTTService) { - presenter.hideUILocker() + + + func mqttServiceDidConnect(_ mqttService: MQTTService) { + DispatchQueue.main.async { + self.presenter.hideUILocker() + } } - - func didDisconnect(_ mqttService: MQTTService) { - self.logout() - presenter.changeScreenToAuth() + + func mqttServiceDidReceiveAuthenticationFailure(_ mqttService: MQTTService) { + DispatchQueue.main.async { + self.logout() + self.presenter.changeScreenToAuth() + } } func saveLogoutState() { diff --git a/Nynja/Modules/Main/Interactor/MainInteractor.swift.orig b/Nynja/Modules/Main/Interactor/MainInteractor.swift.orig deleted file mode 100644 index b461751dd..000000000 --- a/Nynja/Modules/Main/Interactor/MainInteractor.swift.orig +++ /dev/null @@ -1,99 +0,0 @@ -// -// MainMainInteractor.swift -// Nynja -// -// Created by Bohdan Paliychuk on 19/07/2017. -// Copyright © 2017 TecSynt Solutions. All rights reserved. -// - -class MainInteractor: MainInteractorInputProtocol, VoxServiceDelegate, EditPhotoDelegate, MQTTServiceDelegate { - - weak var presenter: MainInteractorOutputProtocol! - - var contact: Contact? { - return ContactDAO.currentContact - } - - init() { - let notificationsSettings = UIUserNotificationSettings(types: [.sound, .alert, .badge], categories: nil) - UIApplication.shared.registerUserNotificationSettings(notificationsSettings) - UIApplication.shared.registerForRemoteNotifications() - let _ = PushService.sharedInstance - - MQTTService.sharedInstance.addSubscriber(self) - } - - func checkSession() { - MQTTService.sharedInstance.reconnect() - } - - func call(name: String) { - VoxService.sharedInstance.call(username: name, withVideo: false) - } - - func videoCall(name: String) { - VoxService.sharedInstance.call(username: name, withVideo: true) - } - - func logout() { - MQTTService.sharedInstance.logout() -<<<<<<< HEAD - StorageService.sharedInstance.clearStorage() -======= - StorageService.sharedInstance.clearDatabase() - StorageService.sharedInstance.dropToken() - StorageService.sharedInstance.dropProfileID() - - UserSettingsService.shared.reset() ->>>>>>> developer - } - - func setVideoView(view: UIView?) { - VoxService.sharedInstance.remoteView = view - } - - func deleteAccount() { - if let phone = StorageService.sharedInstance.phone, phone != "" { - MQTTService.sharedInstance.deleteUser(number: phone) - } - - VoxService.sharedInstance.voxClient.disconnect() - VoxService.sharedInstance.isCallInProgress = false - - StorageService.sharedInstance.clearStorage() - } - - func photoEditFinished(url: URL) { - updateAvatar(url: url) - } - - func updateAvatar(url: URL) { - guard let roster = RosterDAO.currentRoster else { return } - - let sync = SyncFileManager.sharedInstance - sync.downloader = AmazonManager.shared - - roster.avatar = url.absoluteString.getShortPath() - do { - try StorageService.sharedInstance.perform(action: .updateColumns([RosterTable.Column.avatar.title]), with: roster) - - SyncFileManager.sharedInstance.getExternalFileLink(localUrl: url.path) { (ext, progress,request) in - if ext != nil { - if let id = roster.id { - MQTTService.sharedInstance.updateAvatar(id: id, link: String(describing: ext!)) - } - } - } - } catch {} - } - - // MARK: MQTT subscribing - func didConnect(_ mqttService: MQTTService) { - presenter.hideUILocker() - } - - func didDisconnect(_ mqttService: MQTTService) { - presenter.logout() - } - -} diff --git a/Nynja/Modules/Main/MainProtocols.swift b/Nynja/Modules/Main/MainProtocols.swift index 17dc90102..ba545e5f6 100644 --- a/Nynja/Modules/Main/MainProtocols.swift +++ b/Nynja/Modules/Main/MainProtocols.swift @@ -40,7 +40,6 @@ protocol MainWireFrameProtocol: class { func sendMedia(_ media: Media) func sendLocation(with stringOfLocation: String) func sendLocation(type: LocationType) - func sendTyping(_ isTyping: Bool) func showMyContacts() func showContactsToShare() @@ -76,7 +75,7 @@ protocol MainWireFrameProtocol: class { func showNoInternetView() func hideNoInternetView() func hideReturnToCallView() - func returnToCall() + func returnToCall(call: NYNCall?) func showSplash() func showChat(_ chat: ChatModel, initialMessage: ChatInitialMessage?) func showImagePreview(imageUrl: URL) @@ -155,7 +154,6 @@ protocol MainPresenterProtocol: class { func sendAudio(withURL url: URL) func sendImage(_ image: UIImage) func sendVideo(with url: URL) - func sendTyping(_ isTyping: Bool) func showMyContacts() func showContactsToShare() @@ -193,6 +191,7 @@ protocol MainPresenterProtocol: class { func showAddParticipants() func showGroupsList() func showGroupsOptions() + func showMarketplace() func showMySelfChat() func getRecentsLocation() -> [LocationType] @@ -203,7 +202,7 @@ protocol MainPresenterProtocol: class { func voiceGroupCall() func videoCall() func videoGroupCall() - func returnToCall() + func returnToCall(call: NYNCall?) func viewShowed() } @@ -235,8 +234,8 @@ protocol MainInteractorInputProtocol: class { func updateAvatar(url: URL) func saveLogoutState() func dialInGroup(name: String) - func createGroupCall(contacts: [Contact], room: Room?) - func createConferenceCall(contacts: [Contact], room: Room?) + func createGroupCall(callId: String?, contacts: [Contact], room: Room?) + func createConferenceCall(callId: String?, contacts: [Contact], room: Room?) func updateGroupCall(contacts: [Contact]) func findContactBy(phoneId: String)->Contact? } diff --git a/Nynja/Modules/Main/Presenter/MainPresenter.swift b/Nynja/Modules/Main/Presenter/MainPresenter.swift index d47d42ab1..1a00ec086 100644 --- a/Nynja/Modules/Main/Presenter/MainPresenter.swift +++ b/Nynja/Modules/Main/Presenter/MainPresenter.swift @@ -8,8 +8,8 @@ class MainPresenter: MainPresenterProtocol, MainInteractorOutputProtocol, ScheduleMessageDelegate, EditParticipantsDelegate { - func returnToCall() { - self.wireFrame.returnToCall() + func returnToCall(call: NYNCall?) { + self.wireFrame.returnToCall(call: call) } weak var view: MainViewProtocol! @@ -43,6 +43,10 @@ class MainPresenter: MainPresenterProtocol, MainInteractorOutputProtocol, Schedu } } } + + func showMarketplace() { + self.wireFrame.openMarketplace() + } func voiceCall() { if let name = wireFrame.getContact() { @@ -63,8 +67,7 @@ class MainPresenter: MainPresenterProtocol, MainInteractorOutputProtocol, Schedu } func videoCall() { - self.wireFrame.showNotImplementedAlert() - //TODO: To enable video call performVideoCall() + performVideoCall(); } func videoGroupCall() { @@ -89,7 +92,7 @@ class MainPresenter: MainPresenterProtocol, MainInteractorOutputProtocol, Schedu func sendImage(_ image: UIImage) { guard let url = ResourceManager(permissionManager: PermissionManager()).savePhoto(image: image, setting: .highest) else { - LogService.log(topic: .fileSystem, text: "error save image") + LogService.log(topic: .fileSystem) { return "error save image" } return } @@ -112,10 +115,6 @@ class MainPresenter: MainPresenterProtocol, MainInteractorOutputProtocol, Schedu return wireFrame.getRecentsMedia() } - func sendTyping(_ isTyping: Bool) { - wireFrame.sendTyping(isTyping) - } - func showMyContacts() { wireFrame.showMyContacts() } @@ -145,19 +144,7 @@ class MainPresenter: MainPresenterProtocol, MainInteractorOutputProtocol, Schedu } func showMessages(contact: Contact, call: NYNCall, callVC: CallInProgressViewProtocol, isVideo: Bool) { - - if isVideo { - // if VoxService.sharedInstance.isRemoveVideoStream { -// TODO: ASK ANGEL -// view.showPartnerVideoViewWithPhotoURL(url: contact.avatarUrl) -// } else { -// let prevView = self.view.showPartnerVideoView() -// self.interactor.setVideoView(view: prevView) -// } - } else { - self.view.showReturnToCall(call: call) - } - + self.view.showReturnToCall(call: call) self.wireFrame.showMessages(contact: contact, callVC: callVC,isVideo: isVideo) } @@ -304,11 +291,11 @@ class MainPresenter: MainPresenterProtocol, MainInteractorOutputProtocol, Schedu func participantsUpdated(result: ParticipantsResult){ switch result { - case let .createGroupCall(contacts, room): - interactor.createGroupCall(contacts: contacts, room: room) + case let .createGroupCall(callId, contacts, room): + interactor.createGroupCall(callId: callId, contacts: contacts, room: room) break - case let .createConferenceCall(contacts: contacts, room: room): - interactor.createConferenceCall(contacts: contacts, room: room) + case let .createConferenceCall(callId: callId, contacts: contacts, room: room): + interactor.createConferenceCall(callId: callId, contacts: contacts, room: room) break default: break diff --git a/Nynja/Modules/Main/View/MainNavigationItem.swift b/Nynja/Modules/Main/View/MainNavigationItem.swift index 28d3f60ea..48cc9a8c4 100644 --- a/Nynja/Modules/Main/View/MainNavigationItem.swift +++ b/Nynja/Modules/Main/View/MainNavigationItem.swift @@ -9,11 +9,12 @@ enum MainNavigationItem: String { // First Level - case home = "wheel_item_home" - case mySelf = "wheel_item_myself" - case calls = "wheel_item_calls" - case channels = "wheel_item_channels" - case search = "wheel_item_search" + case home = "wheel_item_home" + case mySelf = "wheel_item_myself" + case calls = "wheel_item_calls" + case channels = "wheel_item_channels" + case search = "wheel_item_search" + case marketplace = "wheel_item_marketplace" // Channels section case myChannels = "wheel_item_my_channels" diff --git a/Nynja/Modules/Main/View/MainViewController+Container.swift b/Nynja/Modules/Main/View/MainViewController+Container.swift index 6842e9ca2..3bdabdcfe 100644 --- a/Nynja/Modules/Main/View/MainViewController+Container.swift +++ b/Nynja/Modules/Main/View/MainViewController+Container.swift @@ -10,19 +10,20 @@ extension MainViewController { typealias TLICreateInfo = (level: Int, selectedItem: WheelItemModel) func closeWheel(indexPath: IndexPath?) { - hideContainer() - if let index = indexPath { - self.container.deselectItem(at: index) - } + hideContainer(with: indexPath) } // MARK: - Next level - func openNextLevel(indexPath: IndexPath?) { + func openNextLevel(indexPath: IndexPath?, completion: ((ItemModels, Int) -> Void)? = nil) { if let ind = indexPath?.last, let nextLevel = indexPath?.count { let items = WCDataManager.shared.getSubItems(index: ind, of: nextLevel - 1) wheelContainerDS.setItems(items, at: nextLevel) container.reloadData() + + if let items = items { + completion?(items, nextLevel) + } } } diff --git a/Nynja/Modules/Main/View/MainViewController+NavigateProtocol.swift b/Nynja/Modules/Main/View/MainViewController+NavigateProtocol.swift index 3f079c41e..80311b819 100644 --- a/Nynja/Modules/Main/View/MainViewController+NavigateProtocol.swift +++ b/Nynja/Modules/Main/View/MainViewController+NavigateProtocol.swift @@ -37,13 +37,20 @@ extension MainViewController: NavigateProtocol { } func showOptions(indexPath: IndexPath?) { - openNextLevel(indexPath: indexPath) + openNextLevel(indexPath: indexPath) { [weak self] items, level in + self?.container.scroll(to: items.count - 1, at: level) + } } func showCalls(indexPath: IndexPath?) { // TODO: will be implemented in future with 'Calls' module. } + func showMarketplace(indexPath: IndexPath?) { + presenter.showMarketplace() + closeWheel(indexPath: indexPath) + } + func showChannels(indexPath: IndexPath?) { openNextLevel(indexPath: indexPath) } diff --git a/Nynja/Modules/Main/View/MainViewController.swift b/Nynja/Modules/Main/View/MainViewController.swift index 09f4b1e65..794c28002 100644 --- a/Nynja/Modules/Main/View/MainViewController.swift +++ b/Nynja/Modules/Main/View/MainViewController.swift @@ -367,13 +367,11 @@ class MainViewController: BaseVC, MainViewProtocol, HitTestDelegate, UINavigatio @objc func returnToCallTapped() { returnToCallView.isHidden = true - self.presenter.returnToCall() + self.presenter.returnToCall(call: returnToCallView.content.nynCall) } @objc func tapOnVideoView() { partnerVideoView.isHidden = true - partnerVideoView.isHidden = true - self.presenter.returnToCall() } // MARK: Toggle "Next" button @@ -432,12 +430,12 @@ class MainViewController: BaseVC, MainViewProtocol, HitTestDelegate, UINavigatio } } - func hideContainer() { + func hideContainer(with indexPath: IndexPath? = nil) { guard isWheelShown else { return } - wcDataManager.hideContainer() + wcDataManager.hideContainer(with: indexPath) toggleNext(shouldCollapse: true) isWheelShown = false @@ -610,9 +608,9 @@ class MainViewController: BaseVC, MainViewProtocol, HitTestDelegate, UINavigatio exportSession?.exportAsynchronously(completionHandler: { switch exportSession!.status { case .failed: - LogService.log(topic: .videoConverter, text: "export failed") + LogService.log(topic: .videoConverter) { return "export failed" } case .cancelled: - LogService.log(topic: .videoConverter, text: "export canceled") + LogService.log(topic: .videoConverter) { return "export canceled" } case .completed: dispatchAsyncMain { complete(writeUrl) diff --git a/Nynja/Modules/Main/View/NavigateProtocol.swift b/Nynja/Modules/Main/View/NavigateProtocol.swift index e5bedb063..66ccf2481 100644 --- a/Nynja/Modules/Main/View/NavigateProtocol.swift +++ b/Nynja/Modules/Main/View/NavigateProtocol.swift @@ -20,6 +20,7 @@ protocol FirstLevelNavigateProtocol: class { func showContacts(indexPath: IndexPath?) func showOptions(indexPath: IndexPath?) func showCalls(indexPath: IndexPath?) + func showMarketplace(indexPath: IndexPath?) func showChannels(indexPath: IndexPath?) func showHome(indexPath: IndexPath?) func showMySelf(indexPath: IndexPath?) diff --git a/Nynja/Modules/Main/WireFrame/MainWireframe.swift b/Nynja/Modules/Main/WireFrame/MainWireframe.swift index aa632f950..d1b5b8540 100644 --- a/Nynja/Modules/Main/WireFrame/MainWireframe.swift +++ b/Nynja/Modules/Main/WireFrame/MainWireframe.swift @@ -84,13 +84,13 @@ class MainWireFrame: MainWireFrameProtocol, NynjaCommunicatorServiceDelegate { } func showNotImplemented() { - + let alertVC = UIAlertController(title: "voice_call_the_feature_currently_unavailable".localized, message: nil, preferredStyle: .alert) let okAction = UIAlertAction(title: "ok".localized, style: .cancel) { (action) in alertVC.dismiss(animated: true, completion: nil) } alertVC.addAction(okAction) - + self.view?.present(alertVC, animated: true, completion: nil) } @@ -125,7 +125,7 @@ class MainWireFrame: MainWireFrameProtocol, NynjaCommunicatorServiceDelegate { SelectCountryWireFrame().presentSelectCountry(navigation: navigation, main: self, selectCountryDelegate: selectCountryDelegate) } } - + func openMarketplace() { if let navigation = self.navigation { MarketplaceWireFrame().presentMarketplace(navigation: navigation, main: self) @@ -215,20 +215,14 @@ class MainWireFrame: MainWireFrameProtocol, NynjaCommunicatorServiceDelegate { return Room(room: roomDB) } - func sendTyping(_ isTyping: Bool) { - messageinteractor?.sendTypingStatus(isTyping) - } - - var callInProgressVC :CallInProgressViewProtocol? var isVideo: Bool = false var isGroup: Bool = false - + func showMessages(contact: Contact, callVC: CallInProgressViewProtocol, isVideo: Bool = false) { self.isVideo = isVideo self.view?.view.endEditing(true) - self.callInProgressVC = callVC - + if !isVideo { self.showReturnToCallView() } @@ -236,15 +230,14 @@ class MainWireFrame: MainWireFrameProtocol, NynjaCommunicatorServiceDelegate { } func showMessages(room: Room, callVC: CallInProgressViewProtocol, isVideo: Bool = false) { - + self.isVideo = isVideo self.view?.view.endEditing(true) - self.callInProgressVC = callVC - + if !isVideo { self.showReturnToCallView() } - + showChat(room) } @@ -253,7 +246,7 @@ class MainWireFrame: MainWireFrameProtocol, NynjaCommunicatorServiceDelegate { } func viewShowed() { - NynjaCommunicatorService.sharedInstance.delegate = self + NynjaCommunicatorService.sharedInstance.delegates.addDelegate(self) } func showNoInternetView() { @@ -288,20 +281,15 @@ class MainWireFrame: MainWireFrameProtocol, NynjaCommunicatorServiceDelegate { isVideo = false self.showReturnToCallView() self.view?.hidePartnerVideoView() - + // if let call = self.call { // self.view?.showReturnToCall(call: call) // } } - func returnToCall() { - - if callInProgressVC != nil { - - self.navigation?.pushViewController(callInProgressVC as! UIViewController, animated: true) - if !self.isVideo { - self.hideReturnToCallView() - } + func returnToCall(call: NYNCall?) { + if let c = call { + presentCallInProgressViewForCall(call: c) } } @@ -367,7 +355,7 @@ class MainWireFrame: MainWireFrameProtocol, NynjaCommunicatorServiceDelegate { messageinteractor?.sendContact(contact: contact) messageinteractor?.sendTyping(.done) } - + func showWallet(for profile: Profile) { guard let navigation = navigation else { return } WalletBalancesWireFrame().presentWalletBalances(for: profile, navigationController: navigation) @@ -486,53 +474,53 @@ class MainWireFrame: MainWireFrameProtocol, NynjaCommunicatorServiceDelegate { // MARK: Add Participants to group call func showAddParticipantsToCreateCallWith(room: Room) { - + AddParticipantsWireFrame().presentAddParticipants(navigation: contentNavigation!, main: self, selectedContacts: [], delegate: self.external, mode: .createGroupCall, members: room.allMembers, room: room) } - + func showAddParticipantsToCreateConferenceCall() { - + AddParticipantsWireFrame().presentAddParticipants(navigation: contentNavigation!, main: self, selectedContacts: [], delegate: self.external, mode: .createConferenceCall, members: [], room: nil) } - + func showNotImplementedAlert() { - + AlertManager.sharedInstance.showAlertOk(message: "voice_call_the_feature_currently_unavailable".localized) } - + func presentCallInProgressViewForCall(call:NYNCall) { - + self.isVideo = call.recvVideo let callMode: CallInProgressMode = self.isVideo ? .oneToOneVideo : call.isConference() ? .groupAudio : .oneToOneAudio - + if callMode == .groupAudio { - + // TODO: Crashes after invoking this function. Please take a look CallInProgressWireframe().presentCreateGroupCall(navigation: navigation!, callInProgressMode: callMode, main: self, call: call) - + // let callMode: CallMode = self.isVideo ? .outGoingVideoGroupCall : .outGoingGroupCall // CallWireFrame().presentCreateGroupCall(navigation: navigation!, callMode: callMode, main: self, call: call) - + } else { - + var contact:Contact? = nil - + if call.isOutgoing() { - + if let messageVC = contentNavigation.viewControllers.last as? MessageVC { contact = messageVC.presenter.interactor.contact } //contact = self.view?.presenter.interactor.findContactBy(phoneId: call.callee) - + } else { - + contact = self.view?.presenter.interactor.findContactBy(phoneId: call.caller) } - + if let ctc = contact { CallInProgressWireframe().presentDialInCall(navigation: navigation!, callInProgressMode: callMode, contact:ctc, call: call, main: self) } else { - + CallInProgressWireframe().presentDialInCall(navigation: navigation!, callInProgressMode: callMode, contact:nil, call: call, main: self) } } @@ -542,21 +530,21 @@ class MainWireFrame: MainWireFrameProtocol, NynjaCommunicatorServiceDelegate { self.view?.view.endEditing(true) // let callMode: CallMode = self.isVideo ? .outGoingVideoGroupCall : .outGoingGroupCall // CallWireFrame().presentDialInCall(navigation: navigation!, callMode: callMode, call: call, main: self) - + presentCallInProgressViewForCall(call: call) } func incomingCallRinging(call: NYNCall) { // let callMode: CallMode = .incamingGroupCall // CallWireFrame().presentCreateGroupCall(navigation: navigation!, callMode: callMode, main: self, call: call) - + presentCallInProgressViewForCall(call: call) } func creatingGroupCall(name: String, call: NYNCall) { // let callMode: CallMode = self.isVideo ? .outGoingVideoGroupCall : .outGoingGroupCall // CallWireFrame().presentCreateGroupCall(navigation: navigation!, callMode: callMode, main: self, call: call) - + presentCallInProgressViewForCall(call: call) } diff --git a/Nynja/Modules/Map/View/MapViewController.swift b/Nynja/Modules/Map/View/MapViewController.swift index 9fcfe3cf4..972e8f720 100644 --- a/Nynja/Modules/Map/View/MapViewController.swift +++ b/Nynja/Modules/Map/View/MapViewController.swift @@ -96,7 +96,7 @@ class MapViewController: UIViewController, MapViewProtocol, GMSMapViewDelegate { private lazy var accurateLabel: UILabel = { let label = UILabel() - label.font = UIFont(fontName: Constants.fonts.regular, height: Constraints.accurateLabel.fontHeight) + label.font = UIFont.makeFont(with: Constants.fonts.regular, height: Constraints.accurateLabel.fontHeight) label.textColor = Constants.colors.white.getColor() label.textAlignment = .center diff --git a/Nynja/Modules/Message/Interactor/MessageInteractor+Fetch.swift b/Nynja/Modules/Message/Interactor/MessageInteractor+Fetch.swift index 41015a927..8b0bcbfda 100644 --- a/Nynja/Modules/Message/Interactor/MessageInteractor+Fetch.swift +++ b/Nynja/Modules/Message/Interactor/MessageInteractor+Fetch.swift @@ -8,7 +8,7 @@ extension MessageInteractor { - private var fetchType: FetchType? { + var fetchType: FetchType? { if let contact = self.contact, let from = myContact?.phoneId { let ids = [from, contact.phoneId].sorted() return .p2p(from: ids[0], to: ids[1]) @@ -36,11 +36,6 @@ extension MessageInteractor { } } - func fetchPenultimateMessage() { - if let type = fetchType { - penultimateMessage = MessageDAO.fetchPenultimateMessage(of: type) - } - } // MARK: - Fetch Chat Model func fetchRoomFromStorage() { @@ -136,17 +131,20 @@ extension MessageInteractor { downloadMessageIfNeeded(message) } } + + let unreadCount = isNew ? Int(chat.unreadCount) : initialUnreadCount configuration.progressModels = processingManager.progressFor(configuration.messages.messagesUrls) configuration.repliedModels = fetchRepliedModels(type) configuration.position = adjustChatPosition(unreadCount) configuration.reader = chat.otherReader - configuration.unreadCount = isNew ? unreadCount : initialUnreadCount + configuration.unreadCount = unreadCount configuration.shouldShowUnread = !isAfterConnectionAppeared configuration.links = fetchLinks() configuration.mentions = fetchMentions(for: configuration.messages) configuration.translations = fetchTranslations() configuration.transcriptions = fetchTranscriptions() + configuration.transcribingModels = fetchTranscriptionsProgress() isAfterConnectionAppeared = false if let rosterId = StorageService.sharedInstance.rosterId { @@ -229,6 +227,17 @@ extension MessageInteractor { return result } + private func fetchTranscriptionsProgress() -> ChatConfiguration.ConversionsProgress { + var result = ChatConfiguration.ConversionsProgress() + + configuration.messages.forEach { + if let localId = $0.msg_id, let progress = fetchConversionProgress(with: localId) { + result[localId] = progress + } + } + return result + } + private func adjustChatPosition(_ unreadCount: Int) -> PositionType { if let message = initialMessage { if let localId = message.localId, messageBy(localId: localId) != nil { diff --git a/Nynja/Modules/Message/Interactor/MessageInteractor+Forward.swift b/Nynja/Modules/Message/Interactor/MessageInteractor+Forward.swift new file mode 100644 index 000000000..d7775cb51 --- /dev/null +++ b/Nynja/Modules/Message/Interactor/MessageInteractor+Forward.swift @@ -0,0 +1,50 @@ +// +// MessageInteractor+Forward.swift +// Nynja +// +// Created by Volodymyr Hryhoriev on 8/28/18. +// Copyright © 2018 TecSynt Solutions. All rights reserved. +// + +extension MessageInteractor { + + func prepareToForward(localId: MessageLocalId) { + if let message = messageBy(localId: localId) { + forwardMessage = message + } + } + + func didSelectForwardTargets(_ targets: ForwardTargets) { + forwardInfo = scheduleInfo(targets) + forwardMessage = nil + } + + private func scheduleInfo(_ targets: ForwardTargets) -> ScheduleInfo? { + guard let message = forwardMessage else { + return nil + } + return ScheduleInfo(message: message.forwardMessage, targets: targets) + } + + func sendForwardMessage() { + guard let info = forwardInfo else { + return + } + + forwardInfo = nil + + guard let phoneId = storageService.phoneId else { + return + } + + let messages = info.targets.messages(from: info.message, phoneId: phoneId) + mqttService.forwardMessage(phoneId: phoneId, messages: messages) { [weak self] createdJob in + guard let `self` = self, let dbJob = DBJob(job: createdJob) else { return } + + dbJob.type = .forward + + try? self.storageService.perform(action: .save, with: dbJob) + } + } + +} diff --git a/Nynja/Modules/Message/Interactor/MessageInteractor+History.swift b/Nynja/Modules/Message/Interactor/MessageInteractor+History.swift new file mode 100644 index 000000000..7ce3c9129 --- /dev/null +++ b/Nynja/Modules/Message/Interactor/MessageInteractor+History.swift @@ -0,0 +1,107 @@ +// +// MessageInteractor+History.swift +// Nynja +// +// Created by Anton Poltoratskyi on 17.08.2018. +// Copyright © 2018 TecSynt Solutions. All rights reserved. +// + +import Foundation + +extension MessageInteractor { + + struct MessageHistoryGaps { + typealias Range = CountableClosedRange + + private let ranges: [Range]? + + init(ranges: [Range]) { + self.ranges = ranges + } + + var oldestId: MessageServerId? { + return ranges?.last?.lowerBound + } + + var newestId: MessageServerId? { + return ranges?.first?.upperBound + } + + var isEmpty: Bool { + return ranges?.isEmpty ?? true + } + } + + enum HistoryCheckResult { + case validSequence + case updateRequired + case gaps(MessageHistoryGaps) + } + + func checkLocalHistory() -> HistoryCheckResult { + guard chat.last_msg?.statusString != "update" else { + return .updateRequired + } + guard let gaps = fetchAllHistoryGaps(), !gaps.isEmpty else { + return .validSequence + } + return .gaps(gaps) + } + + private func fetchAllHistoryGaps() -> MessageHistoryGaps? { + guard let fetchType = fetchType else { return nil } + + var gaps = [MessageHistoryGaps.Range]() + + var endMessageId: MessageServerId? // Latest message id + var repliedMessageId: MessageServerId? // Oldest replied message id + + let appendGapIfExists = { (message: DBMessage) in + if let end = endMessageId, let start = message.serverId, start <= end { + gaps.append(start...end) + } + endMessageId = nil + } + + var previousMessage = chat.last_msg.flatMap { DBMessage(message: $0) } + + let isValidChain = { (message: DBMessage) -> Bool in + return previousMessage?.next == message.serverId || previousMessage?.isTrusted == true + } + + // Iterate over all messages that isn't filtered by status. + // Ignore first message, because it's equal to chat's last message. + try? MessageDAO.dropFirst(for: fetchType) { message in + guard let id = message.serverId else { return } + + defer { previousMessage = message } + + if id == repliedMessageId { + repliedMessageId = nil + + } else if message.isReply, let link = message.link { + repliedMessageId = repliedMessageId.flatMap { min($0, link) } ?? link + } + + if isValidChain(message) { + appendGapIfExists(message) + + } else if endMessageId == nil { + // Found gap + endMessageId = previousMessage?.serverId + } + } + + previousMessage.map { appendGapIfExists($0) } + /* + if let repliedMessageId = repliedMessageId, + let start = gaps.last?.lowerBound, + let end = gaps.first?.upperBound, + !(start...end).contains(repliedMessageId) { + + gaps.append(repliedMessageId...start) + }*/ + + return MessageHistoryGaps(ranges: gaps) + } +} diff --git a/Nynja/Modules/Message/Interactor/MessageInteractor+Mentions.swift b/Nynja/Modules/Message/Interactor/MessageInteractor+Mentions.swift index 85ccc2a46..447876876 100644 --- a/Nynja/Modules/Message/Interactor/MessageInteractor+Mentions.swift +++ b/Nynja/Modules/Message/Interactor/MessageInteractor+Mentions.swift @@ -21,7 +21,7 @@ extension MessageInteractor { guard let alias = $0.alias else { return false } - return alias.starts(with: filter) && $0.phone_id != currentPhoneId + return alias.starts(with: filter, options: .caseInsensitive) && $0.phone_id != currentPhoneId }.sorted { guard let lhs = $0.alias, let rhs = $1.alias else { return false diff --git a/Nynja/Modules/Message/Interactor/MessageInteractor+StorageSubscriber.swift b/Nynja/Modules/Message/Interactor/MessageInteractor+StorageSubscriber.swift index ae07ad747..1f19440a9 100644 --- a/Nynja/Modules/Message/Interactor/MessageInteractor+StorageSubscriber.swift +++ b/Nynja/Modules/Message/Interactor/MessageInteractor+StorageSubscriber.swift @@ -19,13 +19,22 @@ extension MessageInteractor { if message.statusString == "deleted" { handleMessageDelete(message) } else { + guard info.kind != .delete else { + return + } handleMessageInsertOrUpdate(message) } } else if isProfileUpdated(with: info, type: type) { + readUnreadMessages() loadData() + } else if let updatedStar = updatedStar(with: info, type: type) { + handleChanged(star: updatedStar, kind: info.kind) } } + + // MARK: - Check SubscribeType + private func isMemberUpdated(with info: StorageChange, type: SubscribeType) -> Bool { if case .member(let id) = type, let roomId = id, room?.id == roomId, let _ = info.entity as? DBMember { return true @@ -49,6 +58,13 @@ extension MessageInteractor { return nil } + private func updatedStar(with info: StorageChange, type: SubscribeType) -> DBStar? { + if case .star(_) = type, let star = info.entity as? DBStar { + return star + } + return nil + } + private func changedMessage(with info: StorageChange, type: SubscribeType) -> Message? { if case .chat = type, let message = info.entity as? Message { return message @@ -63,7 +79,9 @@ extension MessageInteractor { return false } + // MARK: - Handle member + private func handleUpdateMember() { guard let room = self.room, room.kind == .channel, @@ -77,6 +95,7 @@ extension MessageInteractor { // MARK: - Update Contact & Room + private func handleUpdate(contact: DBContact) { let contact = Contact(contact: contact) chat = contact @@ -91,22 +110,24 @@ extension MessageInteractor { switch status { case .add, .remove: - let isRemoved = status == .remove - membersChanges(in: room, isRemoved: isRemoved) + if let chat = chat as? Room { + chat.admins = room.admins + chat.members = room.members + } + if let allMembers = room.allMembersWithoutFilter, + let userMember = allMembers.first(where: { $0.phone_id == myContact?.phone_id }) { + presenter?.blockRoom(userMember.isRemoved) + } case .leave: presenter?.deleteAndLeave() default: chat = room } } - - private func membersChanges(in room: Room, isRemoved: Bool) { - if let members = room.members, members.contains(where: { $0.phone_id == myContact?.phone_id }) { - presenter?.blockRoom(isRemoved) - } - } + // MARK: - Handle message + func handleMessageDelete(_ message: Message) { if let id = message.msg_id { self.presenter?.removeMessage(id) @@ -139,14 +160,31 @@ extension MessageInteractor { guard let `self` = self else { return } self.fetchFromStorage() } - } else if let localId = message.msg_id, let index = indexOfMessage(with: localId) { - handleExistedMessage(message, localId: localId, index: index) + } else if isMessageExisted(message) { + handleExistedMessage(message) } else { handleNewMessage(message) } } - private func handleExistedMessage(_ message: Message, localId: String, index: Int) { + private func isMessageExisted(_ message: Message) -> Bool { + if let id = message.id, let lastId = chat.last_msg?.id, id <= lastId { + return true + } + + if let localId = message.msg_id, let _ = indexOfMessage(with: localId) { + return true + } + + return false + } + + private func handleExistedMessage(_ message: Message) { + guard let localId = message.msg_id, + let index = indexOfMessage(with: localId) else { + return + } + let oldMessage = configuration.messages[index] configuration.messages[index] = message @@ -161,7 +199,10 @@ extension MessageInteractor { if message.statusString == "update" { presenter?.scrollToBottomIfNeeded() } - } else { + } else if message.files?.count != oldMessage.files?.count { + let config = createMessageConfiguration(message) + presenter?.updateMessage(with: config) + } else { if isMyselfChat { presenter?.messageRead(localId) } else { @@ -179,6 +220,7 @@ extension MessageInteractor { config = createMessageConfiguration(message) startSendingMessage() + chat.unread = 0 initialUnreadCount = 0 } else { @@ -276,4 +318,48 @@ extension MessageInteractor { mentions: nil) return transcriptionInfo } + + + // MARK: - Handle Star + + private func handleChanged(star: DBStar, kind: StorageChangeKind) { + let star = Star(star: star) + switch kind { + case .insert: + handleAdd(star: star) + case .delete: + handleRemove(star: star) + case .update: + handleUpdate(star: star) + } + } + + private func handleAdd(star: Star) { + guard let localId = star.message?.msg_id, let starLocalId = star.client_id, configuration.stars[localId] == nil else { + return + } + + configuration.stars[localId] = star + presenter?.starMessage(with: localId, starId: starLocalId) + + } + + private func handleUpdate(star: Star) { + guard let starId = star.id, + let localId = star.message?.msg_id, + let oldStar = configuration.stars[localId] else { + return + } + + oldStar.id = starId + } + + private func handleRemove(star: Star) { + guard let localId = star.message?.msg_id, configuration.stars[localId] != nil else { + return + } + + configuration.stars[localId] = nil + presenter?.unstarMessage(with: localId) + } } diff --git a/Nynja/Modules/Message/Interactor/MessageInteractor+Transcription.swift b/Nynja/Modules/Message/Interactor/MessageInteractor+Transcription.swift index deb212855..7370ba72d 100644 --- a/Nynja/Modules/Message/Interactor/MessageInteractor+Transcription.swift +++ b/Nynja/Modules/Message/Interactor/MessageInteractor+Transcription.swift @@ -14,17 +14,22 @@ protocol Transcription { var transcriptionService: TranscribeServiceProtocol { get } var conversationLanguageSettingService: ConversationLanguageSettingServiceProtocol { get } - - func transcribeMessage(localId: String, lang: SelectedLang?, failure: @escaping ()->Void) + + func transcribeMessage(localId: String, lang: SelectedLang?) func cancelTransсribing(localId: String) func untranscribeMessage(localId: String) - func chooseTranscriptionLanguageForMessage(with localId: String, lang: SelectedLang, failure: @escaping ()->Void) + func chooseTranscriptionLanguageForMessage(with localId: String, lang: SelectedLang) + + func subscribeToTranscribeProcessing() + func unsubscribeFromTranscribeProcessing() + + func fetchConversionProgress(with id: String) -> ConvertionProgressModel? } extension MessageInteractor: Transcription { - func transcribeMessage(localId: String, lang: SelectedLang?, failure: @escaping ()->Void) { + func transcribeMessage(localId: String, lang: SelectedLang?) { guard let message = messageBy(localId: localId), message.sendType == .audio, let url = message.mainUrl, @@ -38,66 +43,72 @@ extension MessageInteractor: Transcription { language = extendedLang.language } - transcriptionService.transcribeAudio(with: fileUrl, language: language) { [weak self] result in + transcriptionService.transcribe(.init(message: message, + localUrl: fileUrl, + language: language)) + } + + func cancelTransсribing(localId: String) { + cancelTransсribing(localId: localId, isCancelOperation: true) + } + + func untranscribeMessage(localId: String) { + guard let message = messageBy(localId: localId), + let transcribeId = messageParser.parse(message, to: .transcription).first?.id else { + assertionFailure("Something went wrong") + return + } + + let translateId = messageParser.parse(message, to: .translation(.convenientToMe)).first?.id + let untranscribedMessage = messageFactory.makeUntranscribedMessage(message: message, + transcriptionId: transcribeId, + translationId: translateId) + processingManager.uploadMessage(untranscribedMessage) + } + + func chooseTranscriptionLanguageForMessage(with localId: String, lang: SelectedLang) { + transcribeMessage(localId: localId, lang: lang) + } + + func subscribeToTranscribeProcessing() { + transcriptionService.observeState(self) { [weak self] localId, state in guard let `self` = self else { return } - - switch result { - case .updateProccess(let operation): + + switch state { + case .updateProccess(let operation, _): let convertionModel = ConvertionProgressModel(id: localId, type: .transcribe, operation: operation, status: .atProgress) - + self.configuration.transcribingModels[localId] = convertionModel self.updateConvertionProgress(convertionModel) - case .success(let transcription): + case .success: self.irreversibleTransсribing(localId: localId) - let transribedMessage = self.messageFactory.makeTranscribedMessage(message: message, - text: transcription, - lang: language) - self.processingManager.uploadMessage(transribedMessage) case .failure(let error): switch error { - case .emptyResponse: - self.cancelTransсribing(localId: localId) - failure() + case .emptyResponse(let language): + self.stopTranscribing(localId: localId) + self.presenter?.showChooseAnotherLanguageAlert(localId: localId, language: language) default: break + } + case .unknown: + self.stopTranscribing(localId: localId) + case .cancel: + break } - } - } - } - - func cancelTransсribing(localId: String) { - guard let convertionModel = configuration.transcribingModels[localId] else { - return } - - convertionModel.operation.cancel() - convertionModel.status = .initial - - updateConvertionProgress(convertionModel) - configuration.translatingModels.removeValue(forKey: localId) } - func untranscribeMessage(localId: String) { - guard let message = messageBy(localId: localId), - let transcribeId = messageParser.parse(message, to: .transcription).first?.id else { - assertionFailure("Something went wrong") - return - } - - let translateId = messageParser.parse(message, to: .translation(.convenientToMe)).first?.id - let untranscribedMessage = messageFactory.makeUntranscribedMessage(message: message, - transcriptionId: transcribeId, - translationId: translateId) - processingManager.uploadMessage(untranscribedMessage) + func unsubscribeFromTranscribeProcessing() { + transcriptionService.removeObserver(self) } - func chooseTranscriptionLanguageForMessage(with localId: String, lang: SelectedLang, failure: @escaping ()->Void) { - transcribeMessage(localId: localId, lang: lang, failure: failure) + func fetchConversionProgress(with id: String) -> ConvertionProgressModel? { + return transcriptionService.fetchTranscribeProgress(with: id) } } @@ -113,6 +124,24 @@ private extension MessageInteractor { updateConvertionProgress(convertionModel) } + func stopTranscribing(localId: String) { + cancelTransсribing(localId: localId, isCancelOperation: false) + } + + func cancelTransсribing(localId: String, isCancelOperation: Bool = true) { + guard let convertionModel = configuration.transcribingModels[localId] else { + return + } + + if isCancelOperation { + convertionModel.operation.cancel() + } + convertionModel.status = .initial + + updateConvertionProgress(convertionModel) + configuration.translatingModels.removeValue(forKey: localId) + } + func updateConvertionProgress(_ progress: ConvertionProgressModel) { self.presenter?.updateTranslationProgress(progress) } diff --git a/Nynja/Modules/Message/Interactor/MessageInteractor.swift b/Nynja/Modules/Message/Interactor/MessageInteractor.swift index 2a54c31c3..34321344a 100644 --- a/Nynja/Modules/Message/Interactor/MessageInteractor.swift +++ b/Nynja/Modules/Message/Interactor/MessageInteractor.swift @@ -12,13 +12,15 @@ import CoreLocation final class MessageInteractor: BaseInteractor, MessageInteractorInputProtocol, HistoryHandlerDelegate, TypingHandlerDelegate, IoHandlerDelegate, ReachabilityServiceObserver, MQTTServiceDelegate, MessageProcessingDelegate, MessageHandlerSubscriber, MessageInteractorCallProtocol { + private var callService = NynjaCommunicatorService.sharedInstance + private struct Constants { static let messagesPageSize: Int64 = -15 } override var subscribes: [SubscribeType]? { return [.contact(contact?.phone_id), .room(room?.id), - .chat(chat.id), .profile, .member(room?.id)] + .chat(chat.id), .profile, .member(room?.id), .star(nil)] } // MARK: - Properties @@ -64,11 +66,7 @@ final class MessageInteractor: BaseInteractor, MessageInteractorInputProtocol, H var currentPhoneId: String? { return storageService.phoneId } - - var unreadCount: Int { - get { return Int(chat.unreadCount) } - set { chat.unread = Int64(unreadCount) } - } + var translationCount: Int64? var initialUnreadCount = 0 @@ -81,10 +79,12 @@ final class MessageInteractor: BaseInteractor, MessageInteractorInputProtocol, H var checkpoint: DBChatCheckpoint? private(set) var repliedMessage: Message? + var forwardMessage: Message? + var forwardInfo: ScheduleInfo? var isAfterConnectionAppeared: Bool = false - private let mqttService: MQTTService + let mqttService: MQTTService let muteChatService: MuteChatServiceProtocol let storageService: StorageService @@ -109,21 +109,12 @@ final class MessageInteractor: BaseInteractor, MessageInteractorInputProtocol, H private(set) var translationService = TranslationService(input: TranslationService.Input(apiKey: ThirdPartyServicesFactory.google.serviceConfig.apiKey)) private(set) lazy var transcriptionService : TranscribeServiceProtocol = { - - let config = TranscribeNetworkService.Config(apiKey: ThirdPartyServicesFactory.google.serviceConfig.apiKey) - - let client = URLSessionNetworkClient() - let newtworkService = TranscribeNetworkService(client: client) - - newtworkService.configure(config: config) - - let dependencies = TranscribeService.Dependencies(transcribeNetworkService: newtworkService) - return TranscribeService(dependencies: dependencies) + return TranscribeService.shared }() private(set) lazy var conversationLanguageSettingService: ConversationLanguageSettingServiceProtocol = { guard let settingOwner = settingOwner else { - fatalError("settingOwner = nil") + fatalError("settingOwner = nil") /////TODO: We need to know how this chat was added to our profile. } let dependencies = ConversationLanguageSettingService.Dependencies( settingOwner: settingOwner @@ -137,32 +128,24 @@ final class MessageInteractor: BaseInteractor, MessageInteractorInputProtocol, H }() private(set) lazy var messageFactory: MessageFactoryProtocol = { - let factory = MessageFactory() - let dependencies = MessageFactory.Dependencies( storageService: storageService, payloadBuilder: payloadBuilder ) - factory.inject(dependencies: dependencies) - - return factory + return MessageFactory(dependencies: dependencies) }() private(set) lazy var messageSendingService: MessageSendingServiceProtocol = { - let service = MessageSendingService() let dependencies = MessageSendingService.Dependencies( mqttService: mqttService, storageService: storageService, processingManager: processingManager ) - service.inject(dependencies: dependencies) - - return service + return MessageSendingService(dependencies: dependencies) }() var initialMessage: ChatInitialMessage? - var penultimateMessage: Message? - + // MARK: -- Private private lazy var _contact: Contact? = { @@ -177,7 +160,6 @@ final class MessageInteractor: BaseInteractor, MessageInteractorInputProtocol, H let processingQueue = DispatchQueue(label: "\(Bundle.main.bundleIdentifier).message-interactor", qos: .default) - // MARK: - Init & deinit required init(chat: ChatModel) { self.chat = chat @@ -198,14 +180,18 @@ final class MessageInteractor: BaseInteractor, MessageInteractorInputProtocol, H ReachabilityService.sharedInstance.addRechabilityObserver(self) MessageHandler.addSubscriber(self) NynjaCommunicatorService.sharedInstance.messageInteractorCallProtocol = self + + subscribeToTranscribeProcessing() } deinit { - NynjaCommunicatorService.sharedInstance.messageInteractorCallProtocol = nil + callService.messageInteractorCallProtocol = nil mqttService.removeSubscriber(self) MessageHandler.removeSubscriber(self) ReachabilityService.sharedInstance.removeRechabilityObserver(self) + + unsubscribeFromTranscribeProcessing() } // MARK: - StorageSubscriber @@ -237,8 +223,12 @@ final class MessageInteractor: BaseInteractor, MessageInteractorInputProtocol, H setupChatLanguageIfNeeded() updateTranslationPreview() - readUnreadMessages() + if ReachabilityService.sharedInstance.isReachable { + readUnreadMessages() + } + fetchRoomFromStorage() + sendForwardMessage() } func goAway() { @@ -246,9 +236,16 @@ final class MessageInteractor: BaseInteractor, MessageInteractorInputProtocol, H } func readMessage(localId: String) { - guard let message = messageBy(localId: localId), - let serverId = message.id else { - return + guard let message = messageBy(localId: localId) else { + return + } + + readMessage(message) + } + + private func readMessage(_ message: Message) { + guard let serverId = message.id else { + return } let shouldReadInMySelf = isMyselfChat && message.isCursor @@ -273,38 +270,43 @@ final class MessageInteractor: BaseInteractor, MessageInteractorInputProtocol, H private func requestHistory(from: MessageServerId, to: MessageServerId) { processingQueue.async { [weak self] in - guard let `self` = self, - let phoneId = self.storageService.phoneId, - let request = try? self.historyRequestFactory.makeHistoryRequestModel(rosterId: phoneId, - chat: self.chat, - from: from, - to: to) else { - return + guard let `self` = self, let phoneId = self.storageService.phoneId else { + return + } + do { + let request = try self.historyRequestFactory.makeRequestBetween( + rosterId: phoneId, + chat: self.chat, + from: from, + to: to) + + self.mqttService.sendHistoryRequest(with: request) + } catch { + assertionFailure(error.localizedDescription) } - - self.mqttService.sendHistoryRequest(with: request) } } private func makeHistoryRequest(_ messageID: MessageServerId?) -> HistoryRequestModel? { - guard let msgId = messageID ?? chat.last_msg?.id, let rosterId = storageService.phoneId else { return nil } - - let getAll = (messageID == nil && chat.last_msg?.statusString == "update") - + let needsUpdate = messageID == nil && chat.last_msg?.statusString == "update" + do { - if getAll { - return try historyRequestFactory.makeHistoryRequestModelAll(rosterId: rosterId, chat: chat) - } else { - return try historyRequestFactory.makeHistoryRequestModelPage(rosterId: rosterId, - chat: chat, - pageSize: Constants.messagesPageSize, - lastMessageId: msgId) + let requestModel = needsUpdate + ? try historyRequestFactory.makeGetUpdatesRequest(rosterId: rosterId, chat: chat, messageId: msgId) + : try historyRequestFactory.makePageRequest(rosterId: rosterId, + chat: chat, + pageSize: Constants.messagesPageSize, + lastMessageId: msgId) + if needsUpdate { + isHistoryUpdating = true } - } - catch { + + return requestModel + + } catch { assertionFailure(error.localizedDescription) return nil } @@ -313,51 +315,39 @@ final class MessageInteractor: BaseInteractor, MessageInteractorInputProtocol, H func hasRunningCall() -> Bool { if let r = self.room, let roomId = r.id { - return NynjaCommunicatorService.sharedInstance.hasRunningCallWith(roomId) + return callService.hasRunningCallWith(roomId) } return false } + + func hasCallInProgress() -> Bool { + + return callService.hasCallInProgress() + } func rejoinRunningCall() { if let r = self.room, let roomId = r.id { - return NynjaCommunicatorService.sharedInstance.rejoinRunningCallWith(roomId) + return callService.rejoinRunningCallWith(roomId) } } // MARK: - Fetch Data - func fetchData() { + private func fetchData() { fetchChatModel() - fetchPenultimateMessage() - - if !hasGapInsideHistory() { + switch checkLocalHistory() { + case .updateRequired, .validSequence: fetchMessages() - } else if let startId = chat.last_msg?.id, let endId = penultimateMessage?.id { + + case let .gaps(gaps): processingQueue.async { [weak self] in - guard let `self` = self else { return } - self.fetchFromStorage() + self?.fetchFromStorage() + } + if let oldId = gaps.oldestId, let newId = gaps.newestId { + requestHistory(from: newId, to: oldId) } - requestHistory(from: startId, to: endId) - } - } - - private func hasGapInsideHistory() -> Bool { - guard chat.last_msg?.statusString != "update" else { - return false - } - - guard let id = penultimateMessage?.id, - let lastId = chat.last_msg?.id, - id != lastId else { - return false - } - - guard let prevId = penultimateMessage?.prev else { - return true } - - return prevId != lastId } func fetchChatModel() { @@ -707,44 +697,58 @@ final class MessageInteractor: BaseInteractor, MessageInteractorInputProtocol, H } } - func starMessage(localId: String) { + func starMessage(localId: MessageLocalId) { guard let msg = messageBy(localId: localId), let messageServerId = msg.id, let rosterId = storageService.rosterId else { return } + let message = Message(message: msg) message.created = Date.currentTimestamp as AnyObject + let starLocalId = IdBuilder(format: .starClientId) + .addValueForComponent(messageServerId, .key) + .build() + let star = Star() star.message = message star.starStatus = .add star.roster_id = rosterId - star.client_id = IdBuilder(format: .starClientId) - .addValueForComponent(messageServerId, .key) - .build() + star.client_id = starLocalId + + try? storageService.perform(action: .save, with: star) - if let dbStar = DBStar(star: star) { - try? storageService.perform(action: .save, with: dbStar) + if let starDeleteAction = StarActionDAO.fetchStarAction(for: starLocalId) { + try? storageService.perform(action: .delete, with: starDeleteAction) } + configuration.stars[localId] = star - presenter?.changeStarStatus(star: star, localId: localId) + presenter?.starMessage(with: localId, starId: starLocalId) + mqttService.sendStar(star: star) } - func unstarMessage(localId: String) { + func unstarMessage(localId: MessageLocalId) { guard let star = configuration.stars[localId] else { return } - star.status = StringAtom(string: "remove") - if let dbStar = DBStar(star: star) { - try? storageService.perform(action: .delete, with: dbStar) + star.starStatus = .remove + + if let starLocalId = star.client_id { + if star.isDelivered { + let action = DBStarAction(starLocalId: starLocalId, action: .delete) + try? storageService.perform(action: .save, with: star) + try? storageService.perform(action: .save, with: action) + } else { + try? storageService.perform(action: .delete, with: star) + } } + configuration.stars[localId] = nil - mqttService.sendStar(star: star) - star.client_id = nil + presenter?.unstarMessage(with: localId) - presenter?.changeStarStatus(star: star, localId: localId) + mqttService.sendStar(star: star) } func captureEditingMessage(localId: String) { @@ -770,6 +774,9 @@ final class MessageInteractor: BaseInteractor, MessageInteractorInputProtocol, H } sendMessage(editedMessage) + + editedMessage.status = nil + try? storageService.perform(action: .save, with: editedMessage) updateEditedMessageUI(editedMessage) } @@ -789,14 +796,14 @@ final class MessageInteractor: BaseInteractor, MessageInteractorInputProtocol, H } } - func deleteMessage(localId: String, forBoth: Bool) { + func deleteMessage(localId: MessageLocalId, forBoth: Bool) { guard let message = messageBy(localId: localId), let messageId = message.id, let phoneId = myContact?.phoneId else { return } - var messageAction: DBMessageAction + let messageAction: DBMessageAction var seenBy: AnyObject = -1 as AnyObject switch chat { @@ -807,7 +814,7 @@ final class MessageInteractor: BaseInteractor, MessageInteractorInputProtocol, H messageAction = DBMessageAction(messageId: messageId, seenBy: (seenBy as? Int64) ?? -1, phoneId: "", - action: "delete") + action: .delete) default: if !forBoth { seenBy = phoneId as AnyObject @@ -815,24 +822,27 @@ final class MessageInteractor: BaseInteractor, MessageInteractorInputProtocol, H messageAction = DBMessageAction(messageId: messageId, seenBy: forBoth ? -1 : 0, phoneId: phoneId, - action: "delete") + action: .delete) } try? storageService.perform(action: .save, with: messageAction) - let messageForDelete = messageFactory.makeMessageForDelete(message: message, - seenBy: [seenBy]) + let messageForDelete = messageFactory.makeMessageForDelete(message: message, seenBy: [seenBy]) mqttService.sendMessage(message: messageForDelete) messageForDelete.status = StringAtom(string: "deleted") try? storageService.perform(action: .save, with: messageForDelete) + if let fetchType = fetchType, let newLastMessage = MessageDAO.fetchLastMessage(of: fetchType) { + ChatService.updateLastMessage(newLastMessage, shouldChangeUnread: false) + } + handleMessageDelete(message) } // MARK: - Reply - func prepareToReply(localId: String) { + func prepareToReply(localId: MessageLocalId) { if let message = messageBy(localId: localId) { repliedMessage = message } @@ -882,12 +892,22 @@ final class MessageInteractor: BaseInteractor, MessageInteractorInputProtocol, H // MARK: - MessageProcessingDelegate func updateProgress(_ progress: ProgressModel) { + self.configuration.progressModels[progress.url] = progress self.presenter?.updateProgress(progress) } // MARK: - HistoryHandlerDelegate var isNew = false + + private var isHistoryUpdating: Bool = false + func getHistorySuccess() { + guard !isHistoryUpdating else { + isHistoryUpdating = false + isNew = false + fetchData() + return + } if isNew { processingQueue.async { [weak self] in guard let `self` = self else { return } @@ -935,12 +955,16 @@ final class MessageInteractor: BaseInteractor, MessageInteractorInputProtocol, H } } + // MARK: - MQTTServiceDelegate - func didConnect(_ mqttService: MQTTService) { - presenter?.internetStatusChanged(.connected) - readUnreadMessages() + + func mqttServiceDidConnect(_ mqttService: MQTTService) { + DispatchQueue.main.async { + self.presenter?.internetStatusChanged(.connected) + } } + // MARK: - Update private func chatUpdated(from oldChat: ChatModel, to newChat: ChatModel) { presenter?.chatUpdated(newChat) @@ -965,9 +989,15 @@ final class MessageInteractor: BaseInteractor, MessageInteractorInputProtocol, H private func notifyAboutRead(withOld oldReader: MessageServerId?, new newReader: MessageServerId?) { guard let newReader = newReader, oldReader != newReader, - let index = configuration.messages.index(where: { $0.id == newReader }), - let localId = configuration.messages[index].msg_id else { - return + let localId = configuration.messages + .last(where: { (message) -> Bool in + guard let id = message.id else { + return false + } + return id <= newReader + })? + .msg_id else { + return } presenter?.messageRead(localId) @@ -1055,25 +1085,33 @@ extension MessageInteractor { } // MARK: Read message - private func readUnreadMessages() { - if unreadCount != 0, let serverId = chat.last_msg?.id { - readMessage(serverId) + func readUnreadMessages() { + guard chat.unreadCount != 0, + let lastMessage = chat.last_msg else { + return } + + if let fetchType = self.fetchType, + let lastSentMessage = MessageDAO.fetchLastMessage(of: fetchType), + lastSentMessage.createdInt > lastMessage.createdInt { + + return + } + + readMessage(lastMessage) } private func readMessage(_ messageId: MessageServerId) { guard let rosterId = storageService.phoneId else { return } - do { - let historyModel = try historyRequestFactory.makeHistoryRequestUpdateMessage( + let historyModel = try historyRequestFactory.makeReadRequest( rosterId: rosterId, chat: chat, messageId: messageId) mqttService.sendHistoryRequest(with: historyModel) - } - catch { + } catch { assertionFailure(error.localizedDescription) } } diff --git a/Nynja/Modules/Message/Models/Configurations/ChatConfiguration.swift b/Nynja/Modules/Message/Models/Configurations/ChatConfiguration.swift index cca96c3b8..17b4162f5 100644 --- a/Nynja/Modules/Message/Models/Configurations/ChatConfiguration.swift +++ b/Nynja/Modules/Message/Models/Configurations/ChatConfiguration.swift @@ -12,11 +12,12 @@ struct ChatConfiguration { typealias Mentions = [String: [MentionInfo]] typealias Translations = [String: TranslationInfo] typealias Transcriptions = [String: TranscriptionInfo] + typealias ConversionsProgress = [String: ConvertionProgressModel] var messages: [Message] = [] var progressModels: [URL : ProgressModel] = [:] - var translatingModels: [String: ConvertionProgressModel] = [:] - var transcribingModels: [String: ConvertionProgressModel] = [:] + var translatingModels: ConversionsProgress = ConversionsProgress() + var transcribingModels: ConversionsProgress = ConversionsProgress() var repliedModels: RepliedModels = [:] var stars: [String: Star] = [:] var position: PositionType = .none diff --git a/Nynja/Modules/Message/Models/Mention/InputController/MentionController.swift b/Nynja/Modules/Message/Models/Mention/InputController/MentionController.swift index fefc82377..9cc0c44d1 100644 --- a/Nynja/Modules/Message/Models/Mention/InputController/MentionController.swift +++ b/Nynja/Modules/Message/Models/Mention/InputController/MentionController.swift @@ -23,7 +23,7 @@ protocol MentionControllerInput: class { func handleInputText(_ text: String, cursorPosition: Int) /// Handler for textView(_:shouldChangeTextIn:replacementText:) - func handleReplacementText(_ replacementText: String, in range: NSRange, currentText text: String) + func handleReplacementText(_ replacementText: String, in range: NSRange, currentText: String) -> Bool func setup(_ initialMentions: [Mention]) func reset() @@ -56,13 +56,9 @@ final class MentionController: MentionControllerInput { // MARK: - Utils - private func save(text: String, cursorPosition: Int) { - let shouldUpdate = text != lastInputText + private func save(text: String, cursorPosition: Int?) { lastInputText = text lastCursorPosition = cursorPosition - if shouldUpdate { - handleUpdate(text: text, cursor: cursorPosition) - } } private func handleUpdate(text: String, cursor: Int) { @@ -73,9 +69,21 @@ final class MentionController: MentionControllerInput { private func startIndexOfMention(in text: String, cursorIndex: String.Index) -> String.Index? { return text .prefix(upTo: cursorIndex) - .reversed() - .index(where: { $0 == "@" }) - .map { text.index(before: $0.base) } + .lastIndex(where: { $0 == "@" }) + } + + private func cursorIndex(for cursorPosition: Int, in text: String) -> String.Index { + let cursorIndex = String.Index(encodedOffset: cursorPosition) + + if cursorIndex > text.startIndex { + if cursorIndex < text.endIndex { + return cursorIndex + } else { + return text.endIndex + } + } else { + return text.startIndex + } } } @@ -88,8 +96,8 @@ extension MentionController { if text.isEmpty { mentions.removeAll() } - let cursorIndex = String.Index(encodedOffset: cursorPosition) - let canActivateFilter = (cursorIndex == text.endIndex || text[cursorIndex] == " ") + let cursorIndex = self.cursorIndex(for: cursorPosition, in: text) + let canActivateFilter = cursorIndex == text.endIndex || text[cursorIndex] == " " guard canActivateFilter, let startIndex = startIndexOfMention(in: text, cursorIndex: cursorIndex) else { filterHandler?(.none) @@ -114,13 +122,13 @@ extension MentionController { filterHandler?(filter.isEmpty ? .all : .filter(String(filter))) } - func handleReplacementText(_ replacementText: String, in range: NSRange, currentText text: String) { + func handleReplacementText(_ replacementText: String, in replacementRange: NSRange, currentText: String) -> Bool { var mentionsToRemove: [Mention] = [] - let range: Range = range.lowerBound.. = replacementRange.lowerBound..= range.lowerBound { @@ -129,6 +137,15 @@ extension MentionController { } } mentions.removeAll { mention in mentionsToRemove.contains { $0 === mention } } + + let isAlongsideAfterMention = mentions.contains { $0.indices.upperBound == replacementRange.upperBound } + + guard !mentionsToRemove.isEmpty || isAlongsideAfterMention else { + return true + } + replace(replacementText, in: replacementRange, currentText: currentText) + + return false } /* @@ -169,6 +186,22 @@ extension MentionController { return false } + + /// Manually replace text in output handler + private func replace(_ replacementText: String, in replacementRange: NSRange, currentText text: String) { + let updatedText = (text as NSString).replacingCharacters(in: replacementRange, with: replacementText) as String + + let newCursorPosition: Int + if replacementRange.length > replacementText.utf16.count { + // delete or replace + newCursorPosition = replacementRange.upperBound - (replacementRange.length - replacementText.utf16.count) + } else { + // insert + newCursorPosition = replacementRange.upperBound + (replacementText.utf16.count - replacementRange.length) + } + save(text: updatedText, cursorPosition: newCursorPosition) + handleUpdate(text: updatedText, cursor: newCursorPosition) + } } // MARK: - Insertion @@ -195,7 +228,7 @@ extension MentionController { accountId: params.accountId, alias: params.alias) - if !mentions.contains(where: { $0.indices == mention.indices }) { + if !mentions.contains(where: { $0.indices.intersects(mention.indices) }) { mentions.append(mention) } @@ -238,7 +271,7 @@ extension MentionController { private func shiftExistingMentions(by offset: Int, after position: Int) { for mention in mentions { - guard mention.indices.lowerBound >= position else { + guard mention.indices.lowerBound > position else { continue } mention.indices.shift(by: offset) diff --git a/Nynja/Modules/Message/Presenter/MessagePresenter.swift b/Nynja/Modules/Message/Presenter/MessagePresenter.swift index 228d86eb4..35505f2ee 100644 --- a/Nynja/Modules/Message/Presenter/MessagePresenter.swift +++ b/Nynja/Modules/Message/Presenter/MessagePresenter.swift @@ -10,7 +10,7 @@ import UIKit import CoreLocation.CLLocation -class MessagePresenter: BasePresenter, MessagePresenterProtocol, MessageInteractorOutputProtocol { +class MessagePresenter: BasePresenter, MessagePresenterProtocol, MessageInteractorOutputProtocol, ForwardSelectorDelegate { //MARK: - Properties var isMyselfChat: Bool { @@ -55,7 +55,15 @@ class MessagePresenter: BasePresenter, MessagePresenterProtocol, MessageInteract return ChannelChatItemsFactory(isActionsEnabled: isActionsEnabled) } } else { - return P2pChatItemsFactory(isActionsEnabled: isActionsEnabled, isPaymentEnable: false) + return P2pChatItemsFactory(isActionsEnabled: isActionsEnabled, isPaymentEnabled: false) { [weak self] in + let isBan = self?.interactor.contact?.isBan ?? false + + if isBan { + self?.blockMessageSending() + } + + return !isBan + } } } @@ -96,7 +104,7 @@ class MessagePresenter: BasePresenter, MessagePresenterProtocol, MessageInteract interactor.configure() if wasViewDisappeared { - let unreadCount = interactor.unreadCount + let unreadCount = Int(interactor.chat.unreadCount) if unreadCount != 0 { view.updateUnreadTitle(unreadCount) } @@ -284,7 +292,8 @@ class MessagePresenter: BasePresenter, MessagePresenterProtocol, MessageInteract } func forwardMessage(localId: String) { - wireFrame.showForwardSelector(with: localId) + interactor.prepareToForward(localId: localId) + wireFrame.showForwardSelector(with: localId, delegate: self) view.endInputBarInteraction() } @@ -323,9 +332,7 @@ class MessagePresenter: BasePresenter, MessagePresenterProtocol, MessageInteract guard let `self` = self, case .lang = selectedLang else { return } - self.interactor.chooseTranscriptionLanguageForMessage(with: localId, lang: selectedLang, failure: { - self.showChooseAnotherAlert(localId: localId, language: language) - }) + self.interactor.chooseTranscriptionLanguageForMessage(with: localId, lang: selectedLang) }, saveLangHandler: nil, isNoneAvailable: false) @@ -356,6 +363,11 @@ class MessagePresenter: BasePresenter, MessagePresenterProtocol, MessageInteract wireFrame.showLanguageSelector(input: input) } + func showChooseAnotherLanguageAlert(localId: String, language: String) { + showChooseAnotherAlert(localId: localId, + language: SelectedLang.lang(.init(language: language, name: ""))) + } + func showChooseAnotherAlert(localId: String, language: SelectedLang) { DispatchQueue.main.async { AlertManager.sharedInstance.showAlertWithTwoActions(title: "transcribe_error_title".localized, message: "transcribe_error_message".localized, firstActionTitle: "transcribe_error_action".localized, secondActionTitle: "ok".localized, firstAction: { [weak self] in @@ -368,10 +380,7 @@ class MessagePresenter: BasePresenter, MessagePresenterProtocol, MessageInteract } self.interactor.chooseTranscriptionLanguageForMessage(with: localId, - lang: selectedLang, - failure: { - self.showChooseAnotherAlert(localId: localId, language: language) - }) + lang: selectedLang) }, saveLangHandler: nil, isNoneAvailable: false) self.wireFrame.showLanguageSelector(input: input) @@ -380,11 +389,7 @@ class MessagePresenter: BasePresenter, MessagePresenterProtocol, MessageInteract } func transcribeMessage(localId: String) { - interactor.transcribeMessage(localId: localId, lang: nil) { [weak self] in - self?.interactor.detectMessageLanguage(with: localId, convertionType: .transcribe, success: { language in - self?.showChooseAnotherAlert(localId: localId, language: language) - }) - } + interactor.transcribeMessage(localId: localId, lang: nil) } func untranscribeMessage(localId: String) { @@ -422,11 +427,15 @@ class MessagePresenter: BasePresenter, MessagePresenterProtocol, MessageInteract // MARK: MessageInteractorOutputProtocol func chatUpdated(_ chat: ChatModel) { + var name: String? = "" if let room = chat as? Room { updateMentionsCounter(in: room) + name = room.name } - - let avatarModel = AvatarViewModel(title: chat.name, avatarUrl: chat.avatarUrl, isMuted: chat.isMuted) + if let c = chat as? Contact { + name = c.fullName + } + let avatarModel = AvatarViewModel(title: name, avatarUrl: chat.avatarUrl, isMuted: chat.isMuted) view.updateHeader(with: avatarModel) } @@ -561,30 +570,27 @@ class MessagePresenter: BasePresenter, MessagePresenterProtocol, MessageInteract var unreadIndex: Int? var time: Date! - for i in 0.. 0 { let model = BaseChatCellModel.unreadModel() @@ -633,27 +640,23 @@ class MessagePresenter: BasePresenter, MessagePresenterProtocol, MessageInteract var cells = [BaseChatCellModel]() var time: Date! - for i in 0.. Bool { @@ -982,6 +989,10 @@ class MessagePresenter: BasePresenter, MessagePresenterProtocol, MessageInteract func hasRunningCall() -> Bool { return interactor.hasRunningCall() } + + func hasCallInProgress() -> Bool { + return interactor.hasCallInProgress() + } func rejoinRunningCall() { interactor.rejoinRunningCall() @@ -990,6 +1001,14 @@ class MessagePresenter: BasePresenter, MessagePresenterProtocol, MessageInteract func openMarketplaceScreen() { self.wireFrame.openMarketplaceScreen() } + + + // MAKR: - ForwardSelectorDelegate + + func didSelectForwardTargets(_ targets: ForwardTargets) { + interactor.didSelectForwardTargets(targets) + } + } @@ -1076,4 +1095,5 @@ private extension MessagePresenter { deleteForOthers: deleteMessageForAll) } } + } diff --git a/Nynja/Modules/Message/Protocols/MessageProtocols.swift b/Nynja/Modules/Message/Protocols/MessageProtocols.swift index 898a1d914..ed7dd7ad8 100644 --- a/Nynja/Modules/Message/Protocols/MessageProtocols.swift +++ b/Nynja/Modules/Message/Protocols/MessageProtocols.swift @@ -21,7 +21,7 @@ protocol MessageWireframeProtocol: DocumentInteractionInput { func showProfileScreen(contact: Contact) func showMyProfileScreen() - func showForwardSelector(with localId: String) + func showForwardSelector(with localId: String, delegate: ForwardSelectorDelegate) func presentMessages(navigation: UINavigationController, chat: ChatModel, @@ -123,6 +123,7 @@ protocol MessagePresenterProtocol: BasePresenterProtocol, func handleOpponentAvatarTap(for sender: MessageSender) func hasRunningCall() -> Bool + func hasCallInProgress() -> Bool func rejoinRunningCall() func openMarketplaceScreen() } @@ -137,7 +138,7 @@ protocol MessageInteractorOutputProtocol: class, MentionFetchOutputProtocol, Mes func updateTranslationProgress(_ progress: ConvertionProgressModel) func updateProgress(_ model: ProgressModel) func updateMessage(with configuration: MessageConfiguration) - func removeMessage(_ messageId: String) + func removeMessage(_ messageId: MessageLocalId) func chatUpdated(_ chat: ChatModel) func updateTranslationPreview(selectedLang: SelectedLang, isAuto: Bool) @@ -147,8 +148,8 @@ protocol MessageInteractorOutputProtocol: class, MentionFetchOutputProtocol, Mes func actionStatusChanged(_ status: ActionStatus) func restoreStatus() - func messageSent(_ localId: String) - func messageRead(_ localId: String) + func messageSent(_ localId: MessageLocalId) + func messageRead(_ localId: MessageLocalId) func startSendingMessage() @@ -159,14 +160,17 @@ protocol MessageInteractorOutputProtocol: class, MentionFetchOutputProtocol, Mes func setupInputInChannnel(for role: Room.Role) func deleteAndLeave() - func changeStarStatus(star: Star, localId: String) + func starMessage(with localId: MessageLocalId, starId: MessageLocalId) + func unstarMessage(with localId: MessageLocalId) func removeEditMessageAppearance() func scrollToBottomIfNeeded() - func scrollToOriginOfForward(originLocalId: String) - func showChat(_ chat: ChatModel, originLocalId: String?) + func scrollToOriginOfForward(originLocalId: MessageLocalId) + func showChat(_ chat: ChatModel, originLocalId: MessageLocalId?) func didChangeCallInvitationState(_ call: NYNCall) + + func showChooseAnotherLanguageAlert(localId: MessageLocalId, language: String) } @@ -184,7 +188,6 @@ protocol MessageInteractorInputProtocol: BaseInteractorProtocol, MentionFetchInp var isMyselfChat: Bool { get } var currentPhoneId: String? { get } - var unreadCount: Int { get } var repliedMessage: Message? { get } var editingMessage: Message? { get } @@ -222,10 +225,11 @@ protocol MessageInteractorInputProtocol: BaseInteractorProtocol, MentionFetchInp func detectMessageLanguage(with localId: String, convertionType: ConvertionMessageModel.`Type`, success: (SelectedLang)->Void) - func starMessage(localId: String) - func unstarMessage(localId: String) - func deleteMessage(localId: String, forBoth: Bool) - func prepareToReply(localId: String) + func starMessage(localId: MessageLocalId) + func unstarMessage(localId: MessageLocalId) + func deleteMessage(localId: MessageLocalId, forBoth: Bool) + func prepareToReply(localId: MessageLocalId) + func processForwardMessageTap(serverId: MessageServerId) func declineReply() @@ -241,7 +245,11 @@ protocol MessageInteractorInputProtocol: BaseInteractorProtocol, MentionFetchInp func getMessageSenderContact(for sender: MessageSender) -> Contact? func hasRunningCall() -> Bool + func hasCallInProgress() -> Bool func rejoinRunningCall() + + func prepareToForward(localId: MessageLocalId) + func didSelectForwardTargets(_ targets: ForwardTargets) } //MARK: View - @@ -291,7 +299,7 @@ protocol MessageViewProtocol: class { func removeEditMessageAppearance() func prepareEditMessageUI(with model: RepliedMessageModel) - func displayRejoinBanner(display: Bool, count: Int) + func displayRejoinBanner(display: Bool, count: UInt) } protocol MessageInteractorCallProtocol: class { diff --git a/Nynja/Modules/Message/Protocols/VoiceAudioInteractive.swift b/Nynja/Modules/Message/Protocols/VoiceAudioInteractive.swift new file mode 100644 index 000000000..5a51440ce --- /dev/null +++ b/Nynja/Modules/Message/Protocols/VoiceAudioInteractive.swift @@ -0,0 +1,118 @@ +// +// VoiceAudioInteractive.swift +// Nynja +// +// Created by Anton Poltoratskyi on 30.08.2018. +// Copyright © 2018 TecSynt Solutions. All rights reserved. +// + +import Foundation + +protocol VoiceAudioInteractive: class { + var audioManager: AudioManager { get } + var proximityManager: ProximitySensorManager { get } + var currentPlayingModel: BaseChatCellModel? { get set } +} + +// MARK: - Audio Actions +extension VoiceAudioInteractive { + + func resume() { + currentPlayingModel?.playStatus = .play + currentPlayingModel?.notifyAudioHandler() + audioManager.resume() + } + + func pause() { + currentPlayingModel?.playStatus = .pause + currentPlayingModel?.notifyAudioHandler() + audioManager.pause() + } + + func stop() { + currentPlayingModel?.audioCurrentTime = nil + currentPlayingModel?.playStatus = .stop + currentPlayingModel?.notifyAudioHandler() + currentPlayingModel = nil + } + + func stopPlaying() { + audioManager.stop() + proximityManager.monitoringState = .disabled + // audioManager.speaker = .loud + stop() + } +} + +// MARK: - Proximity Sensor +extension VoiceAudioInteractive where Self: ProximitySensorManagerDelegate { + + func proximityStateChanged(_ manager: ProximitySensorManager, state: ProximityState) { + switch state { + case .closeToUser: + audioManager.speaker = .soft + + if !audioManager.isPlaying { + resume() + } + case .farFromUser: + if audioManager.isPlaying { + pause() + } + } + } +} + +// MARK: - Audio Manager +extension VoiceAudioInteractive where Self: AudioManagerDelegate { + + func didFinishPlayingAudio(_ audioManager: AudioManager, with url: URL) { + stopPlaying() + } + + func didChangedCurrentTime(_ audioManager: AudioManager, currentTime: TimeInterval) { + currentPlayingModel?.audioDuration = audioManager.currentDuration + currentPlayingModel?.audioCurrentTime = currentTime + currentPlayingModel?.notifyAudioHandler() + } +} + +// MARK: - User Actions +extension VoiceAudioInteractive where Self: BaseChatCellDelegate { + + func didPlayTapped(_ cell: BaseChatCell, url: URL) { + proximityManager.monitoringState = .enabled + audioManager.speaker = .loud + + if audioManager.currentUrl != url { + stop() + + if let _ = try? audioManager.play(with: url) { + currentPlayingModel = cell.model + currentPlayingModel?.playStatus = .play + currentPlayingModel?.notifyAudioHandler() + } + } else { + currentPlayingModel = cell.model + resume() + } + } + + func didPauseTapped(_ cell: BaseChatCell, url: URL) { + pause() + } + + func didChangeProgress(_ cell: BaseChatCell, url: URL, progress: Double) { + if audioManager.currentUrl != url { + stopPlaying() + currentPlayingModel = cell.model + currentPlayingModel?.playStatus = .stop + currentPlayingModel?.notifyAudioHandler() + } + audioManager.changedProgress(progress, for: url) + } + + func isCanChangeProgress(_ cell: BaseChatCell, url: URL) -> Bool { + return true + } +} diff --git a/Nynja/Modules/Message/View/MessageVC+CellDelegate.swift b/Nynja/Modules/Message/View/MessageVC+CellDelegate.swift index 413192a6e..64ecea94f 100644 --- a/Nynja/Modules/Message/View/MessageVC+CellDelegate.swift +++ b/Nynja/Modules/Message/View/MessageVC+CellDelegate.swift @@ -8,7 +8,7 @@ import Foundation -extension MessageVC: BaseChatCellDelegate, AudioManagerDelegate, ProximitySensorManagerDelegate { +extension MessageVC: VoiceAudioInteractive, BaseChatCellDelegate, AudioManagerDelegate, ProximitySensorManagerDelegate { // MARK: - BaseChatCellDelegate func didCellTapped(_ cell: BaseChatCell) { @@ -74,45 +74,10 @@ extension MessageVC: BaseChatCellDelegate, AudioManagerDelegate, ProximitySensor return presenter.isThumbnailProcessingFor(url) } - func didPlayTapped(_ cell: BaseChatCell, url: URL) { - proximityManager.monitoringState = .enabled - audioManager.speaker = .loud - - if audioManager.currentUrl != url { - stop() - - if let _ = try? audioManager.play(with: url) { - currentPlayingModel = cell.model - currentPlayingModel?.playStatus = .play - currentPlayingModel?.notifyAudioHandler() - } - } else { - currentPlayingModel = cell.model - resume() - } - } - - func didPauseTapped(_ cell: BaseChatCell, url: URL) { - pause() - } - - func didChangeProgress(_ cell: BaseChatCell, url: URL, progress: Double) { - if audioManager.currentUrl != url { - stopPlaying() - currentPlayingModel = cell.model - currentPlayingModel?.playStatus = .stop - currentPlayingModel?.notifyAudioHandler() - } - audioManager.changedProgress(progress, for: url) - } - - func isCanChangeProgress(_ cell: BaseChatCell, url: URL) -> Bool { - return true - } - func didReplyCounterTapped(_ cell: BaseChatCell) { if let id = cell.model?.id { presenter.openReplies(messageId: id) + endInputBarInteraction() } } @@ -126,68 +91,11 @@ extension MessageVC: BaseChatCellDelegate, AudioManagerDelegate, ProximitySensor // presenter.forwardMessageTapped(serverId: serverId) [NY-1981] } - - // MARK: - ProximitySensorManagerDelegate - - func proximityStateChanged(_ manager: ProximitySensorManager, state: ProximityState) { - switch state { - case .closeToUser: - audioManager.speaker = .soft - - if !audioManager.isPlaying { - resume() - } - case .farFromUser: - if audioManager.isPlaying { - pause() - } - } - } - - - // MARK: - AudioManagerDelegate - - func didFinishPlayingAudio(_ audioManager: AudioManager, with url: URL) { - stopPlaying() - } - - func didChangedCurrentTime(_ audioManager: AudioManager, currentTime: TimeInterval) { - currentPlayingModel?.audioDuration = audioManager.currentDuration - currentPlayingModel?.audioCurrentTime = currentTime - currentPlayingModel?.notifyAudioHandler() - } - - // MARK: Audio Actions - - private func resume() { - currentPlayingModel?.playStatus = .play - currentPlayingModel?.notifyAudioHandler() - audioManager.resume() - } - - private func pause() { - currentPlayingModel?.playStatus = .pause - currentPlayingModel?.notifyAudioHandler() - audioManager.pause() - } - - func stopPlaying() { - audioManager.stop() - proximityManager.monitoringState = .disabled - // audioManager.speaker = .loud - stop() - } - - private func stop() { - currentPlayingModel?.audioCurrentTime = nil - currentPlayingModel?.playStatus = .stop - currentPlayingModel?.notifyAudioHandler() - currentPlayingModel = nil - } - func opponentAvatarTapped(_ cell: BaseChatCell) { - inputBar.endEditing(true) - guard let sender = cell.model?.sender else { return } + guard let sender = cell.model?.sender else { + return + } + endInputBarInteraction() self.presenter.handleOpponentAvatarTap(for: sender) } } diff --git a/Nynja/Modules/Message/View/MessageVC.swift b/Nynja/Modules/Message/View/MessageVC.swift index 87c7775e1..6825a8dde 100644 --- a/Nynja/Modules/Message/View/MessageVC.swift +++ b/Nynja/Modules/Message/View/MessageVC.swift @@ -26,7 +26,7 @@ final class MessageVC: BaseVC, MessageViewProtocol, ReplyPreviewDelegate, BackSw } } - var progressDictionary = [String: [(ProgressModel)->Void]]() + var progressDictionary = [ProgressIdentifier: [(ProgressModel)->Void]]() var loadingStatus = false @@ -98,8 +98,8 @@ final class MessageVC: BaseVC, MessageViewProtocol, ReplyPreviewDelegate, BackSw } } - private let collectionViewLayout: MessageCollectionViewLayout = { - let layout = MessageCollectionViewLayout() + private let collectionViewLayout: ReversedMessageCollectionViewLayout = { + let layout = ReversedMessageCollectionViewLayout() layout.minimumLineSpacing = 0 return layout }() @@ -181,9 +181,9 @@ final class MessageVC: BaseVC, MessageViewProtocol, ReplyPreviewDelegate, BackSw } view.addSubview(inputBar) - inputBar.snp.makeConstraints { make in - make.left.right.equalToSuperview() - adjustVerticalInset(.bottom, make: make, offset: 0) + inputBar.snp.makeConstraints { maker in + maker.left.right.equalToSuperview() + maker.bottom.equalTo(view.keyboardLayoutGuide.snp.top) } return inputBar @@ -393,8 +393,9 @@ final class MessageVC: BaseVC, MessageViewProtocol, ReplyPreviewDelegate, BackSw automaticallyAdjustsScrollViewInsets = false - self.swipeBackHelper.addGesture() + swipeBackHelper.addGesture() + stickerModuleInput.configure() audioManager.delegate = self proximityManager.delegate = self @@ -440,7 +441,7 @@ final class MessageVC: BaseVC, MessageViewProtocol, ReplyPreviewDelegate, BackSw center.addObserver(self, selector: #selector(didEnterBackground), name: .UIApplicationDidEnterBackground, object: nil) center.addObserver(self, selector: #selector(willResignActive), name: .UIApplicationWillResignActive, object: nil) - center.addObserver(self, selector: #selector(didBecomeActive), name: .UIApplicationDidBecomeActive, object: nil) + collectionView.layoutIfNeeded() presenter.viewDidAppear() @@ -450,10 +451,6 @@ final class MessageVC: BaseVC, MessageViewProtocol, ReplyPreviewDelegate, BackSw goAway() } - @objc private func didBecomeActive() { - goAway() - } - override func viewDidDisappear(_ animated: Bool) { super.viewDidDisappear(animated) if viewVisible { @@ -463,7 +460,7 @@ final class MessageVC: BaseVC, MessageViewProtocol, ReplyPreviewDelegate, BackSw NotificationCenter.default.removeObserver(self, name: .UIApplicationDidEnterBackground, object: nil) NotificationCenter.default.removeObserver(self, name: .UIApplicationWillResignActive, object: nil) - NotificationCenter.default.removeObserver(self, name: .UIApplicationDidBecomeActive, object: nil) + goAway() @@ -503,14 +500,6 @@ final class MessageVC: BaseVC, MessageViewProtocol, ReplyPreviewDelegate, BackSw isAllowedEndEditing = true } - - override func keyboardNotified(endFrame: CGRect) { - if endFrame.origin.y >= UIScreen.main.bounds.size.height { - updateToHide(view: inputBar, offset: 0) - } else { - updateToShow(view: inputBar, offset: -endFrame.height) - } - } @objc private func wheelDidOpen() { endInputBarInteraction() @@ -542,7 +531,8 @@ final class MessageVC: BaseVC, MessageViewProtocol, ReplyPreviewDelegate, BackSw } inputBar.textRangeReplaceHandler = { [weak self] textView, range, replacementText in - self?.handleReplacement(in: textView, range: range, replacementText: replacementText) + guard let `self` = self else { return false } + return self.handleReplacement(in: textView, range: range, replacementText: replacementText) } inputBar.shouldBeginEditingHandler = { [weak self] in @@ -592,7 +582,7 @@ final class MessageVC: BaseVC, MessageViewProtocol, ReplyPreviewDelegate, BackSw for mention in mentions { attributedString.setAttributes(mentionAttributes, range: mention.indices.nsRange) } - self?.inputBar.updateInputText(attributedString) + self?.inputBar.updateInputText(attributedString.copy() as! NSAttributedString) } mentionController.cursorUpdateHandler = { [weak self] cursorPosition in @@ -661,13 +651,13 @@ final class MessageVC: BaseVC, MessageViewProtocol, ReplyPreviewDelegate, BackSw DispatchQueue.main.asyncAfter(deadline: .now() + 0.25, execute: stickerSearchWorkItem!) } - private func handleReplacement(in textView: UITextView, range: NSRange, replacementText: String) { + private func handleReplacement(in textView: UITextView, range: NSRange, replacementText: String) -> Bool { switch inputBar.displayMode { case .new, .edit: let currentText = textView.text ?? "" - mentionController.handleReplacementText(replacementText, in: range, currentText: currentText) + return mentionController.handleReplacementText(replacementText, in: range, currentText: currentText) case .stickerSearch, .action, .hide: - break + return true } } @@ -796,7 +786,7 @@ final class MessageVC: BaseVC, MessageViewProtocol, ReplyPreviewDelegate, BackSw } @objc func avatarPressed() { - inputBar.endEditing(true) + endInputBarInteraction() presenter.avatarTapped() } @@ -918,7 +908,7 @@ final class MessageVC: BaseVC, MessageViewProtocol, ReplyPreviewDelegate, BackSw } func openAddContactScreen(contact: Contact) { - inputBar.endEditing(true) + endInputBarInteraction() presenter.openSharedContact(contact: contact) } @@ -951,10 +941,13 @@ final class MessageVC: BaseVC, MessageViewProtocol, ReplyPreviewDelegate, BackSw func updateProgress(progressModel: ProgressModel?) { DispatchQueue.main.async { - guard let url = progressModel?.url.absoluteString else { return } - guard let blocks = self.progressDictionary[url] else { return } - guard let model = progressModel else { return } - blocks.forEach { $0(model) } + guard let url = progressModel?.url.absoluteString, let model = progressModel else { + return + } + self.progressDictionary.keys.forEach { + guard $0.url == url else { return } + self.progressDictionary[$0]?.forEach { block in block(model) } + } } } @@ -963,7 +956,7 @@ final class MessageVC: BaseVC, MessageViewProtocol, ReplyPreviewDelegate, BackSw let id = progressModel?.id self.messageDS.forEach { guard $0.id == id else { return } - $0.translationProgressModel = progressModel + $0.convertProgressModel = progressModel } self.collectionView.reloadData() } @@ -1078,13 +1071,13 @@ final class MessageVC: BaseVC, MessageViewProtocol, ReplyPreviewDelegate, BackSw guard let index = messageDS.index(where: { $0.id == messageId }) else { return } - if status != .read { - let model = messageDS.cellModel(at: index) + let model = messageDS.cellModel(at: index) + if status != .read, model.deliveryStatus != .read { model.deliveryStatus = status - collectionView.reloadData() } else { markMesssagesAsRead(from: index) } + collectionView.reloadData() } func updateHeader(with viewModel: AvatarViewModel) { @@ -1166,7 +1159,7 @@ final class MessageVC: BaseVC, MessageViewProtocol, ReplyPreviewDelegate, BackSw inputBar.displayMode = .edit(text) } - func displayRejoinBanner(display: Bool, count: Int) { + func displayRejoinBanner(display: Bool, count: UInt) { // Use bottom inset, because collectionView has reversed transform: // collectionView.transform = CGAffineTransform(rotationAngle: .pi) var inset = defaultCollectionViewInsets.bottom @@ -1302,7 +1295,6 @@ final class MessageVC: BaseVC, MessageViewProtocol, ReplyPreviewDelegate, BackSw } model.deliveryStatus = .read } - collectionView.reloadData() } private var checkpoint: ChatCheckpoint? { @@ -1340,28 +1332,30 @@ final class MessageVC: BaseVC, MessageViewProtocol, ReplyPreviewDelegate, BackSw } @objc private func willEnterForeground() { - viewVisible = false - goAway() + viewVisible = true + removeUnreadTitle() presenter.appWillEnterForeground() } @objc private func didEnterBackground() { - viewVisible = true - removeUnreadTitle() + viewVisible = false + goAway() presenter.appDidEnterBackground() } func removeMessage(_ messageId: String) { - guard let messageDS = self.messageDS, let index = messageDS.index(where: { $0.id == messageId }) else { + guard let messageDS = messageDS else { return } - let indexPath = IndexPath(item: index, section: 0) - let offset = collectionViewLayout.additionalOffsetForDeletingItem(at: indexPath) - - messageDS.remove(at: index) - collectionView.reloadData() - - collectionViewLayout.applyOffsetIfNeeded(offset: offset) + let indexes = messageDS.indexesForDelete(where: { $0.id == messageId }) + if !indexes.isEmpty { + let offset = collectionViewLayout.additionalOffsetForDeletingItems(at: indexes.map { IndexPath(item: $0, section: 0) }) + + messageDS.remove(at: indexes) + collectionView.reloadData() + + collectionViewLayout.applyOffsetIfNeeded(offset: offset) + } } func getNextPage() { diff --git a/Nynja/Modules/Message/View/MessageVCLayout.swift b/Nynja/Modules/Message/View/MessageVCLayout.swift index ea830de44..a14a149fa 100644 --- a/Nynja/Modules/Message/View/MessageVCLayout.swift +++ b/Nynja/Modules/Message/View/MessageVCLayout.swift @@ -17,7 +17,7 @@ extension MessageVC { enum tableView { static let defaultVerticalInset = CGFloat(4.adjustedByWidth) - static let bottomInset = CGFloat(16.adjustedByWidth) + static let bottomInset = CGFloat(gradientView.height) } enum replyPreview { diff --git a/Nynja/Modules/Message/View/Views/CallInfoView/CallInfoView.swift b/Nynja/Modules/Message/View/Views/CallInfoView/CallInfoView.swift index 420a2d787..9c3514894 100644 --- a/Nynja/Modules/Message/View/Views/CallInfoView/CallInfoView.swift +++ b/Nynja/Modules/Message/View/Views/CallInfoView/CallInfoView.swift @@ -16,7 +16,7 @@ class CallInfoView: UIView { weak var delegate: CallInfoViewDelegate? - var membersCount: Int = 0 { + var membersCount: UInt = 0 { didSet { participantsCount.text = text(for: membersCount) } @@ -133,7 +133,7 @@ class CallInfoView: UIView { self.backgroundColor = Constants.colors.sectionBackgroundColor.getColor() } - private func text(for membersCount: Int) -> String { + private func text(for membersCount: UInt) -> String { return String(format: "call_info_banner_members".localized, membersCount) } diff --git a/Nynja/Modules/Message/View/Views/CollectionView/Attributes/MessageCollectionViewLayoutAttributes.swift b/Nynja/Modules/Message/View/Views/CollectionView/Attributes/MessageCollectionViewLayoutAttributes.swift new file mode 100644 index 000000000..bea0ac7bd --- /dev/null +++ b/Nynja/Modules/Message/View/Views/CollectionView/Attributes/MessageCollectionViewLayoutAttributes.swift @@ -0,0 +1,16 @@ +// +// MessageCollectionViewLayoutAttributes.swift +// Nynja +// +// Created by Anton Poltoratskyi on 27.08.2018. +// Copyright © 2018 TecSynt Solutions. All rights reserved. +// + +import UIKit + +class MessageCollectionViewLayoutAttributes: UICollectionViewLayoutAttributes { + + var isReversed: Bool { + return false + } +} diff --git a/Nynja/Modules/Message/View/Views/CollectionView/Attributes/ReversedMessageCollectionViewLayoutAttributes.swift b/Nynja/Modules/Message/View/Views/CollectionView/Attributes/ReversedMessageCollectionViewLayoutAttributes.swift new file mode 100644 index 000000000..ddcba7678 --- /dev/null +++ b/Nynja/Modules/Message/View/Views/CollectionView/Attributes/ReversedMessageCollectionViewLayoutAttributes.swift @@ -0,0 +1,16 @@ +// +// ReversedMessageCollectionViewLayoutAttributes.swift +// Nynja +// +// Created by Anton Poltoratskyi on 27.08.2018. +// Copyright © 2018 TecSynt Solutions. All rights reserved. +// + +import UIKit + +final class ReversedMessageCollectionViewLayoutAttributes: MessageCollectionViewLayoutAttributes { + + override var isReversed: Bool { + return true + } +} diff --git a/Nynja/Modules/Message/View/Views/CollectionView/Layout/MessageCollectionViewLayout.swift b/Nynja/Modules/Message/View/Views/CollectionView/Layout/MessageCollectionViewLayout.swift new file mode 100644 index 000000000..bda91a646 --- /dev/null +++ b/Nynja/Modules/Message/View/Views/CollectionView/Layout/MessageCollectionViewLayout.swift @@ -0,0 +1,42 @@ +// +// MessageCollectionViewLayout.swift +// Nynja +// +// Created by Anton Poltoratskyi on 08.08.2018. +// Copyright © 2018 TecSynt Solutions. All rights reserved. +// + +import UIKit + +class MessageCollectionViewLayout: UICollectionViewFlowLayout { + + + // MARK: - Layout + + override class var layoutAttributesClass: Swift.AnyClass { + return MessageCollectionViewLayoutAttributes.self + } + + override func shouldInvalidateLayout(forBoundsChange newBounds: CGRect) -> Bool { + if let currentBounds = collectionView?.bounds { + return currentBounds.width != newBounds.width + } else { + return false + } + } + + // Please, don't remove - it will be needed to handle batch updates in the future. +// override func prepare(forCollectionViewUpdates updateItems: [UICollectionViewUpdateItem]) { +// offset = 0 +// super.prepare(forCollectionViewUpdates: updateItems) +// +// for item in updateItems { +// offset += additionalOffset(for: item) +// } +// } +// +// override func finalizeCollectionViewUpdates() { +// super.finalizeCollectionViewUpdates() +// applyOffsetIfNeeded(offset: offset) +// } +} diff --git a/Nynja/Modules/Message/View/Views/CollectionView/MessageCollectionViewLayout.swift b/Nynja/Modules/Message/View/Views/CollectionView/Layout/ReversedMessageCollectionViewLayout.swift similarity index 54% rename from Nynja/Modules/Message/View/Views/CollectionView/MessageCollectionViewLayout.swift rename to Nynja/Modules/Message/View/Views/CollectionView/Layout/ReversedMessageCollectionViewLayout.swift index 4a34812da..f62c917ad 100644 --- a/Nynja/Modules/Message/View/Views/CollectionView/MessageCollectionViewLayout.swift +++ b/Nynja/Modules/Message/View/Views/CollectionView/Layout/ReversedMessageCollectionViewLayout.swift @@ -1,14 +1,14 @@ // -// MessageCollectionViewLayout.swift +// ReversedMessageCollectionViewLayout.swift // Nynja // -// Created by Anton Poltoratskyi on 08.08.2018. +// Created by Anton Poltoratskyi on 27.08.2018. // Copyright © 2018 TecSynt Solutions. All rights reserved. // import UIKit -final class MessageCollectionViewLayout: UICollectionViewFlowLayout { +final class ReversedMessageCollectionViewLayout: MessageCollectionViewLayout { private var offset: CGFloat = 0 @@ -17,56 +17,13 @@ final class MessageCollectionViewLayout: UICollectionViewFlowLayout { return collectionView.contentOffset.y + collectionView.contentInset.top } - override init() { - super.init() - setup() - } - - required init?(coder aDecoder: NSCoder) { - super.init(coder: aDecoder) - setup() - } - - private func setup() {} - - override func shouldInvalidateLayout(forBoundsChange newBounds: CGRect) -> Bool { - if let currentBounds = collectionView?.bounds { - return currentBounds.width != newBounds.width - } else { - return false - } - } - - override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? { - guard let attributes = super.layoutAttributesForElements(in: rect) else { - return nil - } - for attr in attributes { - attr.transform = CGAffineTransform(rotationAngle: .pi) - } - return attributes - } - - override func layoutAttributesForItem(at indexPath: IndexPath) -> UICollectionViewLayoutAttributes? { - let attributes = super.layoutAttributesForItem(at: indexPath) - attributes?.transform = CGAffineTransform(rotationAngle: .pi) - return attributes - } - override func prepare(forCollectionViewUpdates updateItems: [UICollectionViewUpdateItem]) { - offset = 0 - super.prepare(forCollectionViewUpdates: updateItems) - - for item in updateItems { - offset += additionalOffset(for: item) - } - } + // MARK: - Layout - override func finalizeCollectionViewUpdates() { - super.finalizeCollectionViewUpdates() - applyOffsetIfNeeded(offset: offset) + override class var layoutAttributesClass: Swift.AnyClass { + return ReversedMessageCollectionViewLayoutAttributes.self } - + // MARK: - Utils @@ -113,4 +70,10 @@ final class MessageCollectionViewLayout: UICollectionViewFlowLayout { } return -(attributes.size.height + minimumLineSpacing) } + + func additionalOffsetForDeletingItems(at indexPaths: [IndexPath]) -> CGFloat { + return indexPaths + .map { additionalOffsetForDeletingItem(at: $0) } + .reduce(into: CGFloat(0)) { $0 = $0 + $1 } + } } diff --git a/Nynja/Modules/Message/View/Views/CollectionView/MessageCollectionViewDataSource.swift b/Nynja/Modules/Message/View/Views/CollectionView/MessageCollectionViewDataSource.swift index 67090cef2..e07023d47 100644 --- a/Nynja/Modules/Message/View/Views/CollectionView/MessageCollectionViewDataSource.swift +++ b/Nynja/Modules/Message/View/Views/CollectionView/MessageCollectionViewDataSource.swift @@ -75,6 +75,21 @@ final class MessageCollectionViewDataSource: NSObject { } } + func indexesForDelete(where predicate: (BaseChatCellModel) -> Bool) -> [Int] { + guard let presentationIndexRemovingCell = index(where: predicate) else { + return [] + } + let dataIndexRemovingCell = dataIndex(from: presentationIndexRemovingCell) + + //Check if previous cell is date cell then remove it + let dataIndexPreviousCell = dataIndexRemovingCell - 1 + guard cells.count == (dataIndexRemovingCell + 1), dataIndexPreviousCell >= 0, cells[dataIndexPreviousCell].time != nil else { + return [presentationIndexRemovingCell] + } + let presentationIndexPreviousCell = presentationIndex(from: dataIndexPreviousCell) + return [presentationIndexPreviousCell, presentationIndexRemovingCell] + } + func first(where predicate: (BaseChatCellModel) -> Bool) -> BaseChatCellModel? { return isReversed ? cells.last(where: predicate) : cells.first(where: predicate) } @@ -105,7 +120,22 @@ final class MessageCollectionViewDataSource: NSObject { } func addNewMessage(_ cell: BaseChatCellModel) { - cells.append(cell) + guard let lastCell = cells.last, + let lastTime = lastCell.timeStamp, + let time = cell.timeStamp else { + cells.append(cell) + return + } + + var newCells = [cell] + + if lastCell.time == nil && !lastTime.isDayEqual(to: time) { + let timestamp = BaseChatCellModel() + timestamp.time = time + newCells.insert(timestamp, at: 0) + } + + cells.append(contentsOf: newCells) } func insert(_ cell: BaseChatCellModel, at index: Int) { @@ -117,6 +147,10 @@ final class MessageCollectionViewDataSource: NSObject { cells.remove(at: dataIndex(from: index)) } + func remove(at indexes: [Int]) { + indexes.forEach { remove(at: $0) } + } + func move(at index: Int, to newIndex: Int) { let index = dataIndex(from: index) let newIndex = dataIndex(from: newIndex) diff --git a/Nynja/Modules/Message/View/Views/CollectionView/MessageCollectionViewDelegate.swift b/Nynja/Modules/Message/View/Views/CollectionView/MessageCollectionViewDelegate.swift index 28ae329a3..422da5f56 100644 --- a/Nynja/Modules/Message/View/Views/CollectionView/MessageCollectionViewDelegate.swift +++ b/Nynja/Modules/Message/View/Views/CollectionView/MessageCollectionViewDelegate.swift @@ -13,6 +13,8 @@ enum ScrollDirection { case bottom } + + final class MessageCollectionViewDelegate: NSObject, UICollectionViewDelegateFlowLayout { unowned var view: MessageVC @@ -34,7 +36,7 @@ final class MessageCollectionViewDelegate: NSObject, UICollectionViewDelegateFlo guard let cell = cell as? BaseChatCell, let url = cell.model?.progressModel?.url.absoluteString else { return } - view.progressDictionary.removeValue(forKey: url) + view.progressDictionary.removeValue(forKey: .init(id: cell.id, url: url)) } func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize { @@ -133,10 +135,12 @@ final class MessageCollectionViewDelegate: NSObject, UICollectionViewDelegateFlo let block: (ProgressModel) -> Void = { model in cell.updateProgressClosure(model: model) } - if view.progressDictionary[url] == nil { - view.progressDictionary[url] = [block] + + let identifier = ProgressIdentifier(id: cell.id, url: url) + if view.progressDictionary[identifier] == nil { + view.progressDictionary[identifier] = [block] } else { - view.progressDictionary[url]?.append(block) + view.progressDictionary[identifier]?.append(block) } } diff --git a/Nynja/Modules/Message/View/Views/CollectionView/ProgressIdentifier.swift b/Nynja/Modules/Message/View/Views/CollectionView/ProgressIdentifier.swift new file mode 100644 index 000000000..6947400e4 --- /dev/null +++ b/Nynja/Modules/Message/View/Views/CollectionView/ProgressIdentifier.swift @@ -0,0 +1,18 @@ +// +// ProgressIdentifier.swift +// Nynja +// +// Created by Andrey Reznik on 15.08.2018. +// Copyright © 2018 TecSynt Solutions. All rights reserved. +// + +import Foundation + +struct ProgressIdentifier: Hashable { + let id: ObjectIdentifier + let url: String + + var hashValue: Int { + return id.hashValue + url.hashValue + } +} diff --git a/Nynja/Modules/Message/View/Views/MentionPanelView/Panel/MentionPanelView.swift b/Nynja/Modules/Message/View/Views/MentionPanelView/Panel/MentionPanelView.swift index 42f4863d0..7ec513e40 100644 --- a/Nynja/Modules/Message/View/Views/MentionPanelView/Panel/MentionPanelView.swift +++ b/Nynja/Modules/Message/View/Views/MentionPanelView/Panel/MentionPanelView.swift @@ -176,11 +176,17 @@ extension MentionPanelView: UITableViewDataSource { } func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { - return tableView.dequeueReusableCell(withModel: itemModel(at: indexPath), for: indexPath) + guard let model = itemModel(at: indexPath) else { + return UITableViewCell() + } + return tableView.dequeueReusableCell(withModel: model, for: indexPath) } - private func itemModel(at indexPath: IndexPath) -> UserMentionCellModel { - return items[indexPath.row] + private func itemModel(at indexPath: IndexPath) -> UserMentionCellModel? { + guard case let index = indexPath.row, items.indices.contains(index) else { + return nil + } + return items[index] } } @@ -195,15 +201,13 @@ extension MentionPanelView: UITableViewDelegate { func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { tableView.deselectRow(at: indexPath, animated: true) - - let selectedModel = itemModel(at: indexPath) - selectionHandler?(selectedModel) + itemModel(at: indexPath).map { selectionHandler?($0) } } } // MARK: - Layout -extension MentionPanelView { +private extension MentionPanelView { enum Constraints { diff --git a/Nynja/Modules/Message/View/Views/TableView/Cells/ChatCells/BaseChatCell/BaseChatCell.swift b/Nynja/Modules/Message/View/Views/TableView/Cells/ChatCells/BaseChatCell/BaseChatCell.swift index 2df83cb89..54b6ed8be 100644 --- a/Nynja/Modules/Message/View/Views/TableView/Cells/ChatCells/BaseChatCell/BaseChatCell.swift +++ b/Nynja/Modules/Message/View/Views/TableView/Cells/ChatCells/BaseChatCell/BaseChatCell.swift @@ -1,4 +1,4 @@ -// + // // BaseChatCell.swift // Nynja // @@ -11,7 +11,7 @@ import SnapKit class BaseChatCell: UICollectionViewCell, MessageBaseImageViewDataSource, MessageContentAppearance, MessageVoiceViewDelegate, ReplyCounterDelegate, MessageRepliedViewDelegate, MessageTextViewDelegate, MessageForwardViewDelegate, MessageStickerRepliedViewDelegate, MessageConvertionViewDelegate { - private static let senderFont = UIFont(fontName: Constants.fonts.medium, height: CGFloat(22.0.adjustedByWidth)) + private static let senderFont = UIFont.makeFont(with: Constants.fonts.medium, height: CGFloat(22.0.adjustedByWidth)) private static let translatingFont = UIFont(name: Constants.fonts.regular, size: 12) weak var delegate: BaseChatCellDelegate? @@ -228,6 +228,14 @@ class BaseChatCell: UICollectionViewCell, MessageBaseImageViewDataSource, Messag messageView.roundCorners(corners, radius: radius) } + override func apply(_ layoutAttributes: UICollectionViewLayoutAttributes) { + super.apply(layoutAttributes) + guard let layoutAttributes = layoutAttributes as? MessageCollectionViewLayoutAttributes else { + return + } + transform = layoutAttributes.isReversed ? CGAffineTransform(rotationAngle: .pi) : .identity + } + override func prepareForReuse() { super.prepareForReuse() model?.resetHandlers() @@ -304,7 +312,7 @@ class BaseChatCell: UICollectionViewCell, MessageBaseImageViewDataSource, Messag func setupSenderInfo(_ model: BaseChatCellModel) { if BaseChatCell.shouldShowSender(model), let sender = model.sender { - fromLabel.text = sender.nick + fromLabel.text = sender.nick ?? sender.fullname fromImageView.setImage(url: URL(string: sender.avatar ?? ""), placeHolder: #imageLiteral(resourceName: "ava_placeholder")) fromImageView.isHidden = false @@ -369,7 +377,6 @@ class BaseChatCell: UICollectionViewCell, MessageBaseImageViewDataSource, Messag } messageContent?.configure(with: model) - setupProgress() } @@ -519,7 +526,7 @@ class BaseChatCell: UICollectionViewCell, MessageBaseImageViewDataSource, Messag let maxWidth = BaseChatCell.Constraints.bubble.maxWidth - CGFloat(insets) let minWidth: CGFloat = InfoViewFactory.width(for: model) - let text = model.sender?.nick + let text = model.sender?.nick ?? model.sender?.fullname var textSize = text?.boundingRect(with: CGSize(width: maxWidth, height: 0), options: .usesLineFragmentOrigin, attributes: [NSAttributedStringKey.font: BaseChatCell.senderFont as Any], context: nil).size ?? CGSize.zero @@ -583,10 +590,10 @@ private extension BaseChatCell { var conversionProgressModel: ConvertionProgressModel? { set { - model?.translationProgressModel = newValue + model?.convertProgressModel = newValue } get { - return model?.translationProgressModel + return model?.convertProgressModel } } @@ -605,7 +612,7 @@ private extension BaseChatCell { switch progress.status { case .done: hideNetworkProcessingUI() - case .atProgress: + case .atProgress, .offline: showNetworkProcessingUI() setupProcessingCancelButton() progressBar.progress = progress.progress @@ -614,6 +621,11 @@ private extension BaseChatCell { setupProcessingDownloadButton() progressBar.progress = progress.progress } + + guard let model = model else { + return + } + messageContent?.configure(with: model) } func setupConversionProgress() { @@ -662,11 +674,13 @@ private extension BaseChatCell { private extension BaseChatCell { func setupProcessingDownloadButton() { processingButton.setImage(#imageLiteral(resourceName: "ic_btn_start_download"), for: .normal) + processingButton.removeTarget(nil, action: nil, for: .allEvents) processingButton.addTarget(self, action: #selector(startDownloading), for: .touchUpInside) } func setupProcessingCancelButton() { processingButton.setImage(#imageLiteral(resourceName: "ic_btn_stop_download"), for: .normal) + processingButton.removeTarget(nil, action: nil, for: .allEvents) processingButton.addTarget(self, action: #selector(cancelProcessing), for: .touchUpInside) } @@ -689,3 +703,9 @@ private extension BaseChatCell { delegate?.didCancelProcessingTapped(self) } } + +extension BaseChatCell: Identifiable { + var id: ObjectIdentifier { + return ObjectIdentifier(self) + } +} diff --git a/Nynja/Modules/Message/View/Views/TableView/Cells/ChatCells/BaseChatCell/BaseChatCellLayout.swift b/Nynja/Modules/Message/View/Views/TableView/Cells/ChatCells/BaseChatCell/BaseChatCellLayout.swift index 714f14790..454e5cfc7 100644 --- a/Nynja/Modules/Message/View/Views/TableView/Cells/ChatCells/BaseChatCell/BaseChatCellLayout.swift +++ b/Nynja/Modules/Message/View/Views/TableView/Cells/ChatCells/BaseChatCell/BaseChatCellLayout.swift @@ -11,8 +11,7 @@ extension BaseChatCell { struct Constraints { struct bubble { - static let maxWidth = CGFloat(294.0.adjustedByWidth) -// static let minWidth = CGFloat(timeStamp.width + timeStamp.leftInset + timeStamp.rightInset + deliveryIcon.width + deliveryIcon.rightInset).adjustedByWidth + static let maxWidth = CGFloat(290.0.adjustedByWidth) static let verticalInset = CGFloat(4.0.adjustedByWidth) static let horizontalInset = CGFloat((fromImageView.leftOffset + fromImageView.height / 2).adjustedByWidth) diff --git a/Nynja/Modules/Message/View/Views/TableView/Cells/ChatCells/BaseChatCell/OponentChatCell.swift b/Nynja/Modules/Message/View/Views/TableView/Cells/ChatCells/BaseChatCell/OponentChatCell.swift index 622492937..2e5ebd156 100644 --- a/Nynja/Modules/Message/View/Views/TableView/Cells/ChatCells/BaseChatCell/OponentChatCell.swift +++ b/Nynja/Modules/Message/View/Views/TableView/Cells/ChatCells/BaseChatCell/OponentChatCell.swift @@ -97,6 +97,16 @@ class OponentChatCell: BaseChatCell { messageViewTopToAliasMinimizedConstraint?.deactivate() messageViewTopToBubbleConstraint?.deactivate() } + + guard let content = messageContent else { return } + + switch content { + case let content as MessageVoiceView: + content.isSeparatorVisible = OponentChatCell.shouldShowSender(model) ? true : false + default: + break + } + } override func updateData(_ model: BaseChatCellModel) { @@ -124,7 +134,7 @@ class OponentChatCell: BaseChatCell { case .regular: guard let type = model.type else { break } switch type { - case .audio, .audioCall, .contact, .file, .text, .videoCall: + case .audioCall, .contact, .file, .text, .videoCall: return true default: break diff --git a/Nynja/Modules/Message/View/Views/TableView/Cells/Models/BaseChatCellModel.swift b/Nynja/Modules/Message/View/Views/TableView/Cells/Models/BaseChatCellModel.swift index dd8a1710f..542007821 100644 --- a/Nynja/Modules/Message/View/Views/TableView/Cells/Models/BaseChatCellModel.swift +++ b/Nynja/Modules/Message/View/Views/TableView/Cells/Models/BaseChatCellModel.swift @@ -187,7 +187,7 @@ final class BaseChatCellModel { } } } - var translationProgressModel: ConvertionProgressModel? + var convertProgressModel: ConvertionProgressModel? var infoType : InfoType = .full var starID: String? diff --git a/Nynja/Modules/Message/View/Views/TableView/Cells/Models/RepliedMessageModel.swift b/Nynja/Modules/Message/View/Views/TableView/Cells/Models/RepliedMessageModel.swift index b5ea38d6d..081e65bf8 100644 --- a/Nynja/Modules/Message/View/Views/TableView/Cells/Models/RepliedMessageModel.swift +++ b/Nynja/Modules/Message/View/Views/TableView/Cells/Models/RepliedMessageModel.swift @@ -38,6 +38,10 @@ final class RepliedMessageModel { if type == .audio, let duration = desc.audioDuration { self.duration = duration } + if type == .file, let fileName = desc.filename { + self.text = fileName + } + self.date = Date(timestamp: (message.created as? Int64) ?? 0) self.mentions = mentions } @@ -65,6 +69,9 @@ final class RepliedMessageModel { if type == .audio, let duration = cellModel.audioDuration { self.duration = duration } + if type == .file, let fileName = cellModel.text { + self.text = fileName + } self.date = cellModel.time self.mentions = cellModel.mentions diff --git a/Nynja/Modules/Message/View/Views/TableView/Cells/SystemCell.swift b/Nynja/Modules/Message/View/Views/TableView/Cells/SystemCell.swift index b0b0ec9c2..f40b38b31 100644 --- a/Nynja/Modules/Message/View/Views/TableView/Cells/SystemCell.swift +++ b/Nynja/Modules/Message/View/Views/TableView/Cells/SystemCell.swift @@ -8,7 +8,9 @@ final class SystemCell: UICollectionViewCell { - lazy var systemLabel: UILabel = { + // MARK: - Views + + private lazy var systemLabel: UILabel = { let label = UILabel(height: Constraints.Label.height, color: Constants.colors.white.getColor(), fontName: Constants.fonts.regular, @@ -16,22 +18,28 @@ final class SystemCell: UICollectionViewCell { label.numberOfLines = 0 - let horizontalInset = Constraints.Label.horizontalInset - - self.addSubview(label) - label.snp.makeConstraints({ (make) in - make.left.right.equalToSuperview().inset(horizontalInset) - make.center.equalTo(self) - }) + contentView.addSubview(label) + label.snp.makeConstraints { make in + make.left.right.equalToSuperview().inset(Constraints.Label.horizontalInset) + make.center.equalToSuperview() + } return label }() + // MARK: - Life Cycle + + override func apply(_ layoutAttributes: UICollectionViewLayoutAttributes) { + super.apply(layoutAttributes) + contentView.transform = CGAffineTransform(rotationAngle: .pi) + } + + // MARK: - Setup func setup(message: String) { - self.backgroundColor = UIColor.clear + backgroundColor = UIColor.clear systemLabel.text = message } } @@ -39,7 +47,7 @@ final class SystemCell: UICollectionViewCell { // MARK: - Constraints -extension SystemCell { +private extension SystemCell { enum Constraints { enum Label { diff --git a/Nynja/Modules/Message/View/Views/TableView/Cells/TimeCell.swift b/Nynja/Modules/Message/View/Views/TableView/Cells/TimeCell.swift index 46f9c483d..8faac2290 100644 --- a/Nynja/Modules/Message/View/Views/TableView/Cells/TimeCell.swift +++ b/Nynja/Modules/Message/View/Views/TableView/Cells/TimeCell.swift @@ -10,27 +10,42 @@ import Foundation final class TimeCell: UICollectionViewCell { - private lazy var dateFormatter = DateFormatter() + private static let dateFormatter = DateFormatter() - private lazy var timeStamp: UILabel = { + + // MARK: - Views + + private lazy var timeStampLabel: UILabel = { let v = UILabel() v.font = UIFont(name: Constants.fonts.regular, size: 14) v.textColor = Constants.colors.red.getColor() - self.addSubview(v) - v.snp.makeConstraints({ (make) in - make.center.equalTo(self) - }) + contentView.addSubview(v) + v.snp.makeConstraints { maker in + maker.center.equalToSuperview() + } return v }() + + // MARK: - Life Cycle + + override func apply(_ layoutAttributes: UICollectionViewLayoutAttributes) { + super.apply(layoutAttributes) + contentView.transform = CGAffineTransform(rotationAngle: .pi) + } + + + // MARK: - Setup + func setup(date: Date) { - self.backgroundColor = UIColor.clear - timeStamp.text = getText(fromDate: date) + backgroundColor = UIColor.clear + timeStampLabel.text = getText(fromDate: date) } private func getText(fromDate: Date) -> String { - dateFormatter.dateFormat = "MMMM d".localizedDateFormat - dateFormatter.locale = Locale(identifier: Language.current.rawValue) - return dateFormatter.string(from: fromDate) + let formatter = TimeCell.dateFormatter + formatter.dateFormat = "MMMM d".localizedDateFormat + formatter.locale = Locale(identifier: Language.current.rawValue) + return formatter.string(from: fromDate) } } diff --git a/Nynja/Modules/Message/View/Views/TableView/Cells/UnreadCell.swift b/Nynja/Modules/Message/View/Views/TableView/Cells/UnreadCell.swift index c048a7cb4..c87b77f2a 100644 --- a/Nynja/Modules/Message/View/Views/TableView/Cells/UnreadCell.swift +++ b/Nynja/Modules/Message/View/Views/TableView/Cells/UnreadCell.swift @@ -10,17 +10,19 @@ import Foundation final class UnreadCell: UICollectionViewCell { - lazy var timeStamp: UILabel = { + // MARK: - Views + + private lazy var timeStamp: UILabel = { let v = UILabel() v.font = UIFont(name: Constants.fonts.regular, size: 14) v.textColor = Constants.colors.red.getColor() v.text = "Unread".localized - self.addSubview(v) - v.snp.makeConstraints({ (make) in - make.center.equalTo(self) - }) + contentView.addSubview(v) + v.snp.makeConstraints { make in + make.center.equalToSuperview() + } return v }() @@ -29,14 +31,14 @@ final class UnreadCell: UICollectionViewCell { let v = UIView() v.backgroundColor = Constants.colors.red.getColor() - self.addSubview(v) - v.snp.makeConstraints({ (make) in + contentView.addSubview(v) + v.snp.makeConstraints { make in make.height.equalTo(1) make.centerY.equalToSuperview() make.left.equalToSuperview() make.right.equalTo(timeStamp.snp.left).offset(-15) - }) + } return v }() @@ -45,23 +47,33 @@ final class UnreadCell: UICollectionViewCell { let v = UIView() v.backgroundColor = Constants.colors.red.getColor() - self.addSubview(v) - v.snp.makeConstraints({ (make) in + contentView.addSubview(v) + v.snp.makeConstraints { make in make.height.equalTo(1) make.centerY.equalToSuperview() make.right.equalToSuperview() make.left.equalTo(timeStamp.snp.right).offset(15) - }) + } return v }() + + // MARK: - Life Cycle + + override func apply(_ layoutAttributes: UICollectionViewLayoutAttributes) { + super.apply(layoutAttributes) + contentView.transform = CGAffineTransform(rotationAngle: .pi) + } + + + // MARK: - Setup + func setup() { backgroundColor = UIColor.clear timeStamp.isHidden = false line1.isHidden = false line2.isHidden = false } - } diff --git a/Nynja/Modules/Message/View/Views/TableView/Cells/Views/Base/MessageBaseImageView.swift b/Nynja/Modules/Message/View/Views/TableView/Cells/Views/Base/MessageBaseImageView.swift index 797dcdb29..03244ce6c 100644 --- a/Nynja/Modules/Message/View/Views/TableView/Cells/Views/Base/MessageBaseImageView.swift +++ b/Nynja/Modules/Message/View/Views/TableView/Cells/Views/Base/MessageBaseImageView.swift @@ -76,7 +76,7 @@ class MessageBaseImageView: MessageContentView, BubbleImageSizeCalculatable { func setupTransferInfoView(_ model: BaseChatCellModel) { guard let progress = model.progressModel else { return } switch progress.status { - case .done, .notStarted: + case .done, .notStarted, .offline: self.updateTransferInfoContentRes(with: model) case .atProgress: self.updateTransferInfoContent(with: model) diff --git a/Nynja/Modules/Message/View/Views/TableView/Cells/Views/Converting/ConvertionInfoView.swift b/Nynja/Modules/Message/View/Views/TableView/Cells/Views/Converting/ConvertionInfoView.swift index 23d664fa1..dd1584535 100644 --- a/Nynja/Modules/Message/View/Views/TableView/Cells/Views/Converting/ConvertionInfoView.swift +++ b/Nynja/Modules/Message/View/Views/TableView/Cells/Views/Converting/ConvertionInfoView.swift @@ -10,8 +10,8 @@ import SnapKit final class ConvertionInfoView: BaseView { - private static let titleFont = UIFont(fontName: Constants.fonts.regular, height: Constraints.titleLabel.height) - private static let contentFont = UIFont(fontName: Constants.fonts.regular, height: Constraints.contentLabel.height) + private static let titleFont = UIFont.makeFont(with: Constants.fonts.regular, height: Constraints.titleLabel.height) + private static let contentFont = UIFont.makeFont(with: Constants.fonts.regular, height: Constraints.contentLabel.height) private static let payloadRenderer: MessagePayloadRendererInput = { return MessagePayloadRenderer.linkRenderer(font: contentFont!) diff --git a/Nynja/Modules/Message/View/Views/TableView/Cells/Views/FileTransferInfoView.swift b/Nynja/Modules/Message/View/Views/TableView/Cells/Views/FileTransferInfoView.swift index 37420557c..d062fa57e 100644 --- a/Nynja/Modules/Message/View/Views/TableView/Cells/Views/FileTransferInfoView.swift +++ b/Nynja/Modules/Message/View/Views/TableView/Cells/Views/FileTransferInfoView.swift @@ -74,7 +74,7 @@ class FileTransferInfoView: BaseView { switch progress.status { case .atProgress: self.configureTransferInfo(with: model) - case .done, .notStarted: + case .done, .notStarted, .offline: self.configureResolutionInfo(with: model) } } diff --git a/Nynja/Modules/Message/View/Views/TableView/Cells/Views/Message/MessageContactView.swift b/Nynja/Modules/Message/View/Views/TableView/Cells/Views/Message/MessageContactView.swift index 80f38eeff..b5286fbab 100644 --- a/Nynja/Modules/Message/View/Views/TableView/Cells/Views/Message/MessageContactView.swift +++ b/Nynja/Modules/Message/View/Views/TableView/Cells/Views/Message/MessageContactView.swift @@ -35,11 +35,23 @@ class MessageContactView: MessageContentView { return imageView }() + private var nameLabelTopConstraint: Constraint? + private var nameLabelCenterYConstraint: Constraint? + lazy var nameLabel: UILabel = { let size = CGSize(width: 100, height: Constraints.nameLabel.labelHeight) let label = UILabel(size: size, color: Constants.colors.darkLight.getColor(), fontName: Constants.fonts.medium) self.addSubview(label) - label.snp.makeConstraints(self.nameLabelConstraints()) + label.snp.makeConstraints { maker in + let horizontalPadding = Constraints.nameLabel.horizontalPadding + maker.width.equalTo(Constraints.nameLabel.width) + maker.left.equalTo(avatarImageView.snp.right).offset(horizontalPadding) + maker.right.equalTo(-horizontalPadding) + nameLabelTopConstraint = maker.top.equalTo(avatarImageView).inset(Constraints.nameLabel.topInset).constraint + nameLabelCenterYConstraint = maker.centerY.equalTo(avatarImageView).constraint + } + alignVerticallyToTop() + return label }() @@ -84,36 +96,26 @@ class MessageContactView: MessageContentView { return CGSize(width: Constraints.width, height: Constraints.height) } - //MARK: - Private Methods - - //MARK: - Private/Constraint Maker - private func nameLabelConstraints(default isDefault: Bool = true) -> (ConstraintMaker) -> Void { - return { [weak self] make in - guard let this = self else { return } - let horizontalPadding = Constraints.nameLabel.horizontalPadding - - make.width.equalTo(Constraints.nameLabel.width) - if isDefault { - make.top.equalTo(this.avatarImageView).inset(Constraints.nameLabel.topInset) - } else { - make.centerY.equalTo(this.avatarImageView) - } - make.left.equalTo(this.avatarImageView.snp.right).offset(horizontalPadding) - make.right.equalTo(-horizontalPadding) - } - } - - //MARK: - Configure Cell Appearance - + // MARK: - Configure Cell Appearance private func configureViewAppearance(for model: BaseChatCellModel) { if model.contact?.nick != nil { - self.usernameLabel.isHidden = false - self.nameLabel.snp.updateConstraints(self.nameLabelConstraints()) + usernameLabel.isHidden = false + alignVerticallyToTop() } else { - self.usernameLabel.isHidden = true - self.nameLabel.snp.makeConstraints(self.nameLabelConstraints(default: false)) + usernameLabel.isHidden = true + alignVerticallyToCenter() } } + + private func alignVerticallyToTop() { + nameLabelTopConstraint?.activate() + nameLabelCenterYConstraint?.deactivate() + } + + private func alignVerticallyToCenter() { + nameLabelTopConstraint?.deactivate() + nameLabelCenterYConstraint?.activate() + } } // MARK: - Constraints diff --git a/Nynja/Modules/Message/View/Views/TableView/Cells/Views/Message/MessageFileView.swift b/Nynja/Modules/Message/View/Views/TableView/Cells/Views/Message/MessageFileView.swift index eee71880a..4892e812b 100644 --- a/Nynja/Modules/Message/View/Views/TableView/Cells/Views/Message/MessageFileView.swift +++ b/Nynja/Modules/Message/View/Views/TableView/Cells/Views/Message/MessageFileView.swift @@ -116,7 +116,7 @@ class MessageFileView: MessageContentView { let label = CBAutoScrollLabel() label.textColor = Constants.colors.darkLight.getColor() - label.font = UIFont(fontName: Constants.fonts.regular, height: Constraints.filenameLabel.labelHeight) ?? UIFont.systemFont(ofSize: 16.0) + label.font = UIFont.makeFont(with: Constants.fonts.regular, height: Constraints.filenameLabel.labelHeight) ?? UIFont.systemFont(ofSize: 16.0) label.labelSpacing = Constraints.filenameLabel.labelSpace label.scrollSpeed = 30.0 label.fadeLength = Constraints.filenameLabel.fadeLength diff --git a/Nynja/Modules/Message/View/Views/TableView/Cells/Views/Message/MessageForwardView.swift b/Nynja/Modules/Message/View/Views/TableView/Cells/Views/Message/MessageForwardView.swift index a5d91fa03..8bb029321 100644 --- a/Nynja/Modules/Message/View/Views/TableView/Cells/Views/Message/MessageForwardView.swift +++ b/Nynja/Modules/Message/View/Views/TableView/Cells/Views/Message/MessageForwardView.swift @@ -18,7 +18,7 @@ class MessageForwardView: MessageContainerView { weak var delegate: MessageForwardViewDelegate? - private static let forwardFont = UIFont(fontName: Constants.fonts.regular, height: Constraints.forwardLabel.height) + private static let forwardFont = UIFont.makeFont(with: Constants.fonts.regular, height: Constraints.forwardLabel.height) override var coloredViews: [UIView] { return [self, forwardLabel, contentView] diff --git a/Nynja/Modules/Message/View/Views/TableView/Cells/Views/Message/MessageImageView.swift b/Nynja/Modules/Message/View/Views/TableView/Cells/Views/Message/MessageImageView.swift index 692a333f2..659ea7ecd 100644 --- a/Nynja/Modules/Message/View/Views/TableView/Cells/Views/Message/MessageImageView.swift +++ b/Nynja/Modules/Message/View/Views/TableView/Cells/Views/Message/MessageImageView.swift @@ -27,10 +27,14 @@ class MessageImageView: MessageBaseImageView { let size = MessageImageView.sizeСonsideringTimestamp(for: model) adjustConstraints(with: size) - guard let url = URL(string: model.text ?? "") else { + guard var url = URL(string: model.text ?? "") else { return } + if let localUrl = url.thumbUrl { + url = localUrl + } + imageView.setImage(url: url, placeHolder: #imageLiteral(resourceName: "camera")) { [weak self] downloadedUrl, image in self?.imageView.image = image?.resizeImage(targetSize: size, scale: UIScreen.main.scale) } diff --git a/Nynja/Modules/Message/View/Views/TableView/Cells/Views/Message/MessagePaymentView.swift b/Nynja/Modules/Message/View/Views/TableView/Cells/Views/Message/MessagePaymentView.swift index a27683dc0..99163870f 100644 --- a/Nynja/Modules/Message/View/Views/TableView/Cells/Views/Message/MessagePaymentView.swift +++ b/Nynja/Modules/Message/View/Views/TableView/Cells/Views/Message/MessagePaymentView.swift @@ -8,8 +8,8 @@ class MessagePaymentView: MessageContentView { - private static let textFont = UIFont(fontName: Constants.fonts.regular, height: Constraints.textView.labelHeight)! - private static let nynLocaleFont = UIFont(fontName: Constants.fonts.regular, height: Constraints.textView.nynLocaleHeight)! + private static let textFont = UIFont.makeFont(with: Constants.fonts.regular, height: Constraints.textView.labelHeight)! + private static let nynLocaleFont = UIFont.makeFont(with: Constants.fonts.regular, height: Constraints.textView.nynLocaleHeight)! override var coloredViews: [UIView] { return [self, textView] diff --git a/Nynja/Modules/Message/View/Views/TableView/Cells/Views/Message/MessageTextView.swift b/Nynja/Modules/Message/View/Views/TableView/Cells/Views/Message/MessageTextView.swift index 16ade069a..0e66ab19c 100644 --- a/Nynja/Modules/Message/View/Views/TableView/Cells/Views/Message/MessageTextView.swift +++ b/Nynja/Modules/Message/View/Views/TableView/Cells/Views/Message/MessageTextView.swift @@ -16,7 +16,7 @@ class MessageTextView: MessageContentView { weak var delegate: MessageTextViewDelegate? - private static let textFont = UIFont(fontName: Constants.fonts.regular, height: Constraints.textView.labelHeight)! + private static let textFont = UIFont.makeFont(with: Constants.fonts.regular, height: Constraints.textView.labelHeight)! private static let payloadRenderer: MessagePayloadRendererInput = { return MessagePayloadRenderer.linkRenderer(font: textFont) diff --git a/Nynja/Modules/Message/View/Views/TableView/Cells/Views/Message/MessageVideoView.swift b/Nynja/Modules/Message/View/Views/TableView/Cells/Views/Message/MessageVideoView.swift index fa887594c..d8ae21dd9 100644 --- a/Nynja/Modules/Message/View/Views/TableView/Cells/Views/Message/MessageVideoView.swift +++ b/Nynja/Modules/Message/View/Views/TableView/Cells/Views/Message/MessageVideoView.swift @@ -45,10 +45,14 @@ class MessageVideoView: MessageBaseImageView { let size = MessageVideoView.sizeСonsideringTimestamp(for: model) adjustConstraints(with: size) - guard let url = URL(string: model.text ?? "") else { + guard var url = URL(string: model.text ?? "") else { return } + if let localUrl = url.thumbUrl { + url = localUrl + } + imageView.setImage(url: url, placeHolder: #imageLiteral(resourceName: "camera")) { [weak self] downloadedUrl, image in self?.imageView.image = image?.resizeImage(targetSize: size, scale: UIScreen.main.scale) } diff --git a/Nynja/Modules/Message/View/Views/TableView/Cells/Views/Message/MessageVoiceView.swift b/Nynja/Modules/Message/View/Views/TableView/Cells/Views/Message/MessageVoiceView.swift index d32e342eb..f72aaec9f 100644 --- a/Nynja/Modules/Message/View/Views/TableView/Cells/Views/Message/MessageVoiceView.swift +++ b/Nynja/Modules/Message/View/Views/TableView/Cells/Views/Message/MessageVoiceView.swift @@ -44,6 +44,8 @@ class MessageVoiceView: MessageContentView { make.width.height.equalTo(Constraints.micImageView.width) make.top.equalToSuperview() make.centerX.equalToSuperview() + make.left.equalTo(Constraints.micImageView.horisontalInset) + make.right.equalTo(-Constraints.micImageView.horisontalInset) } return imageView }() @@ -263,6 +265,7 @@ extension MessageVoiceView { enum micImageView { static let width = 24.0.adjustedByWidth + static let horisontalInset = 5.0.adjustedByWidth } enum durationLabel { diff --git a/Nynja/Modules/Message/View/Views/TableView/Cells/Views/Reply/MessageRepliedView.swift b/Nynja/Modules/Message/View/Views/TableView/Cells/Views/Reply/MessageRepliedView.swift index 65ef3b11f..b62d8b067 100644 --- a/Nynja/Modules/Message/View/Views/TableView/Cells/Views/Reply/MessageRepliedView.swift +++ b/Nynja/Modules/Message/View/Views/TableView/Cells/Views/Reply/MessageRepliedView.swift @@ -43,6 +43,7 @@ final class MessageRepliedView: MessageContainerView, ReplyInfoViewDelegate { addSubview(view) view.snp.makeConstraints { make in make.top.left.right.equalToSuperview() + make.width.lessThanOrEqualTo(Constraints.replyInfoView.maxWidth) } return view @@ -129,9 +130,8 @@ final class MessageRepliedView: MessageContainerView, ReplyInfoViewDelegate { } func calculateLabelSize(with text: String, font: UIFont) -> CGSize { - let insets = 2 * ReplyInfoView.Constraints.authorLabel.horizontalInset - - let maxWidth = BaseChatCell.Constraints.bubble.maxWidth - CGFloat(insets) + let insets = CGFloat(ReplyInfoView.Constraints.authorLabel.horizontalInset * 2) + let maxWidth = Constraints.replyInfoView.maxWidth - insets var textSize = text.boundingRect(with: CGSize(width: maxWidth, height: 0), options: .usesLineFragmentOrigin, attributes: [NSAttributedStringKey.font: font as Any], context: nil).size @@ -165,6 +165,7 @@ extension MessageRepliedView { static let expandedHeight = replyInfoView.expandedHeight + contentView.inset enum replyInfoView { + static let maxWidth: CGFloat = BaseChatCell.Constraints.bubble.maxWidth static let height = ReplyInfoView.Constraints.height static let expandedHeight = ReplyInfoView.Constraints.expandedHeight } diff --git a/Nynja/Modules/Message/View/Views/TableView/Cells/Views/Reply/ReplyInfoView.swift b/Nynja/Modules/Message/View/Views/TableView/Cells/Views/Reply/ReplyInfoView.swift index 255fc9a14..f6991fc16 100644 --- a/Nynja/Modules/Message/View/Views/TableView/Cells/Views/Reply/ReplyInfoView.swift +++ b/Nynja/Modules/Message/View/Views/TableView/Cells/Views/Reply/ReplyInfoView.swift @@ -14,8 +14,8 @@ protocol ReplyInfoViewDelegate: class { final class ReplyInfoView: BaseView { - private static let authorFont = UIFont(fontName: Constants.fonts.medium, height: Constraints.authorLabel.height)! - private static let contentFont = UIFont(fontName: Constants.fonts.regular, height: Constraints.contentLabel.height)! + private static let authorFont = UIFont.makeFont(with: Constants.fonts.medium, height: Constraints.authorLabel.height)! + private static let contentFont = UIFont.makeFont(with: Constants.fonts.regular, height: Constraints.contentLabel.height)! weak var delegate: ReplyInfoViewDelegate? diff --git a/Nynja/Modules/Message/WireFrame/MessageWireframe.swift b/Nynja/Modules/Message/WireFrame/MessageWireframe.swift index 60acf1192..175dfdb58 100644 --- a/Nynja/Modules/Message/WireFrame/MessageWireframe.swift +++ b/Nynja/Modules/Message/WireFrame/MessageWireframe.swift @@ -68,8 +68,8 @@ class MessageWireFrame: MessageWireframeProtocol, DocumentInteractionWireFrame { main?.showAddContact(contact: contact) } - func showForwardSelector(with localId: String) { - main?.showForwardSelector(with: .create(localId: localId)) + func showForwardSelector(with localId: String, delegate: ForwardSelectorDelegate) { + main?.showForwardSelector(with: .create(localId: localId), delegate: delegate) } func showMyProfileScreen() { diff --git a/Nynja/Modules/OtherUser/Interactor/OtherUserInteractor.swift b/Nynja/Modules/OtherUser/Interactor/OtherUserInteractor.swift index efb5c49b1..5b6c21439 100644 --- a/Nynja/Modules/OtherUser/Interactor/OtherUserInteractor.swift +++ b/Nynja/Modules/OtherUser/Interactor/OtherUserInteractor.swift @@ -83,7 +83,7 @@ class OtherUserInteractor: BaseInteractor, OtherUserInteractorInputProtocol, Set } do { - let historyModel = try historyRequestModelFactory.makeHistoryRequestModelDelete(rosterId: phoneId, chat: contact) + let historyModel = try historyRequestModelFactory.makeDeleteRequest(rosterId: phoneId, chat: contact) MQTTService.sharedInstance.sendHistoryRequest(with: historyModel) } catch { diff --git a/Nynja/Modules/OtherUser/OtherUserProtocols.swift.orig b/Nynja/Modules/OtherUser/OtherUserProtocols.swift.orig deleted file mode 100644 index 328fe10b7..000000000 --- a/Nynja/Modules/OtherUser/OtherUserProtocols.swift.orig +++ /dev/null @@ -1,109 +0,0 @@ -// -// OtherUserProtocols.swift -// Nynja -// -// Created by Andrey Reznik on 12.02.2018. -// Copyright © 2018 TecSynt Solutions. All rights reserved. -// - -import Foundation -import CoreLocation -import MaterialComponents.MaterialFlexibleHeader - -//MARK: - Delegate - -protocol OtherUserProfileDelegate: class { - func otherUserProfile(_ userProfileWireFrame: OtherUserWireframeProtocol, didBlockContact contact: Contact) - func otherUserProfile(_ userProfileWireFrame: OtherUserWireframeProtocol, didUnblockContact contact: Contact) -} -extension OtherUserProfileDelegate { - func otherUserProfile(_ userProfileWireFrame: OtherUserWireframeProtocol, didBlockContact contact: Contact) { } - func otherUserProfile(_ userProfileWireFrame: OtherUserWireframeProtocol, didUnblockContact contact: Contact) { } -} - -//MARK: - Wireframe -protocol OtherUserWireframeProtocol: class { - weak var main: MainWireFrame? {get set} - - func present(navigation: UINavigationController, main: MainWireFrame?, contact: Contact?, delegate: OtherUserProfileDelegate?) - func dismissSelf() - func hide() - - func showAvatar(_ url: URL) - func showMessages(_ contact: Contact) - - func showStorage(_ contact: Contact) -} - - -//MARK: - Presenter -protocol OtherUserPresenterProtocol: BasePresenterProtocol { - - var view: OtherUserViewProtocol! { get set } - var interactor: OtherUserInteractorInputProtocol! { get set } - var wireFrame: OtherUserWireframeProtocol! { get set } - -<<<<<<< HEAD -======= - var contact: Contact! { get set } - - var delegate: OtherUserProfileDelegate? { get set } - - ->>>>>>> developer - /** - * Add here your methods for communication VIEW -> PRESENTER - */ - func hide() - - func showAvatar() - func addToFriend() - func goToMessages() - - // Settings - func showStorage() - func showNotificationOptions() - func showBanOptions() - func clearHistory() -} - -//MARK: - Interactor -protocol OtherUserInteractorOutputProtocol: class { - /* Interactor -> Presenter */ - - func update(_ contact: Contact) -} - -protocol OtherUserInteractorInputProtocol: BaseInteractorProtocol { - - var presenter: OtherUserInteractorOutputProtocol? { get set } - - var contact: Contact! { get set } - - /** - * Add here your methods for communication PRESENTER -> INTERACTOR - */ - - func sendRequestToFriends(_ to: String) - func acceptRequestToFriends(_ from: String) - - func mute(_ isMute: Bool, to: String, settingId: String?) - func block(_ isBlock: Bool, to: String) - func clearHistory(to: String) -} - -//MARK: - View -protocol OtherUserViewProtocol: class { - - var header: MDCFlexibleHeaderViewController! { get set } - - var presenter: OtherUserPresenterProtocol! { get set } - - /** - * Add here your methods for communication PRESENTER -> VIEW - */ - - func setup(_ contact: Contact?) - -} - diff --git a/Nynja/Modules/OtherUser/Presenter/OtherUserPresenter.swift.orig b/Nynja/Modules/OtherUser/Presenter/OtherUserPresenter.swift.orig deleted file mode 100644 index 7fa095851..000000000 --- a/Nynja/Modules/OtherUser/Presenter/OtherUserPresenter.swift.orig +++ /dev/null @@ -1,170 +0,0 @@ -// -// OtherUserPresenter.swift -// Nynja -// -// Created by Andrey Reznik on 12.02.2018. -// Copyright © 2018 TecSynt Solutions. All rights reserved. -// - -enum OtherUserState : Int { - case noFriend - case requested - case friend - case incomingRequest -} - -class OtherUserPresenter: BasePresenter, OtherUserPresenterProtocol, OtherUserInteractorOutputProtocol { - - var contact: Contact { - return interactor.contact - } - - //MARK: - BasePresenter - - override var itemsFactory: WCItemsFactory? { - return GroupOptionsItemsFactory() // TODO: on create or update process? - } - -<<<<<<< HEAD -======= - weak var view: OtherUserViewProtocol! - var interactor: OtherUserInteractorInputProtocol! - var wireFrame: OtherUserWireframeProtocol! - - weak var delegate: OtherUserProfileDelegate? - - private var contactId: String! - var contact: Contact! { - didSet { - contactId = contact.phoneId - } - } - ->>>>>>> developer - - //MARK: - OtherUserPresenterProtocol - - weak var view: OtherUserViewProtocol! - var interactor: OtherUserInteractorInputProtocol! { - didSet { - _interactor = interactor - } - } - var wireFrame: OtherUserWireframeProtocol! - - func hide() { - wireFrame.hide() - } - - - //MARK: -- Actions - - func showAvatar() { - guard let url = contact.avatarUrl else { - return - } - wireFrame.showAvatar(url) - } - - func addToFriend() { - if contact.originalStatus == .authorization { - interactor.acceptRequestToFriends(contact.phoneId) - } else { - interactor.sendRequestToFriends(contact.phoneId) - } - } - - func goToMessages() { - wireFrame.showMessages(contact) - } - - - //MARK: - Settings - - func showNotificationOptions() { - let muteAction = UIAlertAction(title: "mute".localized, style: .default) { _ in - self.receiveNotifications(false) - } - let unmuteAction = UIAlertAction(title: "unmute".localized, style: .default) { _ in - self.receiveNotifications(true) - } - let cancelAction = UIAlertAction(title: "cancel".localized, style: .cancel) - - AlertManager.sharedInstance.showActionSheet(title: nil, message: nil, actions: [muteAction, unmuteAction, cancelAction]) - } - - func showStorage() { - wireFrame.showStorage(contact) - } - - func clearHistory() { - let deleteAction = UIAlertAction(title: "delete".localized, style: .destructive) { _ in - self.clearChatHistory() - } - let cancelAction = UIAlertAction(title: "cancel".localized, style: .cancel) - - AlertManager.sharedInstance.showActionSheet(title: "delete_chat_history".localized, message: nil, actions: [deleteAction, cancelAction]) - } - - func showBanOptions() { -<<<<<<< HEAD - let isBlocked = (contact.originalStatus == .banned) - AlertManager.sharedInstance.showAlertWithTwoActions(title: "", message: isBlocked ? "are_u_sure_unblock".localized : "are_u_sure_block".localized, firstActionTitle: "no".localized.capitalized, secondActionTitle: "yes".localized.capitalized, firstAction: nil, secondAction: { [unowned self] in - self.ban(isBlocked) - }) -======= - ReachabilityService.sharedInstance.performIfConnected { - let isBlocked = (contact.contactStatus == .banned) - AlertManager.sharedInstance.showAlertWithTwoActions(title: "", message: isBlocked ? "are_u_sure_unblock".localized : "are_u_sure_block".localized, firstActionTitle: "no".localized.capitalized, secondActionTitle: "yes".localized.capitalized, firstAction: nil, secondAction: { [unowned self] in - self.ban(isBlocked) - }) - } ->>>>>>> developer - } - - private func receiveNotifications(_ isOn: Bool) { - if contact.notifications == isOn { - return - } - interactor.mute(!isOn, to: contact.phoneId, settingId: contact.settings?.last?.id) - } - - private func ban(_ isOn: Bool) { - interactor.block(!isOn, to: contact.phoneId) - } - - private func clearChatHistory() { - interactor.clearHistory(to: contact.phoneId) - } - - - //MARK: - OtherUserInteractorOutputProtocol - - func update(_ contact: Contact) { -<<<<<<< HEAD - view.setup(contact) -======= - let oldStatus = self.contact.contactStatus - let newStatus = contact.contactStatus - - if oldStatus != newStatus { - switch oldStatus { - case .banned: - if newStatus != .banned { - delegate?.otherUserProfile(self.wireFrame, didUnblockContact: contact) - } - default: - if newStatus == .banned { - delegate?.otherUserProfile(self.wireFrame, didBlockContact: contact) - } - } - } - - self.contact = contact - view.setup(self.contact) ->>>>>>> developer - } - - - //MARK: - Private -} diff --git a/Nynja/Modules/OtherUser/View/TableView/OtherUserTableViewDS.swift b/Nynja/Modules/OtherUser/View/TableView/OtherUserTableViewDS.swift index d952a6460..df3ff3287 100644 --- a/Nynja/Modules/OtherUser/View/TableView/OtherUserTableViewDS.swift +++ b/Nynja/Modules/OtherUser/View/TableView/OtherUserTableViewDS.swift @@ -26,7 +26,7 @@ class OtherUserTableViewDS: NSObject, UITableViewDataSource { case .request: otherUserState = .requested settings = [[]] - case .authorization: + case .authorization, .ignore: otherUserState = .incomingRequest settings = [[]] default: diff --git a/Nynja/Modules/OtherUser/WireFrame/OtherUserWireFrame.swift b/Nynja/Modules/OtherUser/WireFrame/OtherUserWireFrame.swift index 687778bf5..22eac6dd8 100644 --- a/Nynja/Modules/OtherUser/WireFrame/OtherUserWireFrame.swift +++ b/Nynja/Modules/OtherUser/WireFrame/OtherUserWireFrame.swift @@ -90,7 +90,7 @@ class OtherUserWireFrame: OtherUserWireframeProtocol { } func showStorage(_ contact: Contact) { - GroupStorageWireFrame().presentGroupStorage(navigation: navigation!, room: nil, contact: contact, main: main) + GroupStorageWireFrame().presentGroupStorage(navigation: navigation!, chatModel: contact, main: main) } func showLanguageSettings(_ contact: Contact) { diff --git a/Nynja/Modules/QRCodeReader/View/QRCodeReaderViewController.swift b/Nynja/Modules/QRCodeReader/View/QRCodeReaderViewController.swift index 5ca92aaa1..203cfc1a4 100644 --- a/Nynja/Modules/QRCodeReader/View/QRCodeReaderViewController.swift +++ b/Nynja/Modules/QRCodeReader/View/QRCodeReaderViewController.swift @@ -217,7 +217,7 @@ class QRCodeReaderViewController: BaseVC, QRCodeReaderViewProtocol { } device.unlockForConfiguration() } catch { - LogService.log(topic: .QRCode, text: error.localizedDescription) + LogService.log(topic: .QRCode) { return error.localizedDescription } } } @@ -247,12 +247,12 @@ class QRCodeReaderViewController: BaseVC, QRCodeReaderViewProtocol { do { try device.setTorchModeOn(level: 1.0) } catch { - LogService.log(topic: .QRCode, text: error.localizedDescription) + LogService.log(topic: .QRCode) { return error.localizedDescription } } } device.unlockForConfiguration() } catch { - LogService.log(topic: .QRCode, text: error.localizedDescription) + LogService.log(topic: .QRCode) { return error.localizedDescription } } } diff --git a/Nynja/Modules/Replies/View/RepliesVC+CellDelegate.swift b/Nynja/Modules/Replies/View/RepliesVC+CellDelegate.swift index d4ef6afb2..f2c18d26f 100644 --- a/Nynja/Modules/Replies/View/RepliesVC+CellDelegate.swift +++ b/Nynja/Modules/Replies/View/RepliesVC+CellDelegate.swift @@ -8,14 +8,12 @@ import Foundation -extension RepliesVC: BaseChatCellDelegate { +extension RepliesVC: VoiceAudioInteractive, BaseChatCellDelegate, AudioManagerDelegate, ProximitySensorManagerDelegate { - // MARK: - BaseChatCellDelegate func didCellTapped(_ cell: BaseChatCell) { guard let model = cell.model, let id = model.id else { return } - presenter.didTapMessage(id) - } -} + } +} \ No newline at end of file diff --git a/Nynja/Modules/Replies/View/RepliesVC.swift b/Nynja/Modules/Replies/View/RepliesVC.swift index 179e588d4..135b520b1 100644 --- a/Nynja/Modules/Replies/View/RepliesVC.swift +++ b/Nynja/Modules/Replies/View/RepliesVC.swift @@ -22,9 +22,11 @@ final class RepliesVC: BaseVC, RepliesViewProtocol { var collectionViewDataSource: RepliesDS? var collectionViewDelegate: RepliesCollectionViewDelegate? + var audioManager = AudioManager.sharedInstance + var proximityManager = ProximitySensorManager.shared + var currentPlayingModel: BaseChatCellModel? //MARK: - Subviews - private lazy var repliesNumberLabel: UILabel = { let label = UILabel(height: Constraints.repliesNumberLabel.height, color: Constants.colors.white.getColor(), @@ -44,7 +46,7 @@ final class RepliesVC: BaseVC, RepliesViewProtocol { }() private lazy var collectionView: UICollectionView = { - let collectionView = UICollectionView(frame: .zero, collectionViewLayout: UICollectionViewFlowLayout()) + let collectionView = UICollectionView(frame: .zero, collectionViewLayout: MessageCollectionViewLayout()) collectionView.backgroundColor = .clear collectionView.alwaysBounceVertical = true @@ -86,7 +88,6 @@ final class RepliesVC: BaseVC, RepliesViewProtocol { //MARK: - BaseVC - override func initialize() { super.initialize() @@ -101,10 +102,17 @@ final class RepliesVC: BaseVC, RepliesViewProtocol { collectionView.dataSource = collectionViewDataSource collectionView.delegate = collectionViewDelegate + + audioManager.delegate = self + proximityManager.delegate = self } - //MARK: - RepliesViewProtocol + override func viewDidDisappear(_ animated: Bool) { + super.viewDidDisappear(animated) + stopPlaying() + } + //MARK: - RepliesViewProtocol func updateRepliesNumberLabel(replied: Int) { repliesNumberLabel.text = "\(replied)" } @@ -152,14 +160,12 @@ final class RepliesVC: BaseVC, RepliesViewProtocol { //MARK: - Actions - @objc func closeTapped(_ sender: UIButton) { presenter.hide() } //MARK: - Private methods - private func registerCells(for collectionView: UICollectionView) { MessageCellFactory.selfIdentifiers.forEach { collectionView.register(BaseChatCell.self, forCellWithReuseIdentifier: $0) @@ -190,8 +196,7 @@ final class RepliesVC: BaseVC, RepliesViewProtocol { } } -// MARK: - Layout - +//MARK: - Layout extension RepliesVC { enum Constraints { diff --git a/Nynja/Modules/ScheduleMessage/Interactor/ScheduleMessageInteractor.swift b/Nynja/Modules/ScheduleMessage/Interactor/ScheduleMessageInteractor.swift index 3a4ded37d..4b682e480 100644 --- a/Nynja/Modules/ScheduleMessage/Interactor/ScheduleMessageInteractor.swift +++ b/Nynja/Modules/ScheduleMessage/Interactor/ScheduleMessageInteractor.swift @@ -28,6 +28,9 @@ class ScheduleMessageInteractor: BaseInteractor, ScheduleMessageInteractorInputP private var info: ScheduleInfo? private var job: Job? + // FIXME: remove it - add localId to Job. + private var sentMessagesLocalIds: [MessageLocalId]? + private let timeZoneManager = TimeZoneManager.shared private let mqttService = MQTTService.sharedInstance @@ -58,6 +61,7 @@ class ScheduleMessageInteractor: BaseInteractor, ScheduleMessageInteractorInputP message.created = timestamp as AnyObject let messages = info.targets.messages(from: message, phoneId: phoneId) + sentMessagesLocalIds = messages.compactMap { $0.msg_id } mqttService.scheduleMessage(phoneId: phoneId, messages: messages, timestamp: timestamp, features: features) } @@ -98,7 +102,10 @@ class ScheduleMessageInteractor: BaseInteractor, ScheduleMessageInteractorInputP presenter.scheduledMessageUpdated() } } else if info.kind == .insert, StringAtom.string(job.status) == "pending" { - presenter.processPending(job: job) + let ids = job.messages.compactMap { $0.msg_id } + if ids == sentMessagesLocalIds { + presenter.processPending(job: job) + } } } } diff --git a/Nynja/Modules/ScheduleMessage/View/Views/MessageContent/AudioItemView.swift b/Nynja/Modules/ScheduleMessage/View/Views/MessageContent/AudioItemView.swift index eff163027..cbfdf9404 100644 --- a/Nynja/Modules/ScheduleMessage/View/Views/MessageContent/AudioItemView.swift +++ b/Nynja/Modules/ScheduleMessage/View/Views/MessageContent/AudioItemView.swift @@ -58,7 +58,7 @@ class AudioItemView: BaseView, ScheduleItemProtocol { let label = UILabel(height: CGFloat(Constraints.durationLabel.height), color: Text.durationLabel.fontColor, fontName: Text.durationLabel.fontName) - label.font = UIFont(fontName: Text.durationLabel.fontName, height: Text.durationLabel.fontSize) + label.font = UIFont.makeFont(with: Text.durationLabel.fontName, height: Text.durationLabel.fontSize) self.addSubview(label) label.snp.makeConstraints { make in make.centerY.equalTo(self) diff --git a/Nynja/Modules/ScheduleMessage/View/Views/MessageContent/TextItemView.swift b/Nynja/Modules/ScheduleMessage/View/Views/MessageContent/TextItemView.swift index 6bce75bbe..1bad2e1ae 100644 --- a/Nynja/Modules/ScheduleMessage/View/Views/MessageContent/TextItemView.swift +++ b/Nynja/Modules/ScheduleMessage/View/Views/MessageContent/TextItemView.swift @@ -15,7 +15,7 @@ struct TextItemModel { class TextItemView: BaseView, ScheduleItemProtocol { - static let font = UIFont(fontName: Constants.fonts.regular, height: fontHeight)! + static let font = UIFont.makeFont(with: Constants.fonts.regular, height: fontHeight)! static let textColor = Constants.colors.white.getColor() static let fontHeight = CGFloat(22.adjustedByWidth) diff --git a/Nynja/Modules/SelectCountry/Interactor/SelectCountryInteractor.swift b/Nynja/Modules/SelectCountry/Interactor/SelectCountryInteractor.swift index 399ebf71d..2713cf184 100644 --- a/Nynja/Modules/SelectCountry/Interactor/SelectCountryInteractor.swift +++ b/Nynja/Modules/SelectCountry/Interactor/SelectCountryInteractor.swift @@ -18,7 +18,7 @@ class SelectCountryInteractor: SelectCountryInteractorInputProtocol { } func filterList(with text: String) { - let filtered = self.countriesData.filter { $0.name.starts(with: text) } + let filtered = self.countriesData.filter { $0.name.contains(substring: text, options: .caseInsensitive) } self.prepareCountriesData(filtered) } diff --git a/Nynja/Modules/Settings/DataAndStorage/View/TableView/DataAndStorageTableDelegate.swift b/Nynja/Modules/Settings/DataAndStorage/View/TableView/DataAndStorageTableDelegate.swift index 8eb1dcb34..4cfe40787 100644 --- a/Nynja/Modules/Settings/DataAndStorage/View/TableView/DataAndStorageTableDelegate.swift +++ b/Nynja/Modules/Settings/DataAndStorage/View/TableView/DataAndStorageTableDelegate.swift @@ -70,7 +70,7 @@ class DataAndStorageTableDelegate: NSObject, UITableViewDelegate { let value = header ? sect.headerTitle : sect.footerTitle let lableWidth = tableView.bounds.width - (leftOffset*2) - let font = UIFont(fontName: Constants.fonts.regular, height: labelHeight) + let font = UIFont.makeFont(with: Constants.fonts.regular, height: labelHeight) let height = value.localized.height(withConstrainedWidth: lableWidth, font: font!) return height + (topOffset*2) } diff --git a/Nynja/Modules/Settings/NotificationAlertSounds/Interactor/NotificationAlertSoundsInteractor.swift b/Nynja/Modules/Settings/NotificationAlertSounds/Interactor/NotificationAlertSoundsInteractor.swift index 185450fd8..fac3187eb 100644 --- a/Nynja/Modules/Settings/NotificationAlertSounds/Interactor/NotificationAlertSoundsInteractor.swift +++ b/Nynja/Modules/Settings/NotificationAlertSounds/Interactor/NotificationAlertSoundsInteractor.swift @@ -57,7 +57,7 @@ final class NotificationAlertSoundsInteractor: NotificationAlertSoundsInteractor soundPlayer = try SoundPlayer(soundURL: soundURL) soundPlayer?.play() } catch { - LogService.log(topic: .audioSystem, text: error.localizedDescription) + LogService.log(topic: .audioSystem) { return error.localizedDescription } } } } diff --git a/Nynja/Modules/SettingsGroup/Interactor/SettingsGroupInteractor.swift b/Nynja/Modules/SettingsGroup/Interactor/SettingsGroupInteractor.swift index 21f81aa5e..77e9d8095 100644 --- a/Nynja/Modules/SettingsGroup/Interactor/SettingsGroupInteractor.swift +++ b/Nynja/Modules/SettingsGroup/Interactor/SettingsGroupInteractor.swift @@ -113,7 +113,7 @@ MQTTServiceDelegate, SetInjectable { } do { - let historyModel = try factory.makeHistoryRequestModelDelete(rosterId: phoneId, chat: room) + let historyModel = try factory.makeDeleteRequest(rosterId: phoneId, chat: room) MQTTService.sharedInstance.sendHistoryRequest(with: historyModel) } catch { @@ -159,8 +159,10 @@ MQTTServiceDelegate, SetInjectable { // MARK: - MQTTServiceDelegate - func didConnect(_ mqttService: MQTTService) { - presenter?.reachabilityStatusChanged(true) + func mqttServiceDidConnect(_ mqttService: MQTTService) { + DispatchQueue.main.async { + self.presenter?.reachabilityStatusChanged(true) + } } diff --git a/Nynja/Modules/SettingsGroup/Presenter/SettingsGroupPresenter.swift b/Nynja/Modules/SettingsGroup/Presenter/SettingsGroupPresenter.swift index 346e13c9e..76657b6da 100644 --- a/Nynja/Modules/SettingsGroup/Presenter/SettingsGroupPresenter.swift +++ b/Nynja/Modules/SettingsGroup/Presenter/SettingsGroupPresenter.swift @@ -102,7 +102,7 @@ class SettingsGroupPresenter: BasePresenter, SettingsGroupPresenterProtocol, Cre } func deleteParticipants() { - wireFrame.deleteParticipants(room?.onlyMembers) + wireFrame.deleteParticipants(room?.allMembers) } func admins() { diff --git a/Nynja/Modules/SettingsGroup/WireFrame/SettingsGroupWireFrame.swift b/Nynja/Modules/SettingsGroup/WireFrame/SettingsGroupWireFrame.swift index 887652606..bc6512639 100644 --- a/Nynja/Modules/SettingsGroup/WireFrame/SettingsGroupWireFrame.swift +++ b/Nynja/Modules/SettingsGroup/WireFrame/SettingsGroupWireFrame.swift @@ -93,7 +93,7 @@ class SettingsGroupWireFrame: SettingsGroupWireframeProtocol { guard let room = room else { return } if let contentNavigation = main?.contentNavigation { - GroupStorageWireFrame().presentGroupStorage(navigation: contentNavigation, room: room, main: main) + GroupStorageWireFrame().presentGroupStorage(navigation: contentNavigation, chatModel: room, main: main) } } diff --git a/Nynja/Modules/Splash/Interactor/SplashInteractor.swift b/Nynja/Modules/Splash/Interactor/SplashInteractor.swift index 4e0d441f9..e371c41b1 100644 --- a/Nynja/Modules/Splash/Interactor/SplashInteractor.swift +++ b/Nynja/Modules/Splash/Interactor/SplashInteractor.swift @@ -39,7 +39,7 @@ class SplashInteractor: SplashInteractorInputProtocol { guard let appDelegate = application.delegate else { return } - + badgeService.initCounters() badgeService.observeBadgeNumber(appDelegate) { (badgeNumber) in application.applicationIconBadgeNumber = Int(badgeNumber) } @@ -58,14 +58,19 @@ class SplashInteractor: SplashInteractorInputProtocol { } guard storageService.isUserLogined, let phoneId = storageService.phoneId else { + LogService.log(topic: .db) { return """ + Clear storage: hasPhone = \(storageService.hasPhone), hasToken = \(storageService.hasToken), + phoneId = \(storageService.phoneId ?? "none") + """ } prepareToShowAuth() return } - + LogService.log(topic: .db) { return "Setup DB: Splash" } storageService.setupDatabase(with: phoneId, application: UIApplication.shared) guard let roster = RosterDAO.currentRoster else { + LogService.log(topic: .db) { return "Clear storage: can't find current roster" } prepareToShowAuth() return } @@ -78,6 +83,8 @@ class SplashInteractor: SplashInteractorInputProtocol { } private func prepareToShowAuth() { + LogService.log(topic: .db) { return "Clear storage: Splash" } + storageService.clearStorage() mqttService.reconnect() diff --git a/Nynja/Modules/Splash/Interactor/SplashInteractor.swift.orig b/Nynja/Modules/Splash/Interactor/SplashInteractor.swift.orig deleted file mode 100644 index 37d106cff..000000000 --- a/Nynja/Modules/Splash/Interactor/SplashInteractor.swift.orig +++ /dev/null @@ -1,68 +0,0 @@ -// -// SplashSplashInteractor.swift -// Nynja -// -// Created by Anton Makarov on 23/08/2017. -// Copyright © 2017 TecSynt Solutions. All rights reserved. -// - -class SplashInteractor: SplashInteractorInputProtocol, ProfileHandlerDelegate, IoHandlerDelegate { - - weak var presenter: SplashInteractorOutputProtocol! - - func showed() { - if StorageService.sharedInstance.getFirstRun() { -<<<<<<< HEAD - if StorageService.sharedInstance.token == nil { -======= - let storage = StorageService.sharedInstance - guard storage.hasToken(), storage.profile.hasPhone, let roster = storage.roster else { ->>>>>>> developer - self.presenter.showAuth() - return - } - if roster.hasName { - self.presenter.showMain() - } else { - self.presenter.showEditProfile(roster: roster) - } - } else { - Language.current = .english - self.presenter.showTutorial() - } - } -<<<<<<< HEAD - - // MARK: ProfileHandlerDelegate - func getProfileSuccess(model: Profile) { - if let firstProfile = model.rosters?.first as? Roster { - if firstProfile.names == nil { - self.registered(roster: firstProfile) - } else { - self.loginedWithProfile(roster: firstProfile) - } - } - } - - // MARK: IoHandlerDelegate - func registered(roster: Roster) { - self.presenter.showEditProfile(roster: roster) - } - - func loginedWithProfile(roster: Roster) { - StorageService.sharedInstance.phone = roster.phone - StorageService.sharedInstance.rosterId = roster.id - StorageService.sharedInstance.voxId = roster.myContact?.voxID - self.presenter.showMain() - } - - func sessionNotFound() { - self.presenter.showAuth() - } - - func mismatchUserData() { - self.presenter.showAuth() - } -======= ->>>>>>> developer -} diff --git a/Nynja/Modules/Splash/View/SplashViewController.swift b/Nynja/Modules/Splash/View/SplashViewController.swift index 3b3630e57..c41bc9867 100644 --- a/Nynja/Modules/Splash/View/SplashViewController.swift +++ b/Nynja/Modules/Splash/View/SplashViewController.swift @@ -58,16 +58,14 @@ class SplashViewController: BaseVC, SplashViewProtocol { if let dict = Bundle.main.infoDictionary { if let buildV = dict["CFBundleVersion"] as? String { let host = MQTTService.sharedInstance.host - info.text = "Build: \(buildV) \nServer: \(host) (\"\(host.url)\")" + info.text = "Build: \(buildV)"//" \nServer: \(host) (\"\(host.url)\")" } } } override func viewDidAppear(_ animated: Bool) { - // Ask Anton about this )) super.viewDidAppear(animated) self.presenter?.showed() - let type = Bundle.getProvisionType() } } diff --git a/Nynja/Modules/Splash/WireFrame/SplashWireframe.swift b/Nynja/Modules/Splash/WireFrame/SplashWireframe.swift index 2ce0928d5..3ceab7f73 100644 --- a/Nynja/Modules/Splash/WireFrame/SplashWireframe.swift +++ b/Nynja/Modules/Splash/WireFrame/SplashWireframe.swift @@ -26,7 +26,6 @@ class SplashWireFrame: SplashWireFrameProtocol { presenter.interactor = interactor interactor.presenter = presenter navigation.pushViewController(view as UIViewController, animated: false) - } func showAuth() { diff --git a/Nynja/Modules/Stickers/Interactor/StickersInputInteractor.swift b/Nynja/Modules/Stickers/Interactor/StickersInputInteractor.swift index 6f971a8b9..2d336e14f 100644 --- a/Nynja/Modules/Stickers/Interactor/StickersInputInteractor.swift +++ b/Nynja/Modules/Stickers/Interactor/StickersInputInteractor.swift @@ -9,7 +9,7 @@ import Foundation final class StickersInputInteractor: BaseInteractor, StickersInputInteractorInputProtocol, SetInjectable { - private weak var presenter: StickersInputInteractorOutputProtocol! + private weak var presenter: StickersInputInteractorOutputProtocol? private var mqttService: MQTTService! private var stickersProvider: StickersProviding! @@ -55,29 +55,32 @@ extension StickersInputInteractor { mqttService = dependencies.mqttService stickersProvider = dependencies.stickersProvider - - subscribe() } } // MARK: - StickersInputInteractorInputProtocol extension StickersInputInteractor { + + func configure() { + subscribe() + stickersProvider.fetch() + } + func fetchStickerPackages() { let packages = stickersProvider.fetchStickerPackages() let recentStickers = stickersProvider.fetchRecentStickers() - presenter.didFetchStickerPackages(packages, recentStickers: recentStickers) + presenter?.didFetchStickerPackages(packages, recentStickers: recentStickers) } func search(filter: String) { let result = stickersProvider.searchStickers(filter: filter) - presenter.didReceiveSearchResult(result) + presenter?.didReceiveSearchResult(result) } func search(emoji: String) { let result = stickersProvider.searchStickers(emoji: emoji) - presenter.didReceiveSearchResult(result) + presenter?.didReceiveSearchResult(result) } } - diff --git a/Nynja/Modules/Stickers/Presenter/StickersInputPresenter.swift b/Nynja/Modules/Stickers/Presenter/StickersInputPresenter.swift index 0db06e39e..21b3edbc4 100644 --- a/Nynja/Modules/Stickers/Presenter/StickersInputPresenter.swift +++ b/Nynja/Modules/Stickers/Presenter/StickersInputPresenter.swift @@ -8,12 +8,14 @@ import Foundation -final class StickersInputPresenter: BasePresenter, StickersInputPresenterProtocol, SetInjectable { +final class StickersInputPresenter: StickersInputPresenterProtocol, SetInjectable { weak var delegate: StickerInputModuleDelegate? - private var view: StickersInputViewProtocol! - private var interactor: StickersInputInteractorInputProtocol! - private var wireFrame: StickersInputWireFrameProtocol! + private var view: StickersInputViewProtocol? + private var interactor: StickersInputInteractorInputProtocol? + private var wireFrame: StickersInputWireFrameProtocol? + + private var isViewAppeared: Bool = false } // MARK: - StickerInputModuleProtocol @@ -23,20 +25,30 @@ extension StickersInputPresenter { return view as! UIInputViewController } + func configure() { + interactor?.configure() + } + func search(filter: String) { - interactor.search(filter: filter) + interactor?.search(filter: filter) } func search(emoji: String) { - interactor.search(emoji: emoji) + interactor?.search(emoji: emoji) } } // MARK: - StickersInputPresenterProtocol extension StickersInputPresenter { - func viewDidLoad() { - interactor.fetchStickerPackages() + + func viewWillAppear() { + isViewAppeared = true + interactor?.fetchStickerPackages() + } + + func viewDidDisappear() { + isViewAppeared = false } func didSelectSticker(_ sticker: Sticker) { @@ -56,8 +68,11 @@ extension StickersInputPresenter { extension StickersInputPresenter { func didFetchStickerPackages(_ packages: [StickerPack], recentStickers: [Sticker]) { + guard isViewAppeared else { + return + } let data = StickersInputData(recentStickers: recentStickers, packages: packages) - view.setup(data) + view?.setup(data) } func didReceiveSearchResult(_ searchResult: [Sticker]) { diff --git a/Nynja/Modules/Stickers/StickersInputProtocols.swift b/Nynja/Modules/Stickers/StickersInputProtocols.swift index cc7dd66c1..bbb1a700c 100644 --- a/Nynja/Modules/Stickers/StickersInputProtocols.swift +++ b/Nynja/Modules/Stickers/StickersInputProtocols.swift @@ -8,10 +8,13 @@ import UIKit +// MARK: - Module Input + protocol StickerInputModuleProtocol: class { var delegate: StickerInputModuleDelegate? { get set } var viewController: UIInputViewController { get } + func configure() func search(filter: String) func search(emoji: String) } @@ -24,22 +27,31 @@ protocol StickerInputModuleDelegate: class { func stickerInputContainer(_ stickerInput: StickerInputModuleProtocol, didUpdateSearchResult result: [Sticker]) } +// MARK: - WireFrame + protocol StickersInputWireFrameProtocol: class { init(serviceFactory: ServiceFactory, delegate: StickerInputModuleDelegate) func build() -> StickerInputModuleProtocol } +// MARK: - View + protocol StickersInputViewProtocol: class where Self: UIInputViewController { func setup(_ data: StickersInputData) } -protocol StickersInputPresenterProtocol: BasePresenterProtocol, StickerInputModuleProtocol, StickersInputInteractorOutputProtocol { - func viewDidLoad() +// MARK: - Presenter + +protocol StickersInputPresenterProtocol: StickerInputModuleProtocol, StickersInputInteractorOutputProtocol { + func viewWillAppear() + func viewDidDisappear() func didSelectSticker(_ sticker: Sticker) func didSelectSearch() func stickerPreviewContainer() -> UIView? } +// MARK: - Interactor + protocol StickersInputInteractorOutputProtocol: class { func didFetchStickerPackages(_ packages: [StickerPack], recentStickers: [Sticker]) func didReceiveSearchResult(_ searchResult: [Sticker]) @@ -48,6 +60,7 @@ protocol StickersInputInteractorOutputProtocol: class { protocol StickersInputInteractorInputProtocol: class { func fetchStickerPackages() + func configure() func search(filter: String) func search(emoji: String) } diff --git a/Nynja/Modules/Stickers/View/CollectionView/Cells/StickerPackHeader/StickerPackHeaderModel.swift b/Nynja/Modules/Stickers/View/CollectionView/Cells/StickerPackHeader/StickerPackHeaderModel.swift index 5cd94bbe2..8f787e35f 100644 --- a/Nynja/Modules/Stickers/View/CollectionView/Cells/StickerPackHeader/StickerPackHeaderModel.swift +++ b/Nynja/Modules/Stickers/View/CollectionView/Cells/StickerPackHeader/StickerPackHeaderModel.swift @@ -10,6 +10,10 @@ import NynjaUIKit struct StickerPackHeaderModel: SupplementaryViewModel { + var accessibilityIdentifier: String { + return "sticker_pack_header" + } + let name: String func setup(view: StickerPackHeaderView) { diff --git a/Nynja/Modules/Stickers/View/PreviewView/Container/StickerPreviewContainerView.swift b/Nynja/Modules/Stickers/View/PreviewView/Container/StickerPreviewContainerView.swift index f3349da41..da97d08c0 100644 --- a/Nynja/Modules/Stickers/View/PreviewView/Container/StickerPreviewContainerView.swift +++ b/Nynja/Modules/Stickers/View/PreviewView/Container/StickerPreviewContainerView.swift @@ -52,6 +52,7 @@ final class StickerPreviewContainerView: UIView { backgroundView.alpha = 0 backgroundView.isUserInteractionEnabled = false isUserInteractionEnabled = false + setupTestingKeys() } } @@ -149,3 +150,11 @@ private extension StickerPreviewContainerView { } } } + +// MARK: - Testable +extension StickerPreviewContainerView: TestableViewProtocol { + + func setupTestingKeys() { + backgroundView.accessibilityIdentifier = "sticker_preview_background_view" + } +} diff --git a/Nynja/Modules/Stickers/View/PreviewView/Content/StickerDetailsPreviewView.swift b/Nynja/Modules/Stickers/View/PreviewView/Content/StickerDetailsPreviewView.swift index 418ba207e..beba214fb 100644 --- a/Nynja/Modules/Stickers/View/PreviewView/Content/StickerDetailsPreviewView.swift +++ b/Nynja/Modules/Stickers/View/PreviewView/Content/StickerDetailsPreviewView.swift @@ -97,6 +97,7 @@ final class StickerDetailsPreviewView: UIView, StickerPreviewingContent { stickerImageView.isHidden = false emojiLabel.isHidden = false keywordLabel.isHidden = false + setupTestingKeys() } } @@ -138,3 +139,13 @@ private extension StickerDetailsPreviewView { } } } + +// MARK: - Testable +extension StickerDetailsPreviewView: TestableViewProtocol { + + func setupTestingKeys() { + stickerImageView.accessibilityIdentifier = "sticker_preview_image_view" + emojiLabel.accessibilityIdentifier = "sticker_preview_emoji_label" + keywordLabel.accessibilityIdentifier = "sticker_preview_keyword_label" + } +} diff --git a/Nynja/Modules/Stickers/View/PreviewView/Content/StickerImagePreviewView.swift b/Nynja/Modules/Stickers/View/PreviewView/Content/StickerImagePreviewView.swift index e2530aa5c..65beb3ae8 100644 --- a/Nynja/Modules/Stickers/View/PreviewView/Content/StickerImagePreviewView.swift +++ b/Nynja/Modules/Stickers/View/PreviewView/Content/StickerImagePreviewView.swift @@ -50,6 +50,7 @@ final class StickerImagePreviewView: UIView, StickerPreviewingContent { private func setup() { stickerImageView.isHidden = false + setupTestingKeys() } } @@ -75,3 +76,11 @@ private extension StickerImagePreviewView { } } } + +// MARK: - Testable +extension StickerImagePreviewView: TestableViewProtocol { + + func setupTestingKeys() { + stickerImageView.accessibilityIdentifier = "sticker_preview_image_view" + } +} diff --git a/Nynja/Modules/Stickers/View/ViewController/StickersInputViewController.swift b/Nynja/Modules/Stickers/View/ViewController/StickersInputViewController.swift index 6cc0e3ecb..4d1b57a43 100644 --- a/Nynja/Modules/Stickers/View/ViewController/StickersInputViewController.swift +++ b/Nynja/Modules/Stickers/View/ViewController/StickersInputViewController.swift @@ -11,7 +11,7 @@ import SnapKit import NynjaUIKit final class StickersInputViewController: UIInputViewController, StickersInputViewProtocol, SetInjectable { - private weak var presenter: StickersInputPresenterProtocol! + private weak var presenter: StickersInputPresenterProtocol? // MARK: - Properties @@ -48,19 +48,19 @@ final class StickersInputViewController: UIInputViewController, StickersInputVie // MARK: Menu Items private lazy var searchMenuItemModel: AnyCellViewModel = { - return StickerStaticMenuActionCellModel(image: #imageLiteral(resourceName: "stickers_ic_search"), id: "menu_item_search") { [weak self] in + return StickerStaticMenuActionCellModel(image: #imageLiteral(resourceName: "stickers_ic_search"), id: "sticker_menu_item_search") { [weak self] in self?.selectSearch() } }() private lazy var recentMenuItemModel: AnyCellViewModel = { - return StickerStaticMenuActionCellModel(image: #imageLiteral(resourceName: "stickers_ic_recent"), id: "menu_item_recent") { [weak self] in + return StickerStaticMenuActionCellModel(image: #imageLiteral(resourceName: "stickers_ic_recent"), id: "sticker_menu_item_recent") { [weak self] in self?.selectRecents(animated: true) } }() private lazy var addMenuItemModel: AnyCellViewModel = { - return StickerStaticMenuActionCellModel(image: #imageLiteral(resourceName: "stickers_ic_add"), id: "menu_item_add") { [weak self] in + return StickerStaticMenuActionCellModel(image: #imageLiteral(resourceName: "stickers_ic_add"), id: "sticker_menu_item_add") { [weak self] in self?.selectAdd() } }() @@ -96,14 +96,19 @@ final class StickersInputViewController: UIInputViewController, StickersInputVie override func viewDidLoad() { super.viewDidLoad() setupUI() - presenter.viewDidLoad() } override func viewWillAppear(_ animated: Bool) { super.viewWillAppear(animated) + presenter?.viewWillAppear() setupCurrentSelection() } + override func viewDidDisappear(_ animated: Bool) { + super.viewDidDisappear(animated) + presenter?.viewDidDisappear() + } + override func viewDidLayoutSubviews() { super.viewDidLayoutSubviews() contentView.updateContentItemSize() @@ -341,7 +346,7 @@ final class StickersInputViewController: UIInputViewController, StickersInputVie // MARK: Selection private func selectSearch() { - presenter.didSelectSearch() + presenter?.didSelectSearch() } private func selectRecents(animated: Bool) { diff --git a/Nynja/Modules/TimeZoneSelector/Interactor/TimeZoneSelectorInteractor.swift b/Nynja/Modules/TimeZoneSelector/Interactor/TimeZoneSelectorInteractor.swift index b61eef2eb..4e39adcb6 100644 --- a/Nynja/Modules/TimeZoneSelector/Interactor/TimeZoneSelectorInteractor.swift +++ b/Nynja/Modules/TimeZoneSelector/Interactor/TimeZoneSelectorInteractor.swift @@ -12,6 +12,8 @@ class TimeZoneSelectorInteractor: TimeZoneSelectorInteractorInputProtocol { func filterTimeZones(text: String) -> [TimeZoneLocal] { let timezones = TimeZoneManager.shared.timezones - return text.isEmpty ? timezones : timezones.filter { text.isIn(string: $0.name, options: .caseInsensitive) } + return text.isEmpty ? timezones : timezones.filter { + text.isIn(strings: [$0.name, $0.text], options: .caseInsensitive) + } } } diff --git a/Nynja/MotionManager/MotionManager.swift b/Nynja/MotionManager/MotionManager.swift new file mode 100644 index 000000000..b0829e81e --- /dev/null +++ b/Nynja/MotionManager/MotionManager.swift @@ -0,0 +1,117 @@ +// +// MotionManager.swift +// Nynja +// +// Created by Anton M on 17.08.2018. +// Copyright © 2018 TecSynt Solutions. All rights reserved. +// + +import Foundation +import CoreMotion + +class MotionManager { + + static let shared = MotionManager() + private let motion: CMMotionManager + private var timer: Timer? = nil + + private var axCoords: MotionCoords? + private var gCoords: MotionCoords? + + private init() { + motion = CMMotionManager() + } + + func startAccelerometers() { + #if !RELEASE + // Make sure the accelerometer hardware is available. + let interval: TimeInterval = 1.0 / 60 + + guard self.motion.isGyroAvailable, + self.motion.isAccelerometerAvailable else { return } + + self.motion.accelerometerUpdateInterval = interval + self.motion.gyroUpdateInterval = interval + self.motion.startGyroUpdates() + self.motion.startAccelerometerUpdates() + + self.timer = Timer(fire: Date(), interval: interval, repeats: true) { _ in + self.updates(ax: self.motion.accelerometerData, g: self.motion.gyroData) + } + + RunLoop.current.add(self.timer!, forMode: .defaultRunLoopMode) + #endif + } + + var res = 0 + var i = 1 + var y = 1 + + func updates(ax: CMAccelerometerData?, g: CMGyroData?) { + if let _g = self.gCoords,let new = MotionCoords(data: g) { + res += Int(new.z) + if res > 300, Int(new.z) > 0, i > 0 { + i -= 1 + res = 0 + } + if res < -300, Int(new.z) < 0, y > 0 { + y -= 1 + res = 0 + } + + if i == 0 && y == 0 { + guard let nav = UIApplication.shared.keyWindow?.rootViewController as? UINavigationController else { return } + LogOutputWireFrame().presentLogOutputView(navigation: nav) + i = 1 + y = 1 + res = 0 + } + } else { self.gCoords = MotionCoords(data: g) } + } + +} + +struct MotionCoords { + var x: Double = 0 + var y: Double = 0 + var z: Double = 0 + + private init() { + + } + + init?(data: CMAccelerometerData?) { + guard let _data = data else { return nil } + self.x = _data.acceleration.x + self.y = _data.acceleration.y + self.z = _data.acceleration.z + } + + init?(data: CMGyroData?) { + guard let _data = data else { return nil } + self.x = _data.rotationRate.x + self.y = _data.rotationRate.y + self.z = _data.rotationRate.z + } + + func getDiff(newData: MotionCoords) -> MotionCoords { + var result = MotionCoords() + result.x = self.x + newData.x + result.y = self.y + newData.y + result.z = self.z + newData.z + return result + } + + var description: String { + return "x:\(self.x)\n y: \(self.y)\n z: \(self.z)" + } +} + +extension Double { + func absolute() -> Double { + if self < 0 { + return self * -1 + } + return self + } +} diff --git a/Nynja/NotificationManager.swift b/Nynja/NotificationManager.swift index 2d89c605d..339b5c4f6 100644 --- a/Nynja/NotificationManager.swift +++ b/Nynja/NotificationManager.swift @@ -134,7 +134,7 @@ final class NotificationManager { } navigateToChat(presenter: presenter, container: container) - case .requsted: + case .request: navigateToHistory(presenter: presenter) case .friend: navigateToFriend(presenter: presenter) @@ -253,7 +253,7 @@ final class NotificationManager { guard let contacts = roster.userlist else { return nil } guard let myContactID = roster.myContact?.phone_id else { return nil } - let isCursorInOwn = message.isInOwnChat && message.isInOwnChat + let isCursorInOwn = message.isInOwnChat && message.isCursor if sender == myContactID && !isCursorInOwn { return nil diff --git a/Nynja/OptionsItemsFactory.swift b/Nynja/OptionsItemsFactory.swift index 0838884d2..4f28559b6 100644 --- a/Nynja/OptionsItemsFactory.swift +++ b/Nynja/OptionsItemsFactory.swift @@ -17,10 +17,9 @@ class OptionsItemsFactory: WCBaseItemsFactory { // MARK: - Second lvl override var secondLevelItems: ItemModels { -// return [logout, notifications, changeNumber, wheelPosition, buildNumber, support, languageSettings, theme, dataAndStorage, security, privacy, about, deleteAccount] - return [privacy, security, dataAndStorage, theme, languageSettings, support, buildNumber, wheelPosition, changeNumber, notifications, logout] + return [logout, notifications, changeNumber, wheelPosition, buildNumber, support, languageSettings, theme, dataAndStorage, security, privacy] } -//Language, Theme, Data and Storage, Security, Privacy, notification, change number, Wheel position, Build number, support + // MARK: - Items var logout: ImageActionItemModel { let item = ImageActionItemModel(navItem: .logOut, action: { [weak navigateDelegate] (item, indexPath) in diff --git a/Nynja/P2pChatItemsFactory.swift b/Nynja/P2pChatItemsFactory.swift index bf48ebf45..ddb12e507 100644 --- a/Nynja/P2pChatItemsFactory.swift +++ b/Nynja/P2pChatItemsFactory.swift @@ -7,11 +7,13 @@ // class P2pChatItemsFactory: ChatBaseFactory { - - private let isPaymentEnable: Bool - - init(isActionsEnabled: Bool, isPaymentEnable: Bool) { - self.isPaymentEnable = isPaymentEnable + + private let isPaymentEnabled: Bool + private let canPerformAction: () -> Bool + + init(isActionsEnabled: Bool, isPaymentEnabled: Bool, canPerformAction: @escaping () -> Bool) { + self.isPaymentEnabled = isPaymentEnabled + self.canPerformAction = canPerformAction super.init(isActionsEnabled: isActionsEnabled) } @@ -32,12 +34,25 @@ class P2pChatItemsFactory: ChatBaseFactory { override var payment: ImageActionItemModel { let item = super.payment - item.state = isPaymentEnable ? .normal : .disabled + item.state = isPaymentEnabled ? .normal : .disabled return item } override var secondLevelItems: ItemModels { - return [payment, location, contact, call, media, camera] + let items = [payment, location, contact, call, media, camera] + + items.forEach { item in + let action = item.action + item.action = { params in + if self.canPerformAction() { + action?(params) + } else { + params.item.state = .normal + } + } + } + + return items } } diff --git a/Nynja/ProgressModel.swift b/Nynja/ProgressModel.swift index a9828c2a5..dbee7ce84 100644 --- a/Nynja/ProgressModel.swift +++ b/Nynja/ProgressModel.swift @@ -12,6 +12,7 @@ class ProgressModel { case notStarted case atProgress case done + case offline } public private (set) var speedStringRepresentation: String = "" @@ -38,6 +39,10 @@ class ProgressModel { self.result = result } + var description: String { + return " url: \(url)\n progress:\(progress)\n status:\(status)\n result:\(result)\n" + } + } extension ProgressModel: SpeedStringRepresentable { diff --git a/Nynja/RequestModelFactory/HistoryRequestModelFactory.swift b/Nynja/RequestModelFactory/HistoryRequestModelFactory.swift index 676ab754f..fcb5ff472 100644 --- a/Nynja/RequestModelFactory/HistoryRequestModelFactory.swift +++ b/Nynja/RequestModelFactory/HistoryRequestModelFactory.swift @@ -6,50 +6,41 @@ // Copyright © 2018 TecSynt Solutions. All rights reserved. // -protocol HistoryRequestModelFactoryProtocol { - func makeHistoryRequestModelAll(rosterId: String, chat: ChatModel) throws -> HistoryRequestModel - func makeHistoryRequestModelAllJobs(rosterId: String) throws -> HistoryRequestModel - func makeHistoryRequestModelDelete(rosterId: String, chat: ChatModel) throws -> HistoryRequestModel - func makeHistoryRequestUpdateMessage(rosterId: String, - chat: ChatModel, - messageId: Int64) throws -> HistoryRequestModel - func makeHistoryRequestModelPage(rosterId: String, - chat: ChatModel, - pageSize: Int64, - lastMessageId: Int64) throws -> HistoryRequestModel +// MARK: - Protocol + +protocol HistoryRequestModelFactoryProtocol: class { + // Messages + func makePageRequest(rosterId: String, chat: ChatModel, pageSize: Int64, lastMessageId: MessageServerId) throws -> HistoryRequestModel + func makeDeleteRequest(rosterId: String, chat: ChatModel) throws -> HistoryRequestModel + func makeReadRequest(rosterId: String, chat: ChatModel, messageId: MessageServerId) throws -> HistoryRequestModel + func makeRequestBetween(rosterId: String, chat: ChatModel, from: MessageServerId, to: MessageServerId) throws -> HistoryRequestModel + func makeGetUpdatesRequest(rosterId: String, chat: ChatModel, messageId: MessageServerId) throws -> HistoryRequestModel - func makeHistoryRequestModelStickers(rosterId: String) throws -> HistoryRequestModel + // Jobs + func makeAllJobsRequest(rosterId: String) throws -> HistoryRequestModel - func makeHistoryRequestModel(rosterId: String, - chat: ChatModel, - from: MessageServerId, - to: MessageServerId) throws -> HistoryRequestModel + // Stickers + func makeStickerPackagesRequest(rosterId: String) throws -> HistoryRequestModel } -enum HistoryRequestModelError: Error { - case modelTypeNotFound -} +// MARK: - Factory final class HistoryRequestModelFactory: HistoryRequestModelFactoryProtocol { - private func findModelType(`for` chat: HistoryRequestModelTypeProtocol) throws -> HistoryRequestModel.RequestInput.HistoryType { + enum RequestModelError: Error { + case modelTypeNotFound + } + + // MARK: Messages + + private func findModelType(`for` chat: HistoryRequestModelTypeRepresentable) throws -> HistoryRequestModel.RequestInput.HistoryType { guard let historyType = chat.historyRequestModelType else { - throw HistoryRequestModelError.modelTypeNotFound + throw RequestModelError.modelTypeNotFound } - return historyType } - - func makeHistoryRequestModelAll(rosterId: String, chat: ChatModel) throws -> HistoryRequestModel { - let historyType = try findModelType(for: chat) - let input = HistoryRequestModel.RequestInput(rosterId: rosterId, historyType: historyType, actionType: .getAll) - return HistoryRequestModel(requestInput: input) - } - - func makeHistoryRequestModelPage(rosterId: String, - chat: ChatModel, - pageSize: Int64, - lastMessageId: Int64) throws -> HistoryRequestModel { + + func makePageRequest(rosterId: String, chat: ChatModel, pageSize: Int64, lastMessageId: MessageServerId) throws -> HistoryRequestModel { let historyType = try findModelType(for: chat) let input = HistoryRequestModel.RequestInput(rosterId: rosterId, historyType: historyType, @@ -57,41 +48,51 @@ final class HistoryRequestModelFactory: HistoryRequestModelFactoryProtocol { return HistoryRequestModel(requestInput: input) } - func makeHistoryRequestModelAllJobs(rosterId: String) throws -> HistoryRequestModel { - let input = HistoryRequestModel.RequestInput(rosterId: rosterId, historyType: .job, actionType: .getAll) - return HistoryRequestModel(requestInput: input) - } - - func makeHistoryRequestModelDelete(rosterId: String, chat: ChatModel) throws -> HistoryRequestModel { + func makeDeleteRequest(rosterId: String, chat: ChatModel) throws -> HistoryRequestModel { let historyType = try findModelType(for: chat) let input = HistoryRequestModel.RequestInput(rosterId: rosterId, historyType: historyType, actionType: .delete) return HistoryRequestModel(requestInput: input) } - func makeHistoryRequestUpdateMessage(rosterId: String, chat: ChatModel, messageId: Int64) throws -> HistoryRequestModel { + func makeReadRequest(rosterId: String, chat: ChatModel, messageId: MessageServerId) throws -> HistoryRequestModel { let historyType = try findModelType(for: chat) let input = HistoryRequestModel.RequestInput(rosterId: rosterId, historyType: historyType, - actionType: .update(messageId: messageId)) + actionType: .read(messageId: messageId)) return HistoryRequestModel(requestInput: input) } - func makeHistoryRequestModelStickers(rosterId: String) throws -> HistoryRequestModel { + func makeRequestBetween(rosterId: String, chat: ChatModel, from: MessageServerId, to: MessageServerId) throws -> HistoryRequestModel { + let historyType = try findModelType(for: chat) let input = HistoryRequestModel.RequestInput(rosterId: rosterId, - historyType: .stickers, - actionType: .defaultStickerPack) + historyType: historyType, + actionType: .getBetween(from: from, to: to)) return HistoryRequestModel(requestInput: input) } - func makeHistoryRequestModel(rosterId: String, - chat: ChatModel, - from: MessageServerId, - to: MessageServerId) throws -> HistoryRequestModel { - + func makeGetUpdatesRequest(rosterId: String, chat: ChatModel, messageId: MessageServerId) throws -> HistoryRequestModel { let historyType = try findModelType(for: chat) let input = HistoryRequestModel.RequestInput(rosterId: rosterId, historyType: historyType, - actionType: .getBetween(from: from, to: to)) + actionType: .getUpdates(from: messageId)) + return HistoryRequestModel(requestInput: input) + } + + + // MARK: Jobs + + func makeAllJobsRequest(rosterId: String) throws -> HistoryRequestModel { + let input = HistoryRequestModel.RequestInput(rosterId: rosterId, historyType: .job, actionType: .getAll) + return HistoryRequestModel(requestInput: input) + } + + + // MARK: Stickers + + func makeStickerPackagesRequest(rosterId: String) throws -> HistoryRequestModel { + let input = HistoryRequestModel.RequestInput(rosterId: rosterId, + historyType: .stickers, + actionType: .defaultStickerPack) return HistoryRequestModel(requestInput: input) } } diff --git a/Nynja/Resources/Assets.xcassets/Marketplace/menu/freelance/marketplace_interpretation.imageset/marketplace_interpretation.pdf b/Nynja/Resources/Assets.xcassets/Marketplace/menu/freelance/marketplace_interpretation.imageset/marketplace_interpretation.pdf index dc1a3df1f8405acdbcdca5f00f556c8357f7300c..6d59d611651815e0b10254e81e24b54f122a9430 100644 GIT binary patch delta 1457 zcmaiue>l?#9LGyUtBxMcgss*5xH#^+eZTvO#bWYfiEz)9AKOff))$k8tb=MGD7*{CJ42=g$3i`QtwSeBPhe=kjMN2_@EN3wxgl|)w0 zMYizPuh&nS|I$gy@rq?o>N?0SCiGL41a{uJ=!Ul|sSU$J3l~Ozg%~!jc*L1y(j)I~ zo4yY%qN1|&DUYZ?ThyCndr@O`4Y=c=xbJcjM93-SgToqOScOQG{_sTrN23kWIAf}I_TlQR^H`jTX(1OMy2j15#G)H zDGI)eawE(=qeQge=nXd?;(0hvn%E>J6fN}-ox-m%$Tq~dCTVoJ;5?BlUMP4;oCVjA zy-H*jEH$@Fs6_Ma+vW)e;$6x^*a?OE3|%|&DK|i-UPu4K=;|YTGD})tT=DMqy-k;k42a^8C@eXYyDbo^{d1b0hw4b@!EUw}MMi^94Xt|c=$8t@T z8G@@@_yM-J<+~Rg+>SQsvuf~?`EG6FUE$WS^yt%GWMf(zZ=z3M8-A^EJl8iC+1@pV zV)^+AeR$*wGN?V~KJOWi<`h@eQ6D(?37D#lKT(csEnX44bmT;$yfS~=BIoXTRLB`~ zgs50*TEOkhsdKTik{qj*mDwUQmY8A8B)MzefY#FY5d-<1eiTcUDovvz=0euZK-FW- z>>p+pE7`+k^QDKlDf5BigWh>wgLMwxp-IN;Z3Y44g{an;0XxP^6oH3Q=v>~Dn?SE~ z!*J581}I;ROM0aT!>_0t?`PkyB|P0J!U^GiWfpg4bsQx~Nf%+SnbhL&8Rpe(lz0(N zJA5|IZsc83M%&E36}Tl{GP(ORdSOmKpm0G=n@0F175G?d4C;&qC$uF@kNP z@!0s=$go?;4-pzaj<0zZ-*US=hs|xy&ybFcmejwA3+GqG(GLyqy^f)t=o}EaWgQu- z`MHdC?dfxNi^rJQsK>P?W;i=nZCD>OXJJ)=32E2WhbNe51_)?9Gycyu{NW6ru;xWV zRQmhJy1U+u(oewDs!Z&gehWr%*E{jOwF27ZXuIS5_{02r?{k0m*Pd~1UfLOM9c8V) z{S`hrqpjCfapCGbeY%4fK}ielFMIGb%r6J|T-0=U`0P67uR54S*A9TKo zF5dbWLKG=_np2XNal)PCYikw~VO2spcR18CQ^^c{7hlbGo9xnzc~WB4(Gn4PXrH5j z^=TW!jk+stT~OUwWD)gzzEay)d_GtX%e`K-Z8LmWL8Qh^&IWz`W~kU<9blxA8BDfR zv@hcqb=))6aIWP@3V8qu17SpVx2IXtiMadO-ug=td#7p8(Sv+avVCno(p`xI*oP+9?jj zR@3IS<}=X#qxi~QhD5WW>L|9HgN1URewU5W)9K^$JJd_3SE^g&N~xO+BW?sQoO!H{ z;^F;1^TK3Thr|99ODy%$VRk>#aVAu~G@D6V(A;2uH90%7)V?X0_h!SGN}oJAu_ znKlMO+2WBwi@5r7{_#VS=jnWE7$fe1*X;)tg6R>DD@fs836T$&KXSkRNis3Sgvq&e-YTQ>9(p+yWqk`>Z%16_YrV{fm%>C z$2g$yqOV3C#}%)NGe1?|kBhQ3=umvigo{8R&BkLg>GEa%Dtgg5Cgv3pyK&nfO!umO z&{9ue-6GoD<20VZn81nbbTPV?#GTrynG%D4!N!3QX_e1N^gA6C8IsFA*TZE3MIT+N zQUZ;>r1?yQ?wuP+Ug;`Rn#RS|BxLk^B%6^zg_0PzUiuE3k!;gmqxkZ$D|D0%bA{cl zN{sva^k27*1>Om@^L^GC(Q=PsDfKvFv{0}6$uVIDqR-;SZECluH&G2MvOZ0*h!0b% zK_CN0dJ%sC#W@H!87X6@f zc#mu6g+H_ISTkv+E!P;yDm&+yrMwSc613dyDj z9v9b#Oi=T(dve>AdPTryFD4KWk@z*vNJ$gFz|P6m5v{-Qn|j!u!(3N;{a#9|J>cX^=SaZIfGxh z*1Swb`w~grN!8W;;}d4NR{P*}RUSabBD^M;9YpkaY(7;sn}_Ri-;uIQOc7}mjNuJXA4Ems>KrAH=c}rya_Y!O06>K9Op4v0p`$l%?ZdyUeFS^f>=Q#1Gg z6cC*4A>a}I9jPQ1y+ba*kBR#MJD}flb1_0baX%G~zkHy4-yo{09%cqkDnD?JebN>8 zrV&k;!_g>(FsrNfGunn$_fNX(Xr4opGQ-m#laa=;40yCQ`p7?;+Lx^fw!f4|tKI zfTig}VeVBSnEp{?Hgk%f;>qSWtk*egK;Rmaql9#B~z z4eD(=!FF$mcs?<2ujepD7v~-MA4r=`j5O*GM$ytu@;D3Cc*a+qUvME5BA$h#ZtZE( zdqxV=Y47fdJwer_*6~-G@Lr*rYswowgFB4p{i)#w7?zx%ZWcb(P3+MTh%J<`EB?Fm ztbFePNyK~5L$n5v*Uawg&XM)yMopL$dXU63;U#db=Jh>KAfU+Cj7WvZD9y|ml#-d( zVSo*Y1z1uYADo76I&oPYH}w1(nYG=H&>6NE(+aSmAhMoR zI%CBW;UQzazN{guBX~jF!qXnZYFpewxn}a(3){E8)PJ%fT}R35_iOkkN1MQ9*^BG@ z9o`a%RBK^`?;rWN#LDN`#=9z?-;<2vLufg?uz5jjAf)P|T)33O_m0Y&+Hic6yp!l5 z`>6|&x7OkNSxRTKQ)KCM@PjS=pk@zeqEv(M8KTyBhtJ388|%_btl+>; zYbL^BwDdK-lTm8vq}Qs^@FvX!EhW{Hn#$*{4)mb;Mf-m8_eKHSS+6yr;Ny;yLY%bF z5-RCtwGWIOao00X0VY7_b|*jjBJP3hBiWGP9@oHYI0aHOh`e&z21$u#dPBX6)c$GqX;WSu? zcKJc98^;O?=!9GOQ`tfc8q#;{W`D~!Qq6Ut8ED!BZbvAl%?C z@2U*{Yx)tuK$p=iL*2TB>OxA)2ZE=0MlIY%Ci0J=Q)6D2^H*5g**ciOij z!Ospyj>)r&07RzwdCm*FOITry=yzolsG1Ne@$V5csaJ4)yO>_ zAm8V3-a6ooXJn_>yWqich^GS z?L;PMBLuHzczPsq_H)Y~*(M&mYosl56Z-|bf5D7xYD`Rxr4^usBIJ>^WwfcxbTAkQ z21`}r@S=vMR&mHMBB3w{1TGJOr>=6~q~P*!I0*co0eOL#{=0yoauC^nGB^?{pPIrc zf`cOEe+!{~WU;jW~Cub%ESQgAA KDvC0A!1Qmy@uPGA diff --git a/Nynja/Resources/Assets.xcassets/ic_marketplace_wheel_context_menu.imageset/Contents.json b/Nynja/Resources/Assets.xcassets/ic_marketplace_wheel_context_menu.imageset/Contents.json new file mode 100644 index 000000000..7c424c00c --- /dev/null +++ b/Nynja/Resources/Assets.xcassets/ic_marketplace_wheel_context_menu.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "ic_marketplace_wheel_context_menu.pdf" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Nynja/Resources/Assets.xcassets/ic_marketplace_wheel_context_menu.imageset/ic_marketplace_wheel_context_menu.pdf b/Nynja/Resources/Assets.xcassets/ic_marketplace_wheel_context_menu.imageset/ic_marketplace_wheel_context_menu.pdf new file mode 100644 index 0000000000000000000000000000000000000000..1c8939e7598728d78603aad2e84bab2f4a9e2081 GIT binary patch literal 6309 zcmbtZbyQSa)F&kcl$0EKbj}Pp(hMC^A{{d@Gz<>iAt5CtA}u8Xf+#3m(h@_5NJ&Wu z5|0%52ID>bd~1Dwe0SY-@11@3*>~@K_W7-Se#5Si0m?48^ZD9qgn=8jN6SRt(u0BLDrH+K{Q=1lC3 zt$6ui9jg6sN1fnHfQ~c8Qa}vo&q@@~rj8l-sj>w906>-vv;li#z7M8{i@}x)&k;uy^tPXR5SONGm;r zJHP~ER6z+~1`ssVf;%9r+yNJfRB{FZ#D0wa#lVH=r-2{27XPO8pSc$N?(;Z#f<+p>H7MuE=<=+8}_KB4T$k^ zdrb=cbM60H*kzj zUs3OF@V?QpHJw87eMup{8OC)5e{iy2iE;suVLX)(28xj@a&{F;yulj7A!{A(#;ISD zVw-uHBIiKIUff4It9#WTHMTdmkR&!)O`ou*rf`i&72Pm?%c7*tI7`=n+3m5@@!+eI z#H!%m6&gNwT%fPJuHs^MI(;z~py% z2f~iZp4qt6mzXJ-;l{1QGjb0$-mQdN5<|rxq7As?_d3==?*zP1cXugw&Rx*FXRlHl z=#r5MFb;X<#pR5KB34B}iRbYi)O?AlaBbIsSpI9$c03DDJX-hD_VE>{?t;ZqZk{X^ zUGm^^`$I8d>7x|8F8*G5#{KOmEvhzvZ?wLHSegCL+=vQr8?-oUQe$jYGc*$)+Qu@Bw5*Rc?-$$OA(0UR0efN zg{VB;s-3qDj0@-?mBGrbRQ zv1*jPfFhlcC_Pu072?|pgt~zN zAkepU`F(-?fEduPnDql9KtF)$CI+ztTrFNdkp$? zUy~s{#PtIs@5cBGppEX__3E#wX9}y*O_Hk|>0eHb(a2RMCtLSzvJGeqlLYXDeuyb; zQk83H@Z>g>BS1WmWAM%m{n6+$WZY@ zcRYrW@Rg>>U-8EG_J0v6csTD#rrn~I#PELfmf}9Ee*9WPqvDgY;FiE?+x-HlDc-NfcyWIrzEAt8fj1x$@~D#?Mv?U$f%Y{qelTTuZE( zGYKMCnvLuW>W;GXQodBqSBIQ;8ul@+ZJOzo(fO)Mw9b`(uKI|)r=lw?k%iG+3pVN{ zl0qe@DKcwe#p5=cmod>mT8j45#xf}zx@|w*V&~YU`m&r#9Qm>(zoi)^#l3%z{mi7g zGK{+sZl)O= zhvs|q&ODZ9_|f!6<&jP0eDR{4lD*gV@HHEEJLjYthbt=Ac{omP_%4}Ck$<77VK(fj zW1e&k;0npXVK_2TMm88bWH*i=? rtQs$x_N6h?$4gls7+`mogZ4KRtwfo#q}~iV zeoX6}lI8bxeXwwZ-O6XwN|E-cG|lAN4WX?U3oLCx5e9AB;WDJ>0*(9uJOS)ksnV$e z$*g7x0%{iOb80N=7BwgJ{Rw^tY+zb9_FbBhv9gNliARPI9%L%Z!^K^)V;!GyeeT_R zlI%rGW))6e=>803VKr)hnJdrZ)NR%9gVB(wmN@R**(0p}m`O3*y>7$&#n1M6bMRTx z8${0A$rHu*!Y8TJk!f4@pgps>Rs|q)Y{J#|OqoPC@yUta2Ryu6Z<*1W{;~h^-U81F z&$6h@^(IcL(lM=c<61bc-93b+euv_R zO}SjN7BbbwVmr(X-Z6H2{kJ!A4o%AX$>)RaehmG}@;Jv||4-EWhC~+_2Lg)ygL~f^ zhYKO56;g#lCQ1V_++w3(nEedhlI*RmdZ-O`()i9|PwH4a{71+eea z`w{_$B~+;KbfQ#)wQ(ZCI6M{bm8h<8-?qNhf~_3MYNbd;ayJw{L9|Pu-Px+6ptw8} z>qxX+CbBo1e%56)H?>+lF=e|QFj0r4{UsFdjk_UsuyC#+)z(I<=~c?!_Cp*x3T!e% z?3M)^8$9JB1)R|@jZ$f8PgOcA54Hq?t?Q@xnzcFH#?ONiS;e4im#~Nx$3!IqIcS%0 z9_UqeTmzruz1>gY21kIvG`U21&sZ1RpF15Ct?<_OJWAzQ%B43NxYi(?9b2zGo6f$F zAB&f3`iOnrHs7BB(RfQW9c%fJYh>goQv(Y~m1fbm%8~071y-cGwiUIDgGKnD9nydz zi#dkaWn+0iv0aMBi&BL@xiay{e)(g!&B;!s-Z31+`=Cv8;hwh!dcTVFK9Dx}Ir5<* zHoK8?nqA_jGR9AzjK){&j+>|Em;?3Bq`G}i^>;Wn(b;mb*+z3to6iIrX|N91j5C-j zn^N?#s+=#$E-yUO+6G|l;$v0KX6`XZIGHE4e<0-~iP_wAa>LGX6yW_RcA684eA^rW zIZW%_I=wCNgjOcxFI8$GgyQm(q9^JKeSM^4sc7r7LyGgo7KnX+Pa9umP7Cc$_(y%6 zj}M5#qU`F!B|{7o1gwd}g2ont=tMaQt_5Z}QrUS}U?(7$lmTx5K4 z0ya!2K*Zfm4R&RT&)wp|zF${QSV&BxT^2OtSoKy8NaL+h?0J0G^5?>wT5+x>5V$`k z77h^r;_z8sW(*c`#hnFUg$1in;6B6^P{Fa6#d|3yJ&8pu2b#q8yzAhKH5%eOh<7to z)b)}noZU;0f zfg(xvc#z{M9nam8T&T9x{$K9&b%bVjPaYEt0)#J}VF!d=d4w|pyF7{YI(Vu=f{{@9 zP4B#dH+3cfZOin6S`CdfS@j#*1%}U9eBl;vk_lzI32}#%3<()_6yy^O*|4u%cT8k0 zz$sQtParE`sEt!eyt9ORDKZ!Kh>oW7+G3

;n8YY#goz9D8K-=(Yy!il_~RP{?s; zA$;t`6GP5tGM+@DS2E)G+izROTbfj)zQCP?ZAvhbb+ugkz~~xSLuU$h5h=&3CZC9{ z58i0Iyuh*J-M}+TwH->{>h>A#f4zw~h%tzHE}o^cnKOaN7|$`3{Q5%xn-kkD3VOm- z!n3d|Z-jM1o@%+XMo{>)o5HzY+@F+dP-Wz}nM|b`!A7gg#+_Cba!b{L)r>=mqw#uj z&kbv36rGW*xmGz_4#0w)JV{3T0T&0AmKvj?{M1c|TCJ>)tPiwM_wrMhQNxl)by>z* zjS2y30fNfOLaBSHi#=t@rHPIy8>xQmS`Q{ty;3a%ayTun2dRC1@}&kxr&YF9FC@R> zcCOMw)f=gBRmJqY6!<-OIy?$~_k&1>4tqmVNK*Axtzy}=iedJ`>-6#?g+9YIRhm^d z-XY!rnUuu}PDhg0I`&yUHtrK4*byenT+58h^vjiRbuYaES{@1MUMq}Ej;)W4-xFS` zMK7YKMW5T7pEq!)9P_yF_@}U@*rZ^m#FuF3Bgd%5vPzswv_^pr91bvtDTmWBfzmj1 z5!x#oon;|>zVsz>y`nD4x8}s*@3wWR^5o^!oIv!kr@5(p}Pf(wEa$g}bWk zj7eVM87~{JzA^`;=T(w?t()O-B-O* z(W-Lb$;`^DvSR)@&*o1766t0YGc@%#>(K{f2dt+Yryv5UNbkr-vcpMtPpC>EuTlH*rZl9TynmVgFjDfKt01Hi9|@vs88JJ1o(uXacuh3=z9lxd*SiJ6&0ZE^(yh* z!`;d;aWv{QUP?X69g1Vhnu>mksfy~UhI~eqOG>fEv4AI1G%O6%er%qo;=|z?^~lA| z55$XG0z#$_D>vVvUM&~*qRSI3T8-}TFf*_)%~703QNAf&)+pb%sP$ULn@FyYJWX2d zF{#aRvD4=R;TaE%KlRNl1ndB&tA`Ipooo#mxpbW=huiYts+~FWM$#w<)+iMH@(bS z>lXfQ+K6`sIIB#3?p5lILom#_M9+@uJl3gbvb3w*Ug?ytmDgElAc<~<3$-p{Pn?HQ)Qjm)P8 z%jeC?miu<|cB|_RHk)q4Ozv2c+s9kSSAn0tY*c&hpgdNzUa%fQAD_O`xb9c|DeGO= z^hEQVbI=HO9Bv1JG&2=rLs0jjwqu!`a|i7G!m~E?M&5c|vu%(?kisXGokX;9xN>3F zn|O~giO~tMr(zF8-^3+#zF54|g*==88|p_{x~46ZUwecnejT#&q3eicJDXH;$|pLY zWR3GI=iv3AakO#n`1|pw*9D^Xej0vrYr!K~Oan~(G8yOo`>_XU>6LG7)zOIq)R`HO z9IwD#`Ofg2m?+xF8wT#ubyZ8lS>+Ywv%Mjy&&_vgQN`1Zr(g6_OKr;`W)cBe0gay` z&f>$4wPYzEq|z1nteTyw2Fd$?G3zX?BJ^~CH{aQ2`sVEO)e76Wd~rIRgzCcks@ z_fl&Cvu8sD#pLEPeZJync4tpWpwOu?2PY={f^JQv8f}A;mzD%UmvCnmF5I`oi^ISd)hxc z4sE2#sLeiYf}D&WIImiNTpIDR-+odQJzLr&a}aWtdup}cmAJO4b)=P&6DQ;4PxNKi zo5R^=W$rL+@AN;K+MmoJSQG^Qr(eC;oBz?&ekTO~X=*R_+;r3Wy?(0T$OApuc9o=)*e!@t2Jk~BA_~FSzBm9bKV?9$FlJ_c zmx&0A{HX^7W77M(9!B)1ObGmkJ)n>{W*&YY3lb9f51AOIbbi+ZiwghYi-<57QwG25 zfrN!I#q_%jEDHYP8(>lKKlDU|AQ-Oc_pzAFVN(9TWTL=7;uXQ<_+LGD6wKZUf%?Y% z>)HEa<_&`3jgd%9m0naL=05;c7aJs|WWPTvV2V}}AtVG67ZHI73yF$~S%X0kpfDU$ oD`KKxI1DB(Y%NXvf2({idN+4WNq#E?5iv0^F+01WmJ;#*0Og&zNdN!< literal 0 HcmV?d00001 diff --git a/Nynja/Resources/ChannelsConfig.xcconfig b/Nynja/Resources/ChannelsConfig.xcconfig deleted file mode 100644 index df4f1afd5..000000000 --- a/Nynja/Resources/ChannelsConfig.xcconfig +++ /dev/null @@ -1,17 +0,0 @@ -// -// ChannelsConfig.xcconfig -// Nynja -// -// Created by AshCenso on 4/19/18. -// Copyright © 2018 TecSynt Solutions. All rights reserved. -// - - -BundleIdentifier = com.nynja.dev.mobile.communicator -ExtensionBundleIdentifier = com.nynja.dev.mobile.communicator.NynjaShare -ServerURL = "18.237.94.157 removed" -AppName = NYNJAChannels -ServerPort = 1883 -Config = dev -AppGroup = group.com.nynja.mobile.communicator.dev -ModelsVersion = 7 diff --git a/Nynja/Resources/Constants.swift b/Nynja/Resources/Constants.swift index 9ff01b517..82bc2a0a8 100644 --- a/Nynja/Resources/Constants.swift +++ b/Nynja/Resources/Constants.swift @@ -70,6 +70,8 @@ struct Constants { static let middleGray = Color(hex: "#cccccc") static let blackWithoutOpacity = Color(hex: "#000000", alpha: 0) static let blackWithOpacity = Color(hex: "#000000", alpha: 0.6) + static let callGradientStart = Color(hex: "#2c2e33", alpha: 1) + static let callGradientEnd = Color(hex: "#2c2e33", alpha: 0) static let separatorGrayColor = Color(hex: "#3f3f3f") static let sectionBackgroundColor = Color(hex: "#3f3f3f") static let backgroundColor = Color(hex: "#272a30") @@ -103,7 +105,7 @@ struct Constants { } struct marketplaceMunu { - static let activeIcon = Color(hex: "#d80027") + static let activeIcon = colors.white static let inactiveIcon = Color(hex: "#505255") static let activeTitle = colors.white static let inActiveTitle = colors.darkGray diff --git a/Nynja/Resources/Info.plist b/Nynja/Resources/Info.plist index 10e520c01..2de84006f 100644 --- a/Nynja/Resources/Info.plist +++ b/Nynja/Resources/Info.plist @@ -21,7 +21,7 @@ CFBundleShortVersionString 1.0 CFBundleVersion - 0.2.138 + 0.2.153 ConfServerAddress $(ConfServerAddress) ConfServerPort diff --git a/Nynja/Resources/Sounds/Call/ringback.m4a b/Nynja/Resources/Sounds/Call/ringback.m4a new file mode 100644 index 0000000000000000000000000000000000000000..e629d73cd07952706c5c183e143d913edbf1ad34 GIT binary patch literal 19987 zcmeFZ1y@{6(=I${7$iXkhXi*BnxF{~Ah^4`LvVKs?hb>ydvJFRZo%ClKyWxilKXk? z_g!aw>-zzxRyW<-rhE6U>aMz~8UO%5U})!TCBe!^1c1#P!3?v4?@EQ{k(}CC;5%Qs^hFAGeG4H3Wa5_UfU`P>f7!(x~`BsTniRV#?J+1Fw z4)^O;b|qD3^z-}h9T7#|7P9qFa#nIBh)nXq%MpvTeXOVmNKMPKdga4UjC19;yxnGW zy+jni1B@lP|8?cspynr*@iP&8Sbyg+a@0_0xo@^X(JJFfg~{#NC(`n*HO(Cb@Iq;b z7d_juHG0Ffv`9ls$TuS0>uGEo|7mCE(c`Oy23YHI@uc~dcW)!(Wg6F;$+5b2v5XA> z{M$(2_j@H;T5+ON)KhF?NiB&9%MO)*{!>~86;gCqVWC^C^^5AKFwcCU+qa(g)|aQh z_I3A%`L$-eKRw-?5<5O!5@};A!U10Ap^X)L7z^1H6<|~^+uY`e9p5G0{h)m*6u&<^&Cb;Ff_|}@!Nl7; zH;#m3?+yF9&yrCVsj9RMo|_$H_DfeB<4*=6BMn6OvV$dJIKJEQ;Y~1(i5hdiKj92tMJJ_eDDv z-*uVd7wA_2pH*UHkV0Z?S7iH~unc~v0OFU)+uQQ&n--0g3Q8=Z5f&X>;R{MQ8C}V4CzC`pZkmIY;_yAcXeU{Q&o|49X<`~Zi~ImQ%HyFMigFVtH%TV?4ZYz` zhMa`a3r+LwVI8oJACv!=t0fJbcpL;6goGg-$v1(JPQQABkQc0f5?+^JeK&8;%IM>0 z&F#6PLlgOrcr2ZMQxji74H@|nh6DSS0keprq%W);#Q&S<4x4g5^M%mOq->W^`mIaH zcth@><~IcD6#*N`*4m>u^~uOKXGX>iF}y@4{3e047K|yT4aSH56h&Z2 zB-i)FQ`Wjq!%WFmF?H;nAPk}R0!9s`vF|!s8bTF48%U_~)obMc8;OjC(~BvjCt&lM z!bQAt&F@$u+0%}^q*g6au3D-GW7ty(qzQQ4-pq*~Hh%4xi};U_>>pOEDQ-JuGX?hdFQp#jE;O3uHJasy zP|zh-=?;WYQ2Z$%N6aQ66pW7&Q87Xi(z|3%tA6eI*t?^nqn4x7l`M5Q17YU!ruw{H zwM-S-dfM%S0)aTsWW_|>-g}}+AX>kn=iOv!kwTfiS;lRBR&X9#EB~l)LMEgn^FG>@ z;RH|L&{>_NDF&FhN*+ zPBysUca8M6v&*O34q}K-bXmbvkW_0NZ9?&%6a|Km57GW2Mn?55y9MDn$@t!99V19j z+xxwvVDMUKh|KwN-$A|@N+KJqY}PvzYI2fhk3smuig`N6TH;5lZx^=Hg>nR&$l>bk z2m=a2m!Br|YK|sC`GLir#+I>6rue6bk=HG&fER!H8~|&O3NKHgN_k97W((Xy7e{`Q#mj~XlQ zUmm2bd`v+vD-T+twNt@waENgzflaMDBxa23omQQ$D=D3dK#cZYDi$|Bt2YTO-+HHl zp16ayw=Jv6!6B0WlGv|9;O)iLEu9Jml4bwKy$4dgKZ_666deYX&ie|I*bh?1|8yBL z+~A~)HNK!JT_V}E57yI{Gp%KhiFN1MQ07Xsafy090GJqZ@i-Py!`OKNUy)dq`C;7o z|Ij^m;@Ye~`#TU94?_Tcr@ku<3om?J=;fnLA#~5jYiK^{H`vVp=q*E>RWHEt zpV5VMRU?Uz-zqb9ok)(uQ}f50(OQ@DXsvi<=rHdce@E!WEZ4bE+jUoFrH=|M&65(W z&&e~zJ=rAhcS^veicZB6)(#NBZhZiCuQ92jPI(ivkgqzh1S$CpHVP%@Skvt=yO_D9 zm@e4TRBWOQxQKwSXf^Gh|)^}NAS?{IQBT=+HJ|H8o zgvl4fG5e$%3qzo5=LJU|wXD3(bvUG}JgJBS75&mQBzP#@zNkJaJ3EJCk0&OQAt$`N zeL4o+#cWR2scL*5{ZgZq2MGwl{{CdiA~{Lh&R{Shm@#R)Q%5^l1Pt~M4}Xz>BINYc zCP`;ov8t^vbVc6Fv!oE3;YC9EG?qyqz4;J_1&5G48)SiB3-&qM-!bnQFEBCRGJ0n{ zB9o-%rKqA8tjxeTFD5^j1 z49Mgle{*y6w0WR5bGzvel>$2;M{b&;oW)=o@EUrM_%mNR@5!Y4kGCCUz=2ZFZ(Sm} z`x+a$)m@%r_wusUgH?s{^5U_h1p6Vw>kr)|zaCK6k3Q9jb9JGlp=G~xL!%4|(^a?9 z`A~eE9b5g$@q?MGpg1iZ$z4QHb~w%GP@*i(rxafRl7FR_WDLriYp>s$;q%f#n2P*K zcTSy(nJcs#@#BZq}rdJdEhlvko~Q>5X3=VRRGi(@EGW?K^9 zVwCh%%+%krrcU)wWXNdJ#7skf)WOQ&tkqR28A(%`x!51&SGYPrXuOV0-|e~!*wSA2 zqTKmP)3`?x!AxwUBqOK%y3~=hcB(ZSFHx2&B50e2pDHuv!^(IdO(4z(?4-7QH_BuqwHF{2K-*C$rJmB zys7+OGNDqpr9Mv7c=DJ&HY7}mA|j8E!ViCCG`Re#&O(^#9-Uaec%KHP^M~~El|*K+ zo(Y~jbVp?iY=o|+LDS$}zlz-xF+|tJJVo7NWGb02Cp-Jq;Ig-@e1pJz$~&kvjok3@ zHqHlCXQ(M{x9s!N7+zH&TlXZLvD9a|5wTiDH2*9nzkQe2=M?0ETZU+o+VE(c|6bh` zQp5Ya3WNg;i0Dqz$NXD44@MhZ6^>65>yEt^wmzu-h~u%@qQ@ZBVs(UzF0M~vkKu~< zH|M!<-MfIy5>;5pu}w%-iwweQ@dpS;$~XvIiE*gTLAn&_xtDmw=$#Xtuec-b3~X^& zPtFB$*yJPbZVopn^!wU4wMxrB??XGxf8xQjJ|HAQo{Qn`Br3 z6kle=9^8X`TT!BegdhfQB02Fk>&p zeLhAHG}ifqDEmF$uO2Jf!_}EoV z6gaXZudbK>?ck!&F;v%)sj;sr7Pjf80+I2CCj+2f)CG$QW%Zx6&>|_FPbL@y5#9|R z7qj9GE!VaTvwA>QU4sJT-Ms+$N&Db#aAmAsdfpE_KDMF?rQeFtvvTyCnwq&Pk5d{= z_kOaOUx$%~)C8<~35T~=foTLJ?~dBGK@v1JgKve{g1cD#bwm|Jna*-)N^ueR5#LOv zzacW2*n-Q9xwx;HW-2oa*E4&2$8m749JXd1*=^TOFQ$OHgLpmE|<2AxeJ;#^D?oc|T9)j3k zT|vLZ0mYfi?_Cbz74Az>ctm(0jecQto@f${e)Ew1`nRP;_tA=a^WK)IE+w<+h4Z1Q zkVzHFWQuYrx->bqXtS@jzew_!`};c+D_N?mCoYNOH@uY2li{n(O@gYPKUxyuZ5pP)op)BKJlhAZF~D8 zF@7W=K0m^APPZ0Y5gWcj_Uq8$Zyn`%4S=Xr?qD#VbVgvXm&(wNVdLzQr)uOk6X+mi z_1wGr!?(|5eB` z0Lmue{CjMtcz%&C9@qO0{#evTgsRPlN5!3?~bb;umfM#r=)#Ngofj`&a_) zoksB1YqbD~wKnekOflP1x^5zuD3u<1#+cIcD3NpU0#>iR`srX`v^jilK4vS=IElL? z2jekSM4=V(Apy?412yN2UUl5cRGnuaza)~&V(BgOD50^?2_PSg*0>kP1d~%I`^z)k z5L)n=?|*t<5X&dtY4(i_YKwRqU zz!3oOcjO_=!G)t(pZX7II(jM#Zvbm1{S;T37R3=)#Y{6OZYS6t#i_+Re)M5dfiU$pH}LAbg*&A zS`EHM1MOCH5)>T2O4x(n-X20Nb{`Sn5Ly4iq{?2}CYkFzyQMChyQ@Q9CK`5Z>%af> zb{HBz4mexA#;+s87hsE^q3nfJf$@7Tf+%Nyxt=fsvEQ30FcAIO#N=^#VE%Y_Vb|ZE zfn9ehH#vM6+))@`WFI`P$x|(>oQx&S#8_QjQJujuaUG$~n1*wMvM^~wR5!v8NMm&O zcpx5&59`AHr+gBtR0h z0-nsQc$}J~9P=VorAwA2v-%)y6P_gOv<%DE)0jDlxs>=j&~lh)l4=fXYXU_-A_PSI z44LHFQiG5xG6Dy!WWSyNA~e$~xn?gMi%FzL7)CO}>aBJ7==N(UM_&Lx}pqF@*nYqD_f*SKAv zzqrgsHvT)5(%f=B?2+cZ_3h$hOt)6WGv0M|5e$L`@7{JFtmqFGSDKUo>ZP3fwkZRR zNroozzdCh;N6#^o-JkAzbuEo?o8JmV)Hjo2hb#!q4|k(pzlGE~tg{C4vfoy`&p+xrjVQI;pk zNqq%7L^u}96n8B|i~S{!tu2pHu`S3Nc<~es$#7Q?Q{chUt%`?oTV}0F#Be(Y13tFu zJ1Cw#lYIm%rHglh;rHp#2EwU%+C>sxu)A)WrvMnQ|GWwt_>~ArMxYfY5w+$M%g@&O zndX({^-J0~HP(`ZPzL$UumQq_+T8ZpD@QXVcGL z)gE(3X)fomHvv%2RNTm zki3H{$kBAMUpt}^E}XSzptWj;dKY;j5+ePg=L7&~kc)m#<_KXp1@H^;ii*fA5JoI$ z_~blf@9H3AH^G?s=Um{NrD6ZG)XCOXu*n0s_#& zeD4tv0m)Nq|JC;(9Yp#s)7gWgKI;U}uT1PAXj*C*r^kG^-bM5k!4gW>riM zUQ?YtlXw>i9G5^7D&!&M6rK=2`gMAA>hPOP=cx;^hE_k( zmvZn)#{%hMMfGd8?5A7ZpH_T$tD&>%?L0)2jf2IPJ*l9q{~}L9IGg`yf{=xdFew6| z5>=Us89%lg(W7r?vUrxj-JYWCNZH5B(pN}NcosJQ>c#1?_*eEA41u1DF}gowH^f~; zvn`eMLC+RnrvajG6r?k|V(ddsx$p?w#MxtPOC>J77=6Re(QU3d`yIN9ju%w*5~2^w z2)E$%GP1na(QPg&Hh!&~e8IcqR-Z-uiP%O6zpX9gUB#s&Sw}zsV{&v16HQcrE{OtI ze9fu3HBygJlA448=Q>qIIn$Qo!)FE4T7d>#yMLgmzw?>|7z`B|9cxG=+iKKSFgsJ< z)-yz64xLe2Ca-hPdqkdNe}&VK#qMalQ{pS{r~I@PM9j?m3>QN@3B#cc$2&u5N3J)= zJ55>)ZY?wCTkejFb=%*-jxDD@%4u|D1*Oa{zj+_AmsJ+)nQUIAeel%y#@#y{+aLK_ z*x9^>Q6*er!U8KtKSp(bxIKziu>yvWh)I2QZUU)zY%D!L8Y`*DJAOx#{b5`INwgn) zl1`chTLCO*^Uh!FXUA5ze*Qu~c4A6_LA)U?n%(yG8`(wU9k6KNLobZ|fE@ zlt&m{97rlL4=eN^E-6%{Zqq^U^XB|faPtp-vOn8QJgH%#I%q%I^X|XwB!kV$%1`n| zFcJ~t;LU|tYhpCtZw9?pRywa)HxnPx_+|5;a&L?y_O9M7yU8*$LMvq}_j4uwC7?d-BCi3t;lLrPNj6Iw|hkYGb~ue0tPQBkL} z$IX`wlRQm(r!orAX)`$afWUu?xg^n7SHhrRS5nK!zfUYgXVlcHuNPKPBaYC?cK`C% zwsCheS>t$x1ceuNA|mQ0=S*TqDBzmwkH-iJF%O85$l+2AWI`Y& z#T$>*dh{S_^oscacG+_gHY0L&Hcb(W)GY8j)5MGm zf_|eP>hJ8*{eF%W8D#t2_>7U7d>)XMckUr)5gqz0$$bB&*26mA)k?fNu@GS+n1Qxw zLJSByX7t_L=Q|e3Wk62XAcZ>c;~f{rHVN@K<;o;8imC~o{@@SYuo;zt&T zbCvGP7fE6TLZq?OCSy}JH8k8qPXdWDu)6R}RZy@SR*W$k!Zj2W>$*Iij`{G=e9`;0 zu96cN`UQg7mQjq#6^I%C1tB3*D}5@0pRckql{LQKAkb`OWp?;!D5%_jA>}QicU*-} zuPWtNUK`dO5}9BA`%rRpf<7I;Z8vZ4f{8+tq~TgQ8geq#LQ`|o=rYrw9C3Yt0fE>! zrthFZmfMyZ;b0v}LJK$Ba)Vc-I4(&Jb)r9+P=B+h+aPL$slZexWjGsE8a3z&cR zU~Y*s8=^lT0a5*sTQzLsx|h|4T@bd_cJ=tYAnaa7L@e($um1u{U*N|6+A0WV;#@a# zx25?IIyCoZ9#0<>9kknDa}-3M5hALSDZiS1o&Mptwzqz^`#SZE{=GS@9;2=*rLE)8 zqzC(ti`-eVSPW^a2t=me;Kq3uB4aipV~sqsG^^BGDbnYo`49?H#H64+>gYT%reYlx zT>W^R=x;BD*bOCcS-U&HJ_)(^jAq7V$tp~4R-cV}kqgU?3%}5q@blXgSe0>1#&E_@ zotRoiL@mh{e1Y>%v@X-0F)&r=lT&DwEA}}n{V^?daP8qo4ZANvO1ZzTOeK#T$inZX z>m~j5*(dOKB~SpP03$2>1PmbajN@4OXVvTV$MZpgT?+&qh`P~GNNUKeQJ@_Y&{a9HZ(3;}FnN}9HqJHN^ChCP7eg)6ST`>;E&v$J`*$e|*nHMu>66lRb%?slSWu}?_FEiUYx~|P zxhs|dl`VTV80jZbtOSlwU3Fatzmk4<3HvIz^ABAb#XJ8_o+l9r`HId)TW3EnhVm1* ztA{>3CfN<;iN!srgxA}IU?wDPXZzx~M;;W%hhwx}W)-|ZDCFYwtD1Ptq4x!Oj{f^{ z=cqd~FY990SIN@!FNSd+P-@Qyb!V0bRk6b(KkMz)RD$u~rC~gKx4fX=V35&?;iSFl z{)rA>R zC>o-{OE+xTW4FOWv={4NFh6>TQu(PixChguo#Yw*T#Rxb1`aXpgPy`)Snw`BPJ{;W6^pv&dv+ngQ3V%BJhm9TqunOR`HBul* zh*9Ro!+*s3y6K+bq#iLeH*sO>^YZ3no>hr$MVy*c%Dm#w%$@Jm8$FlLSXf>ux;PaT z6?MLT6YCh=t%J#YXOM$Jf`v!oSZE+4Q2`SPG_--s`^S*PF<=~AI1^&i!dFsVO2Dfe z{3gUojQsm+Bf5iFgHr|=Zn#lr+dI9@6cStKqqQeO#K1|F39kB*h5uNlWYcQ&Gnq^fhUiX=!LYR&l>I)(-5Qr6J-yMZK&wA5$Eu z&C#9<p)k|cu4lHrF9N!hlP#HwF%$I?y<= zuU_Ab=xD!liEqUk6a5wfya;pGn6puuW+%*<8%C{}IKe|xBv1Fa-{DY?a1<~tt$X`z zc6`4$ZpfzbWx}`DflUD9nOy`b=nLrBXH6S!Fc6g+mZ$v>S3x91Ff|02$r!1xgc}HQ zIeo}3weFibSU|!N1=eqJW-mzbf7*Vb!SqufJ3U_IZC^yfZ^sGtvir`m`|efw^1Tbs zoOc5IyW6+fj%@fQ5)@hDqiX1XLLKb>0^^VZq$8x8MAaEeo3Dq+ zGDM9fE(cLmqoNX%F=NmQ+#~Y+5cvHN;e}+4m4y8uw~1tmvT$>hiU$w%o}j~%J2}vV zuqks`3v8Ci)!VNM2!qMbmhn6E+QW9ih%(I1+h;7Zf_m&d6OEqXw2Z}xqXXF8zp|{c znv;a2z6*d*vtPw5{KmpT5=rU&I#JT1(z3R9u3)q9+MWl~<2*H*GckQVY+CLYls`TB zbY~*gu%8pHo?_&6RCkI+Vc{tPQtAW zwg^1ZB|%zaGq-_x1*5RNyr$6HZO~dYHT&3WnYANVq@lU4@%o*#sF0D`QDddRcX%2f ziqMakgJVzw_vt_W!*%o-2m?_S!U3NlT(^_%a#Q>?CRyW`^FwGIzx~*2l&Lz1eqZdA zX7;0OV|T$CU4}A*WM!+ieBF-Qtkeq)20x={#2436V)rU3y}ysx`&g!l-Ne~hjL_T7 zl9dv$RKyHhfW7zq9G+F#oPVQbqnhmE3jd_*%@uEA8;Ios(d1G?LztD81KG6l`kt(u zb()ZR*}zydbN8t(S=O6}>&-dS5lQQy{w9;K?5 zZsuNfXBmJ9{iDfXwT2YS7LR;pm2P#H5xE)(tzS|_=q3E#1O9RblXw8#k8HEjsPBn9 zPMZlC3FRg@xDwZv(VV*)I3?#-?}(Jopxb{%@@%m&*Gr3+is@nA*!SK!3w)MtCZ~OP zJ-7fX#~8>~mL9rYtR$M$^f6-{s4n?$>>I-T^hvJ}G8y|v$%&{V(jI<}?W|pzC2&oI z5>d~kne6sjj`KVa99ycC)!vN58h*8wYp&YU*4DE%k^sN6n@LI?l2bJ(tPV1!moQ11 zE0rJ34(<106+q)sr~WLE@8HbZodIp-EWJvHyem$}%afFxh=UKd+V!&aJ*)R8=?Oq? zQ^^auqWLh=A|*VV5%P?S4!FD{x~QJmVU1T)@my({h-K3p08?3%Fx4^ z#%F${r|Y6PJd>ulJ6?em9^wcs_(`q~^y}XBAcY`HAPq2zs16B%I2tN7OPdyzC+F<+ zy{1ZJCk(j*(Kv47O>S+t(4p3L&6z-|Nm|IYBDjwGvX^v*{Y;v9M++a3E=<$cz0|^x-8bRsOuK}R7FsIhH;V;N} zW+X9+ME&B>(PPQBCNhaxuRbN9Z=ykUd-GsA1aZ7iDHlQjiq_VPsFUpZr8dg@t=bt9 zM*o0k3IBFxrXlY~Ax8T;4UueB_%L8~x%}Afmz;Kn)NeHwq>vhfz{jgD%tr%pN*H~4 zeR!bmfqlIfYGy1n*l0LQZPmB8uk%%yf$}JBfTJLo0WFU++4f-_Vp9Z;i;`_~gzF`< zD$iCo)XrBHo8{G_-;nd*?krPpHX35Ykkdusp=iNZpP$#qJM;<)R206)p_*9G5U5YH zpVn%54TqQiz8K-sy@S(9?z)sMs5`k+hp66NYtS5!(PFlEBiboS;4A=TEedgxGT=P? zcVT18@R#5ZlGkMv^GNUAdHJF7K=CK5Et)rVGw>U!(v9v5QOh=S?*(L1iuV`zhfzV? zd))*AdoR#YNbDl5fD?S}d^su1=EH=ar1<0vWLn$2d=q6XAKrR6V6K@#Vg8xEKll-Mga}mB=sT99- z*6e~mYcQ+G3tBhykGfF+31R@i{rxPEH&Mt4gNs3%@_~e}zCZs}{#A8j{qg5fV=aG% zqNG@yvbGO^QtW1}u3)2zgF%}DR%Iwbwm_xhnU|gunJ%e7E1$Jk@Dt`uWm>Ah`Wpit ztlq&cnorp898R0Dqwo9pX$g+y5EA)mqNvwH@R~qw;SR0<5XFmJti}1}8LB+g^AF3W zd(mB#{;d0)4|KA4tqy(U(wJ(Z*|UqN(7bQKK-8jGFB)Z;ufNfJg#ZL1=v9btOJsZ% z=7YciB>WH|aB;{K-xkeoxifKhHGw6E#FzCJ0F}!k-V4Qd`j}kltK($M%S^uNo=eDn3J@C zjv+~K+U0+Rl?YQTtneCWFSe|&+SHG9BCO0BZ4Z|-q{-@c0cdnD)>RaF;g4yH@upWg z7u(S)B>(k?1)8`9DrG=@?yDY19Elh2KOX?HJXoZ?A%>U{5NN>W@q-7Yy4h*dT}KR{ z`{huaG?7!F6Cw&`UPYpPOi>G5&)hqm=9c>+X;Gj+tgvs{%!XoLWWX>$EX$q|KC z4P13ucTNy_iTKHosCt_Uxf`orlTWRZ%u@w0xd7PMu=R6_B4uF~096N_@!Jlf$>2SE z-U09&QT*z2B~cI}L=F~zjlCeWFME5kg=7OIR8H!~KZlg@q_gShM7XaT28e=G>~ z%dxyLy`9o~&!wyQDoDV}FeWg0;PY^r^G!ZP2`w8CZeh~ziNO+*P%@if+dpy<5)~3D zp2V7--#?5EjdK{$sFs{9F3Hi49%o{W9gj+M1ux^g+p9QrG0VMOzCA1NLN+@nPusW3 zld#df7+_7D&05gN$!kbQm#6LKstdHkg3(i#Lkz(mR;k_N|C9&~-yG=ZCLGE1f^0V= z(_-7>hKKQk&@Wqr8X@8yn8r~BD`B?c_#gGEM ze639ag#@~6oRAW(yn*brsF3VdkdOkj3^_XE>!DB>PDCA+m4<*M+>!4pmsw}Q44*@a zH{Ei(Uzh6*F#Qt5l&+_!BjLKpDkG7leh|BTXDSnc{@Xr|oSH za=Xa+ITCWCSOLs)JQ)FmuXs`;pVhlj8C0sXwzTh$PSYz>%G%7Rn}p-IcpbK%nPwVV zE7UOkll0JDWAQ%6(m>MH2Sv-pA&tfr%Z+cEN7_|{b^~nr7^gkdu|8$7 z>dB-kQ3~nA!Q8S{gza#{>BwdMZm<=L7!4AeN?DJzPhPULR)iXlgkyWBAn^noS%vbOH1<%i42T z*xyhL$V5q)8aPxDBrWhBqVn6`dYF{e&#oU|vSQZo`WwQ&ytNfsC+r>P$vLc2ey?)5 z*8A*W|K^^cVjn;MC$(wJXc+vsar-vk#?`PeOFx8Vnc$qDu7#o6GrPTN)I$KLZ>gbd zgf;A}M@Np)1u9rd(ZOJ&e^DD*Q0fwf=NZf!9)d%uj6!1+$2FzP65L1?DF5SjS&CI* zQ%5nW^N5c7K|^9$#;-g0@l9u{`4KVQ|1er0LP-dY5vr*8ehi5}I=gi`b)E&AE$mDM z`D?b*GJ3e4eq8P+-FTbRP^fw%tJ!dIH^^-7Kz zx!~@(lIj^Xw)hb)@}M5cG2s?m7t^_+w-0wLtm1E}<;V|?@`|i__BxGB&${p=@=9wZ z0>1U^f%2gx3C0@YEvmSgd(`=Rl0yMC;2 z5n~0Qt=RWy&F+7^Xt^1$y_IE4aU^`Sx{jpcB2UvuHcFVyHv}g;m~7-5z2r2W6qHvG zh?y4DEF*A|C<@20N|xlbL%{*nkVruzow|P!h-4xa*5l6=XXQddwzSsDi8q)8q9l@) ziV$QDXXXW@vN3E((Y`*aUM#ly!S)R~)5`dxV09AFDUDweE&NluXHCFdQDBfCWWU>` zh%=mVs$C9_s23F+f?L}6-(kX#Xg-lj-Y7(AnZ1K0&Y8x$CH00=gHdgATBt?zaO1sE z=C(ueF=I87f_J2N;?Lf6=ZW6|e1-lvGXF~bouO3IvqeWP0l@*HjRVD0NOJatgWa1y zHSGjw^r<7WgzRy}Yp{~y1anMCVz_{Z0>W`f?d{9&nf2q#)@K_gXo2|e1?Kj*50=Nc zhX?gO=}}jWGGUn?e43vz!h6Xu>{P+@C1{@` zQW`8g!*C^r_9ovS4vs8o@n~&v_3)XFDhC@H2Z{!YvJDEWvPHwFS)<~R_;NsB;XuAr z^nA*9cu$qLBbxgqHnR(8DKnZauGyY3a8#`+>m7BegRTV#Y`dSXcdq8T8x zPTuHqH|x`bC$r>WWjI02>stVc=wQan_n@`wG6kJSgjGmnE{HHG>T^ZEvVZ{|9KScJ zPa2Qy!3yVrx^>?-P3^jNz$u6|^(=8U_FgCFSUjC9CPulK{TZJ4O=EOFjhC{Tv$3@+ z=a`&=ZiWapQ}rCehH0fo`0C&i>AGy;yEUzDKb^pBrDxsk=OfRQpjNkOG zOOo~*nA)>{JH5-Mjje|IxKvtY#rl~)>yO9n?`+JLMyR8m;pZm^!CUUQE{D|Z%bPbw zk_lpobOyHs5CG7x?-CX@#&PjcqN+zOjYo)~9dDQVWfA#AKv5!u@qIK~M1j;KktXsJ zhDFa&ez|0Y^vvVTNk!37^q)AbsE7=)@wKYGbekws8L?ic(u!6z)Bv1o4{#I^!M|_QSfjkaIIkKX{D#!J(aj2wml+16MB*OxjlB!+ZrY~1ju_I#_{{>s)@I95kUDduq0pN~451u%G(SneWf z#K?SDn`@)=6&~g#xocgE9z_8QeFa{c8hH*7d=Vt-wrq!*l51keGkC)CpDV>ars+%{r)%~u;zSL~t5iCm! zK#Z%}`na-bD<+TSl4pw{u9Dvv)c1MBq52P1KwJ_zv8KtQWGQvwuS?afvgJ)p^NI{i z+{oBWWJQvdkEbIN9$yV>Y>pYqF`=;!QB~5{bQ5+W2ffWhwZdQu8a4z{@oiV5NdeXY zylYz4OK!)ht}WL9Oj^F#J)a35B}022Fg~ys=>-uSbR-0M^la-thA<9&Y8V6+3p7T@ zkAz?tEgsICv#J|=wyW(q`hxHK72LZR>jHT=hHc36MNin0IDWSNF*s?*e?#36w0mdS zvqdAbtn^#5638iGz1O+8hkHI4IKT~pqCFe-xX6bhP$uT$L}up%Hln~4k8Yg&1~w zme6zqo66$mq6`xEeIY6d_eCK!si=6Kd6|h!|QaLQFlHjW*o^`ACfrkNP*1b$)@{%-gMWdq|VPO^ektB8Za9lNE*H+wn z$K*OZI5YHSlUkc4k7jUumXI3PtJUqnsvX-7`}U{W2LLR_o4C2i7f>IbCHSAvT!>ml zo{H@2O>`(s!tdv^h6WvTbTa1pblzguduvXGwhR?Va?$C?@2DRUNgG)b&B9v&Gz5yh z5(J}O(o9Wfoq~^fT_ z%+cxc``+y%t`P_~fPjstZ28fT4%+^)mcQBk1@qRb#4lSkX{<)Q^E@1WiYu++b9KwX zo`N`?n@9d0Q&gNJ_<=}xRN(i2ZT5TyS)PM)NI)66=eJIS9|^7d`b4ZOi8AzbtqCTS zXSU^ddpp!(<8+6AVTCcerhQIx9X2Tp?J${VN_z3DaRZo_m$&WSvPkh(!`EDtaWDE1 zj0Rgt$x6NVV+5hU{rl7OV`fI`H)~SiAVwyfL|@-pZe0R~&Y929mh8guWqv;fEZ7A= zChn2zI|D&9JVQ(Jv~rtSOe+E73Yca=3cbpmGz_JFMo-)jr@F1Qx1-8URv;a|k)NuP z7ofK~)*ndu>JPr)SwL#qsR3L@E^y?g5G4<{k(s`v_(RFf8Eiqd9M+FTmO+hTjqY_x-VS}9a*?X;b%$2&Z%ZCng3)TzwH*!DlA?}mH zNH|tV#LGh4tl zCXXDKe#}m#i3M3`0p`sa6#AQqUPG-Py$RbpS$VHprP#Y`L**sFccZeQ#=a95$h^dL zpPsk$gw*o^Tu^%~A{%mJaoBXHMmI7Y?z>QZ?~r3wQx|cRKQ)L~q&xk===5_)h&>no z@SgOLm+X;t+v`i}E=1@DIap`agxixY$>h6ce>Jx*OpT51ijG4f-`5=`CgW`f@QKj8 zcmP<}rzb5d6p0n1!5O5)p;d-YXX?dKPQ2VOJrJ}HB-k413jv>rI#i6*n`cqfBrWDXAmoy7%?gx$W0k6g^Tc_ zZIFcBq&9L>SScusmA4ut+E&+X`X@Q}1?y{H4Ad_Pf~XVp;lL`=q`3ky4>7i!)9>I! zTMa%1V}ndU)^C6Umb?s^4+^#GZFb*lHcs|_D)r3ypx5&gTHBq+{CuAe>abJL^~&G( zhU=-I)%Ly*Y53rN<)Arz-NxohG}x+JC2TAN*wXtXZkp7-~bN^B|z zh)M!m;Xxb6To~x}HTmP&@ACll!Z>k(-%eK8K*&Y3!Z$CXvY$|YUxYYdry_g@AVc(g zE%vnj&Gmnu%lB`8f{fmuf8p`}{{Fua_zmvB)0FKOd&__7&*i5t zHU>lz29RGO;OYF|gIQeXhWGd3ljQ zN?1q<;>85uK%Tkyo#7y7pwUs`Aq5m^(*ns;kX#SR&(ClEpLWh&rGX#{pyNN-8Bwe(G{i@to%q;J5cC6F z;ue8y643mCm0w|HC4!|76g8qj!3PKy+6exDr8b_khQ-RpHUo#5y?5?ohP$^MxB%^L z-~jYxoHi(Xg?qS$D|i5P@1Si9wBG@9@mu{F9KtOa?*-1`3G}C*eJ~GwyXP77WzL@M z6tpqU5vad}UDyV7@&@LrZ(~}^ci?#*VGm9~ThHcQnAaC*zXhw%4+ZE0@4ZX+4*seE z{_Ap`I&rvfB!q!9gqh(G=BfV$>Nc|GKAUSBG4n^7WHZY!^t%tzJM*5XG8?T*b=@b^ zjz<~Hg*1t?ZdtCU#kkR^CdJB1vzFL#dTVi-Xr*L%acP$2CNHiTwMY42sWW~SQ Int64? { - return dbManager.fetch { db in - return try DBRoom - .filter(Column(RoomTable.Column.id.title) == roomId) - .select([Column(RoomTable.Column.reader.title)]) - .asRequest(of: Int64.self) - .fetchOne(db) + static func fetchReader(for roomId: String, kind: ReaderKind) -> Int64? { + if kind == .other { + return dbManager.fetch { db in + return try DBRoom + .filter(Column(RoomTable.Column.id.title) == roomId) + .select([Column(RoomTable.Column.reader.title)]) + .asRequest(of: Int64.self) + .fetchOne(db) + } + } else { + guard let phoneId = StorageService.sharedInstance.phoneId else { + return nil + } + + return MemberDAO.findMemberBy(roomId: roomId, phoneId: phoneId)?.reader } } @@ -114,7 +122,15 @@ class RoomDAO: RoomDAOProtocol { } // MARK: -- Fields - static func updateReader(_ reader: Int64, roomId: String) { + static func updateReader(_ reader: Int64, roomId: String, kind: ReaderKind) { + if kind == .other { + updateOtherReader(reader, roomId: roomId) + } else { + updateOwnReader(reader, roomId: roomId) + } + } + + private static func updateOtherReader(_ reader: Int64, roomId: String) { // Remove mentions that has been read. let unreadMentions = fetchMentions(for: roomId)?.compactMap { $0 > reader ? $0 as AnyObject : nil } @@ -127,6 +143,16 @@ class RoomDAO: RoomDAOProtocol { try? dbManager.perform(action: .updateColumns(columns), with: room) } + private static func updateOwnReader(_ reader: Int64, roomId: String) { + guard let phoneId = StorageService.sharedInstance.phoneId, + let member = MemberDAO.findMemberBy(roomId: roomId, phoneId: phoneId) else { + return + } + + member.reader = reader + MemberDAO.updateColumns([.reader], member: member) + } + static func updatedMentions(with message: Message, roomId: String) -> UpdateResult<[Int64]> { guard message.hasMentions, let phoneId = storageService.phoneId, diff --git a/Nynja/RoomDAOProtocol.swift b/Nynja/RoomDAOProtocol.swift index 6f2361f8b..2dd4ef83b 100644 --- a/Nynja/RoomDAOProtocol.swift +++ b/Nynja/RoomDAOProtocol.swift @@ -21,7 +21,7 @@ protocol RoomDAOProtocol: DAOProtocol { static func fetchUserRooms(with contactId: String, isAdmin: Bool, kind: Room.Kind?) -> [Room] // MARK: -- Reader - static func fetchReader(for roomId: String) -> Int64? + static func fetchReader(for roomId: String, kind: ReaderKind) -> Int64? // MARK: - Update // MARK: -- Room @@ -29,7 +29,7 @@ protocol RoomDAOProtocol: DAOProtocol { static func updateColumns(_ columns: Set, room: Room) // MARK: -- Fields - static func updateReader(_ reader: Int64, roomId: String) + static func updateReader(_ reader: Int64, roomId: String, kind: ReaderKind) static func updatedMentions(with message: Message, roomId: String) -> UpdateResult<[Int64]> // MARK: - Contains Members diff --git a/Nynja/Security/Anti-debugging/AntiDebuggingService.swift b/Nynja/Security/Anti-debugging/AntiDebuggingService.swift new file mode 100644 index 000000000..ccd311749 --- /dev/null +++ b/Nynja/Security/Anti-debugging/AntiDebuggingService.swift @@ -0,0 +1,43 @@ +// +// AntiDebuggingService.swift +// Nynja +// +// Created by Volodymyr Hryhoriev on 8/13/18. +// Copyright © 2018 TecSynt Solutions. All rights reserved. +// + +class AntiDebuggingService { + + private let preventers: [DebuggingPreventing] = [ + DetectorDebuggingPreventer(), + PtraceDebuggingPreventer() + ] + + private var timer: DispatchSourceTimer? + + func startTracking() { + #if !DEBUG + guard timer == nil else { + return + } + + timer = makeTimer { [weak self] in + self?.preventDebugging() + } + + timer?.resume() + #endif + } + + private func makeTimer(with handler: @escaping DispatchSourceProtocol.DispatchSourceHandler) -> DispatchSourceTimer { + let timer = DispatchSource.makeTimerSource() + timer.schedule(deadline: .now(), repeating: .seconds(3), leeway: .seconds(1)) + timer.setEventHandler(handler: handler) + return timer + } + + private func preventDebugging() { + preventers.forEach { $0.preventDebugging() } + } + +} diff --git a/Nynja/Security/Anti-debugging/DebuggingDetector/DebuggingDetector.swift b/Nynja/Security/Anti-debugging/DebuggingDetector/DebuggingDetector.swift new file mode 100644 index 000000000..d02e6d60b --- /dev/null +++ b/Nynja/Security/Anti-debugging/DebuggingDetector/DebuggingDetector.swift @@ -0,0 +1,15 @@ +// +// DebuggingDetector.swift +// Nynja +// +// Created by Volodymyr Hryhoriev on 8/13/18. +// Copyright © 2018 TecSynt Solutions. All rights reserved. +// + +class DebuggingDetector: DebuggingDetectorProtocol { + + let mechanisms: [DDMechanism] = [ + DDSysctlMechanism() + ] + +} diff --git a/Nynja/Security/Anti-debugging/DebuggingDetector/DebuggingDetectorProtocol.swift b/Nynja/Security/Anti-debugging/DebuggingDetector/DebuggingDetectorProtocol.swift new file mode 100644 index 000000000..118812207 --- /dev/null +++ b/Nynja/Security/Anti-debugging/DebuggingDetector/DebuggingDetectorProtocol.swift @@ -0,0 +1,18 @@ +// +// DebuggingDetectorProtocol.swift +// Nynja +// +// Created by Volodymyr Hryhoriev on 8/13/18. +// Copyright © 2018 TecSynt Solutions. All rights reserved. +// + +protocol DebuggingDetectorProtocol { + var mechanisms: [DDMechanism] { get } + var isDebugging: Bool { get } +} + +extension DebuggingDetectorProtocol { + var isDebugging: Bool { + return mechanisms.reduce(false) { $0 || $1.isDebugging } + } +} diff --git a/Nynja/Security/Anti-debugging/DebuggingDetector/Mechanisms/DDMechanism.swift b/Nynja/Security/Anti-debugging/DebuggingDetector/Mechanisms/DDMechanism.swift new file mode 100644 index 000000000..097c077b8 --- /dev/null +++ b/Nynja/Security/Anti-debugging/DebuggingDetector/Mechanisms/DDMechanism.swift @@ -0,0 +1,11 @@ +// +// DDMechanism.swift +// Nynja +// +// Created by Volodymyr Hryhoriev on 8/13/18. +// Copyright © 2018 TecSynt Solutions. All rights reserved. +// + +protocol DDMechanism { + var isDebugging: Bool { get } +} diff --git a/Nynja/Security/Anti-debugging/DebuggingDetector/Mechanisms/DDSysctlMechanism.swift b/Nynja/Security/Anti-debugging/DebuggingDetector/Mechanisms/DDSysctlMechanism.swift new file mode 100644 index 000000000..67137ecb5 --- /dev/null +++ b/Nynja/Security/Anti-debugging/DebuggingDetector/Mechanisms/DDSysctlMechanism.swift @@ -0,0 +1,33 @@ +// +// DDSysctlMechanism.swift +// Nynja +// +// Created by Volodymyr Hryhoriev on 8/13/18. +// Copyright © 2018 TecSynt Solutions. All rights reserved. +// + +import Foundation + +class DDSysctlMechanism: DDMechanism { + + var isDebugging: Bool { + var mib = [ + CTL_KERN, + KERN_PROC, // determines that we want to get sturct with process entries + KERN_PROC_PID, // determines that we want to select process using pid + getpid() + ] + + var info = kinfo_proc() // contains information about the process + info.kp_proc.p_flag = 0 // if the flag contains setted `P_TRACES` bit then we are in debug mode + + var size = MemoryLayout.size + + guard sysctl(&mib, u_int(mib.count), &info, &size, nil, 0) != -1 else { + return false + } + + return (info.kp_proc.p_flag & P_TRACED) != 0 + } + +} diff --git a/Nynja/Security/Anti-debugging/DebuggingPreventer/DebuggingPreventing.swift b/Nynja/Security/Anti-debugging/DebuggingPreventer/DebuggingPreventing.swift new file mode 100644 index 000000000..e2c08ac49 --- /dev/null +++ b/Nynja/Security/Anti-debugging/DebuggingPreventer/DebuggingPreventing.swift @@ -0,0 +1,11 @@ +// +// DebuggingPreventing.swift +// Nynja +// +// Created by Volodymyr Hryhoriev on 8/13/18. +// Copyright © 2018 TecSynt Solutions. All rights reserved. +// + +protocol DebuggingPreventing { + func preventDebugging() +} diff --git a/Nynja/Security/Anti-debugging/DebuggingPreventer/DetectorDebuggingPreventer.swift b/Nynja/Security/Anti-debugging/DebuggingPreventer/DetectorDebuggingPreventer.swift new file mode 100644 index 000000000..732c8db5a --- /dev/null +++ b/Nynja/Security/Anti-debugging/DebuggingPreventer/DetectorDebuggingPreventer.swift @@ -0,0 +1,20 @@ +// +// DetectorDebuggingPreventer.swift +// Nynja +// +// Created by Volodymyr Hryhoriev on 8/13/18. +// Copyright © 2018 TecSynt Solutions. All rights reserved. +// + +class DetectorDebuggingPreventer: DebuggingPreventing { + + private let detector = DebuggingDetector() + + func preventDebugging() { + if detector.isDebugging { + LogService.log(topic: .system) { return "The app is under debug." } + fatalError("Something went wrong") + } + } + +} diff --git a/Nynja/Security/Anti-debugging/DebuggingPreventer/PtraceDebuggingPreventer.swift b/Nynja/Security/Anti-debugging/DebuggingPreventer/PtraceDebuggingPreventer.swift new file mode 100644 index 000000000..179b2bf4d --- /dev/null +++ b/Nynja/Security/Anti-debugging/DebuggingPreventer/PtraceDebuggingPreventer.swift @@ -0,0 +1,40 @@ +// +// PtraceDebuggingPreventer.swift +// Nynja +// +// Created by Volodymyr Hryhoriev on 8/13/18. +// Copyright © 2018 TecSynt Solutions. All rights reserved. +// + +class PtraceDebuggingPreventer: DebuggingPreventing { + private typealias PtraceFunction = @convention(c) (CInt, Any, Any, Any) -> Void + + private let RTLD_SELF = UnsafeMutableRawPointer(bitPattern: -3) + private let PT_DENY_ATTACH: CInt = 31 + + private var cachedPtraceFunction: PtraceFunction? + + private var ptraceFunction: PtraceFunction? { + if let cached = cachedPtraceFunction { + return cached + } + + let functionName = "\("p")\("t")\("r")\("a")\("c")\("e")" + + guard let pointer = dlsym(RTLD_SELF, functionName) else { + return nil + } + + cachedPtraceFunction = unsafeBitCast(pointer, to: PtraceFunction.self) + return cachedPtraceFunction + } + + func preventDebugging() { + guard let function = ptraceFunction else { + return + } + + function(PT_DENY_ATTACH, 0, 0, 0) + } + +} diff --git a/Nynja/JailbreakDetector/JailbreakDetector.swift b/Nynja/Security/JailbreakDetector/JailbreakDetector.swift similarity index 100% rename from Nynja/JailbreakDetector/JailbreakDetector.swift rename to Nynja/Security/JailbreakDetector/JailbreakDetector.swift diff --git a/Nynja/JailbreakDetector/JailbreakDetectorProtocol.swift b/Nynja/Security/JailbreakDetector/JailbreakDetectorProtocol.swift similarity index 100% rename from Nynja/JailbreakDetector/JailbreakDetectorProtocol.swift rename to Nynja/Security/JailbreakDetector/JailbreakDetectorProtocol.swift diff --git a/Nynja/JailbreakDetector/Mechanisms/JDCydiaUrlMechanism.swift b/Nynja/Security/JailbreakDetector/Mechanisms/JDCydiaUrlMechanism.swift similarity index 100% rename from Nynja/JailbreakDetector/Mechanisms/JDCydiaUrlMechanism.swift rename to Nynja/Security/JailbreakDetector/Mechanisms/JDCydiaUrlMechanism.swift diff --git a/Nynja/JailbreakDetector/Mechanisms/JDFileBasedMechanism.swift b/Nynja/Security/JailbreakDetector/Mechanisms/JDFileBasedMechanism.swift similarity index 100% rename from Nynja/JailbreakDetector/Mechanisms/JDFileBasedMechanism.swift rename to Nynja/Security/JailbreakDetector/Mechanisms/JDFileBasedMechanism.swift diff --git a/Nynja/JailbreakDetector/Mechanisms/JDFilePermissionMechanism.swift b/Nynja/Security/JailbreakDetector/Mechanisms/JDFilePermissionMechanism.swift similarity index 100% rename from Nynja/JailbreakDetector/Mechanisms/JDFilePermissionMechanism.swift rename to Nynja/Security/JailbreakDetector/Mechanisms/JDFilePermissionMechanism.swift diff --git a/Nynja/JailbreakDetector/Mechanisms/JDMechanism.swift b/Nynja/Security/JailbreakDetector/Mechanisms/JDMechanism.swift similarity index 100% rename from Nynja/JailbreakDetector/Mechanisms/JDMechanism.swift rename to Nynja/Security/JailbreakDetector/Mechanisms/JDMechanism.swift diff --git a/Nynja/JailbreakDetector/UIDevice+Jailbreak.swift b/Nynja/Security/JailbreakDetector/UIDevice+Jailbreak.swift similarity index 100% rename from Nynja/JailbreakDetector/UIDevice+Jailbreak.swift rename to Nynja/Security/JailbreakDetector/UIDevice+Jailbreak.swift diff --git a/Nynja/ServerModel/Model/Contact.swift b/Nynja/ServerModel/Model/Contact.swift index 6393c02aa..22f979fb6 100644 --- a/Nynja/ServerModel/Model/Contact.swift +++ b/Nynja/ServerModel/Model/Contact.swift @@ -14,4 +14,6 @@ class Contact { var services: [Service]? var presence: StringAtom? var status: AnyObject? + + var lastMessageId: Int64? } diff --git a/Nynja/ServerModel/Model/Message.swift b/Nynja/ServerModel/Model/Message.swift index 0b65786ac..c6d66da65 100644 --- a/Nynja/ServerModel/Model/Message.swift +++ b/Nynja/ServerModel/Model/Message.swift @@ -20,4 +20,10 @@ class Message { var feedName: String? var senderName: String? var senderAvatar: String? + + /// Property is needed in order to handle gaps in message history. + /// Property 'isTrusted = true' when sequence of [message BEFORE (serverId = 1), self message (serverId = 2)] + /// form a valid history chain inside a p2p of muc, + /// so we could ignore some of history gaps based on 'prev' and 'next' fields. + var isTrusted: Bool? } diff --git a/Nynja/ServerModel/Model/Room.swift b/Nynja/ServerModel/Model/Room.swift index 9f42ccfb4..85575efc0 100755 --- a/Nynja/ServerModel/Model/Room.swift +++ b/Nynja/ServerModel/Model/Room.swift @@ -18,4 +18,6 @@ class Room { var update: Int64? var created: Int64? var status: AnyObject? -} \ No newline at end of file + + var lastMessageId: Int64? +} diff --git a/Nynja/ServerModel/Spec/History_Spec.swift b/Nynja/ServerModel/Spec/History_Spec.swift index 21a9f1796..a2338aaf6 100644 --- a/Nynja/ServerModel/Spec/History_Spec.swift +++ b/Nynja/ServerModel/Spec/History_Spec.swift @@ -6,7 +6,9 @@ func get_History() -> Model { Model(value:Chain(types:[ Model(value:List(constant:"")), get_p2p(), - get_muc()])), + get_muc(), + get_act(), + get_StickerPack()])), Model(value:Chain(types:[ Model(value:List(constant:"")), Model(value:Number())])), @@ -23,4 +25,5 @@ func get_History() -> Model { Model(value:Atom(constant:"get")), Model(value:Atom(constant:"update")), Model(value:Atom(constant:"last_loaded")), - Model(value:Atom(constant:"last_msg"))]))]))} + Model(value:Atom(constant:"last_msg")), + Model(value:Atom(constant:"get_reply"))]))]))} diff --git a/Nynja/Services/Amazon+FileSync.swift b/Nynja/Services/Amazon+FileSync.swift index 1dbf362c8..5ccd350c3 100644 --- a/Nynja/Services/Amazon+FileSync.swift +++ b/Nynja/Services/Amazon+FileSync.swift @@ -30,7 +30,9 @@ extension AmazonManager: FileNetworkProtocol { let progress = Float(totalBytesWritten) / Float(totalBytesExpectedToWritten) let speed: Double = Double(totalBytesWritten) / elapsedTime let transferInfo = TransferInfo(progress: progress, speed: speed, fileSize: totalBytesExpectedToWritten) + result?(nil, transferInfo) + self.delegate?.requestDidChangeProgress(request: downloadRequest!, progress: progress) } @@ -56,12 +58,7 @@ extension AmazonManager: FileNetworkProtocol { let url = URL(fileURLWithPath: localUrl) let fileName = url.lastPathComponent - let dotIndex = fileName.range(of: ".", options: .backwards) - let name = fileName[.. String? { if Aps.notificationSettings.alertSound.url?.pathExtension == "caf" { - SoundService.sharedInstance.playAlertSound() + SoundService.sharedInstance.playPushSound() return nil } else { return Aps.notificationSettings.alertSound.fileName diff --git a/Nynja/Services/HandleServices/ContactHandler.swift b/Nynja/Services/HandleServices/ContactHandler.swift index fa9efe997..84e0d24f4 100644 --- a/Nynja/Services/HandleServices/ContactHandler.swift +++ b/Nynja/Services/HandleServices/ContactHandler.swift @@ -10,6 +10,15 @@ import Foundation class ContactHandler: BaseHandler { + // MARK: - Dependencies + + static var storageService: StorageService { + return .sharedInstance + } + + + // MARK: - Handler + static func executeHandle(data: BertTuple) { guard let contact = get_Contact().parse(bert: data) as? Contact, let status = contact.originalStatus else { @@ -25,15 +34,20 @@ class ContactHandler: BaseHandler { handleLastMessage(contact) case .friend: handleFriend(contact, data: data) + case .ban, .banned: + handleBan(contact) case .authorization: handleAuthorization(contact, data: data) default: - try? StorageService.sharedInstance.perform(action: .save, with: contact) + try? storageService.perform(action: .save, with: contact) } } + + // MARK: - Statuses + private static func handleDeleted(_ contact: Contact) { - try? StorageService.sharedInstance.perform(action: .delete, with: contact) + try? storageService.perform(action: .delete, with: contact) } private static func handleInternal(_ contact: Contact) { @@ -61,23 +75,40 @@ class ContactHandler: BaseHandler { } do { - try StorageService.sharedInstance.perform(action: .save, with: contact) + try storageService.perform(action: .save, with: contact) if [.request, .authorization].contains(prevStatus) { NotificationManager.shared.handle(bert: data, type: .friend) } } catch { - LogService.log(topic: .db, text: "Storage Service Error: can't save contact with status 'friend'") + LogService.log(topic: .db) { return "Storage Service Error: can't save contact with status 'friend'" } } } private static func handleAuthorization(_ contact: Contact, data: BertTuple) { do { - try StorageService.sharedInstance.perform(action: .save, with: contact) - NotificationManager.shared.handle(bert: data, type: .requsted) + try storageService.perform(action: .save, with: contact) + NotificationManager.shared.handle(bert: data, type: .request) } catch { - LogService.log(topic: .db, text: "Storage Service Error: can't save contact with status 'authorization'") + LogService.log(topic: .db) { return "Storage Service Error: can't save contact with status 'authorization'" } } } + private static func handleBan(_ contact: Contact) { + guard + let phoneId = contact.phone_id, + let prevContact = ContactDAO.findContactBy(phoneId: phoneId) else { + return + } + + if contact.last_msg == nil { + contact.last_msg = prevContact.last_msg + } + + do { + try storageService.perform(action: .save, with: contact) + } catch { + LogService.log(topic: .db) { return "Storage Service Error: can't save contact with status '\(contact.originalStatus?.rawValue ?? "")'" } + } + } } diff --git a/Nynja/Services/HandleServices/HandlerService.swift.orig b/Nynja/Services/HandleServices/HandlerService.swift.orig deleted file mode 100644 index b703f49ec..000000000 --- a/Nynja/Services/HandleServices/HandlerService.swift.orig +++ /dev/null @@ -1,136 +0,0 @@ -// -// HandleService.swift -// Nynja -// -// Created by Anton Makarov on 14.06.2017. -// Copyright © 2017 TecSynt Solutions. All rights reserved. -// - -<<<<<<< HEAD:Nynja/Services/HandleServices/HandlerService.swift -class HandlerService: HandlerServiceProtocol { -======= -import Foundation -import CocoaMQTT - -protocol BaseHandler { - static func executeHandle(data: BertTuple) - static func executeHandle(data: BertList) -} -extension BaseHandler { - static func executeHandle(data: BertTuple) { } - static func executeHandle(data: BertList) { } -} - -enum Handlers : String { - case io - case profile - case roster - case contact - case history - case message - case search - case room - case member - case typing - case star - case job - case extendedStar = "extendedstar" - case auth -} - -class HandlerService { - - static func handle(response: CocoaMQTTMessage) { - let data = NSData(bytes: response.payload, length: response.payload.count) - do { - let result = try Bert.decode(data:data) - switch result { - case let result as BertTuple: - guard let handler = handler(of: result) else { return } - executeHandle(handler: handler, params: result) - - case let result as BertList where !result.elements.isEmpty: - guard let tuple = result.elements[0] as? BertTuple, let handler = handler(of: tuple) else { return } - executeHandle(handler: handler, params: result) - - default: - break - } - } catch { } - } - - static func handler(of tuple: BertTuple) -> Handlers? { - guard !tuple.elements.isEmpty, let header = tuple.elements[0] as? BertAtom else { - return nil - } - return Handlers(rawValue: header.value.lowercased()) - } ->>>>>>> developer:Nynja/Services/HandleServices/HandleService.swift - - static func executeHandle(handler: Handlers, params: BertTuple) { - switch handler { - case .io: - IoHandler.executeHandle(data: params) - case .profile: - ProfileHandler.executeHandle(data: params) - case .roster: - RosterHandler.executeHandle(data: params) - case .contact: - ContactHandler.executeHandle(data: params) - case .history: - HistoryHandler.executeHandle(data: params) - case .message: - MessageHandler.executeHandle(data: params) - case .search: - SearchHandler.executeHandle(data: params) - case .room: - RoomHandler.executeHandle(data: params) - case .member: - MemberHandler.executeHandle(data: params) - case .typing: - TypingHandler.executeHandle(data: params) - case .star: - StarHandler.executeHandle(data: params) - case .job: - JobHandler.executeHandle(data: params) - case .extendedStar: - ExtendedStarHandler.executeHandle(data: params) - case .auth: - AuthHandler.executeHandle(data: params) - } - } - - static func executeHandle(handler: Handlers, params: BertList) { - switch handler { - case .io: - IoHandler.executeHandle(data: params) - case .profile: - ProfileHandler.executeHandle(data: params) - case .roster: - RosterHandler.executeHandle(data: params) - case .contact: - ContactHandler.executeHandle(data: params) - case .history: - HistoryHandler.executeHandle(data: params) - case .message: - MessageHandler.executeHandle(data: params) - case .search: - SearchHandler.executeHandle(data: params) - case .room: - RoomHandler.executeHandle(data: params) - case .member: - MemberHandler.executeHandle(data: params) - case .typing: - TypingHandler.executeHandle(data: params) - case .star: - StarHandler.executeHandle(data: params) - case .job: - JobHandler.executeHandle(data: params) - case .extendedStar: - ExtendedStarHandler.executeHandle(data: params) - case .auth: - AuthHandler.executeHandle(data: params) - } - } - -} diff --git a/Nynja/Services/HandleServices/HistoryHandler.swift b/Nynja/Services/HandleServices/HistoryHandler.swift index 6c1a6106d..03ac100d1 100644 --- a/Nynja/Services/HandleServices/HistoryHandler.swift +++ b/Nynja/Services/HandleServices/HistoryHandler.swift @@ -24,6 +24,9 @@ final class HistoryHandler: BaseHandler { static weak var delegate: HistoryHandlerDelegate? + + // MARK: - Dependencies + static var storageService: StorageService { return StorageService.sharedInstance } @@ -36,61 +39,123 @@ final class HistoryHandler: BaseHandler { return StickersDownloadingService() }() + + // MARK: - Handler + static func executeHandle(data: BertTuple) { - guard let history = get_History().parse(bert: data) as? History, let data = history.data else { + guard let history = get_History().parse(bert: data) as? History else { return } - - if let messages = data as? [Message] { - updateMessageHistory(messages) + switch history.feed { + case is muc, is p2p: + if let messages = history.data as? [Message] { + updateMessageHistory(messages) + } else if let messageId = history.entity_id, let fetchType = fetchType(from: history.feed) { + markHistoryAsTrusted(before: messageId, in: fetchType) + } delegate?.getHistorySuccess() - - } else if let jobs = data as? [Job] { - updateJobsHistory(jobs) - delegate?.getJobsHistorySuccess() - - } else if let stickerPacks = data as? [StickerPack] { - updateStickerPacks(stickerPacks) - delegate?.getStickerPacksSuccess() + case is act: + if let jobs = history.data as? [Job] { + updateJobsHistory(jobs) + delegate?.getJobsHistorySuccess() + } + case is StickerPack: + if let stickerPacks = history.data as? [StickerPack] { + updateStickerPacks(stickerPacks) + delegate?.getStickerPacksSuccess() + } + default: + break } } + + // MARK: - Data + + /// The first message is new, the last is old. private static func updateMessageHistory(_ messages: [Message]) { var stackForSave = [Message]() var stackForDelete = [Message]() + var deletedActions = [DBMessageAction]() var systemClearMessage: Message? var isHistoryCleared = false for message in messages { - if message.statusString == "delete", !isHistoryCleared { - stackForDelete.append(message) - } else if case .override = messageEditService.getMergeActionForMessage(message) { - if message.isSystem, message.isStatusClear { - systemClearMessage = message - isHistoryCleared = true - stackForSave.append(message) - } else if !isHistoryCleared { - stackForSave.append(message) + message.isTrusted = true + + if message.isStatusDelete { + if let id = message.link, let action = MessageActionDAO.fetchMessageAction(by: id) { + deletedActions.append(action) + } + if !isHistoryCleared { + stackForDelete.append(message) + } + } else if let id = message.id, MessageActionDAO.containsDeleteAction(for: id) { + // Ignore received message if we have local 'delete' action for it. + continue + } else { + switch messageEditService.getMergeActionForMessage(message) { + case .override: + if message.isSystem, message.isStatusClear { + systemClearMessage = message + isHistoryCleared = true + stackForSave.append(message) + + } else if !isHistoryCleared { + stackForSave.append(message) + } + case .skip: + // Ignore received message if we have local 'edit' action for it. + break } } } + if messages.count > 1 { + messages.last?.isTrusted = nil + } if let systemClearMessage = systemClearMessage { ChatService.clearMessages(before: systemClearMessage) } + try? MessageDAO.saveMessages(stackForSave) ChatService.removeMessages(stackForDelete) + try? MessageActionDAO.delete(deletedActions) } - private static func updateJobsHistory(_ jobs: [Job]) { - JobDAO.deleteJobs() - for job in jobs { - guard StringAtom.string(job.status) == "pending" else { - continue + private static func fetchType(from feed: AnyObject?) -> FetchType? { + switch feed { + case let feed as muc: + guard let name = feed.name else { + return nil + } + return .muc(name: name) + case let feed as p2p: + guard let from = feed.from, let to = feed.to else { + return nil } - try? storageService.perform(action: .save, with: job) + return .p2p(from: from, to: to) + default: + return nil + } + } + + /// Mark messages with 'serverId' <= id as trusted + private static func markHistoryAsTrusted(before id: MessageServerId, in fetchType: FetchType) { + try? MessageDAO.trustMessages(before: id, in: fetchType) + } + + private static func updateJobsHistory(_ jobs: [Job]) { + let stackForSave = jobs.filter { StringAtom.string($0.status) == "pending" } + + let deleteStatuses = ["delete", "complete"] + let stackForDelete = jobs.filter { job in + deleteStatuses.contains { status in status == StringAtom.string(job.status) } } + + try? storageService.perform(action: .delete, with: stackForDelete) + try? storageService.perform(action: .save, with: stackForSave) } private static func updateStickerPacks(_ stickerPacks: [StickerPack]) { diff --git a/Nynja/Services/HandleServices/MessageHandler.swift b/Nynja/Services/HandleServices/MessageHandler.swift index 2b6b0be23..2b20d6385 100644 --- a/Nynja/Services/HandleServices/MessageHandler.swift +++ b/Nynja/Services/HandleServices/MessageHandler.swift @@ -8,23 +8,6 @@ import Foundation -protocol MessageHandlerSubscriber: class { - func willSave(_ message: Message) - func didSave(_ message: Message) -} -extension MessageHandlerSubscriber { - func willSave(_ message: Message) { } - func didSave(_ message: Message) { } -} - -final class MessageHandlerSubscriberReference { - weak var subscriber: MessageHandlerSubscriber? - - init(_ subscriber: MessageHandlerSubscriber) { - self.subscriber = subscriber - } -} - final class MessageHandler: BaseHandler { // MARK: - Subscribers @@ -55,7 +38,7 @@ final class MessageHandler: BaseHandler { case let (types, status) where types.contains("sys") && status == "clear": ChatService.clearHistory(message) case let (types, _) where types.contains("read"): - updateOtherReader(from: message) + updateReader(from: message) case let (_, status) where status == "delete": deleteMessage(message) case let (_, status) where status == "edit": @@ -67,22 +50,16 @@ final class MessageHandler: BaseHandler { } } - private static func updateOtherReader(from message: Message) { - if shouldUpdateOtherReader(from: message) { - ChatService.updateReader(from: message) + private static func updateReader(from message: Message) { + if shouldUpdateOwnReader(from: message) { + ChatService.updateReader(from: message, kind: .own) + } else { + ChatService.updateReader(from: message, kind: .other) } } - private static func shouldUpdateOtherReader(from message: Message) -> Bool { - var shouldUpdate = false - - if message.isInOwnChat { - shouldUpdate = true - } else if let phoneId = StorageService.sharedInstance.phoneId, message.from != phoneId { - shouldUpdate = true - } - - return shouldUpdate + private static func shouldUpdateOwnReader(from message: Message) -> Bool { + return message.isOwn } private static func deleteMessage(_ message: Message) { @@ -93,7 +70,7 @@ final class MessageHandler: BaseHandler { private static func editMessage(_ message: Message) { try? saveIntoDatabase(message: message) - guard let link: Int64 = message.link else { + guard let link = message.link else { return } @@ -107,7 +84,7 @@ final class MessageHandler: BaseHandler { try saveIntoDatabase(message: oldMessage) ChatService.updateLastMessage(oldMessage) { $0 == link } } catch { - LogService.log(topic: .db, text: "EditMessage update error: \(error.localizedDescription)") + LogService.log(topic: .db) { return "EditMessage update error: \(error.localizedDescription)" } } let messageEditService: MessageEditServiceProtocol = { @@ -123,39 +100,37 @@ final class MessageHandler: BaseHandler { private static func updateMessage(_ message: Message) { do { try saveIntoDatabase(message: message) + ChatService.update(message: message) ChatService.updateLastMessage(message) { $0 == message.id } } catch { - LogService.log(topic: .db, text: "EditMessage update error: \(error.localizedDescription)") + LogService.log(topic: .db) { return "EditMessage update error: \(error.localizedDescription)" } } } private static func saveMessage(_ message: Message, data: BertTuple) { - if let desc = message.files?.first, - desc.mime == SendMessageType.audioCall.rawValue, - desc.data?.first?.key == FeatureKeys.File.Call.users.rawValue, - let ids = desc.data?.first?.value?.splitByComma(), - !ids.contains { $0 == StorageService.sharedInstance.phoneId } { - + guard !shouldSkipMessage(message) else { return } - for subscriber in subscribers { - subscriber.subscriber?.willSave(message) - } + + // Notify subscribers about saving process + subscribers.forEach { $0.subscriber?.willSave(message) } defer { - for subscriber in subscribers { - subscriber.subscriber?.didSave(message) - } + subscribers.forEach { $0.subscriber?.didSave(message) } } - var isMessageExistsInLocalDatabase = false - - if let serverId = message.id, MessageDAO.fetchMessage(serverId: serverId) != nil { - isMessageExistsInLocalDatabase = true - } + let isMessageExistsInLocalDatabase = message.id + .flatMap { try? MessageDAO.messageExists(serverId: $0) } ?? false if message.isInOwnChat, !message.isCursor { - ChatService.updateReader(from: message) + ChatService.updateReader(from: message, kind: .other) + ChatService.updateReader(from: message, kind: .own) + } else if !message.isInOwnChat, shouldUpdateOwnReader(from: message) { + ChatService.updateReader(from: message, kind: .own) + ChatService.resetUnreadCount(from: message) + } else if !shouldUpdateOwnReader(from: message) { + ChatService.updateReader(from: message, kind: .other) } + try? saveIntoDatabase(message: message) if message.isForward { @@ -176,7 +151,21 @@ final class MessageHandler: BaseHandler { } } + private static func shouldSkipMessage(_ message: Message) -> Bool { + if let desc = message.files?.first, + desc.mime == SendMessageType.audioCall.rawValue, + desc.data?.first?.key == FeatureKeys.File.Call.users.rawValue, + let ids = desc.data?.first?.value?.splitByComma(), + !ids.contains { $0 == StorageService.sharedInstance.phoneId } { + + return true + } + + return false + } + private static func saveIntoDatabase(message: Message) throws { + try? MessageDAO.trustIfNextMessageExists(before: message) try? StorageService.sharedInstance.perform(action: .save, with: message) } diff --git a/Nynja/Services/HandleServices/MessageHandlerSubscriber.swift b/Nynja/Services/HandleServices/MessageHandlerSubscriber.swift new file mode 100644 index 000000000..35f8bf9ef --- /dev/null +++ b/Nynja/Services/HandleServices/MessageHandlerSubscriber.swift @@ -0,0 +1,24 @@ +// +// MessageHandlerSubscriber.swift +// Nynja +// +// Created by Anton Poltoratskyi on 16.08.2018. +// Copyright © 2018 TecSynt Solutions. All rights reserved. +// + +protocol MessageHandlerSubscriber: class { + func willSave(_ message: Message) + func didSave(_ message: Message) +} +extension MessageHandlerSubscriber { + func willSave(_ message: Message) { } + func didSave(_ message: Message) { } +} + +final class MessageHandlerSubscriberReference { + weak var subscriber: MessageHandlerSubscriber? + + init(_ subscriber: MessageHandlerSubscriber) { + self.subscriber = subscriber + } +} diff --git a/Nynja/Services/HandleServices/ProfileHandler.swift b/Nynja/Services/HandleServices/ProfileHandler.swift index 4cb64801b..a5da4b8ee 100644 --- a/Nynja/Services/HandleServices/ProfileHandler.swift +++ b/Nynja/Services/HandleServices/ProfileHandler.swift @@ -7,6 +7,8 @@ // class ProfileHandler: BaseHandler { + + // MARK: - Dependencies static var mqttService: MQTTService { return MQTTService.sharedInstance @@ -27,6 +29,9 @@ class ProfileHandler: BaseHandler { static var alertManager: AlertManager { return AlertManager.sharedInstance } + + + // MARK: - Handler static func executeHandle(data: BertTuple) { guard let profile = get_Profile().parse(bert: data) as? Profile, @@ -37,35 +42,38 @@ class ProfileHandler: BaseHandler { switch status { case "get", "init", "patch": if !storageService.isUserLogined, let phoneId = profile.phoneId { + LogService.log(topic: .db) { return "Setup DB: Prifile Handler" } storageService.setupDatabase(with: phoneId, application: UIApplication.shared) } handleGetInit(profile) + messageBackgroundTaskHandler.performTask() case "remove": handleRemove(profile) default: break } - - messageBackgroundTaskHandler.performTask() } - // MARK: - Handle statuses - // MARK: -- Get & Init + + // MARK: - Statuses + + // MARK: Get & Init + private static func handleGetInit(_ profile: Profile) { do { - try storageService.perform(action: .save, with: profile) - guard let roster = (profile.rosters as? [Roster])?.first else { return } storageService.setupUserInfo(from: roster) + prepareForReceived(roster) + + try storageService.perform(action: .save, with: profile) guard let phoneId = storageService.phoneId else { return } configureTestFairy(with: roster) - configureVoxService(profile) configureNynjaCommunicatorService(profile) requestJobs(with: phoneId) @@ -73,6 +81,48 @@ class ProfileHandler: BaseHandler { } catch { } } + + private static func prepareForReceived(_ newRoster: Roster) { + guard let currentRoster = RosterDAO.currentRoster else { + return + } + + // Just ignore last message from newRoster if user have local 'delete' or 'edit' action for it. + func deleteActionExists(for messageServerId: MessageServerId) -> Bool { + return MessageActionDAO.containsDeleteAction(for: messageServerId) + } + + func editActionExists(for messageServerId: MessageServerId) -> Bool { + return MessageEditActionDAO.containsEditAction(for: messageServerId) + } + + newRoster.userlist?.forEach { contact in + guard let contactId = contact.id, + let id = contact.last_msg?.id, + (deleteActionExists(for: id) || editActionExists(for: id)) else { + return + } + contact.last_msg = currentRoster.userlist?.first { $0.id == contactId }?.last_msg + } + + newRoster.roomlist?.forEach { room in + guard let roomId = room.id, + let id = room.last_msg?.id, + (deleteActionExists(for: id) || editActionExists(for: id)) else { + return + } + room.last_msg = currentRoster.roomlist?.first { $0.id == roomId }?.last_msg + } + + newRoster.favorite?.forEach { extendedStar in + guard let star = extendedStar.star else { + return + } + if StarActionDAO.containsDeleteAction(for: star) { + star.starStatus = .remove + } + } + } private static func configureTestFairy(with roster: Roster) { TestFairy.setUserId("\(roster.myContact?.phone_id ?? "")_\(roster.myContact?.fullName ?? "")") @@ -84,17 +134,9 @@ class ProfileHandler: BaseHandler { } } - private static func configureVoxService(_ profile: Profile) { - guard let voxService = profile.services?.first(where: { - ($0.type as? StringAtom)?.string == "vox" - }), let login = voxService.login, let password = voxService.password else { - return - } - } - private static func requestJobs(with phoneId: String) { do { - let historyModel = try historyFactory.makeHistoryRequestModelAllJobs(rosterId: phoneId) + let historyModel = try historyFactory.makeAllJobsRequest(rosterId: phoneId) mqttService.sendHistoryRequest(with: historyModel) } catch { assertionFailure(error.localizedDescription) @@ -103,7 +145,7 @@ class ProfileHandler: BaseHandler { private static func requestStickerPacks(with rosterId: String) { do { - let historyModel = try historyFactory.makeHistoryRequestModelStickers(rosterId: rosterId) + let historyModel = try historyFactory.makeStickerPackagesRequest(rosterId: rosterId) mqttService.sendHistoryRequest(with: historyModel) } catch { assertionFailure(error.localizedDescription) @@ -111,7 +153,7 @@ class ProfileHandler: BaseHandler { } - // MARK: - Remove + // MARK: Remove private static func handleRemove(_ profile: Profile) { try? storageService.perform(action: .delete, with: profile) diff --git a/Nynja/Services/HandleServices/RoomHandler.swift b/Nynja/Services/HandleServices/RoomHandler.swift index 53cbd6255..34d7b905c 100644 --- a/Nynja/Services/HandleServices/RoomHandler.swift +++ b/Nynja/Services/HandleServices/RoomHandler.swift @@ -8,11 +8,23 @@ class RoomHandler: BaseHandler { + // MARK: - Dependencies + + static var storageService: StorageService { + return .sharedInstance + } + + static var notificationManager: NotificationManager { + return .shared + } + + + // MARK: - Handler + static func executeHandle(data: BertTuple, codes: StatusCodes) { guard let room = get_Room().parse(bert: data) as? Room else { return } - if codes.isEmpty { handle(room: room, data: data) } else { @@ -24,7 +36,6 @@ class RoomHandler: BaseHandler { guard let status = room.originalStatus else { return } - switch status { case .get: RoomDAO.updateRoom(room) { (new, old) in @@ -32,15 +43,19 @@ class RoomHandler: BaseHandler { return new } case .create: - try? StorageService.sharedInstance.perform(action: .save, with: room) - NotificationManager.shared.handle(bert: data, type: .message) + room.last_msg?.isTrusted = true + try? storageService.perform(action: .save, with: room) + notificationManager.handle(bert: data, type: .message) case .patch: RoomDAO.patchRoom(room) case .add: + trustLastMessageIfNeeded(for: room) handleAddMember(room) case .remove: + trustLastMessageIfNeeded(for: room) handleRemoveMember(room) case .leave: + trustLastMessageIfNeeded(for: room) handleLeave(room) case .lastMessage: RoomDAO.updateColumns([.unread], room: room) @@ -55,12 +70,12 @@ class RoomHandler: BaseHandler { } - // MARK: - Handle statuses - - // MARK: - Handle add + // MARK: - Statuses + // MARK: - Add Member private static func handleAddMember(_ room: Room) { guard let id = room.id, let oldRoom = RoomDAO.findRoom(by: id) else { - try? StorageService.sharedInstance.perform(action: .save, with: room) + try? storageService.perform(action: .save, with: room) return } @@ -72,7 +87,7 @@ class RoomHandler: BaseHandler { } - // MARK: - Handle channel add + // MARK: - Add Member Channel private static func handleChannelAddMember(_ room: Room, oldRoom: Room) { if let features = room.settings { @@ -80,11 +95,11 @@ class RoomHandler: BaseHandler { } oldRoom.status = room.status - try? StorageService.sharedInstance.perform(action: .save, with: oldRoom) + try? storageService.perform(action: .save, with: oldRoom) } - // MARK: - Handle group add + // MARK: - Add Member Room private static func handleGroupAddMember(_ room: Room, oldRoom: Room) { addNotExistedMembers(from: room, to: oldRoom) @@ -96,7 +111,7 @@ class RoomHandler: BaseHandler { oldRoom.status = room.status oldRoom.last_msg = room.last_msg - try? StorageService.sharedInstance.perform(action: .save, with: oldRoom) + try? storageService.perform(action: .save, with: oldRoom) } private static func addNotExistedMembers(from room: Room, to oldRoom: Room) { @@ -154,7 +169,7 @@ class RoomHandler: BaseHandler { } - // MARK: - Handle remove + // MARK: - Remove Member private static func handleRemoveMember(_ room: Room) { if let id = room.id, let oldRoom = RoomDAO.findRoom(by: id) { @@ -164,18 +179,19 @@ class RoomHandler: BaseHandler { } }) oldRoom.status = room.status - try? StorageService.sharedInstance.perform(action: .save, with: oldRoom) + try? storageService.perform(action: .save, with: oldRoom) } } - // MARK: - Handle leave + // MARK: - Leave private static func handleLeave(_ room: Room) { if let id = room.id, let oldRoom = RoomDAO.findRoom(by: id) { - if let _ = room.allMembersWithRemoved?.first(where: { $0.phone_id == StorageService.sharedInstance.phoneId}) { + let phoneId = storageService.phoneId + if let _ = room.allMembersWithRemoved?.first(where: { $0.phone_id == phoneId }) { room.unread = 0 - try? StorageService.sharedInstance.perform(action: .save, with: room) + try? storageService.perform(action: .save, with: room) } else { if let ad = oldRoom.admins?.first(where: { $0.phone_id == room.allMembers?.first?.phone_id }) { ad.status = StringAtom(string: "removed") @@ -184,9 +200,18 @@ class RoomHandler: BaseHandler { } oldRoom.status = StringAtom(string: "get") oldRoom.unread = 0 - try? StorageService.sharedInstance.perform(action: .save, with: oldRoom) + try? storageService.perform(action: .save, with: oldRoom) } } } + + // MARK: - Last Message + + private static func trustLastMessageIfNeeded(for room: Room) { + guard let lastMessage = room.last_msg else { + return + } + try? MessageDAO.trustIfNextMessageExists(before: lastMessage) + } } diff --git a/Nynja/Services/HandleServices/StarHandler.swift b/Nynja/Services/HandleServices/StarHandler.swift index 9032e3756..416651962 100644 --- a/Nynja/Services/HandleServices/StarHandler.swift +++ b/Nynja/Services/HandleServices/StarHandler.swift @@ -15,7 +15,9 @@ class StarHandler: BaseHandler { do { switch status { case .add: - try StorageService.sharedInstance.perform(action: .save, with: star) + if !StarActionDAO.containsDeleteAction(for: star) { + try StorageService.sharedInstance.perform(action: .save, with: star) + } case .remove: try StorageService.sharedInstance.perform(action: .delete, with: star) } diff --git a/Nynja/Services/LocationService.swift b/Nynja/Services/LocationService.swift index 1419e9d22..4d291b129 100644 --- a/Nynja/Services/LocationService.swift +++ b/Nynja/Services/LocationService.swift @@ -86,7 +86,7 @@ class LocationService: NSObject, CLLocationManagerDelegate { } public func didReceiveLocation(location:CLLocation) { - LogService.log(topic: .locationSystem, text: "Location Manager Did Receive Location: \(location)") + LogService.log(topic: .locationSystem) { return "Location Manager Did Receive Location: \(location)" } //getCountry() } @@ -95,7 +95,7 @@ class LocationService: NSObject, CLLocationManagerDelegate { let geoCoder = CLGeocoder() geoCoder.reverseGeocodeLocation(self.lastLocation, completionHandler: { (placemarks, error) -> Void in if let err = error{ - LogService.log(topic: .locationSystem, text: "Error Reverse Geocoding Location: \(err.localizedDescription)") + LogService.log(topic: .locationSystem) { return "Error Reverse Geocoding Location: \(err.localizedDescription)" } return } if let countryCode = placemarks?[0].addressDictionary?["CountryCode"] as? String { diff --git a/Nynja/Services/MQTT/MQTTService.swift b/Nynja/Services/MQTT/MQTTService.swift index f4831103f..d3c995c20 100644 --- a/Nynja/Services/MQTT/MQTTService.swift +++ b/Nynja/Services/MQTT/MQTTService.swift @@ -21,6 +21,11 @@ struct Host { final class MQTTService: NSObject, CocoaMQTTDelegate, ReachabilityServiceObserver { + enum ConnectionState { + case connected + case disconnected + } + var mqtt: CocoaMQTT? static let version = Bundle.main.modelsVersion @@ -144,12 +149,12 @@ final class MQTTService: NSObject, CocoaMQTTDelegate, ReachabilityServiceObserve mqtt?.willMessage = CocoaMQTTWill(topic: "version/\(MQTTService.version)", message: "") mqtt?.keepAlive = 0 mqtt?.delegate = self +// mqtt?.enableSSL = true - let log = "setup clientID: \(mqtt?.clientID ?? "") & password: \(mqtt?.password ?? "")" - LogService.log(topic: .MQTT, text: log) + LogService.log(topic: .MQTT) { return "setup clientID: \(mqtt?.clientID ?? "") & password: \(mqtt?.password ?? "")" } } - - var isConnectedSuccess = false + + private(set) var isConnectedSuccess = false func mqtt(_ mqtt: CocoaMQTT, didConnect host: String, port: Int) { //sendPresence() @@ -161,25 +166,26 @@ final class MQTTService: NSObject, CocoaMQTTDelegate, ReachabilityServiceObserve if ack == .badUsernameOrPassword { if let actualToken = StorageService.sharedInstance.token { if actualToken == mqtt.password { + LogService.log(topic: .MQTT) { return "Bad Username or Password" } #if !SHARE_EXTENSION + LogService.log(topic: .db) { return "Clear storage: bad username" } StorageService.sharedInstance.clearStorage() self.state = .notAuthenticated(isLoggedOutFromServer: true) MQTTService.sharedInstance.queue = Queue() IoHandler.delegate?.sessionNotFound() - notifySubscribersDisconnect() + notifySubscribersAuthFailure() self.reconnect() #endif - LogService.log(topic: .MQTT, text: "Bad Username or Password") } } else { + LogService.log(topic: .MQTT) { return "Bad protocol version" } notifyWrongVersion() } } if ack == .accept { - let log = "clientID: \(mqtt.clientID ) & password: \(mqtt.password ?? "") & clearSession: \(mqtt.cleanSession) state: \(mqtt.connState)" - LogService.log(topic: .MQTT, text: log) + LogService.log(topic: .MQTT) { return "clientID: \(mqtt.clientID ) & password: \(mqtt.password ?? "") & clearSession: \(mqtt.cleanSession) state: \(mqtt.connState)" } isConnectedSuccess = true #if !SHARE_EXTENSION @@ -221,11 +227,13 @@ final class MQTTService: NSObject, CocoaMQTTDelegate, ReachabilityServiceObserve } func reconnect() { + LogService.log(topic: .MQTT) { return "Reconnect" } self.setup() myConnect() } func disconnect() { + LogService.log(topic: .MQTT) { return "Disconnect" } ReachabilityService.sharedInstance.removeRechabilityObserver(self) timer?.invalidate() mqtt?.disconnect() @@ -242,13 +250,13 @@ final class MQTTService: NSObject, CocoaMQTTDelegate, ReachabilityServiceObserve } func mqttDidDisconnect(_ mqtt: CocoaMQTT, withError err: Error?) { - let log = "clientID: \(mqtt.clientID ) & password: \(mqtt.password ?? "") & clearSession: \(mqtt.cleanSession) state: \(mqtt.connState) & error: \(err?.localizedDescription)" - LogService.log(topic: .MQTT, text: log) + LogService.log(topic: .MQTT) { return "clientID: \(mqtt.clientID ) & password: \(mqtt.password ?? "") & clearSession: \(mqtt.cleanSession) state: \(mqtt.connState) & error: \(err?.localizedDescription)" } if let error = err as NSError?, error.code == 7 { - LogService.log(topic: .MQTT, text: "Something went wrong") + LogService.log(topic: .MQTT) { return "Something went wrong" } } isConnectedSuccess = false + notifySubscribersDisconnect() } func mqtt(_ mqtt: CocoaMQTT, didSubscribeTopic topic: String) {} @@ -280,7 +288,7 @@ final class MQTTService: NSObject, CocoaMQTTDelegate, ReachabilityServiceObserve private func notifySubscribersConnect() { subscribersQueue.sync { subscribers.forEach { (weak) in - (weak.value as? MQTTServiceDelegate)?.didConnect(self) + (weak.value as? MQTTServiceDelegate)?.mqttServiceDidConnect(self) } } } @@ -288,7 +296,15 @@ final class MQTTService: NSObject, CocoaMQTTDelegate, ReachabilityServiceObserve private func notifySubscribersDisconnect() { subscribersQueue.sync { subscribers.forEach { (weak) in - (weak.value as? MQTTServiceDelegate)?.didDisconnect(self) + (weak.value as? MQTTServiceDelegate)?.mqttServiceDidDisconnect(self) + } + } + } + + private func notifySubscribersAuthFailure() { + subscribersQueue.sync { + subscribers.forEach { (weak) in + (weak.value as? MQTTServiceDelegate)?.mqttServiceDidReceiveAuthenticationFailure(self) } } } @@ -296,23 +312,22 @@ final class MQTTService: NSObject, CocoaMQTTDelegate, ReachabilityServiceObserve private func notifyWrongVersion() { subscribersQueue.sync { subscribers.forEach { (weak) in - (weak.value as? MQTTServiceDelegate)?.wrongServerVersion() + (weak.value as? MQTTServiceDelegate)?.mqttServiceDidReceiveWrongServerVersion() } } } } protocol MQTTServiceDelegate: class { - - func didConnect(_ mqttService: MQTTService) - func didDisconnect(_ mqttService: MQTTService) - func wrongServerVersion() - + func mqttServiceDidConnect(_ mqttService: MQTTService) + func mqttServiceDidDisconnect(_ mqttService: MQTTService) + func mqttServiceDidReceiveAuthenticationFailure(_ mqttService: MQTTService) + func mqttServiceDidReceiveWrongServerVersion() } extension MQTTServiceDelegate { - - func didConnect(_ mqttService: MQTTService) {} - func didDisconnect(_ mqttService: MQTTService) {} - func wrongServerVersion() {} + func mqttServiceDidConnect(_ mqttService: MQTTService) {} + func mqttServiceDidDisconnect(_ mqttService: MQTTService) {} + func mqttServiceDidReceiveAuthenticationFailure(_ mqttService: MQTTService) {} + func mqttServiceDidReceiveWrongServerVersion() {} } diff --git a/Nynja/Services/MQTT/MQTTServiceHelper.swift b/Nynja/Services/MQTT/MQTTServiceHelper.swift index c490ef7fb..734736e23 100644 --- a/Nynja/Services/MQTT/MQTTServiceHelper.swift +++ b/Nynja/Services/MQTT/MQTTServiceHelper.swift @@ -28,7 +28,7 @@ extension MQTTService { } if shouldShowLog(for: bert) { - LogService.log(topic: .MQTT, text: desc.0) + LogService.log(topic: .MQTT) { return desc.0 } } } diff --git a/Nynja/Services/MQTT/MQTTServiceSchedule.swift b/Nynja/Services/MQTT/MQTTServiceSchedule.swift index 3007a1ca9..cbf2822af 100644 --- a/Nynja/Services/MQTT/MQTTServiceSchedule.swift +++ b/Nynja/Services/MQTT/MQTTServiceSchedule.swift @@ -11,14 +11,14 @@ import Foundation extension MQTTService { func forwardMessage(phoneId: String, messages: [Message], jobHandler: ((Job) -> Void)? = nil) { - let job = Job(phoneId: phoneId, messages: messages, timestamp: nil, features: nil) + let job = Job(phoneId: phoneId, container: nil, messages: messages, timestamp: nil, features: nil) markSelfMessageAsCursor(in: messages) jobHandler?(job) sendJob(job) } func scheduleMessage(phoneId: String, messages: [Message], timestamp: Int64, features: [Feature]) { - let job = Job(phoneId: phoneId, messages: messages, timestamp: timestamp, features: features) + let job = Job(phoneId: phoneId, container: nil, messages: messages, timestamp: timestamp, features: features) markSelfMessageAsCursor(in: messages) sendJob(job) } diff --git a/Nynja/Services/Member/MemberDAO.swift b/Nynja/Services/Member/MemberDAO.swift index 342ed07a1..9af0b48c9 100644 --- a/Nynja/Services/Member/MemberDAO.swift +++ b/Nynja/Services/Member/MemberDAO.swift @@ -52,4 +52,12 @@ class MemberDAO: MemberDAOProtocol { return Member(member: member) } + + // MARK: - Update + + static func updateColumns(_ columns: Set, member: Member) { + let columns = Set(columns.map { $0.title }) + try? dbManager.perform(action: .updateColumns(columns), with: member) + } + } diff --git a/Nynja/Services/Member/MemberDAOProtocol.swift b/Nynja/Services/Member/MemberDAOProtocol.swift index 0d4920aa2..379bd5a8b 100644 --- a/Nynja/Services/Member/MemberDAOProtocol.swift +++ b/Nynja/Services/Member/MemberDAOProtocol.swift @@ -15,4 +15,9 @@ protocol MemberDAOProtocol: DAOProtocol { static func findMemberBy(id: Int64) -> Member? static func findMemberBy(roomId: String, phoneId: String) -> Member? + + // MARK: - Update + + static func updateColumns(_ columns: Set, member: Member) + } diff --git a/Nynja/Services/MessageProcessing/Operations/DownloadOperation.swift b/Nynja/Services/MessageProcessing/Operations/DownloadOperation.swift index aa9faaea0..2275cc9cb 100644 --- a/Nynja/Services/MessageProcessing/Operations/DownloadOperation.swift +++ b/Nynja/Services/MessageProcessing/Operations/DownloadOperation.swift @@ -23,10 +23,7 @@ class DownloadOperation: AsyncOperation { } override func main() { - TransferManager.shared.download(url: url, listener: listener) { [weak self] result in - guard let `self` = self else { return } - self.delegate?.updateProgress(TransferManager.shared.progressForUrl(self.url)) - self.state = .finished - } + TransferManager.shared.download(url: url, listener: listener) + state = .finished } } diff --git a/Nynja/Services/MessageProcessing/Operations/UploadOperation.swift b/Nynja/Services/MessageProcessing/Operations/UploadOperation.swift index 032e634d3..c048cd077 100644 --- a/Nynja/Services/MessageProcessing/Operations/UploadOperation.swift +++ b/Nynja/Services/MessageProcessing/Operations/UploadOperation.swift @@ -59,7 +59,7 @@ private extension UploadOperation { func uploadingCompletion(uploadingResult: ResponseResult) { switch uploadingResult { case let .success(serverUrl, resultInfo): - let progress = ProgressModel(url: url, status: .done, result: serverUrl, transferInfo: resultInfo) + let progress = ProgressModel(url: serverUrl, status: .done, result: url, transferInfo: resultInfo) delegate?.updateProgress(progress) uploadOperationDelegate?.updateMessageAfterUpload(message: message, url: url, @@ -92,7 +92,8 @@ private extension UploadOperation { case .success: uploadOperationDelegate?.sendMessageAndDismissStatusIfNeeded(message: message) case .error: - break + let progress = ProgressModel(url: url, status: .offline, result: nil, transferInfo: .zero) + delegate?.updateProgress(progress) } state = .finished diff --git a/Nynja/Services/MessageSendingService/MessageSendingService.swift b/Nynja/Services/MessageSendingService/MessageSendingService.swift index 99f2adbea..3856ab129 100644 --- a/Nynja/Services/MessageSendingService/MessageSendingService.swift +++ b/Nynja/Services/MessageSendingService/MessageSendingService.swift @@ -8,51 +8,41 @@ import Foundation -protocol MessageSendingServiceProtocol { +protocol MessageSendingServiceProtocol: class { func isCanSendMessageTo(contact: Contact?) -> Bool func sendMessage(_ message: Message) func sendReplayedMessage(_ repliedMessage: Message, message: Message) - func sendMessageWithTranslation(_ message: Message) func sendTyping(_ type: TypingModelType, contact: Contact?, room: Room?) } -final class MessageSendingService: MessageSendingServiceProtocol, SetInjectable { - private var mqttService: MQTTService! - private var storageService: StorageService! - private var processingManager: MessageProcessingManagerInterface! -} - -// MARK: - Injectable - -extension MessageSendingService { +final class MessageSendingService: MessageSendingServiceProtocol, InitializeInjectable { + private let mqttService: MQTTService + private let storageService: StorageService + private let processingManager: MessageProcessingManagerInterface + struct Dependencies { let mqttService: MQTTService let storageService: StorageService let processingManager: MessageProcessingManagerInterface } - - func inject(dependencies: MessageSendingService.Dependencies) { + + init(dependencies: Dependencies) { mqttService = dependencies.mqttService storageService = dependencies.storageService processingManager = dependencies.processingManager } -} -// MARK: - MessageSendingServiceProtocol - -extension MessageSendingService { + + // MARK: - MessageSendingServiceProtocol + func isCanSendMessageTo(contact: Contact?) -> Bool { guard let contact = contact else { return true } - return contact.originalStatus != .ban + return !contact.isBan } - func sendMessageWithTranslation(_ message: Message) { - try? storageService.perform(action: .save, with: message) - } - func sendMessage(_ message: Message) { try? storageService.perform(action: .save, with: message) @@ -67,6 +57,9 @@ extension MessageSendingService { } func sendTyping(_ type: TypingModelType, contact: Contact?, room: Room?) { + if let myPhoneId = storageService.phoneId, contact?.phone_id == myPhoneId { + return + } let typing = Typing() typing.comments = type.rawValue as AnyObject diff --git a/Nynja/Services/Models/HistoryRequestModel.swift b/Nynja/Services/Models/HistoryRequestModel.swift index 5373aa866..cab3be79b 100644 --- a/Nynja/Services/Models/HistoryRequestModel.swift +++ b/Nynja/Services/Models/HistoryRequestModel.swift @@ -10,7 +10,7 @@ import Foundation final class HistoryRequestModel: BaseMQTTModel { - fileprivate struct Keys { + private struct Keys { static let topic = "History" static let p2p = "p2p" static let muc = "muc" @@ -38,18 +38,25 @@ final class HistoryRequestModel: BaseMQTTModel { case getAll case getPage(from: MessageServerId, pageSize: MessageServerId) case getBetween(from: MessageServerId, to: MessageServerId) + case getUpdates(from: MessageServerId) case delete - case update(messageId: Int64) + case read(messageId: MessageServerId) case defaultStickerPack } } let requestInput : RequestInput + + // MARK: - Init + init(requestInput: RequestInput) { self.requestInput = requestInput super.init() } + + + // MARK: - Bert override func getBert() -> [UInt8] { var result = [UInt8]() @@ -64,7 +71,7 @@ final class HistoryRequestModel: BaseMQTTModel { private var bert: BertTuple { let topic = BertAtom(fromString: Keys.topic) let rosterId = Bert.getBin(requestInput.rosterId) - let action = BertAtom(fromString: actionName) + let status = BertAtom(fromString: self.status) var feedId: BertObject { let prefixBertBin = BertAtom(fromString: feedIdPrefix) @@ -107,17 +114,19 @@ final class HistoryRequestModel: BaseMQTTModel { return BertList(fromElements: list) } - return BertTuple(fromElements: [topic, rosterId, feedId, size, messageId, data, action]) + return BertTuple(fromElements: [topic, rosterId, feedId, size, messageId, data, status]) } } +// MARK: - Properties + extension HistoryRequestModel { var feedIdPrefix: String { switch requestInput.historyType { - case .p2p(_): + case .p2p: return Keys.p2p - case .muc(_): + case .muc: return Keys.muc case .job: return Keys.act @@ -143,13 +152,15 @@ extension HistoryRequestModel { switch requestInput.actionType { case .getAll, .defaultStickerPack: return 0 - case .getPage(let from, _): + case let .getPage(from, _): return from - case .getBetween(let from, _): + case let .getBetween(from, _): return from + case let .getUpdates(messageId): + return messageId case .delete: return nil - case .update(let messageId): + case let .read(messageId): return messageId } } @@ -158,8 +169,9 @@ extension HistoryRequestModel { switch requestInput.actionType { case .getAll, .getBetween, + .getUpdates, .delete, - .update: + .read: return nil case .getPage(_, let pageSize): return pageSize @@ -177,13 +189,13 @@ extension HistoryRequestModel { } } - var actionName: String { + var status: String { switch requestInput.actionType { - case .getAll, .getPage, .getBetween, .defaultStickerPack: + case .getAll, .getPage, .getBetween, .getUpdates, .defaultStickerPack: return Keys.get case .delete: return Keys.delete - case .update: + case .read: return Keys.update } } diff --git a/Nynja/Services/NynjaCommunicatorService.swift b/Nynja/Services/NynjaCommunicatorService.swift index 4579d5a1a..cbda6c055 100644 --- a/Nynja/Services/NynjaCommunicatorService.swift +++ b/Nynja/Services/NynjaCommunicatorService.swift @@ -8,12 +8,15 @@ import Foundation import NynjaSDK +import MulticastDelegateSwift protocol NynjaCommunicatorServiceDelegate: class { func dialing(call: NYNCall) func creatingGroupCall(name: String, call: NYNCall) func incomingCallRinging(call: NYNCall) func callEnded(call: NYNCall, isError: Bool) + func didAllocateConference(requestId: String, call: NYNCall?, error: Error?) + func didEndConference(requestId: String, error: Error?) } extension NynjaCommunicatorServiceDelegate { @@ -21,6 +24,8 @@ extension NynjaCommunicatorServiceDelegate { func creatingGroupCall(name: String, call: NYNCall){} func incomingCallRinging(call: NYNCall){} func callEnded(call: NYNCall, isError: Bool){} + func didAllocateConference(requestId: String, call: NYNCall?, error: Error?){} + func didEndConference(requestId: String, error: Error?){} } protocol NynjaCallDelegate: class { @@ -46,27 +51,28 @@ extension NynjaCallDelegate { } class NynjaCommunicatorService: NSObject, NynjaCommunicatorDelegate, NYNCallDelegate, NYNCallManagerDelegate { - - + + + let nynComm: NynjaCommunicator var isCallInProgress = false - weak var delegate : NynjaCommunicatorServiceDelegate? + let delegates = MulticastDelegate() weak var callDelegate : NynjaCallDelegate? var call:NYNCall? private var creators: [String: CallCreatorMediator] = [String: CallCreatorMediator]() var username: String? lazy var myDeviceId: String = (UIDevice.current.identifierForVendor?.uuidString)! - + weak var messageInteractorCallProtocol: MessageInteractorCallProtocol? - + override init() { var logDir:String? let searchPaths = NSSearchPathForDirectoriesInDomains(.applicationSupportDirectory, .userDomainMask, true) - + if searchPaths.count > 0 { var path:String = searchPaths[0] - + if path.last! == "/" { path.append("log") } else { @@ -76,25 +82,25 @@ class NynjaCommunicatorService: NSObject, NynjaCommunicatorDelegate, NYNCallDele if false == FileManager.default.fileExists(atPath: path) { do { try FileManager.default.createDirectory(atPath: path, withIntermediateDirectories: true, attributes: nil) - + logDir = path } catch let error as NSError { - LogService.log(topic: .callSystem, text: error.localizedDescription) + LogService.log(topic: .callSystem) { return error.localizedDescription } } } else { logDir = path } - + } - + if let lgDir = logDir { self.nynComm = NynjaCommunicator.start(withLogDir: lgDir, andLogLevel: NYN_LOG_LEVEL.VERBOSE) } else { self.nynComm = NynjaCommunicator.sharedInstance() } - + super.init() - + let server = Bundle.main.confServerAddress let port = Bundle.main.confServerPort let secure = false @@ -105,7 +111,7 @@ class NynjaCommunicatorService: NSObject, NynjaCommunicatorDelegate, NYNCallDele } static let sharedInstance: NynjaCommunicatorService = NynjaCommunicatorService() - + func login(userName: String, password: String) { self.username = userName self.nynComm.login(withName: userName, @@ -115,13 +121,13 @@ class NynjaCommunicatorService: NSObject, NynjaCommunicatorDelegate, NYNCallDele func dialInGroup(groupname: String) { // NOT IMPLEMENTED } - + func createConference(contacts: [Contact], room: Room?) { if isCallInProgress { AlertManager.sharedInstance.showAlertOk(message: "You_can't_call_when_you_calling".localized) return } - + isCallInProgress = true let roomName = (room != nil && room?.name != nil) ? room?.name! : "unnamed" let members = makeMembers(contacts: contacts) @@ -131,21 +137,61 @@ class NynjaCommunicatorService: NSObject, NynjaCommunicatorDelegate, NYNCallDele roomId: roomId!, name: roomName!) self.creators[creator.createId] = creator - + self.nynComm.getCallManager().createConference(withRequestId: creator.createId, withExternalId: UUID().uuidString, withRequestInfo: creator.roomId, withSubject: creator.name) } - + + func updateConferenceInfo(callId: String, subject:String) { + self.nynComm.getCallManager().updateConferenceInfo(withRequestId: UUID().uuidString, + withCallId: callId, + withSubject: subject) + } + + func addAndStartConference(callId: String, contacts: [Contact], room: Room?) { + if isCallInProgress { + AlertManager.sharedInstance.showAlertOk(message: "You_can't_call_when_you_calling".localized) + return + } + + isCallInProgress = true + let roomName = room?.name ?? "unnamed" + let members = makeMembers(contacts: contacts) + let roomId = room?.id ?? "" + let creator = CallCreatorMediator(createId: UUID().uuidString, + members: members, + roomId: roomId, + name: roomName) + self.creators[creator.createId] = creator + + // call create handler as if the create would succeed + createConference(withRequest: creator.createId, didSucceedWithId: callId) + } + + func allocateConference(requestId:String, subject: String, roomId: String) { + + self.nynComm.getCallManager().createConference(withRequestId: requestId, + withExternalId: requestId, + withRequestInfo: roomId, + withSubject: subject) + } + + func endConference(requestId:String, conferenceId: String) { + + self.nynComm.getCallManager().endConference(withRequestId: requestId, + withConferenceId: conferenceId) + } + func addConferenceMember(conferenceId:String, phoneId: String, name:String) { - + let nynCall = self.nynComm.getCallManager().getCallById(conferenceId) - + if nil != nynCall { - + let request = UUID().uuidString - + self.nynComm.getCallManager().addConferenceMember(withRequestId: request, withConferenceId: conferenceId, withAccountId: phoneId, @@ -153,16 +199,16 @@ class NynjaCommunicatorService: NSObject, NynjaCommunicatorDelegate, NYNCallDele withMySelf: false) } } - + func removeConferenceMember(conferenceId: String, memberId: String) { let request = UUID().uuidString - + self.nynComm.getCallManager().removeConferenceMember(withRequestId: request, withConferenceId: conferenceId, withMemberId: memberId) } - + func acceptConference(call: NYNCall) { if (self.call == call) { if (self.call?.isConference())! { @@ -180,32 +226,33 @@ class NynjaCommunicatorService: NSObject, NynjaCommunicatorDelegate, NYNCallDele } } } - + func rejectConference(call: NYNCall) { if (self.call == call) { - + + SoundService.sharedInstance.stopIncomingCallPlayer() + if (self.call?.isConference())! { let joinId = UUID().uuidString - SoundService.sharedInstance.stopIncomingCallPlayer() - + self.nynComm.getCallManager().rejectConference(withRequestId: joinId, withConferenceId: call.callId, withMemberId: call.memberId) - + // clear out the call self.call?.setDelegate(nil) } else { self.call?.reject() } - + self.call = nil } } - + func handlePushNotification(with payload: String) { self.nynComm.handlePush(withPayload: payload) } - + func handleNotificationResponse(identifier: String, userInfo: [AnyHashable: Any]?) { @@ -216,14 +263,19 @@ class NynjaCommunicatorService: NSObject, NynjaCommunicatorDelegate, NYNCallDele func hasRunningCallWith(_ roomId: String) -> Bool { if let call = self.nynComm.getCallManager().getCallForRunningCall(withRoom: roomId) { let callState = call.callState - + return (NYNCallState.closed == callState || NYNCallState.new == callState || NYNCallState.failed == callState) } - + return false } - + + func hasCallInProgress() -> Bool { + + return (nil != self.call) + } + func rejoinRunningCallWith(_ roomId: String) { if let c = self.call { if !c.externalInfo.elementsEqual(roomId) { @@ -231,7 +283,7 @@ class NynjaCommunicatorService: NSObject, NynjaCommunicatorDelegate, NYNCallDele return } } - + if let call = self.nynComm.getCallManager().getCallForRunningCall(withRoom: roomId) { let callState = call.callState @@ -240,29 +292,34 @@ class NynjaCommunicatorService: NSObject, NynjaCommunicatorDelegate, NYNCallDele { self.call = call self.call?.setDelegate(self) - self.delegate?.creatingGroupCall(name: call.subject, call: call) + + delegates.invokeDelegates { delegate in + delegate.creatingGroupCall(name: call.subject, call: call) + } + self.nynComm.getCallManager().joinConference(withRequestId: call.callId, withConferenceId: call.callId, withMemberId: call.memberId, withDeviceId: self.myDeviceId) + SoundService.sharedInstance.voipCallStarted() } } } - + func didChangeConferenceState(_ state: NYNCallState) { if let c = self.call { self.callDelegate?.stateDidChange(call: c, state: state) } } - - + + func call(user: String, withVideo: Bool) { var callerName: String = "" - + if let mySelf = getMySelf() { callerName = "\(mySelf.names ?? "") \(mySelf.surnames ?? "")" } - + self.nynComm.getCallManager().startCall(toUser: user, callerName: callerName, withSendVideo: withVideo, @@ -270,29 +327,38 @@ class NynjaCommunicatorService: NSObject, NynjaCommunicatorDelegate, NYNCallDele if let nc = ncall { self.call = nc self.call?.setDelegate(self) - self.delegate?.dialing(call: nc) + self.delegates.invokeDelegates { delegate in + delegate.dialing(call: nc) + } + SoundService.sharedInstance.voipCallStarted() } }; } - + func attachRemoteVideoRenderer(inView view: UIView) { if let call = self.call { call.attachRemoteRenderer(to: view) } } + func detachRemoteVideoRenderer(inView view: UIView) { + if let call = self.call { + call.detachRemoteRenderer(from: view) + } + } + func attachLocalVideoPreview(inView view: UIView) { if let call = self.call { call.attachLocalPreview(to: view) } } - + func detachLocalVideoPreview(inView view: UIView) { if let call = self.call { call.detachLocalPreview(from: view) } } - + func switchCamera() { if let call = self.call { call.switchCamera() @@ -311,60 +377,60 @@ class NynjaCommunicatorService: NSObject, NynjaCommunicatorDelegate, NYNCallDele for i in contacts { members.append(Member(contact: i)) } - + if let mySelf = getMySelf() { let myMember = Member(contact: mySelf) members.append(myMember) } - + return members } - + func getCreator(createId: String) -> CallCreatorMediator? { return self.creators[createId] } func getCreator(reqMemberId: String) -> CallCreatorMediator? { - + for (_, creator) in self.creators { if(creator.contains(requestMember: reqMemberId)) { return creator } } - + return nil; } func getCreator(startId: String) -> CallCreatorMediator? { - + for (_, creator) in self.creators { if(creator.contains(startId: startId)) { return creator } } - + return nil; } func getCreator(joinId: String) -> CallCreatorMediator? { - + for (_, creator) in self.creators { if(creator.contains(joinId: joinId)) { return creator } } - + return nil; } func getCreator(getMembersId: String) -> CallCreatorMediator? { - + for (_, creator) in self.creators { if(creator.contains(getMembersId: getMembersId)) { return creator } } - + return nil; } @@ -373,7 +439,7 @@ class NynjaCommunicatorService: NSObject, NynjaCommunicatorDelegate, NYNCallDele self.creators.removeValue(forKey: createId) } - + //MARK: NynjaCommunicatorDelegate func loginDidFinishWithError(_ error: Error?) { if (error == nil) { @@ -381,7 +447,7 @@ class NynjaCommunicatorService: NSObject, NynjaCommunicatorDelegate, NYNCallDele self.nynComm.getCallManager().delegate = self } } - + func logoutDidFinishWithError(_ error: Error?) { self.nynComm.getCallManager().delegate = nil } @@ -390,26 +456,41 @@ class NynjaCommunicatorService: NSObject, NynjaCommunicatorDelegate, NYNCallDele func callDidEnd(_ call: NYNCall) { isCallInProgress = false if let c = self.call, call.callId.elementsEqual(c.callId) { + LogService.log(topic: .callSystem) { return "callEnded: \(c.callId)" } self.callDelegate?.callEnded(call: c, isError: false) - self.delegate?.callEnded(call: c, isError: false) + delegates.invokeDelegates { delegate in + delegate.callEnded(call: c, isError: false) + } + self.call?.setDelegate(nil) self.call = nil + SoundService.sharedInstance.voipCallStopped() + SoundService.sharedInstance.stopRingbackPlayer() } } - + func call(_ callid: String?, didChange state: NYNCallState) { - let log = NSString(format: "didChange state: ", self) as String - LogService.log(topic: .callSystem, text: log) + LogService.log(topic: .callSystem) { return NSString(format: "didChange state: ", self) as String } if let c = self.call, let cid = callid { if c.callId.elementsEqual(cid) { self.callDelegate?.stateDidChange(call: c, state: state) + + if c.callState == .ringing { + SoundService.sharedInstance.playRingbackSound() + } else if c.callState == .connected { + SoundService.sharedInstance.stopRingbackPlayer() + if c.recvVideo { + AudioManager.sharedInstance.speaker = .loud + } + } else { + SoundService.sharedInstance.stopRingbackPlayer() + } } } } func callParticipantsDidUpdate(_ callid: String?) { - let log = NSString(format: "callParticipantsDidUpdate: ", self) as String - LogService.log(topic: .callSystem, text: log) + LogService.log(topic: .callSystem) { return NSString(format: "callParticipantsDidUpdate: ", self) as String } if let c = self.call, let cid = callid { if cid.elementsEqual(c.callId) { self.callDelegate?.participantsUpdated(call: c) @@ -418,8 +499,7 @@ class NynjaCommunicatorService: NSObject, NynjaCommunicatorDelegate, NYNCallDele } func call(_ call: NYNCall, didAddRemoteVideoTrack trackId: String) { - let log = NSString(format: "callWithIdDidAddStream: ", self) as String - LogService.log(topic: .callSystem, text: log) + LogService.log(topic: .callSystem) { return NSString(format: "callWithIdDidAddStream: ", self) as String } if let c = self.call{ if c.callId.elementsEqual(call.callId) { self.callDelegate?.didAddVideoStreamForCall(call: call) @@ -428,15 +508,14 @@ class NynjaCommunicatorService: NSObject, NynjaCommunicatorDelegate, NYNCallDele } func call(_ call: NYNCall, didRemoveRemoteVideoTrack trackId: String) { - let log = NSString(format: "callWithIdDidRemoVeStream: ", self) as String - LogService.log(topic: .callSystem, text: log) + LogService.log(topic: .callSystem) { return NSString(format: "callWithIdDidRemoVeStream: ", self) as String } if let c = self.call { if c.callId.elementsEqual(call.callId) { self.callDelegate?.didRemoveVideoStreamForCall(call: call) } } } - + func callDidStartLocalCapturer(_ call: NYNCall) { if let c = self.call{ if c.callId.elementsEqual(call.callId) { @@ -444,7 +523,7 @@ class NynjaCommunicatorService: NSObject, NynjaCommunicatorDelegate, NYNCallDele } } } - + func callDidStopLocalCapturer(_ call: NYNCall) { if let c = self.call{ if c.callId.elementsEqual(call.callId) { @@ -456,7 +535,7 @@ class NynjaCommunicatorService: NSObject, NynjaCommunicatorDelegate, NYNCallDele //MARK: NYNCallManagerDelegate func createConference(withRequest requestId: String, didSucceedWithId conferenceId: String) { let creator = getCreator(createId: requestId) - + if let cr = creator { cr.conferenceId = conferenceId @@ -464,13 +543,17 @@ class NynjaCommunicatorService: NSObject, NynjaCommunicatorDelegate, NYNCallDele if let nc = nynCall { self.call = nc self.call?.setDelegate(self) - self.delegate?.creatingGroupCall(name: cr.name, call: nc) + delegates.invokeDelegates { delegate in + delegate.creatingGroupCall(name: cr.name, call: nc) + } + + SoundService.sharedInstance.voipCallStarted() for m in cr.members { let request = UUID().uuidString var isMe: Bool = false cr.addMemberRequest(request: request) - + if (m.phone_id!.elementsEqual(self.username!)) { cr.myMemberRequest = request isMe = true @@ -484,16 +567,43 @@ class NynjaCommunicatorService: NSObject, NynjaCommunicatorDelegate, NYNCallDele withMySelf: isMe) } } + } else { + let call = self.nynComm.getCallManager().getCallById(conferenceId) + delegates.invokeDelegates { delegate in + delegate.didAllocateConference(requestId: requestId, call: call, error: nil) + } } } - + func createConference(withRequestDidFail requestId: String) { - isCallInProgress = false - removeCreator(createId: requestId) - - AlertManager.sharedInstance.showAlertOk(message: "Create_conference_call_failed".localized) + if let cr = getCreator(createId: requestId) { + isCallInProgress = false + removeCreator(createId: cr.createId) + AlertManager.sharedInstance.showAlertOk(message: "Create_conference_call_failed".localized) + } else { + let error:NSError = NSError.init(domain: "Calling", code: 100, userInfo: [NSLocalizedDescriptionKey : "Create_conference_call_failed".localized]) + delegates.invokeDelegates { delegate in + delegate.didAllocateConference(requestId: requestId, call: nil, error: error) + } + + } } - + + func endConference(withRequestDidSucceed requestId: String) { + delegates.invokeDelegates { delegate in + delegate.didEndConference(requestId: requestId, error: nil) + } + + } + + func endConference(withRequestDidFail requestId: String) { + let error:NSError = NSError.init(domain: "Calling", code: 100, userInfo: [NSLocalizedDescriptionKey : "End_conference_call_failed".localized]) + delegates.invokeDelegates { delegate in + delegate.didEndConference(requestId: requestId, error: error) + } + + } + func addConferenceMember(withReqiestId requestId: String, didSucceesWithMemberId memberId: String) { if let cr = getCreator(reqMemberId: requestId) { cr.removeMemberRequest(request: requestId) @@ -507,7 +617,7 @@ class NynjaCommunicatorService: NSObject, NynjaCommunicatorDelegate, NYNCallDele if cr.requestMembersCount() == 0 { // start the conference cr.startId = UUID().uuidString - + self.nynComm.getCallManager().startConference(withRequestId: cr.startId!, withConferenceId: cr.conferenceId!) guard let call = self.call else { return } @@ -515,7 +625,7 @@ class NynjaCommunicatorService: NSObject, NynjaCommunicatorDelegate, NYNCallDele } } } - + func addConferenceMember(withReqiestIdDidFail requestId: String) { if let cr = getCreator(reqMemberId: requestId) { cr.removeMemberRequest(request: requestId) @@ -531,10 +641,10 @@ class NynjaCommunicatorService: NSObject, NynjaCommunicatorDelegate, NYNCallDele AlertManager.sharedInstance.showAlertOk(message: "Add_conference_member_failed".localized) } } - + func startConference(withRequestId requestId: String, didSucceedWithConferenceId conferenceId: String) { if let cr = getCreator(startId: requestId) { - + cr.joinId = UUID().uuidString self.nynComm.getCallManager().joinConference(withRequestId: cr.joinId!, withConferenceId: cr.conferenceId!, @@ -542,30 +652,33 @@ class NynjaCommunicatorService: NSObject, NynjaCommunicatorDelegate, NYNCallDele withDeviceId: self.myDeviceId) } } - + func startConference(withRequestId requestId: String, didFailWithConferenceId conferenceId: String) { if let cr = getCreator(startId: requestId) { removeCreator(createId: cr.createId) - + // clean up call if let c = self.call, c.callId.elementsEqual(cr.conferenceId!) { + LogService.log(topic: .callSystem) { return "callEnded: \(c.callId)" } self.callDelegate?.callEnded(call: c, isError: true) self.call?.setDelegate(nil) self.call = nil isCallInProgress = false + SoundService.sharedInstance.voipCallStopped() + SoundService.sharedInstance.stopRingbackPlayer() } } } - + func joinConference(withRequestId requestId: String, withConferenceId conferenceId: String, didSucceedWithParticipantId participantId: String) { if let cr = getCreator(joinId: requestId) { removeCreator(createId: cr.createId) } else if let call = self.call, call.callId.elementsEqual(requestId) { // rejoin conference success - + } } - + func joinConference(withRequestId requestId: String, didFailWithConferenceId conferenceId: String) { if let cr = getCreator(joinId: requestId) { removeCreator(createId: cr.createId) @@ -575,10 +688,13 @@ class NynjaCommunicatorService: NSObject, NynjaCommunicatorDelegate, NYNCallDele // clean up call if let c = self.call, call.callId.elementsEqual(cr.conferenceId!) { + LogService.log(topic: .callSystem) { return "callEnded: \(c.callId)" } self.callDelegate?.callEnded(call: c, isError: true) self.call?.setDelegate(nil) self.call = nil isCallInProgress = false + SoundService.sharedInstance.voipCallStopped() + SoundService.sharedInstance.stopRingbackPlayer() } } } @@ -588,18 +704,18 @@ class NynjaCommunicatorService: NSObject, NynjaCommunicatorDelegate, NYNCallDele if let cr = getCreator(getMembersId: requestId) { } } - + func getMembersWithRequestId(_ requestId: String, didFailWithConferenceId conferenceId: String) { if let cr = getCreator(getMembersId: requestId) { removeCreator(createId: cr.createId) } } - + func didReceiveIncomingCall(_ call: NYNCall) { var shouldRing:Bool = false - + if self.call != nil { - + if (self.call?.callState == NYNCallState.closed || self.call?.callState == NYNCallState.failed) { @@ -613,16 +729,20 @@ class NynjaCommunicatorService: NSObject, NynjaCommunicatorDelegate, NYNCallDele withConferenceId: call.callId, withMemberId: call.memberId) } - + } else { shouldRing = true } - + // show ringing if (shouldRing) { self.call = call self.call?.setDelegate(self) - self.delegate?.incomingCallRinging(call: call) + delegates.invokeDelegates { delegate in + delegate.incomingCallRinging(call: call) + } + + SoundService.sharedInstance.voipCallStarted() SoundService.sharedInstance.playCallSound() } } @@ -630,25 +750,45 @@ class NynjaCommunicatorService: NSObject, NynjaCommunicatorDelegate, NYNCallDele func didStopRingingIncomingCall(withId conferenceId: String) { // clean up call if let c = self.call, c.callId.elementsEqual(conferenceId) { + LogService.log(topic: .callSystem) { return "callEnded: \(c.callId)" } self.callDelegate?.callEnded(call: c, isError: false) self.call?.setDelegate(nil) self.call = nil isCallInProgress = false + SoundService.sharedInstance.voipCallStopped() SoundService.sharedInstance.stopIncomingCallPlayer() + SoundService.sharedInstance.stopRingbackPlayer() } } - + func didReceiveAcceptedCall(_ call: NYNCall) { if self.call == nil { self.call = call self.call?.setDelegate(self) - self.delegate?.incomingCallRinging(call: call) + delegates.invokeDelegates { delegate in + delegate.incomingCallRinging(call: call) + } + + SoundService.sharedInstance.voipCallStarted() } } - + + func didAcceptElsewhereCall(withId callId: String) { + // clean up call + if let c = self.call, c.callId.elementsEqual(callId) { + LogService.log(topic: .callSystem) { return "callAcceptedElsewhere: \(c.callId)" } + self.callDelegate?.callEnded(call: c, isError: false) + self.call?.setDelegate(nil) + self.call = nil + isCallInProgress = false + SoundService.sharedInstance.voipCallStopped() + SoundService.sharedInstance.stopIncomingCallPlayer() + } + } + func rejectConference(withRequestId requestId: String, didSucceedWithConferenceId conferenceId: String) { } - + func rejectConference(withRequestId requestId: String, didFailWithConferenceId conferenceId: String) { } @@ -658,7 +798,13 @@ class NynjaCommunicatorService: NSObject, NynjaCommunicatorDelegate, NYNCallDele didFinishWithSuccess sucess: Bool) { } - + + func updateConferenceInfo(withRequestId requestId: String, + withCallId callId: String, + didFinishWithSuccess sucess: Bool) + { + } + // Call invitation state did change func callStateDidChange(_ call: NYNCall) { self.messageInteractorCallProtocol?.didChangeCallInvitationState(call) diff --git a/Nynja/Services/PushService.swift b/Nynja/Services/PushService.swift index df7c996cb..ff6185ada 100644 --- a/Nynja/Services/PushService.swift +++ b/Nynja/Services/PushService.swift @@ -10,14 +10,15 @@ import Foundation import PushKit import UserNotifications -class PushService: NSObject, PKPushRegistryDelegate, UserSettingsRespondable { +final class PushService: NSObject, PKPushRegistryDelegate, UserSettingsRespondable { - static let sharedInstance : PushService = { + static let sharedInstance: PushService = { let instance = PushService() instance.registerForPushNotifications() return instance }() + // MARK: - Init override init() { @@ -30,20 +31,23 @@ class PushService: NSObject, PKPushRegistryDelegate, UserSettingsRespondable { } - // MARK: - + // MARK: - Setup func registerForPushNotifications() { let voipRegistry = PKPushRegistry(queue: DispatchQueue.main) voipRegistry.delegate = self voipRegistry.desiredPushTypes = [.voIP] } + + + // MARK: - PKPushRegistryDelegate func pushRegistry(_ registry: PKPushRegistry, didUpdate credentials: PKPushCredentials, for type: PKPushType) { let voipPushToken = credentials.token let token = voipPushToken.map { String(format: "%02.2hhx", $0) }.joined() //MQTTService.sharedInstance.updatePushToken(push_token: token) MQTTService.sharedInstance.push = token - LogService.log(topic: .system, text: "Register push token: \(token)") + LogService.log(topic: .system) { return "Register push token: \(token)" } } @objc(pushRegistry:didReceiveIncomingPushWithPayload:forType:) func pushRegistry(_ registry: PKPushRegistry, didReceiveIncomingPushWith payload: PKPushPayload, for type: PKPushType) { @@ -60,17 +64,19 @@ class PushService: NSObject, PKPushRegistryDelegate, UserSettingsRespondable { handleNynjaNotifications(with: aps, data: payload.dictionaryPayload) } - // MAKR: - Handle Notifications + + // MARK: - Handle Notifications + private func handleNynjaNotifications(with aps: Aps, data: [AnyHashable: Any]) { - var alreadyReceived = false if let contact = aps.contact { alreadyReceived = self.alreadyReceived(type: aps.nynja.type, msg: contact.lastMessage) - try? StorageService.sharedInstance.perform(action: .save, with: contact) + save(contact) + } else if let room = aps.room { alreadyReceived = self.alreadyReceived(type: aps.nynja.type, msg: room.lastMessage) - try? StorageService.sharedInstance.perform(action: .save, with: room) + save(room) } guard aps.shouldNotify, !alreadyReceived else { return } @@ -86,7 +92,29 @@ class PushService: NSObject, PKPushRegistryDelegate, UserSettingsRespondable { return false } - func sendNewNotification(title: String, sound: String?, id: String, data: [AnyHashable: Any]) { + private func save(_ contact: Contact) { + do { + if let lastMessage = contact.last_msg { + try MessageDAO.trustIfNextMessageExists(before: lastMessage) + } + try StorageService.sharedInstance.perform(action: .save, with: contact) + } catch { + LogService.log(topic: .db) { return error.localizedDescription } + } + } + + private func save(_ room: Room) { + do { + if let lastMessage = room.last_msg { + try MessageDAO.trustIfNextMessageExists(before: lastMessage) + } + try StorageService.sharedInstance.perform(action: .save, with: room) + } catch { + LogService.log(topic: .db) { return error.localizedDescription } + } + } + + private func sendNewNotification(title: String, sound: String?, id: String, data: [AnyHashable: Any]) { let content = UNMutableNotificationContent() content.body = title content.userInfo = data @@ -102,11 +130,6 @@ class PushService: NSObject, PKPushRegistryDelegate, UserSettingsRespondable { let center = UNUserNotificationCenter.current() center.add(request, withCompletionHandler: nil) } - // MARK: - UserSettingsRespondable - - func userSettingsDidChange(_ newSettings: UserSettings) { - Aps.notificationSettings = newSettings.notifications - } func handleLocalPushNotification(request: UNNotificationRequest) { let identifier = request.identifier.split(separator: "_").first @@ -116,8 +139,15 @@ class PushService: NSObject, PKPushRegistryDelegate, UserSettingsRespondable { guard let aps = Aps(data: userInfo) else { return } NotificationManager.shared.handleNotification(aps: aps) default: - NynjaCommunicatorService.sharedInstance.handleNotificationResponse(identifier: request.identifier, - userInfo: request.content.userInfo) + NynjaCommunicatorService.sharedInstance.handleNotificationResponse(identifier: request.identifier, + userInfo: request.content.userInfo) } } + + + // MARK: - UserSettingsRespondable + + func userSettingsDidChange(_ newSettings: UserSettings) { + Aps.notificationSettings = newSettings.notifications + } } diff --git a/Nynja/Services/REST/TranscribeNetworkService/TranscribeNetworkService.swift b/Nynja/Services/REST/TranscribeNetworkService/TranscribeNetworkService.swift index 4ec837b45..71f7c672b 100644 --- a/Nynja/Services/REST/TranscribeNetworkService/TranscribeNetworkService.swift +++ b/Nynja/Services/REST/TranscribeNetworkService/TranscribeNetworkService.swift @@ -11,11 +11,11 @@ import Foundation protocol TranscribeNetworkServiceProtocol: NetworkService { func transcribeShortAudio(input: TranscribeShortRequestData, - completion: @escaping (HTTPResponseResult) -> Void) -> URLSessionTask + completion: ((HTTPResponseResult) -> Void)?) -> URLSessionTask func transcribeLongAudio(input: TranscribeLongRequestData, - completion: @escaping (HTTPResponseResult) -> Void) -> URLSessionTask + completion: ((HTTPResponseResult) -> Void)?) -> URLSessionTask - func loadTranscriptionProcessingResult(name: String, completion: @escaping (HTTPResponseResult) -> Void) -> URLSessionTask + func loadTranscriptionProcessingResult(name: String, completion: ((HTTPResponseResult) -> Void)?) -> URLSessionTask } final class TranscribeNetworkService: TranscribeNetworkServiceProtocol { @@ -29,7 +29,7 @@ final class TranscribeNetworkService: TranscribeNetworkServiceProtocol { } @discardableResult - func transcribeShortAudio(input: TranscribeShortRequestData, completion: @escaping (HTTPResponseResult) -> Void) -> URLSessionTask { + func transcribeShortAudio(input: TranscribeShortRequestData, completion: ((HTTPResponseResult) -> Void)?) -> URLSessionTask { guard let params: HTTPParameters = input.dictionary else { assertionFailure("Something went wrong") @@ -41,12 +41,12 @@ final class TranscribeNetworkService: TranscribeNetworkServiceProtocol { let target = TranscribeNetworkRouter.speechRecognize(params: params, queryItems: queryItems) return client.request(to: target) { (result: HTTPResponseResult) in - completion(result) + completion?(result) } } @discardableResult - func transcribeLongAudio(input: TranscribeLongRequestData, completion: @escaping (HTTPResponseResult) -> Void) -> URLSessionTask { + func transcribeLongAudio(input: TranscribeLongRequestData, completion: ((HTTPResponseResult) -> Void)?) -> URLSessionTask { guard let params: HTTPParameters = input.dictionary else { assertionFailure("Something went wrong") @@ -58,18 +58,18 @@ final class TranscribeNetworkService: TranscribeNetworkServiceProtocol { let target = TranscribeNetworkRouter.speechLongRecognize(params: params, queryItems: queryItems) return client.request(to: target) { (result: HTTPResponseResult) in - completion(result) + completion?(result) } } @discardableResult - func loadTranscriptionProcessingResult(name: String, completion: @escaping (HTTPResponseResult) -> Void) -> URLSessionTask { + func loadTranscriptionProcessingResult(name: String, completion: ((HTTPResponseResult) -> Void)?) -> URLSessionTask { let queryItems: HTTPQueryItems = [Keys.apiKey: apiKey] let target = TranscribeNetworkRouter.loadTranscriptionProcessingResult(name: name, queryItems: queryItems) return client.request(to: target) { (result: HTTPResponseResult) in - completion(result) + completion?(result) } } } diff --git a/Nynja/Services/ReachabilityService.swift b/Nynja/Services/ReachabilityService.swift index ed5665628..637ce84f1 100644 --- a/Nynja/Services/ReachabilityService.swift +++ b/Nynja/Services/ReachabilityService.swift @@ -39,12 +39,12 @@ class ReachabilityService: NSObject { do { try Network.reachability?.start() } catch let error as Network.Error { - LogService.log(topic: .network, text: "Reachability error: \(error.localizedDescription)") + LogService.log(topic: .network) { return "Reachability error: \(error.localizedDescription)" } } catch { - LogService.log(topic: .network, text: "Reachability error: \(error.localizedDescription)") + LogService.log(topic: .network) { return "Reachability error: \(error.localizedDescription)" } } } catch { - LogService.log(topic: .network, text: "Reachability error: \(error.localizedDescription)") + LogService.log(topic: .network) { return "Reachability error: \(error.localizedDescription)" } } NotificationCenter.default.addObserver(self, selector: #selector(statusManager), name: .flagsChanged, object: Network.reachability) } diff --git a/Nynja/Services/ResourceManager/ResourceManager.swift b/Nynja/Services/ResourceManager/ResourceManager.swift index 3a22501d3..278e439de 100644 --- a/Nynja/Services/ResourceManager/ResourceManager.swift +++ b/Nynja/Services/ResourceManager/ResourceManager.swift @@ -92,7 +92,7 @@ extension ResourceManager { try saveData(data: imgData, to: url) return url } catch let error { - LogService.log(topic: .fileSystem, text: error.localizedDescription) + LogService.log(topic: .fileSystem) { return error.localizedDescription } return nil } } @@ -106,7 +106,7 @@ extension ResourceManager { try saveData(data: data, to: url) return url } catch let error { - LogService.log(topic: .fileSystem, text: error.localizedDescription) + LogService.log(topic: .fileSystem) { return error.localizedDescription } return nil } } @@ -124,7 +124,7 @@ extension ResourceManager { return newUrl } - LogService.log(topic: .fileSystem, text: error.localizedDescription) + LogService.log(topic: .fileSystem) { return error.localizedDescription } return nil } } @@ -228,14 +228,14 @@ extension ResourceManager { operation.state = .finished }, failure: { (error) in - LogService.log(topic: .fileSystem, text: error.localizedDescription) + LogService.log(topic: .fileSystem) { return error.localizedDescription } }) case .video: self.fetchVideo(item: item, completion: { (item) in arr.append((ResourceManagerMediaType.video, item.videoUrl)) operation.state = .finished }, failure: { (error) in - LogService.log(topic: .fileSystem, text: error.localizedDescription) + LogService.log(topic: .fileSystem) { return error.localizedDescription } }) case .audio, .unknown: operation.state = .finished } @@ -463,7 +463,7 @@ private extension ResourceManager { } func fetchMediaWithFailurePermission() { - LogService.log(topic: .fileSystem, text: "fetchMediaWithFailurePermission") + LogService.log(topic: .fileSystem) { return "fetchMediaWithFailurePermission" } } func fetchCollectionsWithSuccessPermission() -> [PHAssetCollection] { diff --git a/Nynja/Services/ServiceFactory/ServiceFactory.swift b/Nynja/Services/ServiceFactory/ServiceFactory.swift index 243c17e63..0e604f265 100644 --- a/Nynja/Services/ServiceFactory/ServiceFactory.swift +++ b/Nynja/Services/ServiceFactory/ServiceFactory.swift @@ -48,14 +48,12 @@ protocol ServiceFactoryProtocol { final class ServiceFactory: ServiceFactoryProtocol { func makeMessageSendingService() -> MessageSendingServiceProtocol { - let messageSendingService = MessageSendingService() - let sendingServiceDep = MessageSendingService.Dependencies( + let dependencies = MessageSendingService.Dependencies( mqttService: makeMQTTService(), storageService: makeStorageService(), processingManager: makeMesageProcessingManager()) - messageSendingService.inject(dependencies: sendingServiceDep) - - return messageSendingService + + return MessageSendingService(dependencies: dependencies) } func makeResourceManager() -> ResourceManagerProtocol { @@ -65,13 +63,9 @@ final class ServiceFactory: ServiceFactoryProtocol { } func makeMessageFactory() -> MessageFactoryProtocol { - let messageFactory = MessageFactory() - - let dep = MessageFactory.Dependencies(storageService: makeStorageService(), - payloadBuilder: makeMessagePayloadBuilder()) - messageFactory.inject(dependencies: dep) - - return messageFactory + let dependencies = MessageFactory.Dependencies(storageService: makeStorageService(), + payloadBuilder: makeMessagePayloadBuilder()) + return MessageFactory(dependencies: dependencies) } func makeMessagePayloadBuilder() -> MessagePayloadBuilderInput { diff --git a/Nynja/Services/SoundService.swift b/Nynja/Services/SoundService.swift index b78589769..11f2e376f 100644 --- a/Nynja/Services/SoundService.swift +++ b/Nynja/Services/SoundService.swift @@ -15,6 +15,10 @@ final class SoundService { private var incomingCallPlayer: SoundPlayer? private var incomingMessagePlayer: SoundPlayer? private var outcomingMessagePlayer: SoundPlayer? + private var ringbackCallPlayer: SoundPlayer? + + + // MARK: - VoIP calls private(set) lazy var soundBundle: SoundBundle = { let dataURL = Bundle.main.url(forResource: "Sounds", withExtension: "json")! @@ -27,18 +31,107 @@ final class SoundService { private var isInChatSoundEnabled: Bool { return userSettingsService.notifications.isInChatSoundEnabled } + + private let lock = NSLock() + + private var isVoipCallActive: Bool = false + // MARK: - Init - static let sharedInstance : SoundService = { - let instance = SoundService() - return instance - }() + static let sharedInstance = SoundService() + + private var cachedSounds: [URL: SoundInfo] = [:] + + + // MARK: - VoIP calls + + func voipCallStarted() { + lock.lock() + isVoipCallActive = true + lock.unlock() + } + + func voipCallStopped() { + lock.lock() + isVoipCallActive = false + lock.unlock() + } + + + // MARK: - Play Sound + + func playSound(with url: URL) { + guard canPlaySound(with: url) else { + return + } + + dispatchAsyncMain { + let soundId = self.makeSoundId(with: url) + self.cachedSounds[url] = SoundInfo(soundId: soundId, lastTimeSoundPlayed: CFAbsoluteTimeGetCurrent()) + AudioServicesPlaySystemSound(soundId) + } + } + + private func canPlaySound(with url: URL) -> Bool { + return isInChatSoundEnabled && isSafeIntervalPassed(for: url) + } + + private func isSafeIntervalPassed(for url: URL) -> Bool { + guard let lastTimeSoundPlayed = cachedSounds[url]?.lastTimeSoundPlayed else { + return true + } + + let diff = abs(CFAbsoluteTimeGetCurrent() - lastTimeSoundPlayed) + return diff > 0.25 + } + + private func makeSoundId(with url: URL) -> SystemSoundID { + var soundId: SystemSoundID = 0 + + if let info = cachedSounds[url] { + soundId = info.soundId + } else { + AudioServicesCreateSystemSoundID(url as CFURL, &soundId) + } + + return soundId + } + + func playPushSound() { + guard let soundUrl = userSettingsService.notifications.alertSound.url else { + return + } + + playSound(with: soundUrl) + } + + func playIncomingMessageSound() { + guard canPlayMessageSound(), + let soundUrl = soundBundle.defaultIncomingMessage.url else { + return + } + + playSound(with: soundUrl) + } + func playOutcomingMessageSound() { + guard canPlayMessageSound(), + let soundUrl = soundBundle.defaultOutcomingMessage.url else { + return + } + + playSound(with: soundUrl) + } - // MARK: - Actions + private func canPlayMessageSound() -> Bool { + let isAppActive = UIApplication.shared.applicationState == .active + return !isVoipCallActive && isAppActive + } - // MARK: Incoming Call + + // MARK: - Calls + // MARK: Incoming func playCallSound() { guard let soundURL = soundBundle.defaultCall.url else { @@ -52,73 +145,57 @@ final class SoundService { incomingCallPlayer = try SoundPlayer(soundURL: soundURL, isInfinite: true, shouldVibrate: shouldVibrate) incomingCallPlayer?.play() } catch let error as NSError { - LogService.log(topic: .audioSystem, text: error.localizedDescription) + LogService.log(topic: .audioSystem) { return error.localizedDescription } } } func stopIncomingCallPlayer() { incomingCallPlayer?.stop() } + - // MARK: Chat + // MARK: Ring back tone - func playIncomingMessageSound() { - guard prepareToPlayMessageSound() else { - return - } - guard let soundURL = soundBundle.defaultIncomingMessage.url else { + func playRingbackSound() { + guard let soundURL = soundBundle.defaultRingback.url else { + LogService.log(topic: .audioSystem) { return "ringback sound not found" } return } + do { - incomingMessagePlayer?.stop() - incomingMessagePlayer = try SoundPlayer(soundURL: soundURL) - incomingMessagePlayer?.play() + if ringbackCallPlayer == nil { + LogService.log(topic: .audioSystem) { return "create ringback player" } + ringbackCallPlayer = try SoundPlayer(soundURL: soundURL, isInfinite: true, shouldVibrate: false) + LogService.log(topic: .audioSystem) { return "ringback sound: \(soundURL)" } + } + + if ringbackCallPlayer?.isPlaying ?? false { + LogService.log(topic: .audioSystem) { return "ringback is playing" } + return + } + + LogService.log(topic: .audioSystem) { return "play ringback" } + ringbackCallPlayer?.play() } catch let error as NSError { - LogService.log(topic: .audioSystem, text: error.localizedDescription) + LogService.log(topic: .audioSystem) { return "Error init ringback player \(error.localizedDescription)" } } } - func playAlertSound() { - guard isInChatSoundEnabled else { - return - } - guard let soundURL = userSettingsService.notifications.alertSound.url else { - return - } - var soundId: SystemSoundID = 0 - AudioServicesCreateSystemSoundID(soundURL as CFURL, &soundId) - AudioServicesAddSystemSoundCompletion(soundId, nil, nil, { (soundId, clientData) -> Void in - AudioServicesDisposeSystemSoundID(soundId) - }, nil) - AudioServicesPlaySystemSound(soundId) + func stopRingbackPlayer() { + LogService.log(topic: .audioSystem) { return "stop ringback" } + ringbackCallPlayer?.stop() } + +} + + +// MARK: - SoundInfo + +extension SoundService { - func playOutcomingMessageSound() { - guard prepareToPlayMessageSound() else { - return - } - guard let soundURL = soundBundle.defaultOutcomingMessage.url else { - return - } - do { - outcomingMessagePlayer?.stop() - outcomingMessagePlayer = try SoundPlayer(soundURL: soundURL) - outcomingMessagePlayer?.play() - } catch let error as NSError { - LogService.log(topic: .audioSystem, text: error.localizedDescription) - } + struct SoundInfo { + let soundId: SystemSoundID + let lastTimeSoundPlayed: CFAbsoluteTime? } - private func prepareToPlayMessageSound() -> Bool { - do { - guard try audioSessionManager.request(category: .message) else { - return false - } - } catch { - LogService.log(topic: .audioSystem, text: error.localizedDescription) - return false - } - - return isInChatSoundEnabled - } } diff --git a/Nynja/Services/StickersProvider/StickersProvider.swift b/Nynja/Services/StickersProvider/StickersProvider.swift index ad344c00e..397acc27a 100644 --- a/Nynja/Services/StickersProvider/StickersProvider.swift +++ b/Nynja/Services/StickersProvider/StickersProvider.swift @@ -30,6 +30,16 @@ final class StickersProvider: StickersProviding, InitializeInjectable { // MARK: - StickersProviding + func fetch() { + let packages = StickerPackDAO.fetchAllPacks() + stickersCache.removeAll() + for package in packages { + if let stickers = package.stickers { + stickersCache.append(contentsOf: stickers) + } + } + } + func fetchStickerPackages() -> [StickerPack] { let packages = StickerPackDAO.fetchAllPacks() stickersCache.removeAll() diff --git a/Nynja/Services/StickersProvider/StickersProviding.swift b/Nynja/Services/StickersProvider/StickersProviding.swift index 68aea7852..dd119b433 100644 --- a/Nynja/Services/StickersProvider/StickersProviding.swift +++ b/Nynja/Services/StickersProvider/StickersProviding.swift @@ -12,6 +12,7 @@ typealias Sticker = Desc typealias DBSticker = DBDesc protocol StickersProviding: class { + func fetch() func fetchStickerPackages() -> [StickerPack] func fetchRecentStickers() -> [Sticker] diff --git a/Nynja/Services/StorageService.swift b/Nynja/Services/StorageService.swift index 0f65edab1..8870942b7 100644 --- a/Nynja/Services/StorageService.swift +++ b/Nynja/Services/StorageService.swift @@ -34,6 +34,10 @@ class StorageService { return countriesProvider.fetchCountries() }() + /// It is used only for debug purposes. + /// Note: You should make clear instalation (don't upgrade old version of app). + private let shouldEncryptDB: Bool = true + // MARK: - Singleton static let sharedInstance = StorageService() @@ -45,26 +49,45 @@ class StorageService { func setupDatabase(with name: String, application: UIApplication) { #if !SHARE_EXTENSION + LogService.log(topic: .db) { return "Setup DB: name = \(name)" } + guard dbPool == nil else { - return + LogService.log(topic: .db) { return "Setup DB: dbPool -> NIL" } + return } let name = name.md5() + + if shouldEncryptDB { + setupSecureDatabase(with: name, application: application) + } else { + setupInsecureDatabase(with: name, application: application) + } + + #endif + } + + #if !SHARE_EXTENSION + private func setupSecureDatabase(with name: String, application: UIApplication) { let newPassphrase = makePassphrase(with: name) let oldPassphrase = keychain.string(forKey: passphraseKey) - - LogService.log(topic: .passphrase, text: "Old Passphrase: \(oldPassphrase ?? "")") - LogService.log(topic: .passphrase, text: "New Passphrase: \(newPassphrase)") - + + LogService.log(topic: .passphrase) { return "Old Passphrase: \(oldPassphrase ?? "")" } + LogService.log(topic: .passphrase) { return "New Passphrase: \(newPassphrase)" } + databaseManager.setupDatabase(with: name, - oldPassphrase: oldPassphrase, - newPassphrase: newPassphrase, - application: application) - + encryptionMode: .reencrypt(usingOld: oldPassphrase, new: newPassphrase), + application: application) + keychain.set(newPassphrase, forKey: passphraseKey) - #endif + LogService.log(topic: .passphrase) { return "Store new passphrase: \(keychain.string(forKey: passphraseKey))" } } + private func setupInsecureDatabase(with name: String, application: UIApplication) { + databaseManager.setupDatabase(with: name, encryptionMode: .none, application: application) + } + #endif + private func makePassphrase(with name: String) -> String { let uuid = UUID().uuidString let size = uuid.count @@ -81,7 +104,7 @@ class StorageService { func clearStorage() { #if !SHARE_EXTENSION - databaseManager.clear() + databaseManager.clear() #endif keychain.clear() dropUserInfo() @@ -99,6 +122,14 @@ extension StorageService: DBManagerProtocol { return databaseManager.fetch(closure) } + func rowExists(in table: T.Type, where condition: String, arguments: StatementArguments?) -> Bool { + return databaseManager.rowExists(in: table, where: condition, arguments: arguments) + } + + func write(_ closure: (Database) throws -> Void) throws { + try databaseManager.write(closure) + } + func perform(action: DatabaseAction, with model: DBModelConvertible) throws { try databaseManager.perform(action: action, with: model) } diff --git a/Nynja/Services/TranscribeService/Operations /AudioConvertionOperation.swift b/Nynja/Services/TranscribeService/Operations /AudioConvertOperation.swift similarity index 58% rename from Nynja/Services/TranscribeService/Operations /AudioConvertionOperation.swift rename to Nynja/Services/TranscribeService/Operations /AudioConvertOperation.swift index 4caa643e9..da15a23bc 100644 --- a/Nynja/Services/TranscribeService/Operations /AudioConvertionOperation.swift +++ b/Nynja/Services/TranscribeService/Operations /AudioConvertOperation.swift @@ -1,5 +1,5 @@ // -// AudioConvertionOperation.swift +// AudioConvertOperation.swift // Nynja // // Created by Andrey Reznik on 22.07.2018. @@ -9,29 +9,32 @@ import Foundation import AVFoundation -final class AudioConvertionOperation: AsyncOperation { +final class AudioConvertOperation: TranscribeOperation, InitializeInjectable { + private var dataWrapper: TranscribeServiceDataWrapper private let audioUrl: URL - var outputURL: URL { - let timeStamp = Int64(Date().timeIntervalSince1970.seconds) - let fileName = "voice_\(timeStamp)_\(UUID().uuidString).caf" - let path = FileManagerService.sharedInstance.createFile(folder: Constants.Folders.downloads, name: fileName) ?? "" - return URL(fileURLWithPath: path) + struct Dependencies { + let dataWrapper: TranscribeServiceDataWrapper + let audioUrl: URL + + let completion: TranscribeProcessingInnerHandler } - init(dataWrapper: TranscribeServiceDataWrapper, audioUrl: URL) { - self.dataWrapper = dataWrapper - self.audioUrl = audioUrl + required init(dependencies: Dependencies) { + dataWrapper = dependencies.dataWrapper + audioUrl = dependencies.audioUrl + super.init(completion: dependencies.completion) } override func main() { + super.main() let operation = AudioFileConvertOperation(sourceURL: audioUrl, destinationURL: outputURL, sampleRate: 16000, outputFormat: kAudioFormatLinearPCM) { result in switch result { case .success(_, let url): - self.dataWrapper.outputURL = url + self.dataWrapper.processingURL = url case .error(_, let error): - self.dataWrapper.result = .failure(.convertion(error)) + self.dataWrapper.state = .failure(.convertion(error)) } self.state = .finished } @@ -39,5 +42,19 @@ final class AudioConvertionOperation: AsyncOperation { DispatchQueue.global(qos: .userInteractive).async { operation.start() } + + task = operation + + completion?(.updateProccess(self, .convert)) + } +} + +extension AudioConvertOperation { + + var outputURL: URL { + let timeStamp = Int64(Date().timeIntervalSince1970.seconds) + let fileName = "voice_\(timeStamp)_\(UUID().uuidString).caf" + let path = FileManagerService.sharedInstance.createFile(folder: Constants.Folders.downloads, name: fileName) ?? "" + return URL(fileURLWithPath: path) } } diff --git a/Nynja/Services/TranscribeService/Operations /AudioLongTranscribeOperation.swift b/Nynja/Services/TranscribeService/Operations /AudioLongTranscribeOperation.swift new file mode 100644 index 000000000..9e8a05ddd --- /dev/null +++ b/Nynja/Services/TranscribeService/Operations /AudioLongTranscribeOperation.swift @@ -0,0 +1,62 @@ +// +// AudioLongTranscribeOperation.swift +// Nynja +// +// Created by Andrey Reznik on 24.07.2018. +// Copyright © 2018 TecSynt Solutions. All rights reserved. +// + +final class AudioLongTranscribeOperation: TranscribeOperation, InitializeInjectable { + private var dataWrapper: TranscribeServiceDataWrapper + private let networkService: TranscribeNetworkServiceProtocol + + private let language: String + + struct Dependencies { + let dataWrapper: TranscribeServiceDataWrapper + let networkService: TranscribeNetworkServiceProtocol + + let language: String + + let completion: TranscribeProcessingInnerHandler + } + + required init(dependencies: Dependencies) { + dataWrapper = dependencies.dataWrapper + networkService = dependencies.networkService + language = dependencies.language + super.init(completion: dependencies.completion) + } + + override func main() { + super.main() + guard !dependencies.contains(where: { $0.isCancelled }) else { + cancel() + return + } + guard let uri = dataWrapper.processingResult else { + completion?(dataWrapper.state ?? .unknown) + state = .finished + return + } + let input = TranscribeLongRequestData(config: .init(encoding: "LINEAR16", + sampleRateHertz: 16000, + languageCode: language, + maxAlternatives: 30), + audio: .init(uri: uri)) + + let transcribeTask = networkService.transcribeLongAudio(input: input) { result in + switch result { + case .failure(let error): + self.dataWrapper.state = .failure(.networkClient(error)) + case .success(let response): + self.dataWrapper.processingResult = response.name + } + self.state = .finished + } + + task = transcribeTask + + completion?(.updateProccess(self, .transcribing)) + } +} diff --git a/Nynja/Services/TranscribeService/Operations /AudioLongTranscribeProccessingOperation.swift b/Nynja/Services/TranscribeService/Operations /AudioLongTranscribeProccessingOperation.swift new file mode 100644 index 000000000..d833a14d2 --- /dev/null +++ b/Nynja/Services/TranscribeService/Operations /AudioLongTranscribeProccessingOperation.swift @@ -0,0 +1,100 @@ +// +// AudioLongTranscribeProccessingOperation.swift +// Nynja +// +// Created by Andrey Reznik on 24.07.2018. +// Copyright © 2018 TecSynt Solutions. All rights reserved. +// + +final class AudioLongTranscribeProccessingOperation: TranscribeOperation, InitializeInjectable { + private var dataWrapper: TranscribeServiceDataWrapper + private let networkService: TranscribeNetworkServiceProtocol + + private let language: String + + private var operationGroup: DispatchGroup? + private var operationItem: DispatchWorkItem? + + private var operationInterval: TimeInterval = Constants.defaulOperationInterval + + struct Dependencies { + let dataWrapper: TranscribeServiceDataWrapper + let networkService: TranscribeNetworkServiceProtocol + + let language: String + + let completion: TranscribeProcessingInnerHandler + } + + required init(dependencies: Dependencies) { + dataWrapper = dependencies.dataWrapper + networkService = dependencies.networkService + language = dependencies.language + super.init(completion: dependencies.completion) + } + + override func main() { + super.main() + guard !dependencies.contains(where: { $0.isCancelled }) else { + cancel() + return + } + guard let name = dataWrapper.processingResult else { + completion?(dataWrapper.state ?? .unknown) + state = .finished + return + } + operationGroup = DispatchGroup() + operationGroup?.enter() + transcribeLongOperation(with: name) + } + + override func cancel() { + operationItem?.cancel() + operationGroup?.leave() + super.cancel() + } + + private func transcribeLongOperation(with name: String) { + let processingTask = networkService.loadTranscriptionProcessingResult(name: name){ result in + switch result { + case .failure(let error): + self.completion?(.failure(.networkClient(error))) + self.operationGroup?.leave() + self.state = .finished + case .success(let response): + if response.done == true { + defer { + self.operationGroup?.leave() + self.state = .finished + } + guard let transcription = response.response?.results?.first?.alternatives?.first?.transcript else { + self.dataWrapper.processingResult = nil + self.completion?(.failure(.emptyResponse(self.language))) + return + } + self.dataWrapper.processingResult = transcription + self.completion?(.success(transcription)) + } else { + self.operationItem = DispatchWorkItem { + self.transcribeLongOperation(with: name) + } + if let item = self.operationItem { + DispatchQueue.global(qos: .background).asyncAfter(deadline: DispatchTime.now() + self.operationInterval, execute: item) + } + self.completion?(.updateProccess(self, .transcribeProcessing)) + } + } + } + + task = processingTask + + completion?(.updateProccess(self, .transcribeProcessing)) + } +} + +extension AudioLongTranscribeProccessingOperation { + enum Constants { + static let defaulOperationInterval: TimeInterval = 5.0 + } +} diff --git a/Nynja/Services/TranscribeService/Operations /AudioShortTranscribeOperation.swift b/Nynja/Services/TranscribeService/Operations /AudioShortTranscribeOperation.swift new file mode 100644 index 000000000..45f918882 --- /dev/null +++ b/Nynja/Services/TranscribeService/Operations /AudioShortTranscribeOperation.swift @@ -0,0 +1,69 @@ +// +// AudioShortTranscribeOperation.swift +// Nynja +// +// Created by Andrey Reznik on 22.07.2018. +// Copyright © 2018 TecSynt Solutions. All rights reserved. +// + +final class AudioShortTranscribeOperation: TranscribeOperation, InitializeInjectable { + private var dataWrapper: TranscribeServiceDataWrapper + private let networkService: TranscribeNetworkServiceProtocol + + private let language: String + + struct Dependencies { + let dataWrapper: TranscribeServiceDataWrapper + let networkService: TranscribeNetworkServiceProtocol + + let language: String + + let completion: TranscribeProcessingInnerHandler + } + + required init(dependencies: Dependencies) { + dataWrapper = dependencies.dataWrapper + networkService = dependencies.networkService + language = dependencies.language + super.init(completion: dependencies.completion) + } + + override func main() { + super.main() + guard !dependencies.contains(where: { $0.isCancelled }) else { + cancel() + return + } + guard let url = dataWrapper.processingURL, let data = try? Data(contentsOf: url) else { + completion?(dataWrapper.state ?? .unknown) + state = .finished + return + } + + let content = data.base64EncodedString() + let input = TranscribeShortRequestData(config: .init(encoding: "LINEAR16", + sampleRateHertz: 16000, + languageCode: language, + maxAlternatives: 30), + audio: .init(content: content)) + + let transcribeTask = networkService.transcribeShortAudio(input: input) { result in + switch result { + case .failure(let error): + self.completion?(.failure(.networkClient(error))) + case .success(let response): + guard let transcription = response.results?.first?.alternatives?.first?.transcript else { + self.completion?(.failure(.emptyResponse(self.language))) + self.state = .finished + return + } + self.completion?(.success(transcription)) + } + self.state = .finished + } + + task = transcribeTask + + completion?(.updateProccess(self, .transcribing)) + } +} diff --git a/Nynja/Services/TranscribeService/Operations /AudioTranscribeSendOperation.swift b/Nynja/Services/TranscribeService/Operations /AudioTranscribeSendOperation.swift new file mode 100644 index 000000000..1c6bda89e --- /dev/null +++ b/Nynja/Services/TranscribeService/Operations /AudioTranscribeSendOperation.swift @@ -0,0 +1,56 @@ +// +// AudioTranscribeSendOperation.swift +// Nynja +// +// Created by Andrey Reznik on 21.08.2018. +// Copyright © 2018 TecSynt Solutions. All rights reserved. +// + +import Foundation + +class AudioTranscribeSendOperation: Operation, InitializeInjectable { + private let dataWrapper: TranscribeServiceDataWrapper + private let processingManager: DefaultMessagesProcessingManager + private let messageFactory: MessageFactoryProtocol + + private let message: Message + private let language: String + + private let completion: TranscribeProcessingInnerHandler + + struct Dependencies { + let dataWrapper: TranscribeServiceDataWrapper + let processingManager: DefaultMessagesProcessingManager + let messageFactory: MessageFactoryProtocol + + let message: Message + let language: String + + let completion: TranscribeProcessingInnerHandler + } + + required init(dependencies: Dependencies) { + dataWrapper = dependencies.dataWrapper + processingManager = dependencies.processingManager + messageFactory = dependencies.messageFactory + + message = dependencies.message + language = dependencies.language + completion = dependencies.completion + } + + override func main() { + guard !dependencies.contains(where: { $0.isCancelled }) else { + return + } + guard let transcription = dataWrapper.processingResult else { + completion(.unknown) + return + } + + let transribedMessage = messageFactory.makeTranscribedMessage(message: message, + text: transcription, + lang: language) + processingManager.uploadMessage(transribedMessage) + } +} diff --git a/Nynja/Services/TranscribeService/Operations /AudioUploadOperation.swift b/Nynja/Services/TranscribeService/Operations /AudioUploadOperation.swift index b5f843bbe..bf9c4d390 100644 --- a/Nynja/Services/TranscribeService/Operations /AudioUploadOperation.swift +++ b/Nynja/Services/TranscribeService/Operations /AudioUploadOperation.swift @@ -8,19 +8,30 @@ import Firebase -final class AudioUploadOperation: AsyncOperation { +final class AudioUploadOperation: TranscribeOperation, InitializeInjectable { + private var dataWrapper: TranscribeServiceDataWrapper - private let completion: TranscribeProcessingHandler + struct Dependencies { + let dataWrapper: TranscribeServiceDataWrapper + + let completion: TranscribeProcessingInnerHandler + } - init(dataWrapper: TranscribeServiceDataWrapper, completion: @escaping TranscribeProcessingHandler) { - self.dataWrapper = dataWrapper - self.completion = completion + required init(dependencies: Dependencies) { + dataWrapper = dependencies.dataWrapper + super.init(completion: dependencies.completion) } override func main() { - guard let url = dataWrapper.outputURL, let data = try? Data(contentsOf: url) else { - self.state = .finished + super.main() + guard !dependencies.contains(where: { $0.isCancelled }) else { + cancel() + return + } + guard let url = dataWrapper.processingURL, let data = try? Data(contentsOf: url) else { + completion?(dataWrapper.state ?? .unknown) + state = .finished return } @@ -29,18 +40,21 @@ final class AudioUploadOperation: AsyncOperation { let metadata = StorageMetadata() metadata.contentType = "audio/x-caf" let storageRef = storage.reference(withPath: imagePath) - let task = storageRef.putData(data, metadata: metadata) { metadata, error in + let uploadTask = storageRef.putData(data, metadata: metadata) { metadata, error in if let error = error { - self.dataWrapper.result = .failure(.upload(error)) + self.dataWrapper.state = .failure(.upload(error)) } else { if let metadata = metadata, let name = metadata.name { - self.dataWrapper.outputString = "gs://\(metadata.bucket)/\(name)" + self.dataWrapper.processingResult = "gs://\(metadata.bucket)/\(name)" } else { - self.dataWrapper.result = .failure(.upload(TranscribeServiceResult.UploadError.empty)) + self.dataWrapper.state = .failure(.upload(TranscribeServiceState.UploadError.empty)) } } self.state = .finished } - completion(.updateProccess(task)) + + task = uploadTask + + completion?(.updateProccess(self, .upload)) } } diff --git a/Nynja/Services/TranscribeService/Operations /TranscribeLongAudioOperation.swift b/Nynja/Services/TranscribeService/Operations /TranscribeLongAudioOperation.swift deleted file mode 100644 index 602d5630b..000000000 --- a/Nynja/Services/TranscribeService/Operations /TranscribeLongAudioOperation.swift +++ /dev/null @@ -1,47 +0,0 @@ -// -// TranscribeLongAudioOperation.swift -// Nynja -// -// Created by Andrey Reznik on 24.07.2018. -// Copyright © 2018 TecSynt Solutions. All rights reserved. -// - -final class TranscribeLongAudioOperation: AsyncOperation { - private var dataWrapper: TranscribeServiceDataWrapper - private let networkService: TranscribeNetworkService - - private let language: String - - private let completion: TranscribeProcessingHandler - - init(dataWrapper: TranscribeServiceDataWrapper, networkService: TranscribeNetworkService, language: String, completion: @escaping TranscribeProcessingHandler) { - self.dataWrapper = dataWrapper - self.networkService = networkService - self.language = language - self.completion = completion - } - - override func main() { - guard let uri = dataWrapper.outputString else { - completion(dataWrapper.result!) - self.state = .finished - return - } - let input = TranscribeLongRequestData(config: .init(encoding: "LINEAR16", - sampleRateHertz: 16000, - languageCode: language, - maxAlternatives: 30), - audio: .init(uri: uri)) - - let task = networkService.transcribeLongAudio(input: input) { result in - switch result { - case .failure(let error): - self.dataWrapper.result = .failure(.networkClient(error)) - case .success(let response): - self.dataWrapper.processName = response.name - } - self.state = .finished - } - completion(.updateProccess(task)) - } -} diff --git a/Nynja/Services/TranscribeService/Operations /TranscribeLongAudioProccessingOperation.swift b/Nynja/Services/TranscribeService/Operations /TranscribeLongAudioProccessingOperation.swift deleted file mode 100644 index f1337f8d6..000000000 --- a/Nynja/Services/TranscribeService/Operations /TranscribeLongAudioProccessingOperation.swift +++ /dev/null @@ -1,81 +0,0 @@ -// -// TranscribeLongAudioProccessingOperation.swift -// Nynja -// -// Created by Andrey Reznik on 24.07.2018. -// Copyright © 2018 TecSynt Solutions. All rights reserved. -// - -final class TranscribeLongAudioProccessingOperation: AsyncOperation { - private var dataWrapper: TranscribeServiceDataWrapper - private let networkService: TranscribeNetworkService - - private let completion: TranscribeProcessingHandler - - private var operationGroup = DispatchGroup() - private var operationItem: DispatchWorkItem? - - private var operationInterval: TimeInterval - - init(dataWrapper: TranscribeServiceDataWrapper, networkService: TranscribeNetworkService, operationInterval: TimeInterval = Constants.defaulOperationInterval, completion: @escaping TranscribeProcessingHandler) { - self.dataWrapper = dataWrapper - self.networkService = networkService - self.operationInterval = operationInterval - self.completion = completion - } - - override func main() { - guard let name = dataWrapper.processName else { - completion(dataWrapper.result!) - self.state = .finished - return - } - - operationGroup.enter() - transcribeLongOperation(with: name) - } - - override func cancel() { - super.cancel() - operationItem?.cancel() - operationGroup.leave() - } - - private func transcribeLongOperation(with name: String) { - let task = self.networkService.loadTranscriptionProcessingResult(name: name){ result in - switch result { - case .failure(let error): - self.completion(.failure(.networkClient(error))) - self.operationGroup.leave() - self.state = .finished - case .success(let response): - if response.done == true { - defer { - self.operationGroup.leave() - self.state = .finished - } - guard let transcription = response.response?.results?.first?.alternatives?.first?.transcript else { - self.completion(.failure(.emptyResponse)) - return - } - self.completion(.success(transcription)) - } else { - self.operationItem = DispatchWorkItem { - self.transcribeLongOperation(with: name) - } - if let item = self.operationItem { - DispatchQueue.global(qos: .background).asyncAfter(deadline: DispatchTime.now() + self.operationInterval, execute: item) - } - self.completion(.updateProccess(self)) - } - } - } - self.completion(.updateProccess(task)) - } -} - -extension TranscribeLongAudioProccessingOperation { - enum Constants { - static let defaulOperationInterval: TimeInterval = 5.0 - } -} diff --git a/Nynja/Services/TranscribeService/Operations /TranscribeOperation.swift b/Nynja/Services/TranscribeService/Operations /TranscribeOperation.swift new file mode 100644 index 000000000..1fb48602d --- /dev/null +++ b/Nynja/Services/TranscribeService/Operations /TranscribeOperation.swift @@ -0,0 +1,39 @@ +// +// TranscribeOperation.swift +// Nynja +// +// Created by Andrey Reznik on 26.08.2018. +// Copyright © 2018 TecSynt Solutions. All rights reserved. +// + +import Foundation + +class TranscribeOperation: WrappedTaskOperation { + + internal var completion: TranscribeProcessingInnerHandler? + + override init() { + super.init() + } + + init(completion: TranscribeProcessingInnerHandler?) { + self.completion = completion + super.init() + } + + init(task: Cancelable, completion: TranscribeProcessingInnerHandler?) { + self.completion = completion + super.init(task: task) + } + + override func cancel() { + completion?(.cancel) + completion = nil + debugPrint("\(type(of: self)) canceled with name:\(name!)") + super.cancel() + } + + override func main() { + debugPrint("\(type(of: self)) main with name:\(name!)") + } +} diff --git a/Nynja/Services/TranscribeService/Operations /TranscribeShortAudioOperation.swift b/Nynja/Services/TranscribeService/Operations /TranscribeShortAudioOperation.swift deleted file mode 100644 index 2fad7bc71..000000000 --- a/Nynja/Services/TranscribeService/Operations /TranscribeShortAudioOperation.swift +++ /dev/null @@ -1,54 +0,0 @@ -// -// TranscribeShortAudioOperation.swift -// Nynja -// -// Created by Andrey Reznik on 22.07.2018. -// Copyright © 2018 TecSynt Solutions. All rights reserved. -// - -final class TranscribeShortAudioOperation: AsyncOperation { - private var dataWrapper: TranscribeServiceDataWrapper - private let networkService: TranscribeNetworkService - - private let language: String - - private let completion: (TranscribeServiceResult)->Void - - init(dataWrapper: TranscribeServiceDataWrapper, networkService: TranscribeNetworkService, language: String, completion: @escaping (TranscribeServiceResult)->Void) { - self.dataWrapper = dataWrapper - self.networkService = networkService - self.language = language - self.completion = completion - } - - override func main() { - guard let url = dataWrapper.outputURL, let data = try? Data(contentsOf: url) else { - completion(dataWrapper.result!) - self.state = .finished - return - } - - let content = data.base64EncodedString() - let input = TranscribeShortRequestData(config: .init(encoding: "LINEAR16", - sampleRateHertz: 16000, - languageCode: language, - maxAlternatives: 30), - audio: .init(content: content)) - - let task = networkService.transcribeShortAudio(input: input) { result in - switch result { - case .failure(let error): - self.completion(.failure(.networkClient(error))) - case .success(let response): - guard let transcription = response.results?.first?.alternatives?.first?.transcript else { - self.completion(.failure(.emptyResponse)) - self.state = .finished - return - } - self.completion(.success(transcription)) - } - self.state = .finished - } - completion(.updateProccess(task)) - } -} diff --git a/Nynja/Services/TranscribeService/TranscribeService.swift b/Nynja/Services/TranscribeService/TranscribeService.swift index f5627b148..5e7fc53fa 100644 --- a/Nynja/Services/TranscribeService/TranscribeService.swift +++ b/Nynja/Services/TranscribeService/TranscribeService.swift @@ -8,16 +8,18 @@ import Foundation -enum TranscribeServiceResult { - case updateProccess(Cancelable) +enum TranscribeServiceState { + case updateProccess(Cancelable, DBConvertMessage.Process) case success(String) - case failure(TranscribeServiceError) + case failure(ServiceError) + case cancel + case unknown - enum TranscribeServiceError: LocalizedError { + enum ServiceError: LocalizedError { case upload(Error) case convertion(Error) case networkClient(Error) - case emptyResponse + case emptyResponse(String) var localizedDescription: String { switch self { @@ -42,23 +44,75 @@ enum TranscribeServiceResult { } } -protocol TranscribeServiceProtocol { - func transcribeAudio(with url: URL, language: String, completion: @escaping TranscribeProcessingHandler) +typealias TranscribeProcessingOuterHandler = (String, TranscribeServiceState) -> Void +typealias TranscribeProcessingInnerHandler = (TranscribeServiceState) -> Void + +final class TranscribeServiceDataWrapper { + var state: TranscribeServiceState? + var processingURL: URL? + var processingResult: String? } -typealias TranscribeProcessingHandler = (TranscribeServiceResult) -> Void +struct TranscribeServiceInput { + let message: Message + let localUrl: URL + let language: String +} -final class TranscribeServiceDataWrapper { - var result: TranscribeServiceResult? - var outputURL: URL? - var outputString: String? +protocol TranscribeServiceProtocol { + + var transcribeSubscribers: [AnyWeakSubscriber] { get set } + func observeState(_ subscriber: AnyObject, handler: @escaping TranscribeProcessingOuterHandler) + func removeObserver(_ object: AnyObject) - var processName: String? + func transcribe(_ input: TranscribeServiceInput) + func transcribe(_ convert: DBConvertMessage) + + func fetchTranscribeProgress(with id: String) -> ConvertionProgressModel? } final class TranscribeService: InitializeInjectable { - private let transcribeNetworkService: TranscribeNetworkService + static let shared: TranscribeService = { + + //Network + let config = TranscribeNetworkService.Config(apiKey: ThirdPartyServicesFactory.google.serviceConfig.apiKey) + + let client = URLSessionNetworkClient() + let newtworkService = TranscribeNetworkService(client: client) + + newtworkService.configure(config: config) + + //Processing + let processingManager = DefaultMessagesProcessingManager.shared + + //Storage + let storageService = StorageService.sharedInstance + + //MessageFactory + let payloadBuilder = MessagePayloadBuilder() + let factoryDependencies = MessageFactory.Dependencies( + storageService: storageService, + payloadBuilder: payloadBuilder + ) + let messageFactory = MessageFactory(dependencies: factoryDependencies) + + let dependencies = Dependencies( + transcribeNetworkService: newtworkService, + processingManager: processingManager, + messageFactory: messageFactory, + storageService: storageService) + + return TranscribeService(dependencies: dependencies) + }() + + private let transcribeNetworkService: TranscribeNetworkServiceProtocol + + private let processingManager: DefaultMessagesProcessingManager + private let messageFactory: MessageFactoryProtocol + private let storageService: StorageService + + var transcribeSubscribers: [AnyWeakSubscriber] = [] private let queue: OperationQueue = { let queue = OperationQueue() @@ -67,29 +121,214 @@ final class TranscribeService: InitializeInjectable { init(dependencies: TranscribeService.Dependencies) { transcribeNetworkService = dependencies.transcribeNetworkService + processingManager = dependencies.processingManager + messageFactory = dependencies.messageFactory + storageService = dependencies.storageService } struct Dependencies { - let transcribeNetworkService: TranscribeNetworkService + let transcribeNetworkService: TranscribeNetworkServiceProtocol + let processingManager: DefaultMessagesProcessingManager + let messageFactory: MessageFactoryProtocol + let storageService: StorageService } } extension TranscribeService: TranscribeServiceProtocol { - func transcribeAudio(with url: URL, language: String, completion: @escaping TranscribeProcessingHandler) { + func transcribe(_ input: TranscribeServiceInput) { + guard var processing = saveProcessing(input) else { + return + } + let dataWrapper = TranscribeServiceDataWrapper() + let completion: TranscribeProcessingInnerHandler = { [weak self] state in + guard let `self` = self else { + return + } + processing.process = self.updateProcessing(processing.process, data: dataWrapper, state: state) + self.handle(with: processing.id, state: state) + } - let convertion = AudioConvertionOperation(dataWrapper: dataWrapper, audioUrl: url) - let upload = AudioUploadOperation(dataWrapper: dataWrapper, completion: completion) + let convertion = AudioConvertOperation( + dependencies: .init(dataWrapper: dataWrapper, + audioUrl: input.localUrl, + completion: completion)) + let upload = AudioUploadOperation( + dependencies: .init(dataWrapper: dataWrapper, + completion: completion)) upload.addDependency(convertion) - let transcription = TranscribeLongAudioOperation(dataWrapper: dataWrapper, - networkService: transcribeNetworkService, - language: language, - completion: completion) + let transcription = AudioLongTranscribeOperation( + dependencies: .init(dataWrapper: dataWrapper, + networkService: transcribeNetworkService, + language: input.language, + completion: completion)) transcription.addDependency(upload) - let proccessing = TranscribeLongAudioProccessingOperation(dataWrapper: dataWrapper, - networkService: transcribeNetworkService, - completion: completion) + let proccessing = AudioLongTranscribeProccessingOperation( + dependencies: .init(dataWrapper: dataWrapper, + networkService: transcribeNetworkService, + language: input.language, + completion: completion)) proccessing.addDependency(transcription) - queue.addOperations([convertion, upload, transcription, proccessing], waitUntilFinished: false) + + let send = AudioTranscribeSendOperation( + dependencies: .init(dataWrapper: dataWrapper, + processingManager: processingManager, + messageFactory: messageFactory, + message: input.message, + language: input.language, + completion: completion)) + send.addDependency(proccessing) + + let operations = [convertion, upload, transcription, proccessing, send] + operations.forEach { $0.name = processing.id } + queue.addOperations(operations, waitUntilFinished: false) + } + + func transcribe(_ convert: DBConvertMessage) { + guard let message = MessageDAO.fetchMessage(localId: convert.messageId) else { + return + } + + var convert = convert + let dataWrapper = TranscribeServiceDataWrapper() + dataWrapper.processingResult = convert.value + + let completion: TranscribeProcessingInnerHandler = { [weak self] state in + guard let `self` = self else { + return + } + convert = self.updateProcessing(convert, data: dataWrapper, state: state) + self.handle(with: convert.messageId, state: state) + } + + guard let process = DBConvertMessage.Process(rawValue: convert.process) else { + return + } + var operations: [Operation] = [] + + let setupDependency: ([Operation], Operation) -> () = { dependencies, operation in + guard let dependency = dependencies.last else { + return + } + operation.addDependency(dependency) + } + + switch process { + case .convert: + guard let localUrl = URL(string: convert.value) else { + return + } + let convertion = AudioConvertOperation( + dependencies: .init(dataWrapper: dataWrapper, + audioUrl: localUrl, + completion: completion)) + setupDependency(operations, convertion) + operations.append(convertion) + fallthrough + case .upload: + let upload = AudioUploadOperation( + dependencies: .init(dataWrapper: dataWrapper, + completion: completion)) + setupDependency(operations, upload) + operations.append(upload) + fallthrough + case .transcribing: + let transcription = AudioLongTranscribeOperation( + dependencies: .init(dataWrapper: dataWrapper, + networkService: transcribeNetworkService, + language: convert.language, + completion: completion)) + setupDependency(operations, transcription) + operations.append(transcription) + fallthrough + case .transcribeProcessing: + let proccessing = AudioLongTranscribeProccessingOperation( + dependencies: .init(dataWrapper: dataWrapper, + networkService: transcribeNetworkService, + language: convert.language, + completion: completion)) + setupDependency(operations, proccessing) + operations.append(proccessing) + fallthrough + case .send: + let send = AudioTranscribeSendOperation( + dependencies: .init(dataWrapper: dataWrapper, + processingManager: processingManager, + messageFactory: messageFactory, + message: message, + language: convert.language, + completion: completion)) + setupDependency(operations, send) + operations.append(send) + } + + operations.forEach { $0.name = convert.messageId } + queue.addOperations(operations, waitUntilFinished: false) + } + + //MARK: - Observing + func observeState(_ subscriber: AnyObject, handler: @escaping TranscribeProcessingOuterHandler) { + let container = AnyWeakSubscriber(object: subscriber, handler: handler) + transcribeSubscribers.append(container) + } + + func removeObserver(_ object: AnyObject) { + transcribeSubscribers = transcribeSubscribers.filter { $0.object.value != nil && $0.object.value !== object } + } + + //MARK: - Fetch progress + func fetchTranscribeProgress(with id: MessageLocalId) -> ConvertionProgressModel? { + guard let convert = ConvertMessageDAO.fetchConvertMessage(by: id), let operation = queue.operations.first(where: { $0.name == convert.messageId }) else { + return nil + } + + return ConvertionProgressModel(id: id, type: .transcribe, operation: operation, status: .atProgress) + } +} + +private extension TranscribeService { + + private func saveProcessing(_ input: TranscribeServiceInput) -> (id: String, process: DBConvertMessage)? { + guard let id = input.message.msg_id else { + return nil + } + let process = DBConvertMessage(messageId: id, + type: .transcribe, + process: .convert, + value: input.localUrl.absoluteString, + language: input.language) + + try? storageService.perform(action: .save, with: process) + + return (id, process) + } + + private func updateProcessing(_ process: DBConvertMessage, data: TranscribeServiceDataWrapper, state: TranscribeServiceState) -> DBConvertMessage { + switch state { + case .updateProccess(_, let type): + process.process = type.rawValue + process.value = data.processingResult ?? "" + case .success: + break + case .failure(let error): + switch error { + case .emptyResponse: + try? storageService.perform(action: .delete, with: process) + return process + default: + break + } + case .unknown, .cancel: + try? storageService.perform(action: .delete, with: process) + return process + } + + try? storageService.perform(action: .save, with: process) + + return process + } + + private func handle(with id: String, state: TranscribeServiceState) { + transcribeSubscribers.forEach { $0.handler(id, state) } } } diff --git a/Nynja/Services/WheelContainer/Manager/WCDataManager.swift b/Nynja/Services/WheelContainer/Manager/WCDataManager.swift index d2929f22a..1983432df 100644 --- a/Nynja/Services/WheelContainer/Manager/WCDataManager.swift +++ b/Nynja/Services/WheelContainer/Manager/WCDataManager.swift @@ -123,7 +123,7 @@ class WCDataManager: WCDataManagerProtocol, UserSettingsRespondable { item.state == .selected }) } - + updateCallButtonState() wheelContainer?.reloadData() if let indexPath = _factory.initionalScrollStates, prevItems == nil { @@ -131,7 +131,39 @@ class WCDataManager: WCDataManagerProtocol, UserSettingsRespondable { } } - func hideContainer() { + private func updateCallButtonState() { + var _item: ImageActionItemModel? = nil + if self.wheelContainerDS?.items.count ?? 0 > 1 { + let secondWheelItems = self.wheelContainerDS!.items[1] + for item in secondWheelItems { + guard let i = item as? ImageActionItemModel, i.text == "call".localized else { continue } + _item = i + break + } + } + let needToDisable = NynjaCommunicatorService.sharedInstance.hasCallInProgress() + guard let currentState = _item?.state else { return } + switch currentState { + case .disabled: + _item?.state = needToDisable ? .disabled : .normal + case .normal: + _item?.state = needToDisable ? .disabled : .normal + case .selected: + if needToDisable { + _item?.state = .disabled + var secondWheelItems = self.wheelContainerDS!.items[1] + secondWheelItems.removeAll { (model) -> Bool in + (model as? ImageActionItemModel)?.text == "wheel_item_voiceCall".localized || + (model as? ImageActionItemModel)?.text == "wheel_item_videoCall".localized + } + self.wheelContainerDS?.items[1] = secondWheelItems + } + case .highlighted: + break + } + } + + func hideContainer(with indexPath: IndexPath?) { wheelContainer?.shouldRestoreState = true invalidateRestoreTimer() @@ -143,6 +175,25 @@ class WCDataManager: WCDataManagerProtocol, UserSettingsRespondable { } wheelContainer?.reloadData() + deselectItem(at: indexPath) + } + + private func deselectItem(at indexPath: IndexPath?) { + guard let indexPath = indexPath else { + return + } + + let lastLevel = indexPath.count - 1 + + guard let lastLevelItems = prevItems?[safe: lastLevel], + let selectedIndex = indexPath.last, + let item = lastLevelItems[safe: selectedIndex] else { + return + } + + item.state = .normal + + wheelContainer?.deselectItem(at: indexPath) } func updateActionsState(_ isDisabled: Bool) { diff --git a/Nynja/Services/WheelContainer/Manager/WCDataManagerProtocol.swift b/Nynja/Services/WheelContainer/Manager/WCDataManagerProtocol.swift index 42ce429eb..622b5c887 100644 --- a/Nynja/Services/WheelContainer/Manager/WCDataManagerProtocol.swift +++ b/Nynja/Services/WheelContainer/Manager/WCDataManagerProtocol.swift @@ -23,6 +23,6 @@ protocol WCDataManagerProtocol { // MARK: - Show/Hide container func showContainer() - func hideContainer() + func hideContainer(with indexPath: IndexPath?) } diff --git a/Nynja/SoundBundle.swift b/Nynja/SoundBundle.swift index 5e9a61f35..c889a65b6 100644 --- a/Nynja/SoundBundle.swift +++ b/Nynja/SoundBundle.swift @@ -16,7 +16,8 @@ struct SoundBundle: Codable { let defaultIncomingMessage: Sound let defaultOutcomingMessage: Sound let silence: Sound - + let defaultRingback: Sound + enum CodingKeys: String, CodingKey { case pushSounds = "push_nofitications" case defaultCall = "default_call" @@ -24,5 +25,6 @@ struct SoundBundle: Codable { case defaultIncomingMessage = "default_incoming_message" case defaultOutcomingMessage = "default_outcoming_message" case silence + case defaultRingback = "default_ringback" } } diff --git a/Nynja/SoundPlayer.swift b/Nynja/SoundPlayer.swift index c7bf5c439..49ad0654d 100644 --- a/Nynja/SoundPlayer.swift +++ b/Nynja/SoundPlayer.swift @@ -44,7 +44,7 @@ final class SoundPlayer: NSObject, AVAudioPlayerDelegate { } player?.play() } catch { - LogService.log(topic: .audioSystem, text: "Error while try to play audio: \(error.localizedDescription)") + LogService.log(topic: .audioSystem) { return "Error while try to play audio: \(error.localizedDescription)" } } } diff --git a/Nynja/StarActionDAO.swift b/Nynja/StarActionDAO.swift new file mode 100644 index 000000000..ca97857e4 --- /dev/null +++ b/Nynja/StarActionDAO.swift @@ -0,0 +1,40 @@ +// +// StarActionDAO.swift +// Nynja +// +// Created by Anton Poltoratskyi on 29.08.2018. +// Copyright © 2018 TecSynt Solutions. All rights reserved. +// + +import GRDBCipher + +final class StarActionDAO: StarActionDAOProtocol { + + // MARK: - Fetch + // MARK: -- Action + static func fetchStarAction(for starLocalId: String) -> DBStarAction? { + return dbManager.fetch { db in + return try DBStarAction.starAction(db, starLocalId: starLocalId) + } + } + + static func containsDeleteAction(for star: Star) -> Bool { + guard let starLocalId = star.client_id else { + return false + } + return dbManager.rowExists( + in: StarActionTable.self, + where: "\(StarActionTable.Column.starId) = ?", + arguments: [starLocalId] + ) + } + + + // MARK: -- Actions + + static func fetchStarActions() -> [DBStarAction] { + return dbManager.fetch { db in + return try DBStarAction.fetchAll(db) + } + } +} diff --git a/Nynja/StarActionDAOProtocol.swift b/Nynja/StarActionDAOProtocol.swift new file mode 100644 index 000000000..9fe95f80a --- /dev/null +++ b/Nynja/StarActionDAOProtocol.swift @@ -0,0 +1,20 @@ +// +// StarActionDAOProtocol.swift +// Nynja +// +// Created by Anton Poltoratskyi on 29.08.2018. +// Copyright © 2018 TecSynt Solutions. All rights reserved. +// + +import Foundation + +protocol StarActionDAOProtocol: DAOProtocol { + + // MARK: - Fetch + // MARK: -- Action + static func fetchStarAction(for starLocalId: String) -> DBStarAction? + static func containsDeleteAction(for star: Star) -> Bool + + // MARK: -- Actions + static func fetchStarActions() -> [DBStarAction] +} diff --git a/Nynja/StarDAO.swift b/Nynja/StarDAO.swift index 0f9f34c9b..239ee4757 100644 --- a/Nynja/StarDAO.swift +++ b/Nynja/StarDAO.swift @@ -15,6 +15,12 @@ class StarDAO: StarDAOProtocol { } } + static func fetchStarBy(clientId: String) -> DBStar? { + return dbManager.fetch { db in + return try DBStar.star(db, clientId: clientId) + } + } + static func fetchStars(rosterId: Int64) -> [String: Star] { let stars = dbManager.fetch { db in return try DBStar.stars(from: db, rosterId: rosterId) @@ -31,4 +37,9 @@ class StarDAO: StarDAOProtocol { return result } + static func fetchUndeliveredStars(rosterId: Int64) -> [Star] { + return dbManager + .fetch { try DBStar.undeliveredStars(from: $0, rosterId: rosterId) } + .map { Star(star: $0) } + } } diff --git a/Nynja/StarDAOProtocol.swift b/Nynja/StarDAOProtocol.swift index 6a546213c..be7a6205e 100644 --- a/Nynja/StarDAOProtocol.swift +++ b/Nynja/StarDAOProtocol.swift @@ -11,6 +11,9 @@ protocol StarDAOProtocol: DAOProtocol { // MARK: - Fetch static func fetchStarBy(rowId: Int64) -> DBStar? - static func fetchStars(rosterId: Int64) -> [String: Star] + static func fetchStarBy(clientId: String) -> DBStar? + static func fetchStars(rosterId: Int64) -> [String: Star] + + static func fetchUndeliveredStars(rosterId: Int64) throws -> [Star] } diff --git a/Nynja/StorageService+UserInfo.swift b/Nynja/StorageService+UserInfo.swift index b6540f241..b520bbda0 100644 --- a/Nynja/StorageService+UserInfo.swift +++ b/Nynja/StorageService+UserInfo.swift @@ -20,7 +20,9 @@ extension StorageService: UserInfo { var token: String? { get { - return tokenData.flatMap { String(data: $0, encoding: encoding) } + let token = tokenData.flatMap { String(data: $0, encoding: encoding) } + LogService.log(topic: .userDefaults) { return "Read token: \(token ?? "")" } + return token } set { guard let token = newValue, let data = token.data(using: encoding) else { @@ -30,7 +32,7 @@ extension StorageService: UserInfo { } set(data as NSData, forId: .token) - + LogService.log(topic: .userDefaults) { return "Save token: \(token)" } MQTTService.sharedInstance.reconnect() } } diff --git a/Nynja/SyncFileManager/SyncFileManager.swift b/Nynja/SyncFileManager/SyncFileManager.swift index e445f52c6..f5fa6afb0 100644 --- a/Nynja/SyncFileManager/SyncFileManager.swift +++ b/Nynja/SyncFileManager/SyncFileManager.swift @@ -102,16 +102,16 @@ class SyncFileManager { } func makeDownloadRequest(url: String, from destination: RemoteStorageDestination, completion: ((URL?, TransferInfo?, AWSRequest?) -> ())?) -> AWSRequest? { - return self.downloader.download(url: url, from: destination) { res, progress in - if let localUrl = res { + return self.downloader.download(url: url, from: destination) { localUrl, tranferInfo in + if let localUrl = localUrl { self.updateDB(url: url, localUrl: localUrl) { guard let fullPath = FileManagerService.sharedInstance.getFullPathWith(string: localUrl) else { return } completion?(URL(fileURLWithPath: fullPath), TransferInfo.finished, nil) } - } else if let progress = progress { - completion?(nil, progress, nil) + } else if let tranferInfo = tranferInfo { + completion?(nil, tranferInfo, nil) } else { completion?(nil, nil, nil) } @@ -184,7 +184,7 @@ class SyncFileManager { try DBSyncFile.deleteAll(db) }) } catch { - LogService.log(topic: .db, text: error.localizedDescription) + LogService.log(topic: .db) { return error.localizedDescription } } } } diff --git a/Nynja/ThumbnailGenerator.swift b/Nynja/ThumbnailGenerator.swift index c02ec1094..7676c5310 100644 --- a/Nynja/ThumbnailGenerator.swift +++ b/Nynja/ThumbnailGenerator.swift @@ -27,7 +27,7 @@ class ThumbnailGenerator { return try thumbnailForImage(at: url) } } catch let error { - LogService.log(topic: .fileSystem, text: "error: thumb for \(type.rawValue) \(error)") + LogService.log(topic: .fileSystem) { return "error: thumb for \(type.rawValue) \(error)" } return nil } } diff --git a/Nynja/TransferInfo.swift b/Nynja/TransferInfo.swift index a230133fb..f09370a0d 100644 --- a/Nynja/TransferInfo.swift +++ b/Nynja/TransferInfo.swift @@ -15,4 +15,8 @@ struct TransferInfo { static let finished: TransferInfo = TransferInfo(progress: 1, speed: 0, fileSize: 0) static let zero: TransferInfo = TransferInfo(progress: 0, speed: 0, fileSize: 0) + + var description: String { + return " progress: \(progress)\n speed:\(speed)\n fileSize:\(fileSize)\n" + } } diff --git a/Nynja/TransferManager.swift b/Nynja/TransferManager.swift index 5b229b6b5..7f0f77c9e 100644 --- a/Nynja/TransferManager.swift +++ b/Nynja/TransferManager.swift @@ -15,7 +15,7 @@ protocol TransferProgressListener : class { private class ProcessingDescription { var listener: TransferProgressListener? = nil - var transferInfo: TransferInfo? = TransferInfo(progress: 0, speed: 0, fileSize: 0) + var transferInfo: TransferInfo? var request: AWSRequest? var url: URL? } @@ -31,62 +31,10 @@ class TransferManager { // MARK: Services - func isURLProcessing(_ url: URL) -> Bool { - if let processing = getProcessing(for: url) { - return processing.transferInfo?.progress != 1 - } - return false - } - - func progressForUrl(_ url: URL) -> ProgressModel { - - if let description = getProcessing(for: url) { - guard let transferInfo = description.transferInfo else { - return ProgressModel(url: url, status: .notStarted) - } - if description.request != nil { - if transferInfo.progress == 1.0 { - return ProgressModel(url: url, status: .done, transferInfo: transferInfo) - } else { - return ProgressModel(url: url, status: .atProgress, transferInfo: transferInfo) - } - } else { - return ProgressModel(url: url, status: .notStarted, transferInfo: transferInfo) - } - } - - var pathInDB: String? = nil - SyncFileManager.sharedInstance.checkInDB(url: url.absoluteString, - result: { (id, result) in - pathInDB = result - }) - - if pathInDB != nil { - if let path = FileManagerService.sharedInstance.getFullPathWith(string: pathInDB!) { - return ProgressModel(url: url, status: .done, result: URL(fileURLWithPath: path), transferInfo: getProcessing(for: url)?.transferInfo ?? .finished) - } else { - return ProgressModel(url: url, status: .done, result: nil, transferInfo: getProcessing(for: url)?.transferInfo ?? .finished) - } - } - - return ProgressModel(url: url, status: .notStarted, transferInfo: getProcessing(for: url)?.transferInfo ?? .zero) - } - - func subscribe(listener:TransferProgressListener, toURLs urls:[URL]) { - for url in urls { - subscribe(listener: listener, toURL: url) - } - } - - func subscribe(listener: TransferProgressListener, toURL url: URL) { - getProcessing(for: url)?.listener = listener - } - func upload(url: URL, listener: TransferProgressListener?, completion: ((ResponseResult) -> Void)?) { SyncFileManager.sharedInstance.saveExternalFileLink(localUrl: url.absoluteString) { (serverURL, transferInfo, request) in if request == nil && transferInfo == nil { - // Error occured - self.removeProcessing(url: url) + self.cancelledDownloading(url: url, shouldNotify: false) completion?(.error(nil)) } @@ -116,19 +64,23 @@ class TransferManager { func download(url: URL, from destination: RemoteStorageDestination = .default, listener: TransferProgressListener?, - completion: ((ResponseResult) -> Void)?) { + completion: ((ResponseResult) -> Void)? = nil) { + guard getProcessing(for: url) == nil else { + return + } + var isSetRequest = false SyncFileManager.sharedInstance.getFileLink(url: url.absoluteString, from: destination) { - locUrl, transferInfo, request in - if let req = request { + localUrl, transferInfo, request in + if let request = request { isSetRequest = true - self.addProcessing(url: url, request: req) + self.addProcessing(url: url, request: request) if listener != nil { TransferManager.shared.subscribe(listener: listener!, toURL: url) } } - if let localUrl = locUrl { + if let localUrl = localUrl { if isSetRequest == false && listener != nil { self.addProcessing(url: url, request: nil) TransferManager.shared.subscribe(listener: listener!, toURL: url) @@ -139,48 +91,82 @@ class TransferManager { self.notifyAboutProgress(progress: progress) self.removeProcessing(url: url) } - if let info = transferInfo { - self.updateProgress(url: url, transferInfo: info) + if let transferInfo = transferInfo { + self.updateProgress(url: url, transferInfo: transferInfo) } - if transferInfo == nil && locUrl == nil && request == nil { + if transferInfo == nil && request == nil { self.cancelledDownloading(url: url) completion?(.error(nil)) } } } - func cancelledDownloading(url: URL) { - guard let processing = getProcessing(for: url) else { - return - } - processing.transferInfo = nil - let progress = ProgressModel(url: url, status: .notStarted, transferInfo: nil) - notifyAboutProgress(progress: progress) + func cancelRequestForUrl(_ url: URL) { + let processing = getProcessing(for: url) + _ = processing?.request?.cancel() } - func updateProgress(url: URL, transferInfo: TransferInfo?) { - guard let processing = getProcessing(for: url) else { - return + //MARK: - Subscribes + func subscribe(listener: TransferProgressListener, toURLs urls:[URL]) { + urls.forEach { + subscribe(listener: listener, toURL: $0) } - processing.transferInfo = transferInfo - let progress = ProgressModel(url: url, status: .atProgress, transferInfo: transferInfo) - notifyAboutProgress(progress: progress) } - func cancelRequestForUrl(_ url: URL) { - let processing = getProcessing(for: url) - _ = processing?.request?.cancel() - removeProcessing(url: url) + func subscribe(listener: TransferProgressListener, toURL url: URL) { + getProcessing(for: url)?.listener = listener } - func pauseRequestForUrl(_ url: URL) { - let processing = getProcessing(for: url) - _ = processing?.request?.cancel() - processing?.request = nil + func notifyAboutProgress(progress: ProgressModel) { + getProcessing(for: progress.url)?.listener?.handleProgress(progress: progress) + } +} + +extension TransferManager { + func isURLProcessing(_ url: URL) -> Bool { + guard let info = getProcessing(for: url)?.transferInfo else { + return false + } + return info.progress != 1 } - // MARK: Utils - + func progressForUrl(_ url: URL) -> ProgressModel { + let statusIfNotStarted: ProgressModel.ProgressStatus = url.isLocalURL ? .offline : .notStarted + + if let description = getProcessing(for: url) { + guard let transferInfo = description.transferInfo else { + return ProgressModel(url: url, status: statusIfNotStarted) + } + if description.request != nil { + if transferInfo.progress == 1.0 { + return ProgressModel(url: url, status: .done, transferInfo: transferInfo) + } else { + return ProgressModel(url: url, status: .atProgress, transferInfo: transferInfo) + } + } else { + return ProgressModel(url: url, status: statusIfNotStarted, transferInfo: transferInfo) + } + } + + var pathInDB: String? = nil + SyncFileManager.sharedInstance.checkInDB(url: url.absoluteString, + result: { (id, result) in + pathInDB = result + }) + + if pathInDB != nil { + if let path = FileManagerService.sharedInstance.getFullPathWith(string: pathInDB!) { + return ProgressModel(url: url, status: .done, result: URL(fileURLWithPath: path), transferInfo: getProcessing(for: url)?.transferInfo ?? .finished) + } else { + return ProgressModel(url: url, status: .done, result: nil, transferInfo: getProcessing(for: url)?.transferInfo ?? .finished) + } + } + + return ProgressModel(url: url, status: statusIfNotStarted, transferInfo: .zero) + } +} + +private extension TransferManager { func addProcessing(url: URL, request: AWSRequest?) { let processing = ProcessingDescription() processing.url = url @@ -189,7 +175,7 @@ class TransferManager { setProcessing(processing, forURL: url) } - private func getProcessing(for url: URL) -> ProcessingDescription? { + func getProcessing(for url: URL) -> ProcessingDescription? { var result: ProcessingDescription? isolationQueue.sync { result = processingForURL[url] @@ -197,17 +183,39 @@ class TransferManager { return result } - private func setProcessing(_ processing: ProcessingDescription?, forURL url: URL) { + func setProcessing(_ processing: ProcessingDescription?, forURL url: URL) { isolationQueue.async(flags: .barrier) { self.processingForURL[url] = processing } } func removeProcessing(url: URL) { - setProcessing(nil, forURL: url) + isolationQueue.async(flags: .barrier) { + self.processingForURL.removeValue(forKey: url) + } + } +} + +private extension TransferManager { + func cancelledDownloading(url: URL, shouldNotify: Bool = true, with status: ProgressModel.ProgressStatus = .notStarted) { + guard let processing = getProcessing(for: url) else { + return + } + + processing.transferInfo = nil + if shouldNotify { + let progress = ProgressModel(url: url, status: status, transferInfo: nil) + notifyAboutProgress(progress: progress) + } + removeProcessing(url: url) } - func notifyAboutProgress(progress: ProgressModel) { - getProcessing(for: progress.url)?.listener?.handleProgress(progress: progress) + func updateProgress(url: URL, transferInfo: TransferInfo?) { + guard let processing = getProcessing(for: url) else { + return + } + processing.transferInfo = transferInfo + let progress = ProgressModel(url: url, status: .atProgress, transferInfo: transferInfo) + notifyAboutProgress(progress: progress) } } diff --git a/Nynja/UserInfo.swift b/Nynja/UserInfo.swift index f8e26ab47..abddc6a46 100644 --- a/Nynja/UserInfo.swift +++ b/Nynja/UserInfo.swift @@ -57,6 +57,7 @@ extension UserInfo { } func dropUserInfo() { + LogService.log(topic: .userDefaults) { return "drop user info" } token = nil phone = nil voxId = nil diff --git a/Nynja/WCBaseItemsFactory.swift b/Nynja/WCBaseItemsFactory.swift index 010e6c859..58b219d27 100644 --- a/Nynja/WCBaseItemsFactory.swift +++ b/Nynja/WCBaseItemsFactory.swift @@ -21,7 +21,7 @@ class WCBaseItemsFactory: WCItemsFactory { } var firstLevelItems: ItemModels { - return [calls, options, home, search, mySelf, actions, chats, groups, contacts, channels] + return [marketplace, calls, options, home, search, mySelf, actions, chats, groups, contacts, channels] } var secondLevelItems: ItemModels { @@ -86,6 +86,12 @@ class WCBaseItemsFactory: WCItemsFactory { }) } + var marketplace: ImageActionItemModel { + return ImageActionItemModel(nameImage: "ic_marketplace_wheel_context_menu", navItem: .marketplace) { [weak navigateDelegate] (item, indexPath) in + navigateDelegate?.showMarketplace(indexPath: indexPath) + } + } + var channels: ImageActionItemModel { return ImageActionItemModel(nameImage: "ic_channel_inactive", navItem: .channels) { [weak navigateDelegate] (item, indexPath) in navigateDelegate?.showChannels(indexPath: indexPath) diff --git a/Nynja/WrappedTaskOperation.swift b/Nynja/WrappedTaskOperation.swift new file mode 100644 index 000000000..b513e69ee --- /dev/null +++ b/Nynja/WrappedTaskOperation.swift @@ -0,0 +1,28 @@ +// +// WrappedTaskOperation.swift +// Nynja +// +// Created by Andrey Reznik on 26.08.2018. +// Copyright © 2018 TecSynt Solutions. All rights reserved. +// + +import Foundation + +class WrappedTaskOperation: AsyncOperation { + + internal var task: Cancelable? + + override init() { + super.init() + } + + init(task: Cancelable?) { + self.task = task + super.init() + } + + override func cancel() { + task?.cancel() + super.cancel() + } +} diff --git a/NynjaUnitTests/Services/Network/Models/HistoryRequestModelTests.swift b/NynjaUnitTests/Services/Network/Models/HistoryRequestModelTests.swift index acf0a7ea1..40326f18d 100644 --- a/NynjaUnitTests/Services/Network/Models/HistoryRequestModelTests.swift +++ b/NynjaUnitTests/Services/Network/Models/HistoryRequestModelTests.swift @@ -39,13 +39,13 @@ class HistoryRequestModelTests: XCTestCase { let feedIdComponents = model.makeFeedIdComponents() let messageId = model.messageId let pageSize = model.pageSize - let actionName = model.actionName + let status = model.status XCTAssertEqual(prefix, "p2p") XCTAssertEqual(feedIdComponents as? [String], ["111", "222"]) XCTAssertEqual(messageId, 0) XCTAssertEqual(pageSize, nil) - XCTAssertEqual(actionName, "get") + XCTAssertEqual(status, "get") } func testAllHitoryOfContactMyIdHighterOpponent() { @@ -58,12 +58,12 @@ class HistoryRequestModelTests: XCTestCase { let feedIdComponents = model.makeFeedIdComponents() let messageId = model.messageId let pageSize = model.pageSize - let actionName = model.actionName + let status = model.status XCTAssertEqual(feedIdComponents as? [String], ["222", "555"]) XCTAssertEqual(messageId, 0) XCTAssertEqual(pageSize, nil) - XCTAssertEqual(actionName, "get") + XCTAssertEqual(status, "get") } func testAllHitoryOfMuc() { @@ -80,7 +80,7 @@ class HistoryRequestModelTests: XCTestCase { let feedIdComponents = model.makeFeedIdComponents() let messageId = model.messageId let pageSize = model.pageSize - let actionName = model.actionName + let status = model.status XCTAssertEqual(prefix, "muc") zip(feedIdComponents, ["333"]).forEach { @@ -92,7 +92,7 @@ class HistoryRequestModelTests: XCTestCase { } XCTAssertEqual(messageId, 0) XCTAssertEqual(pageSize, nil) - XCTAssertEqual(actionName, "get") + XCTAssertEqual(status, "get") } func testAllHitoryOfJobs() { @@ -108,13 +108,13 @@ class HistoryRequestModelTests: XCTestCase { let feedIdComponents = model.makeFeedIdComponents() let messageId = model.messageId let pageSize = model.pageSize - let actionName = model.actionName + let status = model.status XCTAssertEqual(prefix, "act") XCTAssertEqual(feedIdComponents as? [String], ["publish", "111"]) XCTAssertEqual(messageId, 0) XCTAssertEqual(pageSize, nil) - XCTAssertEqual(actionName, "get") + XCTAssertEqual(status, "get") } func testPaginationHitoryOfContact() { @@ -124,18 +124,18 @@ class HistoryRequestModelTests: XCTestCase { let input = HistoryRequestModel.RequestInput(rosterId: rosterId, historyType: .p2p(opponentId: opponentId), - actionType: .get(lastMessageId: 123, pageSize: 30)) + actionType: .getPage(from: 123, pageSize: 30)) let model = HistoryRequestModel(requestInput: input) let feedIdComponents = model.makeFeedIdComponents() let messageId = model.messageId let pageSize = model.pageSize - let actionName = model.actionName + let status = model.status XCTAssertEqual(feedIdComponents as? [String], ["111", "222"]) XCTAssertEqual(messageId, 123) XCTAssertEqual(pageSize, 30) - XCTAssertEqual(actionName, "get") + XCTAssertEqual(status, "get") } func testClearHitoryOfContact() { @@ -151,33 +151,54 @@ class HistoryRequestModelTests: XCTestCase { let feedIdComponents = model.makeFeedIdComponents() let messageId = model.messageId let pageSize = model.pageSize - let actionName = model.actionName + let status = model.status XCTAssertEqual(feedIdComponents as? [String], ["111", "222"]) XCTAssertEqual(messageId, nil) XCTAssertEqual(pageSize, nil) - XCTAssertEqual(actionName, "delete") + XCTAssertEqual(status, "delete") } - func testMessageUpdate() { + func testMessageRead() { let rosterId: String = "111" let opponentId: String = "222" let input = HistoryRequestModel.RequestInput(rosterId: rosterId, historyType: .p2p(opponentId: opponentId), - actionType: .update(messageId: 123)) + actionType: .read(messageId: 123)) let model = HistoryRequestModel(requestInput: input) let feedIdComponents = model.makeFeedIdComponents() let messageId = model.messageId let pageSize = model.pageSize - let actionName = model.actionName + let status = model.status XCTAssertEqual(feedIdComponents as? [String], ["111", "222"]) XCTAssertEqual(messageId, 123) XCTAssertEqual(pageSize, nil) - XCTAssertEqual(actionName, "update") + XCTAssertEqual(status, "update") + } + + func testGetMessageUpdates() { + + let rosterId: String = "111" + let opponentId: String = "222" + + let input = HistoryRequestModel.RequestInput(rosterId: rosterId, + historyType: .p2p(opponentId: opponentId), + actionType: .getUpdates(from: 123)) + let model = HistoryRequestModel(requestInput: input) + + let feedIdComponents = model.makeFeedIdComponents() + let messageId = model.messageId + let pageSize = model.pageSize + let status = model.status + + XCTAssertEqual(feedIdComponents as? [String], ["111", "222"]) + XCTAssertEqual(messageId, 123) + XCTAssertEqual(pageSize, nil) + XCTAssertEqual(status, "get") } func testGetDefaultStickerPack() { @@ -193,12 +214,12 @@ class HistoryRequestModelTests: XCTestCase { let feedIdComponents = model.makeFeedIdComponents() let messageId = model.messageId let pageSize = model.pageSize - let actionName = model.actionName + let status = model.status XCTAssertEqual(prefix, "StickerPack") XCTAssertEqual(feedIdComponents as? [Int64?], [nil, nil, nil, nil, nil, nil, nil, nil, Int64(0)]) XCTAssertEqual(messageId, 0) XCTAssertEqual(pageSize, 1) - XCTAssertEqual(actionName, "get") + XCTAssertEqual(status, "get") } } diff --git a/Podfile b/Podfile index fffbdb131..2174edd34 100644 --- a/Podfile +++ b/Podfile @@ -39,9 +39,11 @@ def commonPodsForNynja pod 'MaterialComponents/FlexibleHeader' pod 'JTAppleCalendar', '~> 7.0' - pod 'NynjaSDK', '~> 1.4.1' + pod 'NynjaSDK', '= 1.5.1' pod 'CryptoSwift' + + pod 'MulticastDelegateSwift', '~> 2.1.1' end def commonPodsForNynjaTests diff --git a/Shared/Library/Extensions/Models/Contact/Contact+BaseChatModel.swift b/Shared/Library/Extensions/Models/Contact/Contact+BaseChatModel.swift index 64a33866e..9646e7079 100644 --- a/Shared/Library/Extensions/Models/Contact/Contact+BaseChatModel.swift +++ b/Shared/Library/Extensions/Models/Contact/Contact+BaseChatModel.swift @@ -24,7 +24,7 @@ extension Contact: BaseChatModel { } var name: String? { - return fullName + return nick ?? fullName } var avatarUrl: URL? { diff --git a/Shared/Library/Extensions/Models/Contact/ContactExtension.swift b/Shared/Library/Extensions/Models/Contact/ContactExtension.swift index 19c91664f..7e71bbc1a 100644 --- a/Shared/Library/Extensions/Models/Contact/ContactExtension.swift +++ b/Shared/Library/Extensions/Models/Contact/ContactExtension.swift @@ -227,6 +227,10 @@ extension Contact { } return String(phoneId[phoneId.startIndex.. 0, let phoneId = phoneId { + self.feed_id = p2p(firstId: phoneId, secondId: contactPhoneId) + self.to = contactPhoneId + } else if let room = room, let roomId = room.id { + self.feed_id = muc(name: roomId) + self.to = roomId + } + + self.from = phoneId + } + + var forwardMessage: Message { + let message = Message(message: self) + + let link = message.id + + message.id = nil + + message.msg_id = IdBuilder(format: .defaultId).build() + + message.created = nil + + message.types = ["forward"] + + message.link = link + message.status = nil + message.files = message.files?.filter({ + $0.mime != "translate" + }) + + return message + } + + func cloneForward(with feed: AnyObject, from: String?, to: String?) -> Message { + let msg = Message(message: self) + + msg.feed_id = feed + msg.from = from + msg.to = to + + let builder = IdBuilder(format: .defaultId) + + msg.msg_id = builder.build() + + msg.files?.forEach { $0.id = builder.build() } + + return msg + } + + func cloned() -> Message { + let msg = Message(message: self) + + msg.created = Date.currentTimestamp as AnyObject + + let builder = IdBuilder(format: .defaultId) + + msg.msg_id = builder.build() + msg.files?.forEach { $0.id = builder.build() } + + return msg + } +} + +extension Message { + + var isInOwnChat: Bool { + guard let p2p = p2pFeed, + let phoneId = StorageService.sharedInstance.phoneId else { + return false + } + + return p2p.from == phoneId && p2p.to == phoneId + } + + var isOwn: Bool { + guard let phoneId = StorageService.sharedInstance.phoneId else { + return false + } + + return from == phoneId + } + + var canBeTranslated: Bool { + return mainFile?.type == .text && + !isOwn && + !isSystem + } + + func isFrom(chatId: String) -> Bool { + switch feed_id { + case let p2p as p2p: + return p2p.opponentId == chatId + case let muc as muc: + return muc.name == chatId + default: + return false + } + } +} diff --git a/Shared/Library/Extensions/Models/Message/Message+Files.swift b/Shared/Library/Extensions/Models/Message/Message+Files.swift new file mode 100644 index 000000000..3ea70a95d --- /dev/null +++ b/Shared/Library/Extensions/Models/Message/Message+Files.swift @@ -0,0 +1,68 @@ +// +// Message+Files.swift +// Nynja +// +// Created by Anton Poltoratskyi on 22.08.2018. +// Copyright © 2018 TecSynt Solutions. All rights reserved. +// + +import Foundation + +extension Message { + + var thumbUrl: URL? { + if let url = thumb?.payload { + if id == nil { + return URL(fileURLWithPath: url) + } else { + return URL(string: url) + } + } + + return nil + } + + var thumb: Desc? { + return files?.first { + $0.mime == SendMessageType.thumbnails.rawValue + } + } + + var mainFile: Desc? { + guard let _files = files else { + return nil + } + + if _files.count == 1 { + return _files.first + } + + let types = [SendMessageType.thumbnails.rawValue, + SendMessageType.translate.rawValue, + SendMessageType.transcribe.rawValue, + SendMessageType.autotranslate.rawValue] + + return _files.first { + !types.contains($0.mime ?? "") + } + } + + var mainUrl: URL? { + if let urlString = mainFile?.payload, + let url = URL(string: urlString) { + return url + } + + return nil + } + + var isMainUrlLocal: Bool { + guard let url = mainUrl else { return false } + return url.isFileURL || !url.absoluteString.starts(with: "http") + } + + var sendType : SendMessageType? { + guard let sT = mainFile?.mime else { return nil } + return SendMessageType(rawValue: sT) + } +} diff --git a/Shared/Library/Extensions/Models/Message/MessageExtension.swift b/Shared/Library/Extensions/Models/Message/MessageExtension.swift index ddc0e02fd..10dfdcae5 100644 --- a/Shared/Library/Extensions/Models/Message/MessageExtension.swift +++ b/Shared/Library/Extensions/Models/Message/MessageExtension.swift @@ -8,8 +8,6 @@ import UIKit -typealias MessageServerId = Int64 - extension Message: Hashable { var hashValue: Int { @@ -55,22 +53,8 @@ extension Message { self.feedName = message.feedName self.senderName = message.senderName self.senderAvatar = message.senderAvatar - } - - convenience init(phoneId: String?, contact: Contact?, room: Room?) { - self.init() - - self.msg_id = IdBuilder(format: .defaultId).build() - if let contactPhoneId = contact?.phoneId, contactPhoneId.count > 0, let phoneId = phoneId { - self.feed_id = p2p(firstId: phoneId, secondId: contactPhoneId) - self.to = contactPhoneId - } else if let room = room, let roomId = room.id { - self.feed_id = muc(name: roomId) - self.to = roomId - } - - self.from = phoneId + self.isTrusted = message.isTrusted } var isDelivered: Bool { @@ -85,6 +69,10 @@ extension Message { return mentioned?.contains { Int64($0) == memberId } ?? false } + var createdInt: Int64 { + return (created as? Int64) ?? 0 + } + var createdDate: Date? { if let timestamp = created as? Int64 { let formattedTimestamp = Double(timestamp) / 1000 @@ -111,116 +99,11 @@ extension Message { type = newValue.compactMap { StringAtom(string: $0) } } } - - var forwardMessage: Message { - let message = Message(message: self) - - let link = message.id - - message.id = nil - - message.msg_id = IdBuilder(format: .defaultId).build() - - message.created = nil - - message.types = ["forward"] - - message.link = link - message.status = nil - message.files = message.files?.filter({ - $0.mime != "translate" - }) - - return message - } - - func cloneForward(with feed: AnyObject, from: String?, to: String?) -> Message { - let msg = Message(message: self) - - msg.feed_id = feed - msg.from = from - msg.to = to - - let builder = IdBuilder(format: .defaultId) - - msg.msg_id = builder.build() - - msg.files?.forEach { $0.id = builder.build() } - - return msg - } - - func cloned() -> Message { - let msg = Message(message: self) - - msg.created = Date.currentTimestamp as AnyObject - - let builder = IdBuilder(format: .defaultId) - - msg.msg_id = builder.build() - msg.files?.forEach { $0.id = builder.build() } - - return msg - } } -// MARK: Thumbnails +// MARK: - Feed + extension Message { - var thumbUrl: URL? { - if let url = thumb?.payload { - if id == nil { - return URL(fileURLWithPath: url) - } else { - return URL(string: url) - } - } - - return nil - } - - var thumb: Desc? { - return files?.first { - $0.mime == SendMessageType.thumbnails.rawValue - } - } - - var mainFile: Desc? { - guard let _files = files else { - return nil - } - - if _files.count == 1 { - return _files.first - } - - let types = [SendMessageType.thumbnails.rawValue, - SendMessageType.translate.rawValue, - SendMessageType.transcribe.rawValue, - SendMessageType.autotranslate.rawValue] - - return _files.first { - !types.contains($0.mime ?? "") - } - } - - var mainUrl: URL? { - if let urlString = mainFile?.payload, - let url = URL(string: urlString) { - return url - } - - return nil - } - - var isMainUrlLocal: Bool { - guard let url = mainUrl else { return false } - return url.isFileURL || !url.absoluteString.starts(with: "http") - } - - var sendType : SendMessageType? { - guard let sT = mainFile?.mime else { return nil } - return SendMessageType(rawValue: sT) - } var p2pFeed: p2p? { return feed_id as? p2p @@ -230,37 +113,19 @@ extension Message { return feed_id as? muc } - func isFrom(chatId: String) -> Bool { - switch feed_id { - case let p2p as p2p: - return p2p.opponentId == chatId - case let muc as muc: - return muc.name == chatId - default: - return false + var chatId: String? { + if let p2p = p2pFeed { + return p2p.opponentId + } else if let muc = mucFeed { + return muc.name } - } -} - -extension Message { - - var isInOwnChat: Bool { - let ownerId = StorageService.sharedInstance.phoneId - return from == ownerId && to == ownerId - } - - var isOwn: Bool { - return self.from == StorageService.sharedInstance.phoneId + + return nil } - var canBeTranslated: Bool { - return mainFile?.type == .text && - !isOwn && - !isSystem - } } -// MARK: - Message Types +// MARK: - Types extension Message { @@ -297,11 +162,15 @@ extension Message { } } -// MARK: - Message Statuses +// MARK: - Statuses extension Message { var isStatusClear: Bool { return statusString == "clear" } + + var isStatusDelete: Bool { + return statusString == "delete" + } } diff --git a/Shared/Library/Extensions/Models/Message/MessageIdentifiers.swift b/Shared/Library/Extensions/Models/Message/MessageIdentifiers.swift new file mode 100644 index 000000000..efd1b80ec --- /dev/null +++ b/Shared/Library/Extensions/Models/Message/MessageIdentifiers.swift @@ -0,0 +1,10 @@ +// +// MessageIdentifiers.swift +// Nynja +// +// Created by Anton Poltoratskyi on 22.08.2018. +// Copyright © 2018 TecSynt Solutions. All rights reserved. +// + +typealias MessageServerId = Int64 +typealias MessageLocalId = String diff --git a/Shared/Library/Models/BaseChatModel.swift b/Shared/Library/Models/BaseChatModel.swift index 8a55be03d..4e3a09331 100644 --- a/Shared/Library/Models/BaseChatModel.swift +++ b/Shared/Library/Models/BaseChatModel.swift @@ -14,6 +14,7 @@ protocol BaseChatModel: class { var selfReader: Int64? { get } var otherReader: Int64? { get } var last_msg: Message? { get set } + var lastMessageId: Int64? { get set } var name: String? { get } var avatarUrl: URL? { get } diff --git a/Shared/Services/Handlers/IoHandler.swift b/Shared/Services/Handlers/IoHandler.swift index caf94cdf6..bb2397a01 100644 --- a/Shared/Services/Handlers/IoHandler.swift +++ b/Shared/Services/Handlers/IoHandler.swift @@ -18,7 +18,6 @@ protocol IoHandlerDelegate: class { func sessionNotFound() func attempts_expired() func notAuthorized() - func profileNotFound() func added() func invalidData() func callInProgress() @@ -49,7 +48,6 @@ extension IoHandlerDelegate { func sessionNotFound() {} func attempts_expired() {} func notAuthorized() {} - func profileNotFound() {} func added() {} func invalidData() {} func callInProgress() {} @@ -138,6 +136,7 @@ class IoHandler:BaseHandler { case "number_not_allowed": self.delegate?.numberNotAllowed() case "logout": + LogService.log(topic: .db) { return "Clear storage: IoHandler" } storageService.clearStorage() mqttService.state = .notAuthenticated(isLoggedOutFromServer: true) -- GitLab From bdf7843abf72e0911a4bd13ee966577563639ade Mon Sep 17 00:00:00 2001 From: Angel Terziev Date: Thu, 13 Sep 2018 15:58:57 +0300 Subject: [PATCH 6/8] NY-3538: IOS: App crash randomly on end call NY-3539: iOS: App fails to accept after lock app and wait a while. --- Nynja-Share/Resources/Info.plist | 2 +- .../Splash/Interactor/SplashInteractor.swift | 2 ++ Nynja/Resources/Info.plist | 2 +- .../Services/HandleServices/ProfileHandler.swift | 6 ++++-- Nynja/Services/NynjaCommunicatorService.swift | 15 +++++++++++++-- Podfile | 2 +- 6 files changed, 22 insertions(+), 7 deletions(-) diff --git a/Nynja-Share/Resources/Info.plist b/Nynja-Share/Resources/Info.plist index 241f06e9a..4691cb49f 100644 --- a/Nynja-Share/Resources/Info.plist +++ b/Nynja-Share/Resources/Info.plist @@ -21,7 +21,7 @@ CFBundleShortVersionString 1.0 CFBundleVersion - 0.2.153 + 0.2.153.1 Config $(Config) ModelsVersion diff --git a/Nynja/Modules/Splash/Interactor/SplashInteractor.swift b/Nynja/Modules/Splash/Interactor/SplashInteractor.swift index e371c41b1..068502ef3 100644 --- a/Nynja/Modules/Splash/Interactor/SplashInteractor.swift +++ b/Nynja/Modules/Splash/Interactor/SplashInteractor.swift @@ -13,6 +13,7 @@ class SplashInteractor: SplashInteractorInputProtocol { private let storageService = StorageService.sharedInstance private let mqttService = MQTTService.sharedInstance private let badgeService = BadgeNumberService.shared + private let callService = NynjaCommunicatorService.sharedInstance func showed() { checkJailbreak { [weak self] in @@ -45,6 +46,7 @@ class SplashInteractor: SplashInteractorInputProtocol { } mqttService.initialize() + callService.initialize() connectionSubscriberService.subscribe() MediaDownloadManager.setupAppDataUsageSettingsIfNeeded() diff --git a/Nynja/Resources/Info.plist b/Nynja/Resources/Info.plist index 2de84006f..c39fac7d2 100644 --- a/Nynja/Resources/Info.plist +++ b/Nynja/Resources/Info.plist @@ -21,7 +21,7 @@ CFBundleShortVersionString 1.0 CFBundleVersion - 0.2.153 + 0.2.153.1 ConfServerAddress $(ConfServerAddress) ConfServerPort diff --git a/Nynja/Services/HandleServices/ProfileHandler.swift b/Nynja/Services/HandleServices/ProfileHandler.swift index a5da4b8ee..8528b241a 100644 --- a/Nynja/Services/HandleServices/ProfileHandler.swift +++ b/Nynja/Services/HandleServices/ProfileHandler.swift @@ -129,9 +129,11 @@ class ProfileHandler: BaseHandler { } private static func configureNynjaCommunicatorService(_ profile: Profile) { - if let rosterId = (profile.rosters?.first as? Roster)?.myContact?.phone_id { - NynjaCommunicatorService.sharedInstance.login(userName: rosterId, password: "") + guard let rosterId = (profile.rosters?.first as? Roster)?.myContact?.phone_id else { + return } + + NynjaCommunicatorService.sharedInstance.initialize() } private static func requestJobs(with phoneId: String) { diff --git a/Nynja/Services/NynjaCommunicatorService.swift b/Nynja/Services/NynjaCommunicatorService.swift index cbda6c055..5b9207ec0 100644 --- a/Nynja/Services/NynjaCommunicatorService.swift +++ b/Nynja/Services/NynjaCommunicatorService.swift @@ -53,7 +53,8 @@ extension NynjaCallDelegate { class NynjaCommunicatorService: NSObject, NynjaCommunicatorDelegate, NYNCallDelegate, NYNCallManagerDelegate { - + private var initialized: Bool = false + private let storageService = StorageService.sharedInstance let nynComm: NynjaCommunicator var isCallInProgress = false let delegates = MulticastDelegate() @@ -112,12 +113,22 @@ class NynjaCommunicatorService: NSObject, NynjaCommunicatorDelegate, NYNCallDele static let sharedInstance: NynjaCommunicatorService = NynjaCommunicatorService() - func login(userName: String, password: String) { + private func login(userName: String, password: String) { self.username = userName self.nynComm.login(withName: userName, andSecret: password) } + func initialize() { + guard false == initialized, storageService.isUserLogined, let phoneId = storageService.phoneId else { + return + } + + self.login(userName: phoneId, password: "") + initialized = true + LogService.log(topic: .callSystem) {return "initialized"} + } + func dialInGroup(groupname: String) { // NOT IMPLEMENTED } diff --git a/Podfile b/Podfile index 2174edd34..7591f30d6 100644 --- a/Podfile +++ b/Podfile @@ -39,7 +39,7 @@ def commonPodsForNynja pod 'MaterialComponents/FlexibleHeader' pod 'JTAppleCalendar', '~> 7.0' - pod 'NynjaSDK', '= 1.5.1' + pod 'NynjaSDK', '= 1.5.5' pod 'CryptoSwift' -- GitLab From e23ff0bbf6d61a69262469244a2046ecc36fcf66 Mon Sep 17 00:00:00 2001 From: Anton Makarov Date: Thu, 13 Sep 2018 17:08:23 +0300 Subject: [PATCH 7/8] Removed fix for iOS 12 --- Nynja-Share/Resources/Info.plist | 2 +- Nynja.xcodeproj/project.pbxproj | 4 ++-- .../ImagePreview/View/ImagePreviewViewController.swift | 3 --- Nynja/Resources/Info.plist | 2 +- 4 files changed, 4 insertions(+), 7 deletions(-) diff --git a/Nynja-Share/Resources/Info.plist b/Nynja-Share/Resources/Info.plist index 4691cb49f..6a68225bd 100644 --- a/Nynja-Share/Resources/Info.plist +++ b/Nynja-Share/Resources/Info.plist @@ -21,7 +21,7 @@ CFBundleShortVersionString 1.0 CFBundleVersion - 0.2.153.1 + 0.2.154 Config $(Config) ModelsVersion diff --git a/Nynja.xcodeproj/project.pbxproj b/Nynja.xcodeproj/project.pbxproj index 52d749b8c..60399b03c 100644 --- a/Nynja.xcodeproj/project.pbxproj +++ b/Nynja.xcodeproj/project.pbxproj @@ -13108,7 +13108,7 @@ "${BUILT_PRODUCTS_DIR}/CryptoSwift/CryptoSwift.framework", "${BUILT_PRODUCTS_DIR}/GRDBCipher/GRDBCipher.framework", "${BUILT_PRODUCTS_DIR}/GTMSessionFetcher/GTMSessionFetcher.framework", - "${BUILT_PRODUCTS_DIR}/GoogleUtilities/GoogleUtilities.framework", + "${BUILT_PRODUCTS_DIR}/GoogleToolboxForMac/GoogleToolboxForMac.framework", "${BUILT_PRODUCTS_DIR}/JTAppleCalendar/JTAppleCalendar.framework", "${BUILT_PRODUCTS_DIR}/MDFTextAccessibility/MDFTextAccessibility.framework", "${BUILT_PRODUCTS_DIR}/MaterialComponents/MaterialComponents.framework", @@ -13133,7 +13133,7 @@ "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/CryptoSwift.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/GRDBCipher.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/GTMSessionFetcher.framework", - "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/GoogleUtilities.framework", + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/GoogleToolboxForMac.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/JTAppleCalendar.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/MDFTextAccessibility.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/MaterialComponents.framework", diff --git a/Nynja/Modules/ImagePreview/View/ImagePreviewViewController.swift b/Nynja/Modules/ImagePreview/View/ImagePreviewViewController.swift index 1a80535b9..954f3a2ed 100644 --- a/Nynja/Modules/ImagePreview/View/ImagePreviewViewController.swift +++ b/Nynja/Modules/ImagePreview/View/ImagePreviewViewController.swift @@ -280,9 +280,6 @@ extension ImagePreviewViewController: ImagePreviewTransitionHostVCProtocol { private extension ImagePreviewViewController { func newInset(scrollView: UIScrollView) -> UIEdgeInsets { - if #available(iOS 12.0, *) { - return UIEdgeInsets(top: 0, left: 0, bottom: 0, right: 0) - } var yOffset = imageView.frame.origin.y * scrollView.zoomScale var xOffset = imageView.frame.origin.x * scrollView.zoomScale diff --git a/Nynja/Resources/Info.plist b/Nynja/Resources/Info.plist index c39fac7d2..638040a07 100644 --- a/Nynja/Resources/Info.plist +++ b/Nynja/Resources/Info.plist @@ -21,7 +21,7 @@ CFBundleShortVersionString 1.0 CFBundleVersion - 0.2.153.1 + 0.2.154 ConfServerAddress $(ConfServerAddress) ConfServerPort -- GitLab From baa4a52a04bbf0e5ff26ef682b7f38846adf4154 Mon Sep 17 00:00:00 2001 From: Anton Makarov Date: Wed, 17 Oct 2018 17:35:55 +0300 Subject: [PATCH 8/8] 0.5.2 --- .gitignore | 1 - Nynja-Share/Resources/Info.plist | 4 +- .../Services/MQTT/MQTTServiceAuth.swift | 26 - .../UI/ForwardSelectorInteractor.swift | 34 +- .../UI/ForwardSelectorViewController.swift | 45 +- Nynja.xcodeproj/project.pbxproj | 753 ++++-- Nynja/AppDelegate.swift | 60 +- Nynja/Audio/AudioSessionManager.swift | 138 - Nynja/AudioFileConvertOperation.swift | 8 +- Nynja/ChatService/ChatService.swift | 65 +- Nynja/ChatService/SenderService.swift | 73 + .../Core/Sector/SectionView.swift | 10 +- Nynja/ContactDAO.swift | 4 +- Nynja/ConversationsProvider.swift | 4 +- Nynja/DB/Extensions/DatabaseExtension.swift | 9 + Nynja/DB/Models/DBContact.swift | 15 +- Nynja/DB/Models/DBConvertMessage.swift | 14 + Nynja/DB/Models/DBJobMessage.swift | 12 +- Nynja/DB/Models/DBMessage.swift | 209 +- Nynja/DB/Models/DBMessageLink.swift | 6 +- Nynja/DB/Models/DBRoom.swift | 77 +- Nynja/DB/Models/DBStar.swift | 8 +- Nynja/DB/Models/DBStarMessage.swift | 85 +- ...ension.swift => DBMessage+Extension.swift} | 20 +- .../DB/Models/Extension/DBRoomExtension.swift | 17 + Nynja/DB/Tables/Base/Table.swift | 4 + Nynja/DB/Tables/ContactTable.swift | 2 +- Nynja/DB/Tables/DescTable.swift | 2 +- Nynja/DB/Tables/MessageTable.swift | 6 +- Nynja/DB/Tables/RoomTable.swift | 2 +- Nynja/DB/Tables/StarMessageTable.swift | 10 +- Nynja/DBObserver.swift | 30 +- Nynja/DatabaseManager.swift | 42 +- Nynja/DefaultMessageProcessingManager.swift | 36 +- Nynja/Extensions/Bundle+Keys.swift | 13 +- Nynja/Extensions/Date+Extension.swift | 12 +- Nynja/Extensions/Models/MemberExtension.swift | 8 +- .../Models/Message/Message+DB.swift | 13 +- .../Models/Message/Message+System.swift | 14 +- Nynja/Extensions/Models/StarExtension.swift | 2 +- Nynja/Extensions/Range+Extension.swift | 12 + .../SwiftLibrary/Array/ArrayExtension.swift | 23 +- .../Dictionary/DictionaryExtension.swift | 7 + .../Sequence/SequenceExtension.swift | 19 + .../String/String+LocationURL.swift | 24 +- Nynja/Files/AppGroupFlagContainer.swift | 66 + Nynja/Files/AppGroupFlagObserver.swift | 40 + Nynja/Files/DirectoryWatcher.swift | 96 + Nynja/HomeItemsFactory.swift | 6 +- Nynja/{ => Library}/Debug/DebugLogs.swift | 0 Nynja/{ => Library}/Debug/UILabel+Debug.swift | 0 Nynja/{ => Library}/Debug/UIView+Debug.swift | 0 .../MessageFactory/MessageFactory.swift | 72 +- .../MessageFactoryProtocol.swift | 59 + Nynja/Library/UI/AlertManager.swift | 14 +- Nynja/Library/UI/BaseVC/BaseVC.swift | 80 +- .../Buttons/TopSwipable/ScheduleButton.swift | 132 +- .../UI/Buttons/TopSwipable/TopSwipable.swift | 21 +- .../UI/Extensions/DateExtensions.swift | 24 +- .../UI/UIImageView/UIImageView+SetImage.swift | 33 +- .../Library/UI/Extensions/URLExtensions.swift | 8 + .../ImagePreviewTransitionController.swift | 5 +- .../KeyboardInteractive.swift | 73 + .../KeyboardLayoutGuide.swift | 96 +- .../Cell/ChatListMessageTableViewCell.swift | 19 +- .../UI/LoginView}/LoginView.swift | 13 +- .../UI/LoginView}/LoginViewLayout.swift | 0 .../Library/UI/ReturnToCallContentView.swift | 101 +- .../UI/SwipeBackHelper/CustomPanGesture.swift | 22 - .../UI/SwipeBackHelper/SwipeBackHelper.swift | 36 +- .../UI/TextInput/InputBar/InputBar.swift | 71 +- .../RecordDisplayInputContent.swift | 257 +- .../InputField/DrawableAudioWaveform.swift | 50 +- .../InputField/TextField/TextField.swift | 4 + .../TextInputBar/ALTextInputBar.swift | 72 +- .../TextInputBar/ALTextInputBarDelegate.swift | 50 +- Nynja/MQTTModels/MessageExtension+BERT.swift | 38 +- Nynja/MediaDownloadManager.swift | 6 +- Nynja/MessageDAO.swift | 170 +- Nynja/MessageDAOProtocol.swift | 28 +- Nynja/MigrationManager.swift | 238 +- .../AddContactByUsernameProtocols.swift | 1 + .../AddContactByUsernamePresenter.swift | 4 + .../AddContactByUsernameViewController.swift | 8 +- .../AddContactByUsernameWireframe.swift | 1 - .../AddParticipantsProtocols.swift | 1 + .../AddParticipantsInteractor.swift | 21 +- .../Presenter/AddParticipantsPresenter.swift | 16 +- .../View/AddParticipantsViewController.swift | 64 +- .../AddPaticipantsViewControllerLayout.swift | 10 + Nynja/Modules/Auth/AuthProtocols.swift | 102 - .../Login/Interactor/LoginInteractor.swift | 116 + .../Auth/{ => Login}/Interactor/Modelka.swift | 0 Nynja/Modules/Auth/Login/LoginProtocols.swift | 81 + .../Auth/Login/Presenter/LoginPresenter.swift | 103 + .../View}/LoginViewController.swift | 40 +- .../View/LoginWheelContainerDataSource.swift | 0 .../View/LoginWheelContainerDelegate.swift | 0 .../Auth/Login/WireFrame/LoginWireframe.swift | 61 + .../Auth/Presenter/AuthPresenter.swift | 90 - .../Interactor/VerifyNumberInteractor.swift | 199 ++ .../Presenter/VerifyNumberPresenter.swift | 107 + .../VerifyNumber/VerifyNumberProtocols.swift | 91 + .../View/VerifyNumberViewController.swift | 207 ++ .../Wireframe/VerifyNumberWireFrame.swift | 57 + .../View/ViewController/VerifyNumberVC.swift | 187 -- .../Auth/WireFrame/AuthWireframe.swift | 53 - .../Call/CallInProgressProtocols.swift | 6 +- .../Interactor/CallInProgressInteractor.swift | 91 +- .../Presenter/CallInProgressPresenter.swift | 4 +- .../View/CallInProgressViewController.swift | 153 +- .../WireFrame/CallInProgressWireframe.swift | 32 +- Nynja/Modules/Call/View/BottomCallView.swift | 22 +- ...oupAddParticipantsCollectionViewCell.swift | 6 +- .../Call/View/GroupCollectionViewCell.swift | 6 +- .../Presenter/NewChannelPresenter.swift | 2 +- .../ChannelsList/ChannelsListProtocols.swift | 4 +- .../Interactor/ChannelsListInteractor.swift | 120 +- .../Presenter/ChannelsListPresenter.swift | 24 +- .../View/ChannelsListViewController.swift | 6 +- .../WireFrame/ChannelsListWireFrame.swift | 8 +- .../Interactor/ChatsListInteractor.swift | 17 +- .../View/ChatsListViewController.swift | 37 +- .../WireFrame/ChatsListWireframe.swift | 7 +- .../Interactor/ContactsInteractor.swift | 2 +- .../ContactsViewController.swift | 4 +- .../CreateGroup/CreateGroupProtocols.swift | 2 +- .../Presenter/CreateGroupPresenter.swift | 9 +- .../WireFrame/CreateGroupWireframe.swift | 4 +- .../View/EditName/EditProfileVC.swift | 4 +- .../View/EditProfileViewController.swift | 4 +- .../View/EditUsernameViewController.swift | 4 +- .../View/FavoritesViewController.swift | 2 +- .../WireFrame/FavoritesWireframe.swift | 7 +- .../Camera/View/CameraViewController.swift | 5 + .../Flows/CameraFlow/CameraCoordinator.swift | 1 + .../CameraVideoPreviewProtocols.swift | 2 + .../CameraVideoPreviewInteractor.swift | 15 + .../CameraVideoPreviewPresenter.swift | 4 +- .../CameraVideoPreviewViewController.swift | 2 - .../CameraVideoPreviewWireframe.swift | 2 + .../GalleryFlow/GalleryCoordinator.swift | 6 +- .../MultiplePreviewInteractor.swift | 15 + .../MultiplePreviewProtocols.swift | 2 + .../Presenter/MultiplePreviewPresenter.swift | 5 +- .../View/MultiplePreviewViewController.swift | 2 - .../Wireframe/MultiplePreviewWireframe.swift | 9 +- .../Models/ForwardTargets.swift | 4 + .../ForwardSelectorViewController.swift | 4 +- .../View/GroupRulesViewController.swift | 4 +- .../View/GroupStorageListVC.swift | 1 - .../GroupsList/GroupsListProtocols.swift | 1 - .../Interactor/GroupsListInteractor.swift | 84 +- .../View/GroupsListViewController.swift | 47 +- .../WireFrame/GroupsListWireframe.swift | 11 +- .../History/View/HistoryViewController.swift | 4 +- .../WireFrame/InviteFriendsWireframe.swift | 3 +- .../LanguageSelectroInteractor.swift | 27 +- .../View/LanguageSelectorViewController.swift | 4 +- .../Main/Interactor/MainInteractor.swift | 19 +- .../Main/View/ComingSoonExtension.swift | 15 + .../Main/View/ComingSoonProtocol.swift | 13 + .../Main/View/MainNavigationItem.swift | 2 +- .../MainViewController+NavigateProtocol.swift | 13 +- .../Main/View/MainViewController.swift | 1 + .../Modules/Main/View/NavigateProtocol.swift | 5 +- .../Main/WireFrame/MainWireframe.swift | 45 +- .../MapSearchViewController.swift | 4 +- .../Interactor/MessageInteractor+Fetch.swift | 93 +- .../MessageInteractor+History.swift | 91 +- ...eInteractor+MessageHandlerSubscriber.swift | 2 +- .../MessageInteractor+Schedule.swift | 2 +- .../MessageInteractor+StorageSubscriber.swift | 93 +- .../MessageInteractor+Translation.swift | 33 +- .../Interactor/MessageInteractor+Utils.swift | 11 +- .../Interactor/MessageInteractor.swift | 173 +- .../Configurations/ChatConfiguration.swift | 43 +- .../Models/Mention/Entity/MentionInfo.swift | 1 + .../InputController/MentionController.swift | 20 +- .../BBCodeTags/MessagePayloadParser.swift | 10 +- .../Mention/Text/InputTextStorage.swift | 56 + .../Text/InputTextStorageDelegate.swift | 25 + .../NSAttributedStringKey+Mention.swift | 0 ...essagePresenter+MentionUnreadCounter.swift | 27 +- .../Message/Presenter/MessagePresenter.swift | 86 +- .../Protocols/MentionTransitionProtocol.swift | 10 +- .../Message/Protocols/MessageProtocols.swift | 20 +- .../Protocols/VoiceAudioInteractive.swift | 120 +- .../Message/View/MessageVC+CellDelegate.swift | 27 +- Nynja/Modules/Message/View/MessageVC.swift | 148 +- .../Message/View/MessageVCLayout.swift | 2 +- .../MessageCollectionViewDataSource.swift | 39 +- .../MessageCollectionViewDelegate.swift | 45 +- .../CollectionView/ScrollDirection.swift | 12 + .../ChatCells/BaseChatCell/BaseChatCell.swift | 25 +- .../Cells/Models/BaseChatCellModel.swift | 142 +- .../Cells/Models/ChangableProgress.swift | 40 + .../Cells/Models/ProgressDisplayable.swift | 13 + .../Cells/Models/RepliedMessageModel.swift | 11 +- .../Views/Message/MessageImageView.swift | 2 +- .../Views/Message/MessageLocationView.swift | 8 +- .../Views/Message/MessageStickerView.swift | 2 +- .../Views/Message/MessageVideoView.swift | 2 +- .../Views/Message/MessageVoiceView.swift | 16 +- .../Cells/Views/Reply/ReplyInfoView.swift | 9 +- .../Message/WireFrame/MessageWireframe.swift | 5 +- .../Interactor/MyGroupAliasInteractor.swift | 40 - .../MyGroupAlias/MyGroupAliasProtocols.swift | 36 +- .../Presenter/MyGroupAliasPresenter.swift | 73 +- .../View/MyGroupAliasViewController.swift | 183 +- .../WireFrame/MyGroupAliasWireframe.swift | 26 +- .../Interactor/OtherUserInteractor.swift | 47 +- .../OtherUser/OtherUserProtocols.swift | 17 +- .../Presenter/OtherUserPresenter.swift | 2 +- .../OtherUserContainerViewController.swift | 2 +- .../WireFrame/OtherUserWireFrame.swift | 8 +- .../View/ParticipantsViewController.swift | 4 +- .../View/DetailsView/ProfileDetailsView.swift | 2 - .../Profile/View/ProfileViewController.swift | 43 +- .../ProfileScheduledMesssageCell.swift | 6 +- .../TableView/ProfileTableViewDelegate.swift | 1 + .../Profile/WireFrame/ProfileWireframe.swift | 6 +- .../View/QRCodeReaderViewController.swift | 17 +- .../Replies/Presenter/RepliesPresenter.swift | 4 +- .../View/RepliesCollectionViewDelegate.swift | 6 +- Nynja/Modules/Replies/View/RepliesDS.swift | 4 +- Nynja/Modules/Replies/View/RepliesVC.swift | 3 +- .../ScheduleMessageInteractor.swift | 154 +- .../Models/ScheduleContentType.swift | 11 +- .../Presenter/ScheduleMessagePresenter.swift | 16 +- .../ScheduleMessageProtocols.swift | 2 +- .../View/ScheduleMessageViewController.swift | 122 +- ...heduleMessageViewControllerConstants.swift | 34 +- .../Views/MessageContent/AudioItemView.swift | 207 +- .../WireFrame/ScheduleMessageWireframe.swift | 2 +- .../View/SelectCountryViewController.swift | 4 +- .../ChangeNumberStep2ViewController.swift | 4 +- .../NotificationAlertSoundsInteractor.swift | 21 +- .../PrivacyListViewController.swift | 4 +- .../Interactor/SecurityInteractor.swift | 18 +- .../DataAndStorageOption.swift | 4 +- .../Interactor/SettingsGroupInteractor.swift | 28 +- .../Presenter/SettingsGroupPresenter.swift | 7 +- .../SettingsGroup/SettingsProtocols.swift | 2 +- .../WireFrame/SettingsGroupWireFrame.swift | 8 +- .../Splash/Interactor/SplashInteractor.swift | 5 +- .../Splash/WireFrame/SplashWireframe.swift | 2 +- .../Cells/Sticker/StickerCellModel.swift | 4 +- .../Sticker/StickerCollectionViewCell.swift | 1 + .../TimeZoneSelectorViewController.swift | 4 +- .../Interactor/WebViewInteractor.swift | 13 - .../WebView/Presenter/WebViewPresenter.swift | 20 - .../WebView/View/WebViewViewController.swift | 62 - Nynja/Modules/WebView/WebViewProtocols.swift | 55 - .../WebView/WireFrame/WebViewWireframe.swift | 32 - .../WireFrame/TutorialWireframe.swift | 2 +- Nynja/NotificationManager.swift | 4 +- .../AppNotificationsProvider.swift | 47 + .../AppNotificationsProviding.swift | 14 + Nynja/OptionsItemsFactory.swift | 8 +- .../ic_accept_call_big.imageset/Contents.json | 12 + .../ic_accept_call-68.pdf | 2398 +++++++++++++++++ Nynja/Resources/Constants.swift | 25 +- Nynja/Resources/DevConfig.xcconfig | 4 +- Nynja/Resources/Info.plist | 6 +- Nynja/Resources/PrereleaseConfig.xcconfig | 12 +- Nynja/Resources/ReleaseConfig.xcconfig | 12 +- Nynja/Resources/ThirdPartyServices.swift | 34 +- Nynja/Resources/en.lproj/Localizable.strings | 11 +- Nynja/RoomDAO.swift | 62 +- Nynja/RoomDAOProtocol.swift | 2 +- Nynja/ServerModel/Model/Contact.swift | 2 +- Nynja/ServerModel/Model/Message.swift | 9 +- Nynja/ServerModel/Model/Room.swift | 2 +- Nynja/ServerModel/Source/Decoder.swift | 4 +- Nynja/ServerModel/Spec/Message_Spec.swift | 46 +- Nynja/Services/Aps.swift | 38 +- .../Audio/AudioManager/AudioManager.swift | 234 ++ .../AudioManager}/AudioManagerDelegate.swift | 0 Nynja/Services/Audio/AudioPlayable.swift | 53 + .../Audio/AudioPlayer}/AudioPlayer.swift | 0 .../Audio/AudioRecorder}/AudioRecorder.swift | 57 +- .../AudioSessionManager.swift | 264 ++ .../Audio/SystemSoundManager}/Sound.swift | 8 +- .../SystemSoundManager}/SoundBundle.swift | 6 + .../SystemSoundManager.swift | 129 + Nynja/Services/ConnectionService.swift | 81 + .../Debug/LogService/LogService.swift | 78 + .../Services/Debug/LogService/LogWriter.swift | 76 + .../Debug/MotionManager/MotionManager.swift | 117 + .../SMSCodeProvider/SMSCodeProvider.swift | 60 + .../SMSCodeProvider/SMSCodeProviding.swift | 13 + .../HandleServices/ContactHandler.swift | 2 +- .../HandleServices/HistoryHandler.swift | 133 +- .../HandleServices/MessageHandler.swift | 37 +- .../HandleServices/ProfileHandler.swift | 42 +- .../Services/HandleServices/RoomHandler.swift | 95 +- Nynja/Services/MQTT/MQTTService.swift | 250 +- Nynja/Services/MQTT/MQTTServiceAuth.swift | 24 +- Nynja/Services/MQTT/MQTTServiceHelper.swift | 11 +- Nynja/Services/MQTT/MQTTServiceProfile.swift | 7 +- Nynja/Services/Member/MemberDAO.swift | 9 + Nynja/Services/Member/MemberDAOProtocol.swift | 1 + .../MessageSendingService.swift | 23 +- .../NynjaCommunicatorService.swift | 101 +- .../NynjaCalls/NynjaRingingService.swift | 59 + Nynja/Services/PushService.swift | 190 +- .../TranscribeLongOperationResponseData.swift | 6 +- .../Response/TranscribeResponseData.swift | 41 + .../TranscribeShortResponseData.swift | 24 - .../TranscribeNetworkService.swift | 6 +- Nynja/Services/ReachabilityService.swift | 62 +- .../ServiceFactory/ServiceFactory.swift | 20 +- .../Helpers/Array+Operation.swift | 25 + .../AudioLongTranscribeOperation.swift | 1 + ...ioLongTranscribeProccessingOperation.swift | 7 +- .../AudioShortTranscribeOperation.swift | 5 +- .../TranscribeService/TranscribeService.swift | 223 +- .../Manager/WCDataManager.swift | 59 +- Nynja/TypingStatusCache.swift | 48 + .../Wireframe/WireframeProtocol.swift | 16 + Nynja/WCBaseItemsFactory.swift | 5 +- Podfile | 100 +- Podfile.lock | 182 ++ .../Contact/Contact+BaseChatModel.swift | 17 +- .../Models/Contact/ContactExtension.swift | 32 +- .../Models/Message/Message+Factory.swift | 4 +- .../Models/Message/Message+Files.swift | 2 +- .../Models/Message/MessageExtension.swift | 97 +- .../Models/Room/Room+BaseChatModel.swift | 31 +- Shared/Library/Models/BaseChatModel.swift | 11 +- Shared/Services/Handlers/IoHandler.swift | 12 +- 332 files changed, 10860 insertions(+), 4496 deletions(-) delete mode 100644 Nynja-Share/Services/MQTT/MQTTServiceAuth.swift delete mode 100644 Nynja/Audio/AudioSessionManager.swift create mode 100644 Nynja/ChatService/SenderService.swift rename Nynja/DB/Models/Extension/{DBMessage+TypeExtension.swift => DBMessage+Extension.swift} (71%) create mode 100644 Nynja/DB/Models/Extension/DBRoomExtension.swift create mode 100644 Nynja/Extensions/SwiftLibrary/Sequence/SequenceExtension.swift create mode 100644 Nynja/Files/AppGroupFlagContainer.swift create mode 100644 Nynja/Files/AppGroupFlagObserver.swift create mode 100644 Nynja/Files/DirectoryWatcher.swift rename Nynja/{ => Library}/Debug/DebugLogs.swift (100%) rename Nynja/{ => Library}/Debug/UILabel+Debug.swift (100%) rename Nynja/{ => Library}/Debug/UIView+Debug.swift (100%) create mode 100644 Nynja/Library/MessageFactory/MessageFactoryProtocol.swift create mode 100644 Nynja/Library/UI/KeyboardLayoutGuide/KeyboardInteractive.swift rename Nynja/{Modules/Auth/View => Library/UI/LoginView}/LoginView.swift (96%) rename Nynja/{Modules/Auth/View => Library/UI/LoginView}/LoginViewLayout.swift (100%) delete mode 100644 Nynja/Library/UI/SwipeBackHelper/CustomPanGesture.swift delete mode 100644 Nynja/Modules/Auth/AuthProtocols.swift create mode 100644 Nynja/Modules/Auth/Login/Interactor/LoginInteractor.swift rename Nynja/Modules/Auth/{ => Login}/Interactor/Modelka.swift (100%) create mode 100644 Nynja/Modules/Auth/Login/LoginProtocols.swift create mode 100644 Nynja/Modules/Auth/Login/Presenter/LoginPresenter.swift rename Nynja/Modules/Auth/{View/ViewController => Login/View}/LoginViewController.swift (93%) rename Nynja/Modules/Auth/{ => Login}/View/LoginWheelContainerDataSource.swift (100%) rename Nynja/Modules/Auth/{ => Login}/View/LoginWheelContainerDelegate.swift (100%) create mode 100644 Nynja/Modules/Auth/Login/WireFrame/LoginWireframe.swift delete mode 100644 Nynja/Modules/Auth/Presenter/AuthPresenter.swift create mode 100644 Nynja/Modules/Auth/VerifyNumber/Interactor/VerifyNumberInteractor.swift create mode 100644 Nynja/Modules/Auth/VerifyNumber/Presenter/VerifyNumberPresenter.swift create mode 100644 Nynja/Modules/Auth/VerifyNumber/VerifyNumberProtocols.swift create mode 100644 Nynja/Modules/Auth/VerifyNumber/View/VerifyNumberViewController.swift create mode 100644 Nynja/Modules/Auth/VerifyNumber/Wireframe/VerifyNumberWireFrame.swift delete mode 100644 Nynja/Modules/Auth/View/ViewController/VerifyNumberVC.swift delete mode 100644 Nynja/Modules/Auth/WireFrame/AuthWireframe.swift create mode 100644 Nynja/Modules/Main/View/ComingSoonExtension.swift create mode 100644 Nynja/Modules/Main/View/ComingSoonProtocol.swift create mode 100644 Nynja/Modules/Message/Models/Mention/Text/InputTextStorage.swift create mode 100644 Nynja/Modules/Message/Models/Mention/Text/InputTextStorageDelegate.swift rename Nynja/Modules/Message/Models/Mention/{ => Text}/NSAttributedStringKey+Mention.swift (100%) create mode 100644 Nynja/Modules/Message/View/Views/CollectionView/ScrollDirection.swift create mode 100644 Nynja/Modules/Message/View/Views/TableView/Cells/Models/ChangableProgress.swift create mode 100644 Nynja/Modules/Message/View/Views/TableView/Cells/Models/ProgressDisplayable.swift delete mode 100644 Nynja/Modules/MyGroupAlias/Interactor/MyGroupAliasInteractor.swift delete mode 100644 Nynja/Modules/WebView/Interactor/WebViewInteractor.swift delete mode 100644 Nynja/Modules/WebView/Presenter/WebViewPresenter.swift delete mode 100644 Nynja/Modules/WebView/View/WebViewViewController.swift delete mode 100644 Nynja/Modules/WebView/WebViewProtocols.swift delete mode 100644 Nynja/Modules/WebView/WireFrame/WebViewWireframe.swift create mode 100644 Nynja/Notifications/AppNotificationsProviding/AppNotificationsProvider.swift create mode 100644 Nynja/Notifications/AppNotificationsProviding/AppNotificationsProviding.swift create mode 100644 Nynja/Resources/Assets.xcassets/Call items/ic_accept_call_big.imageset/Contents.json create mode 100644 Nynja/Resources/Assets.xcassets/Call items/ic_accept_call_big.imageset/ic_accept_call-68.pdf create mode 100644 Nynja/Services/Audio/AudioManager/AudioManager.swift rename Nynja/{ => Services/Audio/AudioManager}/AudioManagerDelegate.swift (100%) create mode 100644 Nynja/Services/Audio/AudioPlayable.swift rename Nynja/{ => Services/Audio/AudioPlayer}/AudioPlayer.swift (100%) rename Nynja/{ => Services/Audio/AudioRecorder}/AudioRecorder.swift (80%) create mode 100644 Nynja/Services/Audio/AudioSessionManager/AudioSessionManager.swift rename Nynja/{ => Services/Audio/SystemSoundManager}/Sound.swift (88%) rename Nynja/{ => Services/Audio/SystemSoundManager}/SoundBundle.swift (77%) create mode 100644 Nynja/Services/Audio/SystemSoundManager/SystemSoundManager.swift create mode 100644 Nynja/Services/ConnectionService.swift create mode 100644 Nynja/Services/Debug/LogService/LogService.swift create mode 100644 Nynja/Services/Debug/LogService/LogWriter.swift create mode 100644 Nynja/Services/Debug/MotionManager/MotionManager.swift create mode 100644 Nynja/Services/Debug/SMSCodeProvider/SMSCodeProvider.swift create mode 100644 Nynja/Services/Debug/SMSCodeProvider/SMSCodeProviding.swift rename Nynja/Services/{ => NynjaCalls}/NynjaCommunicatorService.swift (91%) create mode 100644 Nynja/Services/NynjaCalls/NynjaRingingService.swift create mode 100644 Nynja/Services/REST/TranscribeNetworkService/Response/TranscribeResponseData.swift delete mode 100644 Nynja/Services/REST/TranscribeNetworkService/Response/TranscribeShortResponseData.swift create mode 100644 Nynja/Services/TranscribeService/Helpers/Array+Operation.swift create mode 100644 Nynja/TypingStatusCache.swift create mode 100644 Podfile.lock diff --git a/.gitignore b/.gitignore index 7150d5f51..4bbaf767e 100644 --- a/.gitignore +++ b/.gitignore @@ -56,7 +56,6 @@ Rambafile # # Add this line if you want to avoid checking in source code from Carthage dependencies. # Carthage/Checkouts -Podfile.lock Carthage/Build # fastlane diff --git a/Nynja-Share/Resources/Info.plist b/Nynja-Share/Resources/Info.plist index 6a68225bd..ca55e1f5b 100644 --- a/Nynja-Share/Resources/Info.plist +++ b/Nynja-Share/Resources/Info.plist @@ -21,7 +21,7 @@ CFBundleShortVersionString 1.0 CFBundleVersion - 0.2.154 + 0.5.2 Config $(Config) ModelsVersion @@ -69,5 +69,7 @@ UIInterfaceOrientationPortrait + isServerConnectionSecure + $(isServerConnectionSecure) diff --git a/Nynja-Share/Services/MQTT/MQTTServiceAuth.swift b/Nynja-Share/Services/MQTT/MQTTServiceAuth.swift deleted file mode 100644 index a6adc1ffe..000000000 --- a/Nynja-Share/Services/MQTT/MQTTServiceAuth.swift +++ /dev/null @@ -1,26 +0,0 @@ -// -// MQTTService.swift -// Nynja-Share -// -// Created by Bohdan Paliychuk on 10/20/17. -// Copyright © 2017 TecSynt Solutions. All rights reserved. -// - -import Foundation - - -extension MQTTService { - - func connectFromExtension() { - timer?.invalidate() - reconnect() - timer = Timer.scheduledTimer(timeInterval: 5, target: self, selector: #selector(runTimedCode), userInfo: nil, repeats: true) - } - - func disconnectFromExtension() { - timer?.invalidate() - mqtt?.disconnect() - } - -} - diff --git a/Nynja-Share/UI/ForwardSelectorInteractor.swift b/Nynja-Share/UI/ForwardSelectorInteractor.swift index 68c11487c..feabd988e 100644 --- a/Nynja-Share/UI/ForwardSelectorInteractor.swift +++ b/Nynja-Share/UI/ForwardSelectorInteractor.swift @@ -46,6 +46,8 @@ final class ForwardSelectorInteractor: ForwardSelectorInteractorInputProtocol, P weak var presenter: ForwardSelectorInteractorOutputProtocol! + private let appGroupContainer = AppGroupFlagContainer(fileManager: .default, appGroup: Bundle.main.appGroupName) + private(set) var mode: ForwardSelectorMode var handlerServerSignals: ((ServerSignal)->())? @@ -74,19 +76,43 @@ final class ForwardSelectorInteractor: ForwardSelectorInteractorInputProtocol, P private var groups: [ForwardTarget]? + required init(mode: ForwardSelectorMode) { self.mode = mode ProfileHandler.delegate = self MessageHandler.delegate = self ContactHandler.delegate = self IoHandler.delegate = self - setupAmazon() + setupAmazon() + notifyHostApplication() + } + + deinit { + notifyHostApplicationAboutClose() + } + + func notifyHostApplication() { + do { + try appGroupContainer?.prepare() + try appGroupContainer?.setFlag(.shareExtension) + } catch { + LogService.log(topic: .fileSystem) { error.localizedDescription } + } } + func notifyHostApplicationAboutClose() { + do { + try appGroupContainer?.removeFlagIfExists(.shareExtension) + } catch { + LogService.log(topic: .fileSystem) { error.localizedDescription } + } + } + + func connectToServer() { if let token = StorageService.sharedInstance.token { LogService.log(topic: .MQTT) { return "token: \(token)" } - _ = MQTTService.sharedInstance.connectFromExtension() + _ = MQTTService.sharedInstance.reconnect() } else { handlerServerSignals?(.noToken) } @@ -225,7 +251,7 @@ final class ForwardSelectorInteractor: ForwardSelectorInteractorInputProtocol, P self.sendStatus(to: targets, type: t) } }) { (fileUrl, fileName, size) in - + var info: String? if type == .image, let resolution = UIImage(fileUrl: url)?.scaledSize { info = resolution.resolutionString @@ -237,6 +263,8 @@ final class ForwardSelectorInteractor: ForwardSelectorInteractorInputProtocol, P let messages = targets.messages(from: message, phoneId: phoneId) + try? FileManager.default.removeItem(at: url) + self.sendStatus(to: targets, type: .done) MQTTService.sharedInstance.forwardMessage(phoneId: phoneId, messages: messages) } diff --git a/Nynja-Share/UI/ForwardSelectorViewController.swift b/Nynja-Share/UI/ForwardSelectorViewController.swift index 02f2cca0b..ebc0def98 100644 --- a/Nynja-Share/UI/ForwardSelectorViewController.swift +++ b/Nynja-Share/UI/ForwardSelectorViewController.swift @@ -11,7 +11,7 @@ import NynjaUIKit import SnapKit import MobileCoreServices -final class ForwardSelectorViewController: UIViewController, ForwardSelectorViewSetupProtocol { +final class ForwardSelectorViewController: UIViewController, ForwardSelectorViewSetupProtocol, KeyboardInteractive { var presenter: ForwardSelectorPresenterProtocol = { let presenter = ForwardSelectorPresenter() @@ -60,9 +60,10 @@ final class ForwardSelectorViewController: UIViewController, ForwardSelectorView } } + // MARK: - Views - lazy var hud : ProgressHUD = { + private lazy var hud: ProgressHUD = { let hud = ProgressHUD() view.addSubview(hud) hud.snp.makeConstraints({ make in @@ -73,7 +74,7 @@ final class ForwardSelectorViewController: UIViewController, ForwardSelectorView return hud }() - lazy var backgroundImage: UIImageView = { + private lazy var backgroundImage: UIImageView = { let view = UIImageView() view.isUserInteractionEnabled = true setupBackground(for: view) @@ -85,7 +86,7 @@ final class ForwardSelectorViewController: UIViewController, ForwardSelectorView return view }() - lazy var navigationView: NavigationView = { + private lazy var navigationView: NavigationView = { let navView = NavigationView() navView.clipsToBounds = true @@ -289,6 +290,7 @@ final class ForwardSelectorViewController: UIViewController, ForwardSelectorView override func viewDidLoad() { super.viewDidLoad() + getAttachmentContent() presenter.view = self presenter.interactor.handlerServerSignals = { [weak self] signal in @@ -303,17 +305,18 @@ final class ForwardSelectorViewController: UIViewController, ForwardSelectorView override func viewWillAppear(_ animated: Bool) { super.viewWillAppear(animated) - NotificationCenter.default.addObserver(self,selector: #selector(self.keyboardNotification),name: NSNotification.Name.UIKeyboardWillChangeFrame,object: nil) self.view.bringSubview(toFront: navigationView) self.view.bringSubview(toFront: hud) state = .contacts + registerForKeyboardNotifications() } override func viewWillDisappear(_ animated: Bool) { super.viewWillDisappear(animated) - NotificationCenter.default.removeObserver(self) + unregisterForKeyboardNotifications() } + // MARK: - ForwardSelectorViewProtocol func setupUI(with mode: ForwardSelectorDisplayMode) { screenTitle = "forward_screen_logo".localized @@ -348,22 +351,6 @@ final class ForwardSelectorViewController: UIViewController, ForwardSelectorView // MARK: - Keyboard - @objc func keyboardNotification(notification: NSNotification) { - if let userInfo = notification.userInfo as? [String:Any] { - if let endFrame = (userInfo["UIKeyboardFrameEndUserInfoKey"] as? NSValue)?.cgRectValue { - let duration: TimeInterval = (userInfo["UIKeyboardAnimationDurationUserInfoKey"] as? NSNumber)?.doubleValue ?? 0 - let animationCurveRawNSN = userInfo["UIKeyboardAnimationCurveUserInfoKey"] as? NSNumber - let animationCurveRaw = animationCurveRawNSN?.uintValue ?? UIViewAnimationOptions.curveEaseOut.rawValue - let animationCurve:UIViewAnimationOptions = UIViewAnimationOptions(rawValue: animationCurveRaw) - self.keyboardNotified(endFrame: endFrame) - - UIView.animate(withDuration: duration, delay: TimeInterval(0), options: animationCurve, animations: { - self.view.layoutIfNeeded() - }, completion: nil) - } - } - } - func keyboardNotified(endFrame: CGRect) { if endFrame.origin.y >= UIScreen.main.bounds.size.height { updateToHide(view: bottomActionsView, offset: -bottomInset) @@ -402,7 +389,7 @@ final class ForwardSelectorViewController: UIViewController, ForwardSelectorView } func closeShareExtension() { - MQTTService.sharedInstance.disconnectFromExtension() + MQTTService.sharedInstance.disconnect(shouldRemoveConnectionSubscriber: false) if let context = self.extensionContext { context.completeRequest(returningItems: nil, completionHandler: nil) } @@ -459,11 +446,19 @@ final class ForwardSelectorViewController: UIViewController, ForwardSelectorView if attachment.hasItemConformingToTypeIdentifier("public.image") { attachment.loadItem(forTypeIdentifier: "public.image", options: nil, completionHandler: { (coding, error) in - if coding is URL { + if let url = coding as? URL { model.type = .image - model.contentURL = coding as? URL + model.contentURL = url self.attachment = model return + } else if let image = coding as? UIImage { + model.type = .image + let documentDirectory = NSSearchPathForDirectoriesInDomains(.documentDirectory, .userDomainMask, true)[0] as String + let filePath = "\(documentDirectory)/\(IdBuilder(format: .resourceId).build()).jpg" + let url = URL(fileURLWithPath: filePath) + try? UIImageJPEGRepresentation(image, 1)?.write(to: url) + model.contentURL = url + self.attachment = model } }) } else if attachment.hasItemConformingToTypeIdentifier("public.audio") { diff --git a/Nynja.xcodeproj/project.pbxproj b/Nynja.xcodeproj/project.pbxproj index 60399b03c..873c81bdc 100644 --- a/Nynja.xcodeproj/project.pbxproj +++ b/Nynja.xcodeproj/project.pbxproj @@ -131,21 +131,19 @@ 260313AF20A0A50D009AC66D /* TranslationService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 260313AE20A0A50D009AC66D /* TranslationService.swift */; }; 260313C820A0BC80009AC66D /* Array+LangExtended.swift in Sources */ = {isa = PBXBuildFile; fileRef = 260313C720A0BC80009AC66D /* Array+LangExtended.swift */; }; 2604C0962069163C0051E4FB /* HandlerServiceProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = A4A242412060336400B0A804 /* HandlerServiceProtocol.swift */; }; - 26052C7A20FCE7E000E7A6A0 /* LogService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 26052C7920FCE7E000E7A6A0 /* LogService.swift */; }; - 26052C7B20FCE7E000E7A6A0 /* LogService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 26052C7920FCE7E000E7A6A0 /* LogService.swift */; }; 2605311B212740FD002E1CF1 /* LogOutputProtocols.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2605311A212740FD002E1CF1 /* LogOutputProtocols.swift */; }; 2605311D21274116002E1CF1 /* LogOutputView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2605311C21274116002E1CF1 /* LogOutputView.swift */; }; 2605311F21274124002E1CF1 /* LogOutputInteractor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2605311E21274124002E1CF1 /* LogOutputInteractor.swift */; }; 2605312121274133002E1CF1 /* LogOutputPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2605312021274133002E1CF1 /* LogOutputPresenter.swift */; }; 26053123212741C2002E1CF1 /* LogOutputWireFrame.swift in Sources */ = {isa = PBXBuildFile; fileRef = 26053122212741C2002E1CF1 /* LogOutputWireFrame.swift */; }; - 260531262127455C002E1CF1 /* MotionManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 260531252127455C002E1CF1 /* MotionManager.swift */; }; 2605312921298BEF002E1CF1 /* Logoutputcell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2605312821298BEF002E1CF1 /* Logoutputcell.swift */; }; 2605312B21299198002E1CF1 /* LogOutputDS.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2605312A21299198002E1CF1 /* LogOutputDS.swift */; }; 260552A61F9E1CD100D68DE6 /* SearchHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 260552A51F9E1CD100D68DE6 /* SearchHandler.swift */; }; 260629712056EF2800CB8F65 /* LinksCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 260629702056EF2800CB8F65 /* LinksCell.swift */; }; 2606F3BC20BFE20500CF7F15 /* MessageInteractor+Translation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2606F3BB20BFE20400CF7F15 /* MessageInteractor+Translation.swift */; }; - 260D67D92124616A0072F11F /* LogWriter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 260D67D82124616A0072F11F /* LogWriter.swift */; }; - 260D67DF2125A2FE0072F11F /* LogWriter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 260D67D82124616A0072F11F /* LogWriter.swift */; }; + 260E77D9215D3C5000D18789 /* ComingSoonExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 260E77D8215D3C5000D18789 /* ComingSoonExtension.swift */; }; + 260E77DB215D3C7700D18789 /* ComingSoonProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 260E77DA215D3C7700D18789 /* ComingSoonProtocol.swift */; }; + 260E77DC215D3CCD00D18789 /* ComingSoonProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 260E77DA215D3C7700D18789 /* ComingSoonProtocol.swift */; }; 2610D4642076516900E6E2B2 /* Array+Feature.swift in Sources */ = {isa = PBXBuildFile; fileRef = 26DCB25320692237001EF0AB /* Array+Feature.swift */; }; 26131E02210399BA00BE94F9 /* TranscribeService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 26131E01210399BA00BE94F9 /* TranscribeService.swift */; }; 26142B1120472ECD004E5FE4 /* MessageLinkTable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 26142B1020472ECD004E5FE4 /* MessageLinkTable.swift */; }; @@ -164,7 +162,6 @@ 262D43872033417F002F1E45 /* FriendExtansion+BERT.swift in Sources */ = {isa = PBXBuildFile; fileRef = 262D43862033417F002F1E45 /* FriendExtansion+BERT.swift */; }; 262D438820335225002F1E45 /* FriendRequstModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A2374DA1F26458300701045 /* FriendRequstModel.swift */; }; 262D4389203352D4002F1E45 /* FriendExtansion+BERT.swift in Sources */ = {isa = PBXBuildFile; fileRef = 262D43862033417F002F1E45 /* FriendExtansion+BERT.swift */; }; - 2631C512207A4C0C00F9AA55 /* AudioRecorder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2631C511207A4C0C00F9AA55 /* AudioRecorder.swift */; }; 2632139120D797F500C31144 /* TranslationViewProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2632139020D797F500C31144 /* TranslationViewProtocol.swift */; }; 2633EF6E205212F700DB3868 /* MemberDAOProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2633EF6D205212F700DB3868 /* MemberDAOProtocol.swift */; }; 2633EF702052130400DB3868 /* MemberDAO.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2633EF6F2052130400DB3868 /* MemberDAO.swift */; }; @@ -173,7 +170,6 @@ 26342CA920ECBAEF00D2196B /* TranscribeNetworkClient.swift in Sources */ = {isa = PBXBuildFile; fileRef = 26342CA820ECBAEE00D2196B /* TranscribeNetworkClient.swift */; }; 26342CAB20ECBB0100D2196B /* TranscribeNetworkService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 26342CAA20ECBB0100D2196B /* TranscribeNetworkService.swift */; }; 26342CAD20ECD15100D2196B /* TranscribeShortRequestData.swift in Sources */ = {isa = PBXBuildFile; fileRef = 26342CAC20ECD15100D2196B /* TranscribeShortRequestData.swift */; }; - 26342CAF20ECD16A00D2196B /* TranscribeShortResponseData.swift in Sources */ = {isa = PBXBuildFile; fileRef = 26342CAE20ECD16A00D2196B /* TranscribeShortResponseData.swift */; }; 26342CB220ECDDC400D2196B /* Encodable+Dictionary.swift in Sources */ = {isa = PBXBuildFile; fileRef = 26342CB120ECDDC400D2196B /* Encodable+Dictionary.swift */; }; 26342CB420ECFAB600D2196B /* MessageInteractor+Transcription.swift in Sources */ = {isa = PBXBuildFile; fileRef = 26342CB320ECFAB600D2196B /* MessageInteractor+Transcription.swift */; }; 263529132075725200DC6FBD /* SendJob.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0008E9182032F4C8003E316E /* SendJob.swift */; }; @@ -230,7 +226,7 @@ 2651094020ADBB0200F1B38B /* NotificationSettingProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2651093E20ADB81100F1B38B /* NotificationSettingProtocol.swift */; }; 2652D6161FA82EFE005E62C7 /* EditProfileVCLayout.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2652D6151FA82EFE005E62C7 /* EditProfileVCLayout.swift */; }; 2652D6181FA85B28005E62C7 /* ImageSelector.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2652D6171FA85B28005E62C7 /* ImageSelector.swift */; }; - 26534B25210B4BE70003B9BC /* DBMessage+TypeExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 26534B24210B4BE70003B9BC /* DBMessage+TypeExtension.swift */; }; + 26534B25210B4BE70003B9BC /* DBMessage+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 26534B24210B4BE70003B9BC /* DBMessage+Extension.swift */; }; 26541F722007B93400AAEACF /* DBMessageAction.swift in Sources */ = {isa = PBXBuildFile; fileRef = 26541F712007B93400AAEACF /* DBMessageAction.swift */; }; 26541F742007B9A200AAEACF /* MessageActionTable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 26541F732007B9A200AAEACF /* MessageActionTable.swift */; }; 2657BE51201233E300F21935 /* ImageFilledItemModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2657BE50201233E300F21935 /* ImageFilledItemModel.swift */; }; @@ -262,6 +258,7 @@ 26770A571FFD6CAC009AC870 /* SharedParameters.swift in Sources */ = {isa = PBXBuildFile; fileRef = 26770A561FFD6CAC009AC870 /* SharedParameters.swift */; }; 26771CC1212ECE08006112B5 /* ConvertMessageTable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 26771CC0212ECE08006112B5 /* ConvertMessageTable.swift */; }; 26771CC3212ED109006112B5 /* DBConvertMessage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 26771CC2212ED109006112B5 /* DBConvertMessage.swift */; }; + 26773F2B215BE15800C09248 /* Array+Operation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 26773F2A215BE15800C09248 /* Array+Operation.swift */; }; 26791A7C207639E7001A87B8 /* AuthModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3AC321771EEAC4C10068F3C8 /* AuthModel.swift */; }; 267BE2831FDE905D00C47E18 /* SettingsProtocols.swift in Sources */ = {isa = PBXBuildFile; fileRef = 267BE2821FDE905D00C47E18 /* SettingsProtocols.swift */; }; 267BE2851FDE983400C47E18 /* SettingsGroupVC.swift in Sources */ = {isa = PBXBuildFile; fileRef = 267BE2841FDE983400C47E18 /* SettingsGroupVC.swift */; }; @@ -279,8 +276,6 @@ 267BE90A20693F4800153FB8 /* ProfileDAOProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B058F05204EADB6004C7D9F /* ProfileDAOProtocol.swift */; }; 267BE90C2069405200153FB8 /* StarMessageDAOProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = A477CE8120613A0900081D34 /* StarMessageDAOProtocol.swift */; }; 267BE90D2069413A00153FB8 /* Handlers.swift in Sources */ = {isa = PBXBuildFile; fileRef = A4A2424C206038D100B0A804 /* Handlers.swift */; }; - 267C1D5920404EDB0087808F /* AlertImageViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 267C1D5720404EDB0087808F /* AlertImageViewController.swift */; }; - 267C1D5A20404EDB0087808F /* AlertImageViewControllerConstraints.swift in Sources */ = {isa = PBXBuildFile; fileRef = 267C1D5820404EDB0087808F /* AlertImageViewControllerConstraints.swift */; }; 267D465620AB45D200D42242 /* LanguageSettingProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2603136820A0A4B9009AC66D /* LanguageSettingProtocol.swift */; }; 267D465720AB45E200D42242 /* Customizable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 26588E6620A20E49000D3E1A /* Customizable.swift */; }; 267D465920AB4C1500D42242 /* Feature+DB.swift in Sources */ = {isa = PBXBuildFile; fileRef = 267D465820AB4C1400D42242 /* Feature+DB.swift */; }; @@ -291,7 +286,6 @@ 2683F75C203F35BE0003181A /* LongPressWithUpSwipeGestureRecognizer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2683F75B203F35BE0003181A /* LongPressWithUpSwipeGestureRecognizer.swift */; }; 2683F75E203F36150003181A /* actExtension+BERT.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2683F75D203F36140003181A /* actExtension+BERT.swift */; }; 2683F762203F36B10003181A /* BackgroundTaskHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2683F75F203F36B00003181A /* BackgroundTaskHandler.swift */; }; - 2683F763203F36B10003181A /* ConnectionSubscriberService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2683F760203F36B10003181A /* ConnectionSubscriberService.swift */; }; 2683F764203F36B10003181A /* MessageBackgroundTaskHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2683F761203F36B10003181A /* MessageBackgroundTaskHandler.swift */; }; 2683F77A203F38E30003181A /* UIPickerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2683F779203F38E30003181A /* UIPickerView.swift */; }; 2686D3201FC3E39C0079CB75 /* ContentNavigationVC.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2686D31F1FC3E39C0079CB75 /* ContentNavigationVC.swift */; }; @@ -309,6 +303,8 @@ 268C341C21075B4700F1472A /* Cancelable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 268C341B21075B4700F1472A /* Cancelable.swift */; }; 268C62E32008DA0900433705 /* UIImageExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A1DFD7D1F5370A600F3A3D8 /* UIImageExtensions.swift */; }; 2691B76D2075504A00FB207C /* MQTTServiceSchedule.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0008E9122032D5AC003E316E /* MQTTServiceSchedule.swift */; }; + 2695F1FF21625B800095A0FA /* ChangableProgress.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2695F1FE21625B800095A0FA /* ChangableProgress.swift */; }; + 2695F20121625CAB0095A0FA /* ProgressDisplayable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2695F20021625CAB0095A0FA /* ProgressDisplayable.swift */; }; 269666181FB57963009E41C1 /* RoomHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 269666171FB57963009E41C1 /* RoomHandler.swift */; }; 269848C8200E9D0400590D6F /* StarExtension+BERT.swift in Sources */ = {isa = PBXBuildFile; fileRef = 269848C7200E9D0400590D6F /* StarExtension+BERT.swift */; }; 269848CA200E9F1300590D6F /* StarModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = 269848C9200E9F1300590D6F /* StarModels.swift */; }; @@ -399,10 +395,10 @@ 26C1A3ED2031D3030009F7F0 /* OtherUserContainerViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 26C1A3EC2031D3030009F7F0 /* OtherUserContainerViewController.swift */; }; 26C1A3F02031D9E60009F7F0 /* OtherUserTableViewDS.swift in Sources */ = {isa = PBXBuildFile; fileRef = 26C1A3EF2031D9E60009F7F0 /* OtherUserTableViewDS.swift */; }; 26C1A3F32031EED30009F7F0 /* OtherUserHeaderView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 26C1A3F22031EED30009F7F0 /* OtherUserHeaderView.swift */; }; + 26C8555D215123B00037F106 /* AudioPlayable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 26C8555C215123B00037F106 /* AudioPlayable.swift */; }; 26CD3FDB2104D19D00597E62 /* AudioShortTranscribeOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 26CD3FDA2104D19D00597E62 /* AudioShortTranscribeOperation.swift */; }; 26CD3FDD2104D1DD00597E62 /* AudioConvertOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 26CD3FDC2104D1DD00597E62 /* AudioConvertOperation.swift */; }; 26D238E9781B604B721C6643 /* ScheduleMessageViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5245254E61C6EB3C6ACF4D2C /* ScheduleMessageViewController.swift */; }; - 26D35AB81FD0EFA800A5D513 /* AudioPlayer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 26D35AB71FD0EFA800A5D513 /* AudioPlayer.swift */; }; 26D621F42069778400595E13 /* ChatWheelItemView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 26D621F32069778400595E13 /* ChatWheelItemView.swift */; }; 26D6D227212EDA6600EA2419 /* ConvertMessageDAOProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 26D6D226212EDA6600EA2419 /* ConvertMessageDAOProtocol.swift */; }; 26D6D229212EDADC00EA2419 /* ConvertMessageDAO.swift in Sources */ = {isa = PBXBuildFile; fileRef = 26D6D228212EDADC00EA2419 /* ConvertMessageDAO.swift */; }; @@ -423,6 +419,8 @@ 26DCB25420692237001EF0AB /* Array+Feature.swift in Sources */ = {isa = PBXBuildFile; fileRef = 26DCB25320692237001EF0AB /* Array+Feature.swift */; }; 26DCB256206924B3001EF0AB /* FeatureFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 26DCB255206924B3001EF0AB /* FeatureFactory.swift */; }; 26DE8D9120FE1AF500C41096 /* ChatCellFooterView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 26DE8D9020FE1AF500C41096 /* ChatCellFooterView.swift */; }; + 26E0C44721469E9800A58ECD /* ConnectionService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 26E0C44621469E9800A58ECD /* ConnectionService.swift */; }; + 26E0C4482146D5F900A58ECD /* ConnectionService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 26E0C44621469E9800A58ECD /* ConnectionService.swift */; }; 26E3229320E4F19A00271413 /* MessageParser.swift in Sources */ = {isa = PBXBuildFile; fileRef = 26E3229220E4F19A00271413 /* MessageParser.swift */; }; 26E476591FFEE2D400C06C05 /* Modelka.swift in Sources */ = {isa = PBXBuildFile; fileRef = 26E476581FFEE2D400C06C05 /* Modelka.swift */; }; 26E7D04A1FCB8973001C69B7 /* Amazon+FileSync.swift in Sources */ = {isa = PBXBuildFile; fileRef = 26E7D0491FCB8973001C69B7 /* Amazon+FileSync.swift */; }; @@ -437,13 +435,12 @@ 26F03C0D20698B0000712CB0 /* ChatWheelItemModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 26F03C0C20698B0000712CB0 /* ChatWheelItemModel.swift */; }; 26F47052201B7248005D3192 /* ReturnToCallView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 26F47051201B7248005D3192 /* ReturnToCallView.swift */; }; 26F5C8BE206BD49B003A7FF5 /* DefaultActionItemModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 26F5C8BD206BD49B003A7FF5 /* DefaultActionItemModel.swift */; }; + 26F87DF62142B40F000ED2C8 /* SenderService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 26F87DF52142B40F000ED2C8 /* SenderService.swift */; }; 26FA420A2017ADF000E6F6EC /* StarMessageCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 26FA42092017ADF000E6F6EC /* StarMessageCell.swift */; }; 26FA420C2017AE3300E6F6EC /* StarMessageCellLayout.swift in Sources */ = {isa = PBXBuildFile; fileRef = 26FA420B2017AE3300E6F6EC /* StarMessageCellLayout.swift */; }; 26FA420E201812D600E6F6EC /* StarTableDS.swift in Sources */ = {isa = PBXBuildFile; fileRef = 26FA420D201812D600E6F6EC /* StarTableDS.swift */; }; 26FA4210201821B400E6F6EC /* StarHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 26FA420F201821B400E6F6EC /* StarHandler.swift */; }; - 26FF00A51FCC2EC4002170B1 /* MQTTServiceAuth.swift in Sources */ = {isa = PBXBuildFile; fileRef = 359EB27F1F9A2D2E00147437 /* MQTTServiceAuth.swift */; }; 26FF00A61FCC2ED5002170B1 /* MQTTService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A8045CD1F60C8E200AED866 /* MQTTService.swift */; }; - 27F05908F44F464BB3903C89 /* WebViewViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64A724F7272244110EBB528A /* WebViewViewController.swift */; }; 2910A0129CA29C35161DD692 /* EditPhotoInteractor.swift in Sources */ = {isa = PBXBuildFile; fileRef = CA8C095C6950E26F3BC48A1C /* EditPhotoInteractor.swift */; }; 2AC52C9C5598DB3C4D3D9364 /* AddContactViaPhoneWireframe.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0B9B8F45FB35DCF8BCAC82EB /* AddContactViaPhoneWireframe.swift */; }; 2B924FFB43474DF387A06D67 /* VideoPreviewPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7F0193413D570F0548B2E55F /* VideoPreviewPresenter.swift */; }; @@ -488,7 +485,6 @@ 38182BD2C2E0C783796C8AA1 /* QRCodeReaderInteractor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4D247CBC45C1C1267BBBB289 /* QRCodeReaderInteractor.swift */; }; 3819EAEB412EBA913146F443 /* HistoryPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61B964D5CB991533BA5C164C /* HistoryPresenter.swift */; }; 3A0281F71F53794800206871 /* UIViewExtenstions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A0281F61F53794800206871 /* UIViewExtenstions.swift */; }; - 3A1146651ED6E85A006BA132 /* SoundService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A1146641ED6E85A006BA132 /* SoundService.swift */; }; 3A1146681ED6F047006BA132 /* ring.mp3 in Resources */ = {isa = PBXBuildFile; fileRef = 3A1146671ED6F047006BA132 /* ring.mp3 */; }; 3A19FEAD1F3B7F1D00ACE750 /* MessageHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A19FEAC1F3B7F1D00ACE750 /* MessageHandler.swift */; }; 3A1AAFCE1F3DF0470098780A /* DateExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A1AAFCD1F3DF0470098780A /* DateExtensions.swift */; }; @@ -501,7 +497,6 @@ 3A1DFD7E1F5370A600F3A3D8 /* UIImageExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A1DFD7D1F5370A600F3A3D8 /* UIImageExtensions.swift */; }; 3A1EB9A51F3A848A00658E93 /* HistoryHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A1EB9A41F3A848A00658E93 /* HistoryHandler.swift */; }; 3A1F74FA1F5ED344009A11E4 /* PushService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A1F74F91F5ED344009A11E4 /* PushService.swift */; }; - 3A213F7A1F0082AC006DBE91 /* VerifyNumberVC.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A213F791F0082AC006DBE91 /* VerifyNumberVC.swift */; }; 3A213F7C1F0093F0006DBE91 /* LoginView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A213F7B1F0093F0006DBE91 /* LoginView.swift */; }; 3A2171511EFB25C400F34B8B /* BaseVC.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A2171501EFB25C400F34B8B /* BaseVC.swift */; }; 3A21EFFC1F3B154A00AE61EC /* SendModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A21EFFB1F3B154A00AE61EC /* SendModel.swift */; }; @@ -523,7 +518,6 @@ 3A8045D61F60C93D00AED866 /* MQTTServiceChat.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A8045D51F60C93D00AED866 /* MQTTServiceChat.swift */; }; 3A8045D81F60C98200AED866 /* MQTTServiceHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A8045D71F60C98200AED866 /* MQTTServiceHelper.swift */; }; 3A8045DA1F60E18E00AED866 /* Queue.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A8045D91F60E18E00AED866 /* Queue.swift */; }; - 3A82187F1EDEEDF400337B05 /* AlertManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A82187D1EDEEDF400337B05 /* AlertManager.swift */; }; 3A8218881EDF102D00337B05 /* Color.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A8218871EDF102D00337B05 /* Color.swift */; }; 3AA13C761F2252F900BE5D8F /* SearchModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3AA13C751F2252F900BE5D8F /* SearchModel.swift */; }; 3AA4E6ACDBCB060172A7A279 /* FavoritesProtocols.swift in Sources */ = {isa = PBXBuildFile; fileRef = 462440AD41D807CE8957FDD9 /* FavoritesProtocols.swift */; }; @@ -539,7 +533,7 @@ 3AF8E26F1F42E33300D81390 /* ReturnToCallContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3AF8E26E1F42E33300D81390 /* ReturnToCallContentView.swift */; }; 3CDA490701EC3FEAAC2E9AFE /* TopUpAccountInteractor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 498AA2E3A69072FEC336C1ED /* TopUpAccountInteractor.swift */; }; 3D7B572828F83EAFEDA78CEA /* MapViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4489153750EAC34408B967C0 /* MapViewController.swift */; }; - 40C2631343E285717633ADFA /* AuthPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = B051231EAD6BB435200B4C74 /* AuthPresenter.swift */; }; + 40C2631343E285717633ADFA /* LoginPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = B051231EAD6BB435200B4C74 /* LoginPresenter.swift */; }; 40D11B34597AE40C8B71E59C /* AddContactByUsernameInteractor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 22A35AB9BCD04336504B01A9 /* AddContactByUsernameInteractor.swift */; }; 43711F24FF65C36730467BFF /* EditPhotoViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 047FFB74EBFF53E57AB3EB3E /* EditPhotoViewController.swift */; }; 43F333D298934DCBAC8D8192 /* EditGroupNamePresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = CA78C91DFDF5884E382D38FA /* EditGroupNamePresenter.swift */; }; @@ -588,9 +582,27 @@ 4B4266C1204D917800194BC1 /* ActionsView+Layout.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B4266C0204D917800194BC1 /* ActionsView+Layout.swift */; }; 4B4266C3204D923400194BC1 /* Array+UIView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B4266C2204D923400194BC1 /* Array+UIView.swift */; }; 4B5A714D204F069000A551F5 /* ChatService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B5A714C204F069000A551F5 /* ChatService.swift */; }; + 4B71AC36216215F600E4583B /* DBRoomExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B71AC35216215F600E4583B /* DBRoomExtension.swift */; }; + 4B71AC4221622A6A00E4583B /* AppNotificationsProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B71AC4121622A6A00E4583B /* AppNotificationsProvider.swift */; }; + 4B71AC4521622AA700E4583B /* AppNotificationsProviding.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B71AC4421622AA700E4583B /* AppNotificationsProviding.swift */; }; 4B736D4720237C140028F2CB /* CGSizeExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 269D9DEF1FC3AF0D00324263 /* CGSizeExtension.swift */; }; 4B736D4920238FA40028F2CB /* ThumbnailGenerator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B736D4820238FA40028F2CB /* ThumbnailGenerator.swift */; }; + 4B749F05214FEE4F002F3A33 /* VerifyNumberPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B749F00214FEE4F002F3A33 /* VerifyNumberPresenter.swift */; }; + 4B749F06214FEE4F002F3A33 /* VerifyNumberViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B749F01214FEE4F002F3A33 /* VerifyNumberViewController.swift */; }; + 4B749F07214FEE4F002F3A33 /* VerifyNumberProtocols.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B749F02214FEE4F002F3A33 /* VerifyNumberProtocols.swift */; }; + 4B749F08214FEE4F002F3A33 /* VerifyNumberInteractor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B749F03214FEE4F002F3A33 /* VerifyNumberInteractor.swift */; }; + 4B749F09214FEE4F002F3A33 /* VerifyNumberWireFrame.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B749F04214FEE4F002F3A33 /* VerifyNumberWireFrame.swift */; }; 4B7B81C62044790700C2EFCF /* TimeZoneLocal.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B7B81C52044790700C2EFCF /* TimeZoneLocal.swift */; }; + 4B7C73F0215A5509007924DB /* SMSCodeProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B7C73E9215A5508007924DB /* SMSCodeProvider.swift */; }; + 4B7C73F1215A5509007924DB /* SMSCodeProviding.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B7C73EA215A5508007924DB /* SMSCodeProviding.swift */; }; + 4B7C73F2215A5509007924DB /* MotionManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B7C73EC215A5508007924DB /* MotionManager.swift */; }; + 4B7C73F3215A5509007924DB /* LogWriter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B7C73EE215A5508007924DB /* LogWriter.swift */; }; + 4B7C73F4215A5509007924DB /* LogService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B7C73EF215A5508007924DB /* LogService.swift */; }; + 4B7C73F9215A5522007924DB /* DebugLogs.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B7C73F6215A5522007924DB /* DebugLogs.swift */; }; + 4B7C73FA215A5522007924DB /* UIView+Debug.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B7C73F7215A5522007924DB /* UIView+Debug.swift */; }; + 4B7C73FB215A5522007924DB /* UILabel+Debug.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B7C73F8215A5522007924DB /* UILabel+Debug.swift */; }; + 4B7C73FC215A552C007924DB /* LogService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B7C73EF215A5508007924DB /* LogService.swift */; }; + 4B7C73FD215A553F007924DB /* LogWriter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B7C73EE215A5508007924DB /* LogWriter.swift */; }; 4B8996C8204ECE9B00DCB183 /* ContactDAO.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B8996C7204ECE9B00DCB183 /* ContactDAO.swift */; }; 4B8996CA204ECEA700DCB183 /* ContactDAOProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B8996C9204ECEA700DCB183 /* ContactDAOProtocol.swift */; }; 4B8996CD204ED33400DCB183 /* StarDAOProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B8996CC204ED33400DCB183 /* StarDAOProtocol.swift */; }; @@ -605,14 +617,29 @@ 4B8996F2204EF5E900DCB183 /* ChatCheckpointDAO.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B8996F1204EF5E900DCB183 /* ChatCheckpointDAO.swift */; }; 4B8996F5204EF75500DCB183 /* FeedDAOProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B8996F4204EF75500DCB183 /* FeedDAOProtocol.swift */; }; 4B8996F7204EF77100DCB183 /* FeedDAO.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B8996F6204EF77100DCB183 /* FeedDAO.swift */; }; + 4B8AB702215CD02100C69DE1 /* SequenceExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B8AB701215CD02100C69DE1 /* SequenceExtension.swift */; }; + 4B8AB705215CE52300C69DE1 /* SequenceExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B8AB701215CD02100C69DE1 /* SequenceExtension.swift */; }; 4B8BEDE1204979AA00C7D625 /* ImagesView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B8BEDE0204979AA00C7D625 /* ImagesView.swift */; }; 4BAB9CE02035CAE700385520 /* ScheduleInfo.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4BAB9CDF2035CAE700385520 /* ScheduleInfo.swift */; }; 4BAB9CE22035CAF500385520 /* ScheduleContentType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4BAB9CE12035CAF500385520 /* ScheduleContentType.swift */; }; 4BAB9CE42035CB0A00385520 /* ScheduleTarget.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4BAB9CE32035CB0A00385520 /* ScheduleTarget.swift */; }; 4BAB9CE62035CB3800385520 /* ScheduleDisplayInfo.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4BAB9CE52035CB3800385520 /* ScheduleDisplayInfo.swift */; }; + 4BB0EFBB2151347900704136 /* AlertImageViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4BB0EFB82151347900704136 /* AlertImageViewController.swift */; }; + 4BB0EFBC2151347900704136 /* AlertImageViewControllerConstraints.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4BB0EFB92151347900704136 /* AlertImageViewControllerConstraints.swift */; }; + 4BB0EFBD2151347900704136 /* AlertManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4BB0EFBA2151347900704136 /* AlertManager.swift */; }; 4BD53BF4202C8BCA00569C1A /* AVURLAsset+Duration.swift in Sources */ = {isa = PBXBuildFile; fileRef = E77FBDDC1FFE828400BDB255 /* AVURLAsset+Duration.swift */; }; 4BDC7E61203492CA00BCD381 /* TopSwipable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4BDC7E60203492CA00BCD381 /* TopSwipable.swift */; }; 4BDC7E63203494C000BCD381 /* ScheduleButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4BDC7E62203494C000BCD381 /* ScheduleButton.swift */; }; + 4BE2C5D92142EAC500A73DD9 /* AudioPlayer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4BE2C5D02142EAC500A73DD9 /* AudioPlayer.swift */; }; + 4BE2C5DA2142EAC500A73DD9 /* AudioSessionManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4BE2C5D22142EAC500A73DD9 /* AudioSessionManager.swift */; }; + 4BE2C5DB2142EAC500A73DD9 /* AudioRecorder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4BE2C5D42142EAC500A73DD9 /* AudioRecorder.swift */; }; + 4BE2C5DC2142EAC500A73DD9 /* SoundBundle.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4BE2C5D62142EAC500A73DD9 /* SoundBundle.swift */; }; + 4BE2C5DD2142EAC500A73DD9 /* SystemSoundManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4BE2C5D72142EAC500A73DD9 /* SystemSoundManager.swift */; }; + 4BE2C5DE2142EAC500A73DD9 /* Sound.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4BE2C5D82142EAC500A73DD9 /* Sound.swift */; }; + 4BE2C5E22142EB0F00A73DD9 /* AudioManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4BE2C5E02142EB0E00A73DD9 /* AudioManager.swift */; }; + 4BE2C5E32142EB0F00A73DD9 /* AudioManagerDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4BE2C5E12142EB0E00A73DD9 /* AudioManagerDelegate.swift */; }; + 4BE2C5E72142EB5A00A73DD9 /* NynjaCommunicatorService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4BE2C5E52142EB5A00A73DD9 /* NynjaCommunicatorService.swift */; }; + 4BE2C5E82142EB5A00A73DD9 /* NynjaRingingService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4BE2C5E62142EB5A00A73DD9 /* NynjaRingingService.swift */; }; 4BEE89D69CACB85ABEE9046F /* QRCodeGeneratorPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = DFAB7D8D9024C26FA51BF783 /* QRCodeGeneratorPresenter.swift */; }; 4C5EEA13EBC6A8398F08DCD1 /* MainWireframe.swift in Sources */ = {isa = PBXBuildFile; fileRef = 65AAB8F770774CE3AE3FD6E1 /* MainWireframe.swift */; }; 4D53FE7454959323B1CCFD96 /* ProfileViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D270F638DBB2D8FC1BDEB633 /* ProfileViewController.swift */; }; @@ -627,7 +654,6 @@ 5AD8110B5B87B1AB9F1C5B52 /* CreateGroupPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8CBACEAABEE65D7EC5572C4E /* CreateGroupPresenter.swift */; }; 5B5EE777EF301CFC1FDCF307 /* CreateGroupInteractor.swift in Sources */ = {isa = PBXBuildFile; fileRef = AFC76E2B3DD0BCA0A622A5CD /* CreateGroupInteractor.swift */; }; 5BBEF53C212DE09F00F10768 /* ringback.m4a in Resources */ = {isa = PBXBuildFile; fileRef = 5BBEF53B212DE09F00F10768 /* ringback.m4a */; }; - 5BC1D37320D3B3D9002A44B3 /* NynjaCommunicatorService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5BC1D37220D3B3D8002A44B3 /* NynjaCommunicatorService.swift */; }; 5BC1D37920D3B4A8002A44B3 /* GroupCollectionViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5BC1D37420D3B4A6002A44B3 /* GroupCollectionViewCell.swift */; }; 5BC1D37B20D3B4A8002A44B3 /* GroupAddParticipantsCollectionViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5BC1D37620D3B4A7002A44B3 /* GroupAddParticipantsCollectionViewCell.swift */; }; 5BC1D37D20D3B4A8002A44B3 /* GroupCollectionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5BC1D37820D3B4A7002A44B3 /* GroupCollectionView.swift */; }; @@ -636,12 +662,12 @@ 5BC1D38420D3B670002A44B3 /* CallCreatorMediator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5BC1D38320D3B670002A44B3 /* CallCreatorMediator.swift */; }; 5C468A609C445962C0D19DD3 /* HistoryViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3D6900257B1AB2CA0BC834EB /* HistoryViewController.swift */; }; 5DBBAAF3AAB09B2D4E71B806 /* AddContactViaPhoneViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4FB993F14055EAE59F572530 /* AddContactViaPhoneViewController.swift */; }; + 5E0CEA9A21490663004B3F7A /* TypingStatusCache.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5E0CEA9921490663004B3F7A /* TypingStatusCache.swift */; }; 5E278E14F45F56BACB71271C /* VideoPreviewWireframe.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7F5541C91FE7845F3E5C7EB2 /* VideoPreviewWireframe.swift */; }; 5EB13FDBA6153EE67366115F /* ScheduleMessageInteractor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5095F3CF5921F107D81C8652 /* ScheduleMessageInteractor.swift */; }; 5ED473EC698E99DC021E553A /* MapSearchInteractor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2BD49CF323041B47A752603E /* MapSearchInteractor.swift */; }; 619C44B00CC7B169077CDEC2 /* EditProfileProtocols.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5B377AA90A6B6BA0120C31F1 /* EditProfileProtocols.swift */; }; 628E2C26BE0854DB1DF64990 /* SplashWireframe.swift in Sources */ = {isa = PBXBuildFile; fileRef = C22259D46BE5732B494C4C7D /* SplashWireframe.swift */; }; - 62BCFA14D06D96AFFE53D8BE /* WebViewPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = E8C95C96D48216CC93C04682 /* WebViewPresenter.swift */; }; 63E6537BBBD814F6DF3DC589 /* Pods_Nynja.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 35B1AB871FA278D900E65233 /* Pods_Nynja.framework */; }; 6547BE911E492D790E0D4390 /* EditGroupNameInteractor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0FF56F6F8D90FB98A6B42971 /* EditGroupNameInteractor.swift */; }; 65AC1F6564EEFA0439F5C236 /* QRCodeReaderWireframe.swift in Sources */ = {isa = PBXBuildFile; fileRef = C94DE9A5381CE32AC25455A6 /* QRCodeReaderWireframe.swift */; }; @@ -667,7 +693,7 @@ 6DD72F601F1547AC008CFF83 /* GCD.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6DD72F5F1F1547AC008CFF83 /* GCD.swift */; }; 6DEEE1931F1F9CF6000FAF09 /* UIViewController+Child.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6DEEE1921F1F9CF6000FAF09 /* UIViewController+Child.swift */; }; 6E7CD38810BC3B896070C819 /* EditGroupNameWireframe.swift in Sources */ = {isa = PBXBuildFile; fileRef = 273EABBCA8570D21A8683273 /* EditGroupNameWireframe.swift */; }; - 6F3F21025258D8071BCF95EF /* AuthWireframe.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9FB2678C74CCC58C8AAFADD6 /* AuthWireframe.swift */; }; + 6F3F21025258D8071BCF95EF /* LoginWireframe.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9FB2678C74CCC58C8AAFADD6 /* LoginWireframe.swift */; }; 705B483A1FCDEA2273CEFE2C /* EditPhotoWireframe.swift in Sources */ = {isa = PBXBuildFile; fileRef = F56141F2CF85255940EA304F /* EditPhotoWireframe.swift */; }; 731181233D84FD4F41936981 /* EditGroupPhotoProtocols.swift in Sources */ = {isa = PBXBuildFile; fileRef = C3E427A83589B2A635F99BC0 /* EditGroupPhotoProtocols.swift */; }; 73BFE52F809536A538E6A55E /* ImagePreviewViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8181D695D260804FB2F3102E /* ImagePreviewViewController.swift */; }; @@ -716,6 +742,10 @@ 850833DB2037171600587EEF /* FileExtensionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 850833DA2037171600587EEF /* FileExtensionView.swift */; }; 8509452B206E684300B43C1C /* AddParticipantsContactCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8509452A206E684300B43C1C /* AddParticipantsContactCell.swift */; }; 8509AC62206A54420089089B /* ResponseResult.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8509AC61206A54420089089B /* ResponseResult.swift */; }; + 8509FC852158F7D100734D93 /* AppGroupFlagContainer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8509FC842158F7D100734D93 /* AppGroupFlagContainer.swift */; }; + 8509FC872158F7FC00734D93 /* DirectoryWatcher.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8509FC862158F7FC00734D93 /* DirectoryWatcher.swift */; }; + 8509FC89215908B300734D93 /* AppGroupFlagObserver.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8509FC88215908B300734D93 /* AppGroupFlagObserver.swift */; }; + 8509FC8A2159095900734D93 /* AppGroupFlagContainer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8509FC842158F7D100734D93 /* AppGroupFlagContainer.swift */; }; 850A0C6520469AED004F79AD /* UserSettingsRespondable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 850A0C6420469AED004F79AD /* UserSettingsRespondable.swift */; }; 850A0C672046B65D004F79AD /* WCItemsFactoryDecorator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 850A0C662046B65D004F79AD /* WCItemsFactoryDecorator.swift */; }; 850A2BB0203584B000D68FDF /* SearchActionsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 850A2BAF203584B000D68FDF /* SearchActionsView.swift */; }; @@ -738,6 +768,7 @@ 850FC5FA2032F64100832D87 /* ForwardSelectorWireFrame.swift in Sources */ = {isa = PBXBuildFile; fileRef = 850FC5F92032F64100832D87 /* ForwardSelectorWireFrame.swift */; }; 850FC60F203310D200832D87 /* SelectionAvatarView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 850FC60E203310D200832D87 /* SelectionAvatarView.swift */; }; 850FC611203312FA00832D87 /* ForwardSelectorViewControllerLayout.swift in Sources */ = {isa = PBXBuildFile; fileRef = 850FC610203312FA00832D87 /* ForwardSelectorViewControllerLayout.swift */; }; + 851105BB2163D0C800F07019 /* TranscribeResponseData.swift in Sources */ = {isa = PBXBuildFile; fileRef = 851105BA2163D0C800F07019 /* TranscribeResponseData.swift */; }; 8511D3712034427F00B2A620 /* UIView+SafeArea.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8511D3702034427F00B2A620 /* UIView+SafeArea.swift */; }; 8511D3742034596E00B2A620 /* Collection+ViewLayout.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8511D3732034596E00B2A620 /* Collection+ViewLayout.swift */; }; 8512349221221B9E000129A2 /* Collection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8512349121221B9E000129A2 /* Collection.swift */; }; @@ -892,10 +923,8 @@ 8562853920D166E5000C9739 /* CollectionPreviewState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8562853820D166E5000C9739 /* CollectionPreviewState.swift */; }; 8562853B20D16C61000C9739 /* LongPressClosureRecognizer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8562853A20D16C61000C9739 /* LongPressClosureRecognizer.swift */; }; 85629ECA2137EF2400A79C97 /* VoiceAudioInteractive.swift in Sources */ = {isa = PBXBuildFile; fileRef = 85629EC92137EF2400A79C97 /* VoiceAudioInteractive.swift */; }; - 8566771C20C139A000DD4204 /* DebugLogs.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8566771B20C139A000DD4204 /* DebugLogs.swift */; }; 8566771E20C1579C00DD4204 /* StorageSubscriberReference.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8566771D20C1579C00DD4204 /* StorageSubscriberReference.swift */; }; 8566772020C1924500DD4204 /* MessageInteractor+MessageHandlerSubscriber.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8566771F20C1924500DD4204 /* MessageInteractor+MessageHandlerSubscriber.swift */; }; - 856B827821184FDF00917F90 /* AudioSessionManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 856B827721184FDF00917F90 /* AudioSessionManager.swift */; }; 8572C3B62092315B00E4840C /* CollectionViewDataProxy.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8572C3B52092315B00E4840C /* CollectionViewDataProxy.swift */; }; 8572C3B92092364C00E4840C /* StickerPackageDataSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8572C3B82092364C00E4840C /* StickerPackageDataSource.swift */; }; 8572C3BB2092366100E4840C /* StickerCollectionDataSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8572C3BA2092366100E4840C /* StickerCollectionDataSource.swift */; }; @@ -958,14 +987,18 @@ 859B863920486068003272B2 /* CarouselPickerViewControllerLayout.swift in Sources */ = {isa = PBXBuildFile; fileRef = 859B863820486068003272B2 /* CarouselPickerViewControllerLayout.swift */; }; 859C429F2056829300AE3797 /* NotificationSettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = 859C429E2056829300AE3797 /* NotificationSettings.swift */; }; 859C42A5205691FB00AE3797 /* Sounds.json in Resources */ = {isa = PBXBuildFile; fileRef = 859C42A4205691FB00AE3797 /* Sounds.json */; }; - 859C42A82056940500AE3797 /* Sound.swift in Sources */ = {isa = PBXBuildFile; fileRef = 859C42A72056940500AE3797 /* Sound.swift */; }; - 859C42AA2056B05D00AE3797 /* SoundBundle.swift in Sources */ = {isa = PBXBuildFile; fileRef = 859C42A92056B05D00AE3797 /* SoundBundle.swift */; }; 859C42AD2056BF9F00AE3797 /* incoming_message.mp3 in Resources */ = {isa = PBXBuildFile; fileRef = 859C42AC2056BF9F00AE3797 /* incoming_message.mp3 */; }; 859F9B4C2035CB1E009D017A /* ForwardContent.swift in Sources */ = {isa = PBXBuildFile; fileRef = 859F9B4B2035CB1E009D017A /* ForwardContent.swift */; }; + 85A3CA02214129F200E0EDD5 /* KeyboardInteractive.swift in Sources */ = {isa = PBXBuildFile; fileRef = 85A3CA01214129F200E0EDD5 /* KeyboardInteractive.swift */; }; + 85A3CA03214133FD00E0EDD5 /* KeyboardInteractive.swift in Sources */ = {isa = PBXBuildFile; fileRef = 85A3CA01214129F200E0EDD5 /* KeyboardInteractive.swift */; }; + 85ADEB7921621CAD00ABECBD /* InputTextStorage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 85ADEB7821621CAD00ABECBD /* InputTextStorage.swift */; }; + 85ADEB7B2162445200ABECBD /* InputTextStorageDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 85ADEB7A2162445200ABECBD /* InputTextStorageDelegate.swift */; }; 85B0013221270DEC000C89FE /* TableOrder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 85B0013121270DEC000C89FE /* TableOrder.swift */; }; 85B0013421272694000C89FE /* MessageInteractor+History.swift in Sources */ = {isa = PBXBuildFile; fileRef = 85B0013321272694000C89FE /* MessageInteractor+History.swift */; }; 85B750A120334A2B00AD6013 /* ForwardTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 85B750A020334A2B00AD6013 /* ForwardTableViewCell.swift */; }; 85BA176120BEA7BD001EF8AC /* StickerPreviewContainerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 85BA176020BEA7BD001EF8AC /* StickerPreviewContainerView.swift */; }; + 85BDD2B821465EFA00695DE5 /* ScrollDirection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 85BDD2B721465EFA00695DE5 /* ScrollDirection.swift */; }; + 85BDD2BA21467A9500695DE5 /* MessageFactoryProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 85BDD2B921467A9500695DE5 /* MessageFactoryProtocol.swift */; }; 85BEC0E12063F91C0098C99C /* TimeZoneCellModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 85BEC0E02063F91C0098C99C /* TimeZoneCellModel.swift */; }; 85C16C3520D2520E00EDB77E /* StickersDownloadingService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 85C16C3420D2520E00EDB77E /* StickersDownloadingService.swift */; }; 85C16C3C20D261C000EDB77E /* MessageStickerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 85C16C3B20D261C000EDB77E /* MessageStickerView.swift */; }; @@ -1006,11 +1039,9 @@ 85E1DD2520BEBE17008AD211 /* MessageVC+StickerInputModuleDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 85E1DD2420BEBE17008AD211 /* MessageVC+StickerInputModuleDelegate.swift */; }; 85E1DD2720BEE961008AD211 /* ScalableCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 85E1DD2620BEE961008AD211 /* ScalableCell.swift */; }; 85E3AB3D21218A57005FC49A /* SeparatorView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8580BAE620BD9A5600239D9D /* SeparatorView.swift */; }; - 85EBBE032056CF97009BB269 /* SoundPlayer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 85EBBE022056CF97009BB269 /* SoundPlayer.swift */; }; 85EBBE052056E8B2009BB269 /* outcoming_message.mp3 in Resources */ = {isa = PBXBuildFile; fileRef = 85EBBE042056E8B2009BB269 /* outcoming_message.mp3 */; }; 85F0866220D6412300A7762E /* RemoteStorageDestination.swift in Sources */ = {isa = PBXBuildFile; fileRef = 85F0866120D6412300A7762E /* RemoteStorageDestination.swift */; }; 85F0866320D6551500A7762E /* RemoteStorageDestination.swift in Sources */ = {isa = PBXBuildFile; fileRef = 85F0866120D6412300A7762E /* RemoteStorageDestination.swift */; }; - 85F43C2BE9C631868C394BCD /* WebViewWireframe.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1C469B351D6DA4CEBCE9C591 /* WebViewWireframe.swift */; }; 87A3D03524B9258B33726A57 /* HistoryInteractor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5D3E868EE32625048BCB13A8 /* HistoryInteractor.swift */; }; 87DE79674FF430A52D2A0BB7 /* MyGroupAliasProtocols.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8606C1D61AA46EB77821B1B0 /* MyGroupAliasProtocols.swift */; }; 896D51F07E2F79C8B5502DBF /* EditGroupPhotoInteractor.swift in Sources */ = {isa = PBXBuildFile; fileRef = AC691A6DB133F6319B4FCC4F /* EditGroupPhotoInteractor.swift */; }; @@ -1628,10 +1659,9 @@ A9C6233FE6A819AAA64C1A35 /* QRCodeReaderProtocols.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6973AB5F10C55A11E6DC94F5 /* QRCodeReaderProtocols.swift */; }; AB8501A3A8E471294BB618E6 /* DateTimePickerProtocols.swift in Sources */ = {isa = PBXBuildFile; fileRef = ACA246C7DEB7D8740727EE22 /* DateTimePickerProtocols.swift */; }; ACD15567460FFE46A0AAF51E /* EditProfileInteractor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 68ACF4CFD3CBCC2B592BA052 /* EditProfileInteractor.swift */; }; - AF440BA5CEBE5170D082FF60 /* AuthProtocols.swift in Sources */ = {isa = PBXBuildFile; fileRef = CDF62E1A004579220E231142 /* AuthProtocols.swift */; }; + AF440BA5CEBE5170D082FF60 /* LoginProtocols.swift in Sources */ = {isa = PBXBuildFile; fileRef = CDF62E1A004579220E231142 /* LoginProtocols.swift */; }; B06A314CF727CB6D3C9AF2A4 /* AddContactWireframe.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA65B365E335DD42254D1CF4 /* AddContactWireframe.swift */; }; B16EC832C763628A2EBBD383 /* MapSearchProtocols.swift in Sources */ = {isa = PBXBuildFile; fileRef = B28D1FE755A0457DBEDAC068 /* MapSearchProtocols.swift */; }; - B18705FCC88EEA39EA6DCD7E /* WebViewInteractor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8DC3F2CD754DE6EC29F6DF88 /* WebViewInteractor.swift */; }; B1B8ED3EDB12866323C9EE74 /* QRCodeGeneratorInteractor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B15A544CC681BABD1A631AF /* QRCodeGeneratorInteractor.swift */; }; B343D5745AFBA0EE32B622C6 /* AddContactViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D4B94201A3DDCB11C62D6A16 /* AddContactViewController.swift */; }; B3D0F59E1E7BDB7E485AE662 /* GroupStorageWireframe.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9DE44A136617140435B23343 /* GroupStorageWireframe.swift */; }; @@ -1727,7 +1757,6 @@ C9B8BEFC204DDDA20018748C /* CheckmarkCellLayout.swift in Sources */ = {isa = PBXBuildFile; fileRef = C9B8BEFB204DDDA20018748C /* CheckmarkCellLayout.swift */; }; C9B8BEFE204DEBD00018748C /* DataDownloadAndUsageMode.swift in Sources */ = {isa = PBXBuildFile; fileRef = C9B8BEFD204DEBD00018748C /* DataDownloadAndUsageMode.swift */; }; C9C694F9201FA4AB00A57297 /* SlideAnimatedTransitioning.swift in Sources */ = {isa = PBXBuildFile; fileRef = C9C694F8201FA4AB00A57297 /* SlideAnimatedTransitioning.swift */; }; - C9C694FB201FA50100A57297 /* CustomPanGesture.swift in Sources */ = {isa = PBXBuildFile; fileRef = C9C694FA201FA50100A57297 /* CustomPanGesture.swift */; }; C9C694FD201FA55800A57297 /* SwipeBackHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = C9C694FC201FA55800A57297 /* SwipeBackHelper.swift */; }; C9C695032022306D00A57297 /* SelectCountryTableDataSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = C9C695022022306D00A57297 /* SelectCountryTableDataSource.swift */; }; C9C69505202230DD00A57297 /* SelectCountryCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = C9C69504202230DD00A57297 /* SelectCountryCell.swift */; }; @@ -1746,9 +1775,8 @@ D3A30AF05BD7C46A9A8C1FC1 /* GroupStorageProtocols.swift in Sources */ = {isa = PBXBuildFile; fileRef = A9537952568A7532147DE548 /* GroupStorageProtocols.swift */; }; D6DA5ECD070E781A74699FF5 /* MainViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1457809A715A3526EBF39205 /* MainViewController.swift */; }; D764CA9732E3D09DE3DD8EDB /* QRCodeGeneratorProtocols.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8B2389EFD3432F86296722BE /* QRCodeGeneratorProtocols.swift */; }; - D839883F9B7A8CD245A85701 /* MyGroupAliasInteractor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1AAE2417131A6C8ED32A44D2 /* MyGroupAliasInteractor.swift */; }; D883A2CBD629A340B27997EF /* SplashViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 553988DBF434D09AB77837DF /* SplashViewController.swift */; }; - DAE89B7EFAB308A6B48AF5EC /* AuthInteractor.swift in Sources */ = {isa = PBXBuildFile; fileRef = B15F3B55EC2BF6FB5D7A2FAF /* AuthInteractor.swift */; }; + DAE89B7EFAB308A6B48AF5EC /* LoginInteractor.swift in Sources */ = {isa = PBXBuildFile; fileRef = B15F3B55EC2BF6FB5D7A2FAF /* LoginInteractor.swift */; }; DDDA12EC6C743547BC91276F /* ImagePreviewWireframe.swift in Sources */ = {isa = PBXBuildFile; fileRef = 515FBE9F86586E4AEF89AACD /* ImagePreviewWireframe.swift */; }; DE89BF12597D1B7D5BB68AA3 /* TopUpAccountWireframe.swift in Sources */ = {isa = PBXBuildFile; fileRef = A9898C7E717C5DA85654181E /* TopUpAccountWireframe.swift */; }; DF55CCC682DAB5392F2A763D /* FavoritesPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9C4192D925259B75441492A9 /* FavoritesPresenter.swift */; }; @@ -1826,8 +1854,6 @@ E76D132C1FA35CCF00B07F0E /* ProfilePlaceholderCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = E76D132B1FA35CCF00B07F0E /* ProfilePlaceholderCell.swift */; }; E76D132F1FA35D2900B07F0E /* ProfilePlaceholderCellLayout.swift in Sources */ = {isa = PBXBuildFile; fileRef = E76D132E1FA35D2900B07F0E /* ProfilePlaceholderCellLayout.swift */; }; E76D13311FA35F3500B07F0E /* TextCellModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = E76D13301FA35F3500B07F0E /* TextCellModel.swift */; }; - E7718BF51FB5D6080070B402 /* UIView+Debug.swift in Sources */ = {isa = PBXBuildFile; fileRef = E7718BF41FB5D6080070B402 /* UIView+Debug.swift */; }; - E7718BF71FB5D8D70070B402 /* UILabel+Debug.swift in Sources */ = {isa = PBXBuildFile; fileRef = E7718BF61FB5D8D70070B402 /* UILabel+Debug.swift */; }; E77764B41FBDA8B50042541D /* WheelContainerDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = E77764B31FBDA8B50042541D /* WheelContainerDelegate.swift */; }; E77764B61FBDA8E30042541D /* WheelContainerDataSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = E77764B51FBDA8E30042541D /* WheelContainerDataSource.swift */; }; E77764BD1FBDA9B60042541D /* ImageFullWheelItemView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E77764BA1FBDA9B60042541D /* ImageFullWheelItemView.swift */; }; @@ -1904,8 +1930,6 @@ E7F2CFE21F5EEF1E00806E43 /* PermissionManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = E7F2CFE11F5EEF1E00806E43 /* PermissionManager.swift */; }; E7F68D271FA22C45009C98D1 /* EditProfileVCStrings.swift in Sources */ = {isa = PBXBuildFile; fileRef = E7F68D261FA22C45009C98D1 /* EditProfileVCStrings.swift */; }; E7F8F55C1F7BCA090016FDF9 /* DefaultWheelConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = E7F8F55B1F7BCA090016FDF9 /* DefaultWheelConfiguration.swift */; }; - E7FF40B01F9602C400810D1C /* AudioManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = E7FF40AF1F9602C400810D1C /* AudioManager.swift */; }; - E7FF40B41F96089B00810D1C /* AudioManagerDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = E7FF40B31F96089B00810D1C /* AudioManagerDelegate.swift */; }; E8AFC57E49EED25C3F5001B7 /* SecurityProtocols.swift in Sources */ = {isa = PBXBuildFile; fileRef = C2144825E69D46CE96C0B7D8 /* SecurityProtocols.swift */; }; EA7ABD1C9C761A1F58D89F8A /* AddParticipantsWireframe.swift in Sources */ = {isa = PBXBuildFile; fileRef = AD5E1768DC8E5A8BB9BBDD96 /* AddParticipantsWireframe.swift */; }; EA9F305BF0215CAC3602D0D9 /* EditGroupPhotoPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6D5A57913B84E0665E3ABC0E /* EditGroupPhotoPresenter.swift */; }; @@ -2021,7 +2045,6 @@ F6150A15F8A3E399EEB2C724 /* MapWireframe.swift in Sources */ = {isa = PBXBuildFile; fileRef = 17D567D263E2C21DB762E40C /* MapWireframe.swift */; }; F6A317F954DA5B46BFD50E3C /* QRCodeGeneratorWireframe.swift in Sources */ = {isa = PBXBuildFile; fileRef = C45F64793E8126ABF4E69F7B /* QRCodeGeneratorWireframe.swift */; }; F77E514BE70FF0BA3130D312 /* ScheduleMessagePresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = B62646CA6345B6C5AD0C87A0 /* ScheduleMessagePresenter.swift */; }; - F7DFBC93C800B802534D2DE1 /* WebViewProtocols.swift in Sources */ = {isa = PBXBuildFile; fileRef = 425BCBF6F15DC4F7C3674AEC /* WebViewProtocols.swift */; }; F922EF38E4C1662D54CE533D /* LanguageSettingsWireframe.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0B46EEDD33C9AD3C732AD97A /* LanguageSettingsWireframe.swift */; }; FAC3805CCCEB6D6E0758D8AD /* Pods_Nynja_Share.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 293EBD42735B5838AC2EE46E /* Pods_Nynja_Share.framework */; }; FB0AD86B20F3A07100F052CE /* ImagePreviewTransitionAnimatable.swift in Sources */ = {isa = PBXBuildFile; fileRef = FB0AD86A20F3A07100F052CE /* ImagePreviewTransitionAnimatable.swift */; }; @@ -2260,9 +2283,7 @@ 1746BDC1030434814FE63E0A /* DateTimePickerPresenter.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = DateTimePickerPresenter.swift; sourceTree = ""; }; 17B34E74A0246B17348E9597 /* Pods-Nynja.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Nynja.debug.xcconfig"; path = "Pods/Target Support Files/Pods-Nynja/Pods-Nynja.debug.xcconfig"; sourceTree = ""; }; 17D567D263E2C21DB762E40C /* MapWireframe.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = MapWireframe.swift; sourceTree = ""; }; - 1AAE2417131A6C8ED32A44D2 /* MyGroupAliasInteractor.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = MyGroupAliasInteractor.swift; sourceTree = ""; }; 1BA66D21FFC1A74CFD2F63C4 /* ProfileWireframe.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = ProfileWireframe.swift; sourceTree = ""; }; - 1C469B351D6DA4CEBCE9C591 /* WebViewWireframe.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = WebViewWireframe.swift; sourceTree = ""; }; 1E65D98C2F04244854E93EAE /* Pods-Nynja-Share.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Nynja-Share.debug.xcconfig"; path = "Pods/Target Support Files/Pods-Nynja-Share/Pods-Nynja-Share.debug.xcconfig"; sourceTree = ""; }; 1F8247671F2779AD00E5B749 /* iCarousel.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = iCarousel.h; path = iCarousel/iCarousel.h; sourceTree = ""; }; 1F8247681F2779AD00E5B749 /* iCarousel.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = iCarousel.m; path = iCarousel/iCarousel.m; sourceTree = ""; }; @@ -2305,19 +2326,18 @@ 2603139120A0A4B9009AC66D /* LanguageSettings+Helper.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "LanguageSettings+Helper.swift"; sourceTree = ""; }; 260313AE20A0A50D009AC66D /* TranslationService.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TranslationService.swift; sourceTree = ""; }; 260313C720A0BC80009AC66D /* Array+LangExtended.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "Array+LangExtended.swift"; sourceTree = ""; }; - 26052C7920FCE7E000E7A6A0 /* LogService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LogService.swift; sourceTree = ""; }; 2605311A212740FD002E1CF1 /* LogOutputProtocols.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LogOutputProtocols.swift; sourceTree = ""; }; 2605311C21274116002E1CF1 /* LogOutputView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LogOutputView.swift; sourceTree = ""; }; 2605311E21274124002E1CF1 /* LogOutputInteractor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LogOutputInteractor.swift; sourceTree = ""; }; 2605312021274133002E1CF1 /* LogOutputPresenter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LogOutputPresenter.swift; sourceTree = ""; }; 26053122212741C2002E1CF1 /* LogOutputWireFrame.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LogOutputWireFrame.swift; sourceTree = ""; }; - 260531252127455C002E1CF1 /* MotionManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MotionManager.swift; sourceTree = ""; }; 2605312821298BEF002E1CF1 /* Logoutputcell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Logoutputcell.swift; sourceTree = ""; }; 2605312A21299198002E1CF1 /* LogOutputDS.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LogOutputDS.swift; sourceTree = ""; }; 260552A51F9E1CD100D68DE6 /* SearchHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = SearchHandler.swift; path = Services/HandleServices/SearchHandler.swift; sourceTree = ""; }; 260629702056EF2800CB8F65 /* LinksCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LinksCell.swift; sourceTree = ""; }; 2606F3BB20BFE20400CF7F15 /* MessageInteractor+Translation.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "MessageInteractor+Translation.swift"; sourceTree = ""; }; - 260D67D82124616A0072F11F /* LogWriter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LogWriter.swift; sourceTree = ""; }; + 260E77D8215D3C5000D18789 /* ComingSoonExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComingSoonExtension.swift; sourceTree = ""; }; + 260E77DA215D3C7700D18789 /* ComingSoonProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComingSoonProtocol.swift; sourceTree = ""; }; 26131E01210399BA00BE94F9 /* TranscribeService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TranscribeService.swift; sourceTree = ""; }; 26142B1020472ECD004E5FE4 /* MessageLinkTable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageLinkTable.swift; sourceTree = ""; }; 26142B1220473BFD004E5FE4 /* DBMessageLink.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DBMessageLink.swift; sourceTree = ""; }; @@ -2332,7 +2352,6 @@ 2625DBF720EFC5DE00E01C05 /* FourCharCode+StringLiteralConvertible.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "FourCharCode+StringLiteralConvertible.swift"; sourceTree = ""; }; 2625F29E212463E8007C42B5 /* ProgressIdentifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProgressIdentifier.swift; sourceTree = ""; }; 262D43862033417F002F1E45 /* FriendExtansion+BERT.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "FriendExtansion+BERT.swift"; sourceTree = ""; }; - 2631C511207A4C0C00F9AA55 /* AudioRecorder.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AudioRecorder.swift; sourceTree = ""; }; 2632139020D797F500C31144 /* TranslationViewProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TranslationViewProtocol.swift; sourceTree = ""; }; 2633EF6D205212F700DB3868 /* MemberDAOProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MemberDAOProtocol.swift; sourceTree = ""; }; 2633EF6F2052130400DB3868 /* MemberDAO.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MemberDAO.swift; sourceTree = ""; }; @@ -2341,7 +2360,6 @@ 26342CA820ECBAEE00D2196B /* TranscribeNetworkClient.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TranscribeNetworkClient.swift; sourceTree = ""; }; 26342CAA20ECBB0100D2196B /* TranscribeNetworkService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TranscribeNetworkService.swift; sourceTree = ""; }; 26342CAC20ECD15100D2196B /* TranscribeShortRequestData.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TranscribeShortRequestData.swift; sourceTree = ""; }; - 26342CAE20ECD16A00D2196B /* TranscribeShortResponseData.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TranscribeShortResponseData.swift; sourceTree = ""; }; 26342CB120ECDDC400D2196B /* Encodable+Dictionary.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Encodable+Dictionary.swift"; sourceTree = ""; }; 26342CB320ECFAB600D2196B /* MessageInteractor+Transcription.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "MessageInteractor+Transcription.swift"; sourceTree = ""; }; 263529142075729400DC6FBD /* Job+DB.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Job+DB.swift"; sourceTree = ""; }; @@ -2387,7 +2405,7 @@ 2651093E20ADB81100F1B38B /* NotificationSettingProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationSettingProtocol.swift; sourceTree = ""; }; 2652D6151FA82EFE005E62C7 /* EditProfileVCLayout.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EditProfileVCLayout.swift; sourceTree = SOURCE_ROOT; }; 2652D6171FA85B28005E62C7 /* ImageSelector.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageSelector.swift; sourceTree = ""; }; - 26534B24210B4BE70003B9BC /* DBMessage+TypeExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "DBMessage+TypeExtension.swift"; sourceTree = ""; }; + 26534B24210B4BE70003B9BC /* DBMessage+Extension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "DBMessage+Extension.swift"; sourceTree = ""; }; 26541F712007B93400AAEACF /* DBMessageAction.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DBMessageAction.swift; sourceTree = ""; }; 26541F732007B9A200AAEACF /* MessageActionTable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageActionTable.swift; sourceTree = ""; }; 2657BE50201233E300F21935 /* ImageFilledItemModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageFilledItemModel.swift; sourceTree = ""; }; @@ -2415,6 +2433,7 @@ 26770A561FFD6CAC009AC870 /* SharedParameters.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SharedParameters.swift; sourceTree = ""; }; 26771CC0212ECE08006112B5 /* ConvertMessageTable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConvertMessageTable.swift; sourceTree = ""; }; 26771CC2212ED109006112B5 /* DBConvertMessage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DBConvertMessage.swift; sourceTree = ""; }; + 26773F2A215BE15800C09248 /* Array+Operation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Array+Operation.swift"; sourceTree = ""; }; 267BE2821FDE905D00C47E18 /* SettingsProtocols.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsProtocols.swift; sourceTree = ""; }; 267BE2841FDE983400C47E18 /* SettingsGroupVC.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsGroupVC.swift; sourceTree = ""; }; 267BE28D1FDE9FCC00C47E18 /* SettingsGroupWireFrame.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsGroupWireFrame.swift; sourceTree = ""; }; @@ -2426,14 +2445,11 @@ 267BE29C1FE13AB600C47E18 /* ParticipantsPresenter.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ParticipantsPresenter.swift; sourceTree = ""; }; 267BE29F1FE13AB600C47E18 /* ParticipantsViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ParticipantsViewController.swift; sourceTree = ""; }; 267BE2AC1FE13AB600C47E18 /* ParticipantsWireframe.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ParticipantsWireframe.swift; sourceTree = ""; }; - 267C1D5720404EDB0087808F /* AlertImageViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AlertImageViewController.swift; sourceTree = ""; }; - 267C1D5820404EDB0087808F /* AlertImageViewControllerConstraints.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AlertImageViewControllerConstraints.swift; sourceTree = ""; }; 267D465820AB4C1400D42242 /* Feature+DB.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Feature+DB.swift"; sourceTree = ""; }; 2683F750203F34460003181A /* ChatBaseFactory.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ChatBaseFactory.swift; sourceTree = ""; }; 2683F75B203F35BE0003181A /* LongPressWithUpSwipeGestureRecognizer.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LongPressWithUpSwipeGestureRecognizer.swift; sourceTree = ""; }; 2683F75D203F36140003181A /* actExtension+BERT.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "actExtension+BERT.swift"; sourceTree = ""; }; 2683F75F203F36B00003181A /* BackgroundTaskHandler.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BackgroundTaskHandler.swift; sourceTree = ""; }; - 2683F760203F36B10003181A /* ConnectionSubscriberService.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ConnectionSubscriberService.swift; sourceTree = ""; }; 2683F761203F36B10003181A /* MessageBackgroundTaskHandler.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MessageBackgroundTaskHandler.swift; sourceTree = ""; }; 2683F779203F38E30003181A /* UIPickerView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UIPickerView.swift; sourceTree = ""; }; 2686D31F1FC3E39C0079CB75 /* ContentNavigationVC.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentNavigationVC.swift; sourceTree = ""; }; @@ -2449,6 +2465,8 @@ 268C341621074AD000F1472A /* AudioLongTranscribeProccessingOperation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AudioLongTranscribeProccessingOperation.swift; sourceTree = ""; }; 268C341821074D6C00F1472A /* TranscribeLongOperationResponseData.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TranscribeLongOperationResponseData.swift; sourceTree = ""; }; 268C341B21075B4700F1472A /* Cancelable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Cancelable.swift; sourceTree = ""; }; + 2695F1FE21625B800095A0FA /* ChangableProgress.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChangableProgress.swift; sourceTree = ""; }; + 2695F20021625CAB0095A0FA /* ProgressDisplayable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProgressDisplayable.swift; sourceTree = ""; }; 269666171FB57963009E41C1 /* RoomHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = RoomHandler.swift; path = Services/HandleServices/RoomHandler.swift; sourceTree = ""; }; 269848C7200E9D0400590D6F /* StarExtension+BERT.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = "StarExtension+BERT.swift"; path = "Nynja/Services/MQTT/StarExtension+BERT.swift"; sourceTree = SOURCE_ROOT; }; 269848C9200E9F1300590D6F /* StarModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = StarModels.swift; path = Services/Models/StarModels.swift; sourceTree = ""; }; @@ -2494,9 +2512,9 @@ 26C1A3EC2031D3030009F7F0 /* OtherUserContainerViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OtherUserContainerViewController.swift; sourceTree = ""; }; 26C1A3EF2031D9E60009F7F0 /* OtherUserTableViewDS.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OtherUserTableViewDS.swift; sourceTree = ""; }; 26C1A3F22031EED30009F7F0 /* OtherUserHeaderView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OtherUserHeaderView.swift; sourceTree = ""; }; + 26C8555C215123B00037F106 /* AudioPlayable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AudioPlayable.swift; sourceTree = ""; }; 26CD3FDA2104D19D00597E62 /* AudioShortTranscribeOperation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AudioShortTranscribeOperation.swift; sourceTree = ""; }; 26CD3FDC2104D1DD00597E62 /* AudioConvertOperation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AudioConvertOperation.swift; sourceTree = ""; }; - 26D35AB71FD0EFA800A5D513 /* AudioPlayer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AudioPlayer.swift; sourceTree = ""; }; 26D621F32069778400595E13 /* ChatWheelItemView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatWheelItemView.swift; sourceTree = ""; }; 26D6D226212EDA6600EA2419 /* ConvertMessageDAOProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConvertMessageDAOProtocol.swift; sourceTree = ""; }; 26D6D228212EDADC00EA2419 /* ConvertMessageDAO.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConvertMessageDAO.swift; sourceTree = ""; }; @@ -2518,6 +2536,7 @@ 26DCB25320692237001EF0AB /* Array+Feature.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Array+Feature.swift"; sourceTree = ""; }; 26DCB255206924B3001EF0AB /* FeatureFactory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = FeatureFactory.swift; path = Services/FeatureFactory.swift; sourceTree = ""; }; 26DE8D9020FE1AF500C41096 /* ChatCellFooterView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatCellFooterView.swift; sourceTree = ""; }; + 26E0C44621469E9800A58ECD /* ConnectionService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = ConnectionService.swift; path = Services/ConnectionService.swift; sourceTree = ""; }; 26E3229220E4F19A00271413 /* MessageParser.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageParser.swift; sourceTree = ""; }; 26E476581FFEE2D400C06C05 /* Modelka.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Modelka.swift; sourceTree = ""; }; 26E7D0491FCB8973001C69B7 /* Amazon+FileSync.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = "Amazon+FileSync.swift"; path = "Services/Amazon+FileSync.swift"; sourceTree = ""; }; @@ -2532,6 +2551,7 @@ 26F03C0C20698B0000712CB0 /* ChatWheelItemModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatWheelItemModel.swift; sourceTree = ""; }; 26F47051201B7248005D3192 /* ReturnToCallView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReturnToCallView.swift; sourceTree = ""; }; 26F5C8BD206BD49B003A7FF5 /* DefaultActionItemModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DefaultActionItemModel.swift; sourceTree = ""; }; + 26F87DF52142B40F000ED2C8 /* SenderService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SenderService.swift; sourceTree = ""; }; 26FA42092017ADF000E6F6EC /* StarMessageCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = StarMessageCell.swift; path = StarCell/StarMessageCell.swift; sourceTree = ""; }; 26FA420B2017AE3300E6F6EC /* StarMessageCellLayout.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = StarMessageCellLayout.swift; path = StarCell/StarMessageCellLayout.swift; sourceTree = ""; }; 26FA420D201812D600E6F6EC /* StarTableDS.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StarTableDS.swift; sourceTree = ""; }; @@ -2555,7 +2575,6 @@ 357809AA1F9765CF00C9680C /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 359E343E1F55FA0F002F5F3E /* 1-second-of-silence.mp3 */ = {isa = PBXFileReference; lastKnownFileType = audio.mp3; path = "1-second-of-silence.mp3"; sourceTree = ""; }; 359EB27A1F9A28C500147437 /* MessageHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageHandler.swift; sourceTree = ""; }; - 359EB27F1F9A2D2E00147437 /* MQTTServiceAuth.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MQTTServiceAuth.swift; sourceTree = ""; }; 359EB2821F9A2E6A00147437 /* ProfileHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfileHandler.swift; sourceTree = ""; }; 35B1AB811F9FB06500E65233 /* AttachmentModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AttachmentModel.swift; sourceTree = ""; }; 35B1AB871FA278D900E65233 /* Pods_Nynja.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = Pods_Nynja.framework; sourceTree = BUILT_PRODUCTS_DIR; }; @@ -2564,7 +2583,6 @@ 35F2DA601F73CAD400777920 /* NotificationManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NotificationManager.swift; sourceTree = ""; }; 373F47403C65F991B9421E2C /* DateTimePickerViewController.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = DateTimePickerViewController.swift; sourceTree = ""; }; 3A0281F61F53794800206871 /* UIViewExtenstions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UIViewExtenstions.swift; sourceTree = ""; }; - 3A1146641ED6E85A006BA132 /* SoundService.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = SoundService.swift; path = Services/SoundService.swift; sourceTree = ""; }; 3A1146671ED6F047006BA132 /* ring.mp3 */ = {isa = PBXFileReference; lastKnownFileType = audio.mp3; path = ring.mp3; sourceTree = ""; }; 3A19FEAC1F3B7F1D00ACE750 /* MessageHandler.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = MessageHandler.swift; path = Services/HandleServices/MessageHandler.swift; sourceTree = ""; }; 3A1AAFCD1F3DF0470098780A /* DateExtensions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DateExtensions.swift; sourceTree = ""; }; @@ -2577,7 +2595,6 @@ 3A1DFD7D1F5370A600F3A3D8 /* UIImageExtensions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UIImageExtensions.swift; sourceTree = ""; }; 3A1EB9A41F3A848A00658E93 /* HistoryHandler.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = HistoryHandler.swift; path = Services/HandleServices/HistoryHandler.swift; sourceTree = ""; }; 3A1F74F91F5ED344009A11E4 /* PushService.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = PushService.swift; path = Services/PushService.swift; sourceTree = ""; }; - 3A213F791F0082AC006DBE91 /* VerifyNumberVC.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = VerifyNumberVC.swift; sourceTree = ""; }; 3A213F7B1F0093F0006DBE91 /* LoginView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LoginView.swift; sourceTree = ""; }; 3A2171501EFB25C400F34B8B /* BaseVC.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = BaseVC.swift; path = BaseVC/BaseVC.swift; sourceTree = ""; }; 3A21EFFB1F3B154A00AE61EC /* SendModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = SendModel.swift; path = Services/Models/SendModel.swift; sourceTree = ""; }; @@ -2600,7 +2617,6 @@ 3A8045D51F60C93D00AED866 /* MQTTServiceChat.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MQTTServiceChat.swift; sourceTree = ""; }; 3A8045D71F60C98200AED866 /* MQTTServiceHelper.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MQTTServiceHelper.swift; sourceTree = ""; }; 3A8045D91F60E18E00AED866 /* Queue.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = Queue.swift; path = Library/Queue.swift; sourceTree = ""; }; - 3A82187D1EDEEDF400337B05 /* AlertManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AlertManager.swift; sourceTree = ""; }; 3A8218871EDF102D00337B05 /* Color.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = Color.swift; path = Nynja/Library/UI/Color.swift; sourceTree = SOURCE_ROOT; }; 3A8C12DDFD0D831F21959665 /* Pods-Nynja-Share.devautotests.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Nynja-Share.devautotests.xcconfig"; path = "Pods/Target Support Files/Pods-Nynja-Share/Pods-Nynja-Share.devautotests.xcconfig"; sourceTree = ""; }; 3AA13C751F2252F900BE5D8F /* SearchModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SearchModel.swift; sourceTree = ""; }; @@ -2622,7 +2638,6 @@ 3E600F42D8040D91A16CE3D8 /* Pods-NynjaUnitTests.prerelease.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-NynjaUnitTests.prerelease.xcconfig"; path = "Pods/Target Support Files/Pods-NynjaUnitTests/Pods-NynjaUnitTests.prerelease.xcconfig"; sourceTree = ""; }; 40444524B52370D471DC9141 /* EditGroupPhotoViewController.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = EditGroupPhotoViewController.swift; sourceTree = ""; }; 4177485419FF2E8F7CF8FF98 /* EditPhotoPresenter.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = EditPhotoPresenter.swift; sourceTree = ""; }; - 425BCBF6F15DC4F7C3674AEC /* WebViewProtocols.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = WebViewProtocols.swift; sourceTree = ""; }; 4489153750EAC34408B967C0 /* MapViewController.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = MapViewController.swift; sourceTree = ""; }; 45739CD40D10B2FD5E35E0C0 /* SecurityInteractor.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = SecurityInteractor.swift; sourceTree = ""; }; 462440AD41D807CE8957FDD9 /* FavoritesProtocols.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = FavoritesProtocols.swift; sourceTree = ""; }; @@ -2676,8 +2691,24 @@ 4B4266C0204D917800194BC1 /* ActionsView+Layout.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ActionsView+Layout.swift"; sourceTree = ""; }; 4B4266C2204D923400194BC1 /* Array+UIView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Array+UIView.swift"; sourceTree = ""; }; 4B5A714C204F069000A551F5 /* ChatService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatService.swift; sourceTree = ""; }; + 4B71AC35216215F600E4583B /* DBRoomExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DBRoomExtension.swift; sourceTree = ""; }; + 4B71AC4121622A6A00E4583B /* AppNotificationsProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppNotificationsProvider.swift; sourceTree = ""; }; + 4B71AC4421622AA700E4583B /* AppNotificationsProviding.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppNotificationsProviding.swift; sourceTree = ""; }; 4B736D4820238FA40028F2CB /* ThumbnailGenerator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ThumbnailGenerator.swift; sourceTree = ""; }; + 4B749F00214FEE4F002F3A33 /* VerifyNumberPresenter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VerifyNumberPresenter.swift; sourceTree = ""; }; + 4B749F01214FEE4F002F3A33 /* VerifyNumberViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VerifyNumberViewController.swift; sourceTree = ""; }; + 4B749F02214FEE4F002F3A33 /* VerifyNumberProtocols.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VerifyNumberProtocols.swift; sourceTree = ""; }; + 4B749F03214FEE4F002F3A33 /* VerifyNumberInteractor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VerifyNumberInteractor.swift; sourceTree = ""; }; + 4B749F04214FEE4F002F3A33 /* VerifyNumberWireFrame.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VerifyNumberWireFrame.swift; sourceTree = ""; }; 4B7B81C52044790700C2EFCF /* TimeZoneLocal.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimeZoneLocal.swift; sourceTree = ""; }; + 4B7C73E9215A5508007924DB /* SMSCodeProvider.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SMSCodeProvider.swift; sourceTree = ""; }; + 4B7C73EA215A5508007924DB /* SMSCodeProviding.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SMSCodeProviding.swift; sourceTree = ""; }; + 4B7C73EC215A5508007924DB /* MotionManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MotionManager.swift; sourceTree = ""; }; + 4B7C73EE215A5508007924DB /* LogWriter.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LogWriter.swift; sourceTree = ""; }; + 4B7C73EF215A5508007924DB /* LogService.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LogService.swift; sourceTree = ""; }; + 4B7C73F6215A5522007924DB /* DebugLogs.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DebugLogs.swift; sourceTree = ""; }; + 4B7C73F7215A5522007924DB /* UIView+Debug.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "UIView+Debug.swift"; sourceTree = ""; }; + 4B7C73F8215A5522007924DB /* UILabel+Debug.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "UILabel+Debug.swift"; sourceTree = ""; }; 4B8996C7204ECE9B00DCB183 /* ContactDAO.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContactDAO.swift; sourceTree = ""; }; 4B8996C9204ECEA700DCB183 /* ContactDAOProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContactDAOProtocol.swift; sourceTree = ""; }; 4B8996CC204ED33400DCB183 /* StarDAOProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StarDAOProtocol.swift; sourceTree = ""; }; @@ -2692,13 +2723,27 @@ 4B8996F1204EF5E900DCB183 /* ChatCheckpointDAO.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatCheckpointDAO.swift; sourceTree = ""; }; 4B8996F4204EF75500DCB183 /* FeedDAOProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedDAOProtocol.swift; sourceTree = ""; }; 4B8996F6204EF77100DCB183 /* FeedDAO.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedDAO.swift; sourceTree = ""; }; + 4B8AB701215CD02100C69DE1 /* SequenceExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SequenceExtension.swift; sourceTree = ""; }; 4B8BEDE0204979AA00C7D625 /* ImagesView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImagesView.swift; sourceTree = ""; }; 4BAB9CDF2035CAE700385520 /* ScheduleInfo.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ScheduleInfo.swift; sourceTree = ""; }; 4BAB9CE12035CAF500385520 /* ScheduleContentType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ScheduleContentType.swift; sourceTree = ""; }; 4BAB9CE32035CB0A00385520 /* ScheduleTarget.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ScheduleTarget.swift; sourceTree = ""; }; 4BAB9CE52035CB3800385520 /* ScheduleDisplayInfo.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ScheduleDisplayInfo.swift; sourceTree = ""; }; + 4BB0EFB82151347900704136 /* AlertImageViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AlertImageViewController.swift; sourceTree = ""; }; + 4BB0EFB92151347900704136 /* AlertImageViewControllerConstraints.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AlertImageViewControllerConstraints.swift; sourceTree = ""; }; + 4BB0EFBA2151347900704136 /* AlertManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AlertManager.swift; sourceTree = ""; }; 4BDC7E60203492CA00BCD381 /* TopSwipable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TopSwipable.swift; sourceTree = ""; }; 4BDC7E62203494C000BCD381 /* ScheduleButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ScheduleButton.swift; sourceTree = ""; }; + 4BE2C5D02142EAC500A73DD9 /* AudioPlayer.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AudioPlayer.swift; sourceTree = ""; }; + 4BE2C5D22142EAC500A73DD9 /* AudioSessionManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AudioSessionManager.swift; sourceTree = ""; }; + 4BE2C5D42142EAC500A73DD9 /* AudioRecorder.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AudioRecorder.swift; sourceTree = ""; }; + 4BE2C5D62142EAC500A73DD9 /* SoundBundle.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SoundBundle.swift; sourceTree = ""; }; + 4BE2C5D72142EAC500A73DD9 /* SystemSoundManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SystemSoundManager.swift; sourceTree = ""; }; + 4BE2C5D82142EAC500A73DD9 /* Sound.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Sound.swift; sourceTree = ""; }; + 4BE2C5E02142EB0E00A73DD9 /* AudioManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AudioManager.swift; sourceTree = ""; }; + 4BE2C5E12142EB0E00A73DD9 /* AudioManagerDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AudioManagerDelegate.swift; sourceTree = ""; }; + 4BE2C5E52142EB5A00A73DD9 /* NynjaCommunicatorService.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NynjaCommunicatorService.swift; sourceTree = ""; }; + 4BE2C5E62142EB5A00A73DD9 /* NynjaRingingService.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NynjaRingingService.swift; sourceTree = ""; }; 4CDA2BE900351F21464CE687 /* DateTimePickerInteractor.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = DateTimePickerInteractor.swift; sourceTree = ""; }; 4D247CBC45C1C1267BBBB289 /* QRCodeReaderInteractor.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = QRCodeReaderInteractor.swift; sourceTree = ""; }; 4F7C039B61A0663D43BE5AE5 /* SelectCountryProtocols.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = SelectCountryProtocols.swift; sourceTree = ""; }; @@ -2719,7 +2764,6 @@ 5AEEB3D82E9CF02760DA4CE7 /* Pods-Nynja-Share.channels.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Nynja-Share.channels.xcconfig"; path = "Pods/Target Support Files/Pods-Nynja-Share/Pods-Nynja-Share.channels.xcconfig"; sourceTree = ""; }; 5B377AA90A6B6BA0120C31F1 /* EditProfileProtocols.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = EditProfileProtocols.swift; sourceTree = ""; }; 5BBEF53B212DE09F00F10768 /* ringback.m4a */ = {isa = PBXFileReference; lastKnownFileType = file; path = ringback.m4a; sourceTree = ""; }; - 5BC1D37220D3B3D8002A44B3 /* NynjaCommunicatorService.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = NynjaCommunicatorService.swift; path = Services/NynjaCommunicatorService.swift; sourceTree = ""; }; 5BC1D37420D3B4A6002A44B3 /* GroupCollectionViewCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GroupCollectionViewCell.swift; sourceTree = ""; }; 5BC1D37620D3B4A7002A44B3 /* GroupAddParticipantsCollectionViewCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GroupAddParticipantsCollectionViewCell.swift; sourceTree = ""; }; 5BC1D37820D3B4A7002A44B3 /* GroupCollectionView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GroupCollectionView.swift; sourceTree = ""; }; @@ -2727,13 +2771,13 @@ 5BC1D38020D3B54B002A44B3 /* CallInfoViewLayout.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CallInfoViewLayout.swift; sourceTree = ""; }; 5BC1D38320D3B670002A44B3 /* CallCreatorMediator.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CallCreatorMediator.swift; sourceTree = ""; }; 5D3E868EE32625048BCB13A8 /* HistoryInteractor.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = HistoryInteractor.swift; sourceTree = ""; }; + 5E0CEA9921490663004B3F7A /* TypingStatusCache.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TypingStatusCache.swift; sourceTree = ""; }; 5EEA3D18EFB98D7959F993E4 /* AddParticipantsProtocols.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = AddParticipantsProtocols.swift; sourceTree = ""; }; 5F509C0C8B9C738DBC7ABE07 /* SecurityViewController.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = SecurityViewController.swift; sourceTree = ""; }; 61B964D5CB991533BA5C164C /* HistoryPresenter.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = HistoryPresenter.swift; sourceTree = ""; }; 61CB12AA514912C6B8E4F670 /* Pods-Nynja.devautotests.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Nynja.devautotests.xcconfig"; path = "Pods/Target Support Files/Pods-Nynja/Pods-Nynja.devautotests.xcconfig"; sourceTree = ""; }; 628BB7CDB18FDAFAAB6FD17D /* EditGroupPhotoWireframe.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = EditGroupPhotoWireframe.swift; sourceTree = ""; }; 643B61A129DD7717EF6B856A /* Pods-Nynja.dev.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Nynja.dev.xcconfig"; path = "Pods/Target Support Files/Pods-Nynja/Pods-Nynja.dev.xcconfig"; sourceTree = ""; }; - 64A724F7272244110EBB528A /* WebViewViewController.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = WebViewViewController.swift; sourceTree = ""; }; 64CBBFDC7C38F4499D1860E5 /* GroupRulesProtocols.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = GroupRulesProtocols.swift; sourceTree = ""; }; 65AAB8F770774CE3AE3FD6E1 /* MainWireframe.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = MainWireframe.swift; sourceTree = ""; }; 6849EE44C7121EB25C5421DB /* TimeZoneSelectorInteractor.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = TimeZoneSelectorInteractor.swift; sourceTree = ""; }; @@ -2810,6 +2854,9 @@ 850833DA2037171600587EEF /* FileExtensionView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FileExtensionView.swift; sourceTree = ""; }; 8509452A206E684300B43C1C /* AddParticipantsContactCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddParticipantsContactCell.swift; sourceTree = ""; }; 8509AC61206A54420089089B /* ResponseResult.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ResponseResult.swift; sourceTree = ""; }; + 8509FC842158F7D100734D93 /* AppGroupFlagContainer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppGroupFlagContainer.swift; sourceTree = ""; }; + 8509FC862158F7FC00734D93 /* DirectoryWatcher.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DirectoryWatcher.swift; sourceTree = ""; }; + 8509FC88215908B300734D93 /* AppGroupFlagObserver.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppGroupFlagObserver.swift; sourceTree = ""; }; 850A0C6420469AED004F79AD /* UserSettingsRespondable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserSettingsRespondable.swift; sourceTree = ""; }; 850A0C662046B65D004F79AD /* WCItemsFactoryDecorator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WCItemsFactoryDecorator.swift; sourceTree = ""; }; 850A2BAF203584B000D68FDF /* SearchActionsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchActionsView.swift; sourceTree = ""; }; @@ -2832,6 +2879,7 @@ 850FC5F92032F64100832D87 /* ForwardSelectorWireFrame.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ForwardSelectorWireFrame.swift; sourceTree = ""; }; 850FC60E203310D200832D87 /* SelectionAvatarView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SelectionAvatarView.swift; sourceTree = ""; }; 850FC610203312FA00832D87 /* ForwardSelectorViewControllerLayout.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ForwardSelectorViewControllerLayout.swift; sourceTree = ""; }; + 851105BA2163D0C800F07019 /* TranscribeResponseData.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TranscribeResponseData.swift; sourceTree = ""; }; 8511D3702034427F00B2A620 /* UIView+SafeArea.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIView+SafeArea.swift"; sourceTree = ""; }; 8511D3732034596E00B2A620 /* Collection+ViewLayout.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Collection+ViewLayout.swift"; sourceTree = ""; }; 8512349121221B9E000129A2 /* Collection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Collection.swift; sourceTree = ""; }; @@ -2955,10 +3003,8 @@ 8562853820D166E5000C9739 /* CollectionPreviewState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CollectionPreviewState.swift; sourceTree = ""; }; 8562853A20D16C61000C9739 /* LongPressClosureRecognizer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LongPressClosureRecognizer.swift; sourceTree = ""; }; 85629EC92137EF2400A79C97 /* VoiceAudioInteractive.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VoiceAudioInteractive.swift; sourceTree = ""; }; - 8566771B20C139A000DD4204 /* DebugLogs.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DebugLogs.swift; sourceTree = ""; }; 8566771D20C1579C00DD4204 /* StorageSubscriberReference.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StorageSubscriberReference.swift; sourceTree = ""; }; 8566771F20C1924500DD4204 /* MessageInteractor+MessageHandlerSubscriber.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "MessageInteractor+MessageHandlerSubscriber.swift"; sourceTree = ""; }; - 856B827721184FDF00917F90 /* AudioSessionManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AudioSessionManager.swift; sourceTree = ""; }; 8572C3B52092315B00E4840C /* CollectionViewDataProxy.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CollectionViewDataProxy.swift; sourceTree = ""; }; 8572C3B82092364C00E4840C /* StickerPackageDataSource.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StickerPackageDataSource.swift; sourceTree = ""; }; 8572C3BA2092366100E4840C /* StickerCollectionDataSource.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StickerCollectionDataSource.swift; sourceTree = ""; }; @@ -3020,14 +3066,17 @@ 859B863820486068003272B2 /* CarouselPickerViewControllerLayout.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CarouselPickerViewControllerLayout.swift; sourceTree = ""; }; 859C429E2056829300AE3797 /* NotificationSettings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationSettings.swift; sourceTree = ""; }; 859C42A4205691FB00AE3797 /* Sounds.json */ = {isa = PBXFileReference; lastKnownFileType = text.json; path = Sounds.json; sourceTree = ""; }; - 859C42A72056940500AE3797 /* Sound.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Sound.swift; sourceTree = ""; }; - 859C42A92056B05D00AE3797 /* SoundBundle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SoundBundle.swift; sourceTree = ""; }; 859C42AC2056BF9F00AE3797 /* incoming_message.mp3 */ = {isa = PBXFileReference; lastKnownFileType = audio.mp3; path = incoming_message.mp3; sourceTree = ""; }; 859F9B4B2035CB1E009D017A /* ForwardContent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ForwardContent.swift; sourceTree = ""; }; + 85A3CA01214129F200E0EDD5 /* KeyboardInteractive.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeyboardInteractive.swift; sourceTree = ""; }; + 85ADEB7821621CAD00ABECBD /* InputTextStorage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InputTextStorage.swift; sourceTree = ""; }; + 85ADEB7A2162445200ABECBD /* InputTextStorageDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InputTextStorageDelegate.swift; sourceTree = ""; }; 85B0013121270DEC000C89FE /* TableOrder.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TableOrder.swift; sourceTree = ""; }; 85B0013321272694000C89FE /* MessageInteractor+History.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "MessageInteractor+History.swift"; sourceTree = ""; }; 85B750A020334A2B00AD6013 /* ForwardTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ForwardTableViewCell.swift; sourceTree = ""; }; 85BA176020BEA7BD001EF8AC /* StickerPreviewContainerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StickerPreviewContainerView.swift; sourceTree = ""; }; + 85BDD2B721465EFA00695DE5 /* ScrollDirection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ScrollDirection.swift; sourceTree = ""; }; + 85BDD2B921467A9500695DE5 /* MessageFactoryProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageFactoryProtocol.swift; sourceTree = ""; }; 85BEC0E02063F91C0098C99C /* TimeZoneCellModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimeZoneCellModel.swift; sourceTree = ""; }; 85C16C3420D2520E00EDB77E /* StickersDownloadingService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = StickersDownloadingService.swift; path = Services/StickersDownloadingService/StickersDownloadingService.swift; sourceTree = ""; }; 85C16C3B20D261C000EDB77E /* MessageStickerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageStickerView.swift; sourceTree = ""; }; @@ -3064,7 +3113,6 @@ 85D77806211D9B980044E72F /* ScrollPosition.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ScrollPosition.swift; sourceTree = ""; }; 85E1DD2420BEBE17008AD211 /* MessageVC+StickerInputModuleDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "MessageVC+StickerInputModuleDelegate.swift"; sourceTree = ""; }; 85E1DD2620BEE961008AD211 /* ScalableCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ScalableCell.swift; sourceTree = ""; }; - 85EBBE022056CF97009BB269 /* SoundPlayer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SoundPlayer.swift; sourceTree = ""; }; 85EBBE042056E8B2009BB269 /* outcoming_message.mp3 */ = {isa = PBXFileReference; lastKnownFileType = audio.mp3; path = outcoming_message.mp3; sourceTree = ""; }; 85F0866120D6412300A7762E /* RemoteStorageDestination.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RemoteStorageDestination.swift; sourceTree = ""; }; 85F3DD43203F410D00F210C0 /* TimerHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimerHandler.swift; sourceTree = ""; }; @@ -3074,7 +3122,6 @@ 8C986781EE944D55A2B7374C /* GroupStorageInteractor.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = GroupStorageInteractor.swift; sourceTree = ""; }; 8CBACEAABEE65D7EC5572C4E /* CreateGroupPresenter.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = CreateGroupPresenter.swift; sourceTree = ""; }; 8D4ACB985C2F0674717F1045 /* MainProtocols.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = MainProtocols.swift; sourceTree = ""; }; - 8DC3F2CD754DE6EC29F6DF88 /* WebViewInteractor.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = WebViewInteractor.swift; sourceTree = ""; }; 8DD73BCBB9741C19646F0E9D /* TutorialViewController.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = TutorialViewController.swift; sourceTree = ""; }; 8E23E085200614AB00A59B8C /* GroupVideosCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GroupVideosCell.swift; sourceTree = ""; }; 8E23E0872006852F00A59B8C /* GroupVideosListVC.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GroupVideosListVC.swift; sourceTree = ""; }; @@ -3128,7 +3175,7 @@ 9DE44A136617140435B23343 /* GroupStorageWireframe.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = GroupStorageWireframe.swift; sourceTree = ""; }; 9E82188EE0AC1D1C05470692 /* Pods-NynjaUnitTests.channels.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-NynjaUnitTests.channels.xcconfig"; path = "Pods/Target Support Files/Pods-NynjaUnitTests/Pods-NynjaUnitTests.channels.xcconfig"; sourceTree = ""; }; 9EDEA273EC821A0D4317436E /* Pods_Nynja_Nynja_Share.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Nynja_Nynja_Share.framework; sourceTree = BUILT_PRODUCTS_DIR; }; - 9FB2678C74CCC58C8AAFADD6 /* AuthWireframe.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = AuthWireframe.swift; sourceTree = ""; }; + 9FB2678C74CCC58C8AAFADD6 /* LoginWireframe.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = LoginWireframe.swift; sourceTree = ""; }; A169D8E4AB2003F96040DD7A /* Pods-Nynja-Share.dev.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Nynja-Share.dev.xcconfig"; path = "Pods/Target Support Files/Pods-Nynja-Share/Pods-Nynja-Share.dev.xcconfig"; sourceTree = ""; }; A2D9E2484E2189F0E019FF3D /* GroupRulesViewController.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = GroupRulesViewController.swift; sourceTree = ""; }; A402A1CB20DE694A005BFA20 /* PartialCheckableButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PartialCheckableButton.swift; sourceTree = ""; }; @@ -3536,10 +3583,10 @@ AE929B30A3869179E76E59A9 /* FavoritesInteractor.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = FavoritesInteractor.swift; sourceTree = ""; }; AFA5ECB1F18C92E648BC93B0 /* FavoritesViewController.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = FavoritesViewController.swift; sourceTree = ""; }; AFC76E2B3DD0BCA0A622A5CD /* CreateGroupInteractor.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = CreateGroupInteractor.swift; sourceTree = ""; }; - B051231EAD6BB435200B4C74 /* AuthPresenter.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = AuthPresenter.swift; sourceTree = ""; }; + B051231EAD6BB435200B4C74 /* LoginPresenter.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = LoginPresenter.swift; sourceTree = ""; }; B05863F1D1FC27487D496750 /* SelectCountryWireframe.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = SelectCountryWireframe.swift; sourceTree = ""; }; B0E0429CA4EF8A228D791BED /* HistoryWireframe.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = HistoryWireframe.swift; sourceTree = ""; }; - B15F3B55EC2BF6FB5D7A2FAF /* AuthInteractor.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = AuthInteractor.swift; sourceTree = ""; }; + B15F3B55EC2BF6FB5D7A2FAF /* LoginInteractor.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = LoginInteractor.swift; sourceTree = ""; }; B28416F302A40E1E56041080 /* TimeZoneSelectorProtocols.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = TimeZoneSelectorProtocols.swift; sourceTree = ""; }; B28D1FE755A0457DBEDAC068 /* MapSearchProtocols.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = MapSearchProtocols.swift; sourceTree = ""; }; B2B221F69CB3D5C6A1B12456 /* VideoPreviewInteractor.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = VideoPreviewInteractor.swift; sourceTree = ""; }; @@ -3633,7 +3680,6 @@ C9B8BEFB204DDDA20018748C /* CheckmarkCellLayout.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CheckmarkCellLayout.swift; sourceTree = ""; }; C9B8BEFD204DEBD00018748C /* DataDownloadAndUsageMode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DataDownloadAndUsageMode.swift; sourceTree = ""; }; C9C694F8201FA4AB00A57297 /* SlideAnimatedTransitioning.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SlideAnimatedTransitioning.swift; sourceTree = ""; }; - C9C694FA201FA50100A57297 /* CustomPanGesture.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomPanGesture.swift; sourceTree = ""; }; C9C694FC201FA55800A57297 /* SwipeBackHelper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SwipeBackHelper.swift; sourceTree = ""; }; C9C695022022306D00A57297 /* SelectCountryTableDataSource.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SelectCountryTableDataSource.swift; sourceTree = ""; }; C9C69504202230DD00A57297 /* SelectCountryCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SelectCountryCell.swift; sourceTree = ""; }; @@ -3648,7 +3694,7 @@ CB70AD73977CD00AD11C287C /* Pods-NynjaUnitTests.devautotests.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-NynjaUnitTests.devautotests.xcconfig"; path = "Pods/Target Support Files/Pods-NynjaUnitTests/Pods-NynjaUnitTests.devautotests.xcconfig"; sourceTree = ""; }; CBE3BAC9B7EA418FB463EF04 /* EditUsernameInteractor.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = EditUsernameInteractor.swift; sourceTree = ""; }; CCA291E1CE928BC100DD6353 /* Pods-Nynja-Share.translate.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Nynja-Share.translate.xcconfig"; path = "Pods/Target Support Files/Pods-Nynja-Share/Pods-Nynja-Share.translate.xcconfig"; sourceTree = ""; }; - CDF62E1A004579220E231142 /* AuthProtocols.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = AuthProtocols.swift; sourceTree = ""; }; + CDF62E1A004579220E231142 /* LoginProtocols.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = LoginProtocols.swift; sourceTree = ""; }; D1AE7296B9A53355289740D1 /* ProfilePresenter.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = ProfilePresenter.swift; sourceTree = ""; }; D1D5302025583482829BBF2E /* GroupStorageViewController.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = GroupStorageViewController.swift; sourceTree = ""; }; D270F638DBB2D8FC1BDEB633 /* ProfileViewController.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = ProfileViewController.swift; sourceTree = ""; }; @@ -3728,8 +3774,6 @@ E76D132B1FA35CCF00B07F0E /* ProfilePlaceholderCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfilePlaceholderCell.swift; sourceTree = ""; }; E76D132E1FA35D2900B07F0E /* ProfilePlaceholderCellLayout.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfilePlaceholderCellLayout.swift; sourceTree = ""; }; E76D13301FA35F3500B07F0E /* TextCellModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TextCellModel.swift; sourceTree = ""; }; - E7718BF41FB5D6080070B402 /* UIView+Debug.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIView+Debug.swift"; sourceTree = ""; }; - E7718BF61FB5D8D70070B402 /* UILabel+Debug.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UILabel+Debug.swift"; sourceTree = ""; }; E77764B31FBDA8B50042541D /* WheelContainerDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WheelContainerDelegate.swift; sourceTree = ""; }; E77764B51FBDA8E30042541D /* WheelContainerDataSource.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WheelContainerDataSource.swift; sourceTree = ""; }; E77764BA1FBDA9B60042541D /* ImageFullWheelItemView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ImageFullWheelItemView.swift; sourceTree = ""; }; @@ -3800,9 +3844,6 @@ E7F2CFE11F5EEF1E00806E43 /* PermissionManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PermissionManager.swift; sourceTree = ""; }; E7F68D261FA22C45009C98D1 /* EditProfileVCStrings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EditProfileVCStrings.swift; sourceTree = ""; }; E7F8F55B1F7BCA090016FDF9 /* DefaultWheelConfiguration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DefaultWheelConfiguration.swift; sourceTree = ""; }; - E7FF40AF1F9602C400810D1C /* AudioManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AudioManager.swift; sourceTree = ""; }; - E7FF40B31F96089B00810D1C /* AudioManagerDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AudioManagerDelegate.swift; sourceTree = ""; }; - E8C95C96D48216CC93C04682 /* WebViewPresenter.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = WebViewPresenter.swift; sourceTree = ""; }; ECDD1A830A4F5CED79A37CA1 /* ImagePreviewPresenter.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = ImagePreviewPresenter.swift; sourceTree = ""; }; EE1EF22666DC92AE739E2DA5 /* Pods-Nynja.stickers.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Nynja.stickers.xcconfig"; path = "Pods/Target Support Files/Pods-Nynja/Pods-Nynja.stickers.xcconfig"; sourceTree = ""; }; EE2260535ED2762F80FA7A38 /* MapProtocols.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = MapProtocols.swift; sourceTree = ""; }; @@ -4284,18 +4325,10 @@ path = Presenter; sourceTree = ""; }; - 1616C935DFADD3E799901555 /* Presenter */ = { - isa = PBXGroup; - children = ( - E8C95C96D48216CC93C04682 /* WebViewPresenter.swift */, - ); - path = Presenter; - sourceTree = ""; - }; 16172D640373B2B96B22D611 /* Presenter */ = { isa = PBXGroup; children = ( - B051231EAD6BB435200B4C74 /* AuthPresenter.swift */, + B051231EAD6BB435200B4C74 /* LoginPresenter.swift */, ); path = Presenter; sourceTree = ""; @@ -4554,15 +4587,6 @@ path = TranslationService; sourceTree = ""; }; - 26052C7820FCE7C100E7A6A0 /* LogService */ = { - isa = PBXGroup; - children = ( - 26052C7920FCE7E000E7A6A0 /* LogService.swift */, - 260D67D82124616A0072F11F /* LogWriter.swift */, - ); - path = LogService; - sourceTree = ""; - }; 260531122127407A002E1CF1 /* LogOutput */ = { isa = PBXGroup; children = ( @@ -4609,14 +4633,6 @@ path = View; sourceTree = ""; }; - 260531242127454E002E1CF1 /* MotionManager */ = { - isa = PBXGroup; - children = ( - 260531252127455C002E1CF1 /* MotionManager.swift */, - ); - path = MotionManager; - sourceTree = ""; - }; 2607270B203C59D600290545 /* Cells */ = { isa = PBXGroup; children = ( @@ -4744,7 +4760,7 @@ 26342CA720ECB36500D2196B /* Response */ = { isa = PBXGroup; children = ( - 26342CAE20ECD16A00D2196B /* TranscribeShortResponseData.swift */, + 851105BA2163D0C800F07019 /* TranscribeResponseData.swift */, 268C34142107479600F1472A /* TranscribeLongResponseData.swift */, 268C341821074D6C00F1472A /* TranscribeLongOperationResponseData.swift */, ); @@ -4964,7 +4980,8 @@ 26534B23210B4BCA0003B9BC /* Extension */ = { isa = PBXGroup; children = ( - 26534B24210B4BE70003B9BC /* DBMessage+TypeExtension.swift */, + 26534B24210B4BE70003B9BC /* DBMessage+Extension.swift */, + 4B71AC35216215F600E4583B /* DBRoomExtension.swift */, ); path = Extension; sourceTree = ""; @@ -5121,16 +5138,6 @@ path = WireFrame; sourceTree = ""; }; - 267C1D5620404EB80087808F /* CustomPopup */ = { - isa = PBXGroup; - children = ( - 267C1D5720404EDB0087808F /* AlertImageViewController.swift */, - 267C1D5820404EDB0087808F /* AlertImageViewControllerConstraints.swift */, - ); - name = CustomPopup; - path = AlertImageViewController; - sourceTree = ""; - }; 2683F776203F38AE0003181A /* PickerView */ = { isa = PBXGroup; children = ( @@ -5139,14 +5146,6 @@ path = PickerView; sourceTree = ""; }; - 2683F77F204043C80003181A /* CustomPopup */ = { - isa = PBXGroup; - children = ( - 267C1D5620404EB80087808F /* CustomPopup */, - ); - path = CustomPopup; - sourceTree = ""; - }; 2686D3211FC63B550079CB75 /* SyncFileManager */ = { isa = PBXGroup; children = ( @@ -5169,6 +5168,7 @@ isa = PBXGroup; children = ( 268C341B21075B4700F1472A /* Cancelable.swift */, + 26773F2A215BE15800C09248 /* Array+Operation.swift */, ); path = Helpers; sourceTree = ""; @@ -5218,10 +5218,10 @@ isa = PBXGroup; children = ( 26C1A3E22031A95D0009F7F0 /* OtherUserProtocols.swift */, - 26C1A3DE2031A9330009F7F0 /* Presenter */, - 26C1A3DF2031A9330009F7F0 /* WireFrame */, 26C1A3E02031A9330009F7F0 /* View */, + 26C1A3DE2031A9330009F7F0 /* Presenter */, 26C1A3E12031A9330009F7F0 /* Interactor */, + 26C1A3DF2031A9330009F7F0 /* WireFrame */, ); path = OtherUser; sourceTree = ""; @@ -5557,14 +5557,6 @@ path = Resources; sourceTree = ""; }; - 356275741F9D334700D2A7F0 /* MQTT */ = { - isa = PBXGroup; - children = ( - 359EB27F1F9A2D2E00147437 /* MQTTServiceAuth.swift */, - ); - path = MQTT; - sourceTree = ""; - }; 356275751F9D337400D2A7F0 /* Handlers */ = { isa = PBXGroup; children = ( @@ -5590,7 +5582,6 @@ 359EB2721F9A27EB00147437 /* Services */ = { isa = PBXGroup; children = ( - 356275741F9D334700D2A7F0 /* MQTT */, 356275751F9D337400D2A7F0 /* Handlers */, ); path = Services; @@ -5641,6 +5632,7 @@ 3A768DE41ECB3E7600108F7C /* Library */ = { isa = PBXGroup; children = ( + 4B7C73F5215A5522007924DB /* Debug */, B74BAFED21076ADB0049CD27 /* CircleMenuControl */, A46679EF20F10B2B00DBC6B4 /* RequestModelFactory */, A458FAC620ECDAD90075D55E /* Base */, @@ -5653,7 +5645,6 @@ 85380EE42109FF190048042D /* IdBuilder */, 26B32B641FE1715500888A0A /* WeakRef.swift */, E7417E961FBED8FD00E5C124 /* DB */, - E7718BF11FB5D5E30070B402 /* Debug */, 85F3DD45203F411B00F210C0 /* Utils */, E7A77FD61FACC34F004AE609 /* Extensions */, 3A1C87411F6101A50029B0BC /* Reachability.swift */, @@ -5670,15 +5661,17 @@ 3A768E1C1ECD152300108F7C /* Services */ = { isa = PBXGroup; children = ( + 4B71AC4021622A5600E4583B /* Notifications */, + 4B7C73E7215A5508007924DB /* Debug */, + 8509FC832158F7B400734D93 /* Files */, + 4BE2C5E42142EB5A00A73DD9 /* NynjaCalls */, + 4BE2C5CE2142EAC500A73DD9 /* Audio */, 4B0DBA892137F6F800D79163 /* ChatService */, - 260531242127454E002E1CF1 /* MotionManager */, A46C362C212198D500172773 /* Security */, FE2D7CCB211C71AD00520D78 /* WalletService */, - 8513A88621184F9A00B5CA4A /* Audio */, 26B32B5E1FE170FE00888A0A /* MigrationManager.swift */, A409B1CD2108D4720051C20B /* KeychainService */, 26131DFF2103998D00BE94F9 /* TranscribeService */, - 26052C7820FCE7C100E7A6A0 /* LogService */, A458FAC020EBA4D70075D55E /* MuteChatService */, FBCE83F720E525A5003B7558 /* REST */, 26E3229120E4F18100271413 /* MessageParser */, @@ -5698,21 +5691,19 @@ A43E67EB206E859700048916 /* BadgeNumberService */, FB0B721120907D81003B9757 /* MessageEditService */, 4B7B81C4204478F400C2EFCF /* TimeZoneManager */, - 85F3DD41203F3ADE00F210C0 /* ConnectionSubscriberService */, + 85F3DD41203F3ADE00F210C0 /* BackgroundTaskHandler */, 4B06D2F420287007003B275B /* WheelContainer */, 261642841FFAF1D400672BE5 /* Improvements */, E749C5631FD448BE0048DEAC /* TransferManager */, E7EC77A21FD1B9BD00DC8245 /* MessageProcessing */, 2686D3211FC63B550079CB75 /* SyncFileManager */, E7C36C2D1FC438AC00740630 /* Storage */, - E7FF40B21F96088500810D1C /* AudioManager */, 3A8045CC1F60C8E200AED866 /* MQTT */, 3A1DC7371EF151B6006A8E9F /* Handlers */, 3AC321761EEAC4700068F3C8 /* Models */, 85082DDB2045A864000AE4B2 /* UserSettings */, 851769D420D584CA008ACF6B /* Amazon */, B7121EB7205045F300AABBE6 /* MediaDownloadManager.swift */, - 5BC1D37220D3B3D8002A44B3 /* NynjaCommunicatorService.swift */, E70189BA1F9107AD00CA7005 /* ProximitySensorManager.swift */, 859C42A6205693F100AE3797 /* SoundService */, 6D36F8E61F0BBFC300FA1AC8 /* ContactManager.swift */, @@ -5720,6 +5711,7 @@ 3A1F74F91F5ED344009A11E4 /* PushService.swift */, 26ABCA3D21189DA400EA4782 /* Aps.swift */, 3A1C87431F6103820029B0BC /* ReachabilityService.swift */, + 26E0C44621469E9800A58ECD /* ConnectionService.swift */, E7F2CFE11F5EEF1E00806E43 /* PermissionManager.swift */, 35F2DA601F73CAD400777920 /* NotificationManager.swift */, 2652D6171FA85B28005E62C7 /* ImageSelector.swift */, @@ -5728,8 +5720,6 @@ A4166F5B205FE3670008F231 /* JobService.swift */, 26DCB255206924B3001EF0AB /* FeatureFactory.swift */, 8509AC61206A54420089089B /* ResponseResult.swift */, - 26D35AB71FD0EFA800A5D513 /* AudioPlayer.swift */, - 2631C511207A4C0C00F9AA55 /* AudioRecorder.swift */, B7F4C2AA211995A500E48A98 /* Validation */, A42C44E220F340DA00BC3CBB /* StatusCodeManager.swift */, ); @@ -5757,6 +5747,9 @@ 3A82187C1EDEEDF400337B05 /* UI */ = { isa = PBXGroup; children = ( + 4B749EF2214FEABB002F3A33 /* LoginView */, + 4BB0EFBA2151347900704136 /* AlertManager.swift */, + 4BB0EFB62151347900704136 /* CustomPopup */, 8514D52020EE48750002378A /* ContextMenu */, 8514F16520EA219E00883513 /* ContextMenuOLD */, A44B4D4420CE9BDF00CA700A /* UITableViewCells */, @@ -5771,7 +5764,6 @@ 8548340C207769D100604051 /* Documents */, C90E6A9920558BD100D733E0 /* FileSizeFormatter */, 85082DD92045A744000AE4B2 /* CollectionView */, - 2683F77F204043C80003181A /* CustomPopup */, 2683F776203F38AE0003181A /* PickerView */, 8511D3722034595600B2A620 /* LayoutConstraints */, 85801C4020342A3E00CC364C /* BottomActions */, @@ -5789,7 +5781,6 @@ 8511D36F2034426600B2A620 /* SafeArea */, 850D21FE20D2E7AD0018BBA4 /* HapticFeedback */, 3A8218871EDF102D00337B05 /* Color.swift */, - 3A82187D1EDEEDF400337B05 /* AlertManager.swift */, 3A2A99821EFAD2FB002749B3 /* PageControl.swift */, 3A2171501EFB25C400F34B8B /* BaseVC.swift */, 855A393C213E76E20002B8DC /* LoadingInteractive.swift */, @@ -5810,16 +5801,16 @@ path = Library/UI; sourceTree = ""; }; - 3AB452082A8DAEAD93F689D8 /* Auth */ = { + 3AB452082A8DAEAD93F689D8 /* Login */ = { isa = PBXGroup; children = ( - CDF62E1A004579220E231142 /* AuthProtocols.swift */, + CDF62E1A004579220E231142 /* LoginProtocols.swift */, 4FE128396B7355240F8B4C62 /* View */, 16172D640373B2B96B22D611 /* Presenter */, C5B1752ED220171DC9A4A015 /* Interactor */, 3C3CE40BC403AC7A45B46786 /* WireFrame */, ); - path = Auth; + path = Login; sourceTree = ""; }; 3ABCE8E41EC9330D00A80B15 = { @@ -5972,7 +5963,7 @@ 3C3CE40BC403AC7A45B46786 /* WireFrame */ = { isa = PBXGroup; children = ( - 9FB2678C74CCC58C8AAFADD6 /* AuthWireframe.swift */, + 9FB2678C74CCC58C8AAFADD6 /* LoginWireframe.swift */, ); path = WireFrame; sourceTree = ""; @@ -5985,14 +5976,6 @@ path = WireFrame; sourceTree = ""; }; - 4074FC8354B32CD19100C02B /* Interactor */ = { - isa = PBXGroup; - children = ( - 1AAE2417131A6C8ED32A44D2 /* MyGroupAliasInteractor.swift */, - ); - path = Interactor; - sourceTree = ""; - }; 4188F5659F19255180FB387D /* MapSearch */ = { isa = PBXGroup; children = ( @@ -6058,6 +6041,7 @@ 49E75E252CE2F3C96A626230 /* Modules */ = { isa = PBXGroup; children = ( + 4B749F0E214FEFC8002F3A33 /* Auth */, 260531122127407A002E1CF1 /* LogOutput */, FBCE83C320E52351003B7558 /* Payment */, FBF0E38320E5232E00B6FB59 /* WalletBalances */, @@ -6070,9 +6054,7 @@ 26C1A3DD2031A9330009F7F0 /* OtherUser */, 264638181FFFC537002590E6 /* Replies */, A45F10AE20B4218D00F45004 /* Message */, - 3AB452082A8DAEAD93F689D8 /* Auth */, 71C1C60F76F3395F30D450E1 /* Tutorial */, - D6ABBEEBA2D71463B8110D50 /* WebView */, 85433F1C204D593100B373A7 /* WebFullScreen */, E90801F5EE14C0F1931F1B94 /* Main */, 876B96AB0ABCBF19F269E019 /* QRCodeGenerator */, @@ -6274,6 +6256,7 @@ isa = PBXGroup; children = ( 4B5A714C204F069000A551F5 /* ChatService.swift */, + 26F87DF52142B40F000ED2C8 /* SenderService.swift */, ); path = ChatService; sourceTree = ""; @@ -6315,6 +6298,85 @@ path = ActionsView; sourceTree = ""; }; + 4B71AC4021622A5600E4583B /* Notifications */ = { + isa = PBXGroup; + children = ( + 4B71AC4321622A8600E4583B /* AppNotificationsProviding */, + ); + path = Notifications; + sourceTree = ""; + }; + 4B71AC4321622A8600E4583B /* AppNotificationsProviding */ = { + isa = PBXGroup; + children = ( + 4B71AC4421622AA700E4583B /* AppNotificationsProviding.swift */, + 4B71AC4121622A6A00E4583B /* AppNotificationsProvider.swift */, + ); + path = AppNotificationsProviding; + sourceTree = ""; + }; + 4B749EF2214FEABB002F3A33 /* LoginView */ = { + isa = PBXGroup; + children = ( + 3A213F7B1F0093F0006DBE91 /* LoginView.swift */, + E7DD28301F8B6CB200174650 /* LoginViewLayout.swift */, + ); + path = LoginView; + sourceTree = ""; + }; + 4B749EFF214FEE3C002F3A33 /* VerifyNumber */ = { + isa = PBXGroup; + children = ( + 4B749F02214FEE4F002F3A33 /* VerifyNumberProtocols.swift */, + 4B749F0A214FEE53002F3A33 /* View */, + 4B749F0C214FEE61002F3A33 /* Presenter */, + 4B749F0D214FEE68002F3A33 /* Interactor */, + 4B749F0B214FEE57002F3A33 /* Wireframe */, + ); + path = VerifyNumber; + sourceTree = ""; + }; + 4B749F0A214FEE53002F3A33 /* View */ = { + isa = PBXGroup; + children = ( + 4B749F01214FEE4F002F3A33 /* VerifyNumberViewController.swift */, + ); + path = View; + sourceTree = ""; + }; + 4B749F0B214FEE57002F3A33 /* Wireframe */ = { + isa = PBXGroup; + children = ( + 4B749F04214FEE4F002F3A33 /* VerifyNumberWireFrame.swift */, + ); + path = Wireframe; + sourceTree = ""; + }; + 4B749F0C214FEE61002F3A33 /* Presenter */ = { + isa = PBXGroup; + children = ( + 4B749F00214FEE4F002F3A33 /* VerifyNumberPresenter.swift */, + ); + path = Presenter; + sourceTree = ""; + }; + 4B749F0D214FEE68002F3A33 /* Interactor */ = { + isa = PBXGroup; + children = ( + 4B749F03214FEE4F002F3A33 /* VerifyNumberInteractor.swift */, + ); + path = Interactor; + sourceTree = ""; + }; + 4B749F0E214FEFC8002F3A33 /* Auth */ = { + isa = PBXGroup; + children = ( + 3AB452082A8DAEAD93F689D8 /* Login */, + 4B749EFF214FEE3C002F3A33 /* VerifyNumber */, + ); + path = Auth; + sourceTree = ""; + }; 4B7B81C4204478F400C2EFCF /* TimeZoneManager */ = { isa = PBXGroup; children = ( @@ -6323,6 +6385,54 @@ name = TimeZoneManager; sourceTree = ""; }; + 4B7C73E7215A5508007924DB /* Debug */ = { + isa = PBXGroup; + children = ( + 4B7C73E8215A5508007924DB /* SMSCodeProvider */, + 4B7C73EB215A5508007924DB /* MotionManager */, + 4B7C73ED215A5508007924DB /* LogService */, + ); + name = Debug; + path = Services/Debug; + sourceTree = ""; + }; + 4B7C73E8215A5508007924DB /* SMSCodeProvider */ = { + isa = PBXGroup; + children = ( + 4B7C73E9215A5508007924DB /* SMSCodeProvider.swift */, + 4B7C73EA215A5508007924DB /* SMSCodeProviding.swift */, + ); + path = SMSCodeProvider; + sourceTree = ""; + }; + 4B7C73EB215A5508007924DB /* MotionManager */ = { + isa = PBXGroup; + children = ( + 4B7C73EC215A5508007924DB /* MotionManager.swift */, + ); + path = MotionManager; + sourceTree = ""; + }; + 4B7C73ED215A5508007924DB /* LogService */ = { + isa = PBXGroup; + children = ( + 4B7C73EE215A5508007924DB /* LogWriter.swift */, + 4B7C73EF215A5508007924DB /* LogService.swift */, + ); + path = LogService; + sourceTree = ""; + }; + 4B7C73F5215A5522007924DB /* Debug */ = { + isa = PBXGroup; + children = ( + 4B7C73F6215A5522007924DB /* DebugLogs.swift */, + 4B7C73F7215A5522007924DB /* UIView+Debug.swift */, + 4B7C73F8215A5522007924DB /* UILabel+Debug.swift */, + ); + name = Debug; + path = Library/Debug; + sourceTree = ""; + }; 4B8996C6204ECE8500DCB183 /* Contact */ = { isa = PBXGroup; children = ( @@ -6386,6 +6496,14 @@ name = Feed; sourceTree = ""; }; + 4B8AB703215CD32D00C69DE1 /* Sequence */ = { + isa = PBXGroup; + children = ( + 4B8AB701215CD02100C69DE1 /* SequenceExtension.swift */, + ); + path = Sequence; + sourceTree = ""; + }; 4B8BEDDF2049798C00C7D625 /* ImagesView */ = { isa = PBXGroup; children = ( @@ -6417,6 +6535,90 @@ path = Views; sourceTree = ""; }; + 4BB0EFB62151347900704136 /* CustomPopup */ = { + isa = PBXGroup; + children = ( + 4BB0EFB72151347900704136 /* AlertImageViewController */, + ); + path = CustomPopup; + sourceTree = ""; + }; + 4BB0EFB72151347900704136 /* AlertImageViewController */ = { + isa = PBXGroup; + children = ( + 4BB0EFB82151347900704136 /* AlertImageViewController.swift */, + 4BB0EFB92151347900704136 /* AlertImageViewControllerConstraints.swift */, + ); + path = AlertImageViewController; + sourceTree = ""; + }; + 4BE2C5CE2142EAC500A73DD9 /* Audio */ = { + isa = PBXGroup; + children = ( + 4BE2C5DF2142EB0E00A73DD9 /* AudioManager */, + 4BE2C5CF2142EAC500A73DD9 /* AudioPlayer */, + 4BE2C5D12142EAC500A73DD9 /* AudioSessionManager */, + 4BE2C5D32142EAC500A73DD9 /* AudioRecorder */, + 4BE2C5D52142EAC500A73DD9 /* SystemSoundManager */, + 26C8555C215123B00037F106 /* AudioPlayable.swift */, + ); + name = Audio; + path = Services/Audio; + sourceTree = ""; + }; + 4BE2C5CF2142EAC500A73DD9 /* AudioPlayer */ = { + isa = PBXGroup; + children = ( + 4BE2C5D02142EAC500A73DD9 /* AudioPlayer.swift */, + ); + path = AudioPlayer; + sourceTree = ""; + }; + 4BE2C5D12142EAC500A73DD9 /* AudioSessionManager */ = { + isa = PBXGroup; + children = ( + 4BE2C5D22142EAC500A73DD9 /* AudioSessionManager.swift */, + ); + path = AudioSessionManager; + sourceTree = ""; + }; + 4BE2C5D32142EAC500A73DD9 /* AudioRecorder */ = { + isa = PBXGroup; + children = ( + 4BE2C5D42142EAC500A73DD9 /* AudioRecorder.swift */, + ); + path = AudioRecorder; + sourceTree = ""; + }; + 4BE2C5D52142EAC500A73DD9 /* SystemSoundManager */ = { + isa = PBXGroup; + children = ( + 4BE2C5D62142EAC500A73DD9 /* SoundBundle.swift */, + 4BE2C5D72142EAC500A73DD9 /* SystemSoundManager.swift */, + 4BE2C5D82142EAC500A73DD9 /* Sound.swift */, + ); + path = SystemSoundManager; + sourceTree = ""; + }; + 4BE2C5DF2142EB0E00A73DD9 /* AudioManager */ = { + isa = PBXGroup; + children = ( + 4BE2C5E02142EB0E00A73DD9 /* AudioManager.swift */, + 4BE2C5E12142EB0E00A73DD9 /* AudioManagerDelegate.swift */, + ); + path = AudioManager; + sourceTree = ""; + }; + 4BE2C5E42142EB5A00A73DD9 /* NynjaCalls */ = { + isa = PBXGroup; + children = ( + 4BE2C5E52142EB5A00A73DD9 /* NynjaCommunicatorService.swift */, + 4BE2C5E62142EB5A00A73DD9 /* NynjaRingingService.swift */, + ); + name = NynjaCalls; + path = Services/NynjaCalls; + sourceTree = ""; + }; 4BFABA332028CD8800299EE7 /* Contacts */ = { isa = PBXGroup; children = ( @@ -6455,9 +6657,7 @@ 4FE128396B7355240F8B4C62 /* View */ = { isa = PBXGroup; children = ( - A43E67E8206E4C8000048916 /* ViewController */, - 3A213F7B1F0093F0006DBE91 /* LoginView.swift */, - E7DD28301F8B6CB200174650 /* LoginViewLayout.swift */, + F01F3338177726EB10E0D040 /* LoginViewController.swift */, E7E06C651F792AEF00BFC8FA /* LoginWheelContainerDelegate.swift */, E7E06C671F792B0200BFC8FA /* LoginWheelContainerDataSource.swift */, ); @@ -7124,6 +7324,16 @@ path = ThemePicker; sourceTree = ""; }; + 8509FC832158F7B400734D93 /* Files */ = { + isa = PBXGroup; + children = ( + 8509FC842158F7D100734D93 /* AppGroupFlagContainer.swift */, + 8509FC88215908B300734D93 /* AppGroupFlagObserver.swift */, + 8509FC862158F7FC00734D93 /* DirectoryWatcher.swift */, + ); + path = Files; + sourceTree = ""; + }; 850C3015204DA84400DB26C2 /* Privacy */ = { isa = PBXGroup; children = ( @@ -7293,14 +7503,6 @@ path = Collection; sourceTree = ""; }; - 8513A88621184F9A00B5CA4A /* Audio */ = { - isa = PBXGroup; - children = ( - 856B827721184FDF00917F90 /* AudioSessionManager.swift */, - ); - path = Audio; - sourceTree = ""; - }; 8514D52020EE48750002378A /* ContextMenu */ = { isa = PBXGroup; children = ( @@ -7739,6 +7941,7 @@ 8540A330211B34B4007F65AF /* MessageCollectionViewDataSource.swift */, 8540A332211B35A4007F65AF /* MessageCollectionViewDelegate.swift */, 85D77806211D9B980044E72F /* ScrollPosition.swift */, + 85BDD2B721465EFA00695DE5 /* ScrollDirection.swift */, 2625F29E212463E8007C42B5 /* ProgressIdentifier.swift */, ); path = CollectionView; @@ -8177,10 +8380,6 @@ 859C42A6205693F100AE3797 /* SoundService */ = { isa = PBXGroup; children = ( - 859C42A72056940500AE3797 /* Sound.swift */, - 859C42A92056B05D00AE3797 /* SoundBundle.swift */, - 85EBBE022056CF97009BB269 /* SoundPlayer.swift */, - 3A1146641ED6E85A006BA132 /* SoundService.swift */, ); name = SoundService; sourceTree = ""; @@ -8203,6 +8402,16 @@ path = Cell; sourceTree = ""; }; + 85ADEB7721621BC800ABECBD /* Text */ = { + isa = PBXGroup; + children = ( + 85D669F420BD963C00FBD803 /* NSAttributedStringKey+Mention.swift */, + 85ADEB7A2162445200ABECBD /* InputTextStorageDelegate.swift */, + 85ADEB7821621CAD00ABECBD /* InputTextStorage.swift */, + ); + path = Text; + sourceTree = ""; + }; 85B750A32033594100AD6013 /* Cell */ = { isa = PBXGroup; children = ( @@ -8296,7 +8505,7 @@ isa = PBXGroup; children = ( 85D669F120BD963C00FBD803 /* Entity */, - 85D669F420BD963C00FBD803 /* NSAttributedStringKey+Mention.swift */, + 85ADEB7721621BC800ABECBD /* Text */, 85D669F520BD963C00FBD803 /* Payload */, 85D669FC20BD963C00FBD803 /* InputController */, ); @@ -8394,22 +8603,13 @@ path = Models; sourceTree = ""; }; - 85F3DD41203F3ADE00F210C0 /* ConnectionSubscriberService */ = { - isa = PBXGroup; - children = ( - 2683F760203F36B10003181A /* ConnectionSubscriberService.swift */, - 85F3DD42203F3AF300F210C0 /* Tasks */, - ); - name = ConnectionSubscriberService; - sourceTree = ""; - }; - 85F3DD42203F3AF300F210C0 /* Tasks */ = { + 85F3DD41203F3ADE00F210C0 /* BackgroundTaskHandler */ = { isa = PBXGroup; children = ( 2683F75F203F36B00003181A /* BackgroundTaskHandler.swift */, 2683F761203F36B10003181A /* MessageBackgroundTaskHandler.swift */, ); - name = Tasks; + name = BackgroundTaskHandler; sourceTree = ""; }; 85F3DD45203F411B00F210C0 /* Utils */ = { @@ -8696,14 +8896,6 @@ path = Presenter; sourceTree = ""; }; - 9E5100276848F59E81DEE072 /* WireFrame */ = { - isa = PBXGroup; - children = ( - 1C469B351D6DA4CEBCE9C591 /* WebViewWireframe.swift */, - ); - path = WireFrame; - sourceTree = ""; - }; A31F9E0E0E7FEA81D22A0ED7 /* WireFrame */ = { isa = PBXGroup; children = ( @@ -8751,6 +8943,7 @@ A406E397210B453A00435B3E /* SwiftLibrary */ = { isa = PBXGroup; children = ( + 4B8AB703215CD32D00C69DE1 /* Sequence */, 8512349021221B82000129A2 /* Collection */, A43B25B520AB1E7600FF8107 /* String */, A406E398210B454700435B3E /* Dictionary */, @@ -9349,15 +9542,6 @@ path = Models; sourceTree = ""; }; - A43E67E8206E4C8000048916 /* ViewController */ = { - isa = PBXGroup; - children = ( - F01F3338177726EB10E0D040 /* LoginViewController.swift */, - 3A213F791F0082AC006DBE91 /* VerifyNumberVC.swift */, - ); - path = ViewController; - sourceTree = ""; - }; A43E67EB206E859700048916 /* BadgeNumberService */ = { isa = PBXGroup; children = ( @@ -9585,6 +9769,8 @@ A45F10CE20B4218D00F45004 /* BaseChatCellModel.swift */, A45F10CF20B4218D00F45004 /* RepliedMessageModel.swift */, 260225E020F3BA92004FC238 /* ConvertionMessageModel.swift */, + 2695F1FE21625B800095A0FA /* ChangableProgress.swift */, + 2695F20021625CAB0095A0FA /* ProgressDisplayable.swift */, ); path = Models; sourceTree = ""; @@ -10204,6 +10390,8 @@ E75E9F201FBB34490063690C /* WheelContainer */, 1457809A715A3526EBF39205 /* MainViewController.swift */, E7EE893A1F83CEF5009D37F9 /* MainViewControllerLayout.swift */, + 260E77D8215D3C5000D18789 /* ComingSoonExtension.swift */, + 260E77DA215D3C7700D18789 /* ComingSoonProtocol.swift */, ); path = View; sourceTree = ""; @@ -10619,7 +10807,7 @@ isa = PBXGroup; children = ( 26E476581FFEE2D400C06C05 /* Modelka.swift */, - B15F3B55EC2BF6FB5D7A2FAF /* AuthInteractor.swift */, + B15F3B55EC2BF6FB5D7A2FAF /* LoginInteractor.swift */, ); path = Interactor; sourceTree = ""; @@ -10729,7 +10917,6 @@ children = ( C9C694F8201FA4AB00A57297 /* SlideAnimatedTransitioning.swift */, C9C694FC201FA55800A57297 /* SwipeBackHelper.swift */, - C9C694FA201FA50100A57297 /* CustomPanGesture.swift */, 2683F75B203F35BE0003181A /* LongPressWithUpSwipeGestureRecognizer.swift */, 4B2D0639202DDA2000010A0C /* BackSwipable.swift */, ); @@ -10848,18 +11035,6 @@ path = Interactor; sourceTree = ""; }; - D6ABBEEBA2D71463B8110D50 /* WebView */ = { - isa = PBXGroup; - children = ( - 425BCBF6F15DC4F7C3674AEC /* WebViewProtocols.swift */, - E53D9637DF4953E5847905C3 /* View */, - 1616C935DFADD3E799901555 /* Presenter */, - DB1F17663C2FE1EAB9245DDD /* Interactor */, - 9E5100276848F59E81DEE072 /* WireFrame */, - ); - path = WebView; - sourceTree = ""; - }; D806DAD2CA013274207FC865 /* Interactor */ = { isa = PBXGroup; children = ( @@ -10884,14 +11059,6 @@ path = Interactor; sourceTree = ""; }; - DB1F17663C2FE1EAB9245DDD /* Interactor */ = { - isa = PBXGroup; - children = ( - 8DC3F2CD754DE6EC29F6DF88 /* WebViewInteractor.swift */, - ); - path = Interactor; - sourceTree = ""; - }; DB6BCA2999D99C593AB77854 /* Interactor */ = { isa = PBXGroup; children = ( @@ -10970,21 +11137,12 @@ path = View; sourceTree = ""; }; - E53D9637DF4953E5847905C3 /* View */ = { - isa = PBXGroup; - children = ( - 64A724F7272244110EBB528A /* WebViewViewController.swift */, - ); - path = View; - sourceTree = ""; - }; E57956502ACFC6A27ACC9EB9 /* MyGroupAlias */ = { isa = PBXGroup; children = ( 8606C1D61AA46EB77821B1B0 /* MyGroupAliasProtocols.swift */, 4734E7A9B497BB740A55319A /* View */, B8DCBB4ACE8A650987F2D234 /* Presenter */, - 4074FC8354B32CD19100C02B /* Interactor */, 48CBD0E1B8BFC875AB252183 /* WireFrame */, ); path = MyGroupAlias; @@ -10993,8 +11151,8 @@ E61C394BD0E94E3DCF853D4F /* ScheduleMessage */ = { isa = PBXGroup; children = ( - 4BAB9CDE2035CAD800385520 /* Models */, 4948B03AEE34116DB6A7A06D /* ScheduleMessageProtocols.swift */, + 4BAB9CDE2035CAD800385520 /* Models */, CE9D96E59FD1D607EFA72FCE /* View */, F30344C4E0A67F24D44ECB12 /* Presenter */, DA64817AFFB041A05C3D3798 /* Interactor */, @@ -11346,16 +11504,6 @@ path = ProfilePlaceholderCell; sourceTree = ""; }; - E7718BF11FB5D5E30070B402 /* Debug */ = { - isa = PBXGroup; - children = ( - E7718BF41FB5D6080070B402 /* UIView+Debug.swift */, - E7718BF61FB5D8D70070B402 /* UILabel+Debug.swift */, - 8566771B20C139A000DD4204 /* DebugLogs.swift */, - ); - path = Debug; - sourceTree = ""; - }; E77764B21FBDA83F0042541D /* Wheel */ = { isa = PBXGroup; children = ( @@ -11615,6 +11763,7 @@ F11786DC20A9ED54007A9A1B /* Operations */, 8ECC067F1FC5C80C002CF225 /* MessagesProcessingManager.swift */, E749C5661FD4490E0048DEAC /* DefaultMessageProcessingManager.swift */, + 5E0CEA9921490663004B3F7A /* TypingStatusCache.swift */, ); name = MessageProcessing; sourceTree = ""; @@ -11629,15 +11778,6 @@ path = EditName; sourceTree = ""; }; - E7FF40B21F96088500810D1C /* AudioManager */ = { - isa = PBXGroup; - children = ( - E7FF40AF1F9602C400810D1C /* AudioManager.swift */, - E7FF40B31F96089B00810D1C /* AudioManagerDelegate.swift */, - ); - name = AudioManager; - sourceTree = ""; - }; E83E03142578C00B65C468C8 /* Presenter */ = { isa = PBXGroup; children = ( @@ -12033,6 +12173,7 @@ F11786CF20A9867C007A9A1B /* MessageFactory */ = { isa = PBXGroup; children = ( + 85BDD2B921467A9500695DE5 /* MessageFactoryProtocol.swift */, F11786D020A98685007A9A1B /* MessageFactory.swift */, 264C808520DBF397003532FA /* DBFeatureFactory.swift */, ); @@ -12713,6 +12854,7 @@ isa = PBXGroup; children = ( FBDA34E820921079009F4FB6 /* KeyboardLayoutGuide.swift */, + 85A3CA01214129F200E0EDD5 /* KeyboardInteractive.swift */, ); path = KeyboardLayoutGuide; sourceTree = ""; @@ -13247,12 +13389,16 @@ "${SRCROOT}/Pods/Target Support Files/Pods-Nynja/Pods-Nynja-resources.sh", "${PODS_ROOT}/GoogleMaps/Maps/Frameworks/GoogleMaps.framework/Resources/GoogleMaps.bundle", "${PODS_ROOT}/GooglePlaces/Frameworks/GooglePlaces.framework/Resources/GooglePlaces.bundle", + "${PODS_ROOT}/Intercom/Intercom/Intercom.framework/Versions/A/Resources/Intercom.bundle", + "${PODS_ROOT}/Intercom/Intercom/Intercom.framework/Versions/A/Resources/IntercomTranslations.bundle", "${PODS_ROOT}/TestFairy/upload-dsym.sh", ); name = "[CP] Copy Pods Resources"; outputPaths = ( "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/GoogleMaps.bundle", "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/GooglePlaces.bundle", + "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/Intercom.bundle", + "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/IntercomTranslations.bundle", "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/upload-dsym.sh", ); runOnlyForDeploymentPostprocessing = 0; @@ -13317,8 +13463,8 @@ E7A0C73D1FE965A400E00C2B /* TypingExtension.swift in Sources */, A42CE5B620692EDB000889CC /* Room_Spec.swift in Sources */, A42CE5F020692EDB000889CC /* ok2_Spec.swift in Sources */, + 4B7C73FD215A553F007924DB /* LogWriter.swift in Sources */, A42CE60220692EDB000889CC /* sequenceFlow_Spec.swift in Sources */, - 26052C7B20FCE7E000E7A6A0 /* LogService.swift in Sources */, A42CE60020692EDB000889CC /* Message_Spec.swift in Sources */, A4F3DAA42084935400FF71C7 /* Constants.swift in Sources */, E7ED35131FB33806008B5704 /* CellModel.swift in Sources */, @@ -13341,7 +13487,6 @@ 26EEA5482091F84E0066D3B0 /* CollectionsExtensions.swift in Sources */, A42CE5D820692EDB000889CC /* error_Spec.swift in Sources */, 261522642084D5EC00AF72A5 /* Testable.swift in Sources */, - 26FF00A51FCC2EC4002170B1 /* MQTTServiceAuth.swift in Sources */, 267D465A20AB4C4000D42242 /* FeatureExtension.swift in Sources */, A42CE57E20692EDB000889CC /* userTask.swift in Sources */, A4679B9120B2DA640021FE9C /* SelectorCell.swift in Sources */, @@ -13389,6 +13534,7 @@ 85E3AB3D21218A57005FC49A /* SeparatorView.swift in Sources */, A47785A220D18D4A0053E0D2 /* BaseView.swift in Sources */, A42CE58020692EDB000889CC /* error2.swift in Sources */, + 8509FC8A2159095900734D93 /* AppGroupFlagContainer.swift in Sources */, A42CE58820692EDB000889CC /* Cursor.swift in Sources */, A49E1BD320A9AA0E0074DFD3 /* BaseChatModel.swift in Sources */, A42CE5C820692EDB000889CC /* act_Spec.swift in Sources */, @@ -13401,6 +13547,7 @@ 359EB23B1F9A1BC700147437 /* ReachabilityService.swift in Sources */, A46032502105D357009783DA /* InputsCachePolicy.swift in Sources */, 852003FB20D45B47007C0036 /* BertBinConvertible.swift in Sources */, + 260E77DC215D3CCD00D18789 /* ComingSoonProtocol.swift in Sources */, A42CE5EA20692EDB000889CC /* operation_Spec.swift in Sources */, A42CE61020692EDB000889CC /* process_Spec.swift in Sources */, 26A373571FC6EFC500616C21 /* ProgressHUD.swift in Sources */, @@ -13463,6 +13610,7 @@ 26A856292074C7BE00C642EA /* ActionsView+Action.swift in Sources */, 26A0CFE2200513B4006F6617 /* MemberExtension+BERT.swift in Sources */, A4F3DA9D2084910C00FF71C7 /* ContactHandler.swift in Sources */, + 4B7C73FC215A552C007924DB /* LogService.swift in Sources */, 35B1ABB01FA34B2600E65233 /* SearchModel.swift in Sources */, 4B052CB12036193900BC2A9B /* StringAtomExtension.swift in Sources */, 35B98F9E1F9BFDE1009B8DEC /* SendModel.swift in Sources */, @@ -13508,6 +13656,7 @@ A4F3DAAD2084940C00FF71C7 /* RecepientModel.swift in Sources */, 26A8562C2074C84700C642EA /* Collection+ViewLayout.swift in Sources */, 263529182075730500DC6FBD /* actExtension+BERT.swift in Sources */, + 85A3CA03214133FD00E0EDD5 /* KeyboardInteractive.swift in Sources */, A4679B9020B2DA640021FE9C /* SelectorAvatarCell.swift in Sources */, 26C0C1EB2073DC1400C530DA /* ForwardSelectorDataSource.swift in Sources */, 265F5D26209B6AF7008ACCC8 /* Place.swift in Sources */, @@ -13529,6 +13678,7 @@ A42CE5D020692EDB000889CC /* writer_Spec.swift in Sources */, A42CE5EC20692EDB000889CC /* muc_Spec.swift in Sources */, A42CE55A20692EDB000889CC /* Service.swift in Sources */, + 26E0C4482146D5F900A58ECD /* ConnectionService.swift in Sources */, A45F115120B4224100F45004 /* Contact+BaseChatModel.swift in Sources */, 26C061C21FEAA26500A2EBE4 /* FeatureExtension+BERT.swift in Sources */, 359EB23D1F9A1BE600147437 /* Queue.swift in Sources */, @@ -13566,7 +13716,6 @@ F11DF06920BD9E7900F3E005 /* NavigationProtocol.swift in Sources */, 26A8562A2074C7FB00C642EA /* UIView+SafeArea.swift in Sources */, 26C0C1CD2073C94400C530DA /* ForwardSelectorDisplayMode.swift in Sources */, - 260D67DF2125A2FE0072F11F /* LogWriter.swift in Sources */, 26C0C1E92073DB1000C530DA /* Localizable.swift in Sources */, 268C62E32008DA0900433705 /* UIImageExtensions.swift in Sources */, A4330A552109D60D0060BD93 /* QueryFactoryProtocol.swift in Sources */, @@ -13584,6 +13733,7 @@ A45F115220B4224100F45004 /* ContactExtension.swift in Sources */, A4B544EB20EFB36100EB7B0F /* errors.swift in Sources */, 85458CE3212D731200BA8814 /* MessageIdentifiers.swift in Sources */, + 4B8AB705215CE52300C69DE1 /* SequenceExtension.swift in Sources */, 26C0C1EF2073DE2600C530DA /* ForwardSelectorProtocols+ShareExt.swift in Sources */, 26A8563A20750B5D00C642EA /* ScheduleInfo.swift in Sources */, A45F115420B4224100F45004 /* RoomExtension.swift in Sources */, @@ -13617,6 +13767,7 @@ A40F18BB20BFD9C60091B09E /* EmptyStateViewModel.swift in Sources */, A42D52B6206A53AA00EEB952 /* Service_Spec.swift in Sources */, 4B8996C8204ECE9B00DCB183 /* ContactDAO.swift in Sources */, + 4B7C73F3215A5509007924DB /* LogWriter.swift in Sources */, 262D43872033417F002F1E45 /* FriendExtansion+BERT.swift in Sources */, FE58F9B1208F00FE004AFDD3 /* MessageEditActionTable.swift in Sources */, F105C6A0209F71BF0091786A /* CameraInteractor.swift in Sources */, @@ -13634,7 +13785,6 @@ 854A4B302080D6C400759152 /* CellWithImageTableViewCell.swift in Sources */, 3A8045D81F60C98200AED866 /* MQTTServiceHelper.swift in Sources */, 8E9601971FF2EC8100E0C21D /* GroupFilesListVC.swift in Sources */, - 859C42A82056940500AE3797 /* Sound.swift in Sources */, 4B06D3222028A9C6003B275B /* GroupChatItemsFactory.swift in Sources */, A4F3DABE2084990C00FF71C7 /* Roster+DB.swift in Sources */, 26C1A3E72031AA860009F7F0 /* OtherUserPresenter.swift in Sources */, @@ -13661,18 +13811,17 @@ A45F112920B4218D00F45004 /* MessageContentAppearance.swift in Sources */, C9C694F9201FA4AB00A57297 /* SlideAnimatedTransitioning.swift in Sources */, FE2D7CCD211C71AE00520D78 /* WalletService.swift in Sources */, + 4BE2C5E22142EB0F00A73DD9 /* AudioManager.swift in Sources */, E7598F5B1FA1D5D90082FBE7 /* ProfileActionCellLayout.swift in Sources */, 85082DDD2045A873000AE4B2 /* UserSettingsService.swift in Sources */, 1F8247691F2779AD00E5B749 /* iCarousel.m in Sources */, 8580BAF420BD9B8000239D9D /* Range+Extension.swift in Sources */, A42D52C6206A53AA00EEB952 /* Feature_Spec.swift in Sources */, F10AFEB620F7B1B000C7CE83 /* WheelImageFullItemPreview.swift in Sources */, - 3A1146651ED6E85A006BA132 /* SoundService.swift in Sources */, A45F112A20B4218D00F45004 /* BubbleInjectible.swift in Sources */, 4B1D7E14202A0A0200703228 /* GroupMode.swift in Sources */, 3A27B0A71EF307A900B4B3CB /* DeleteUserModel.swift in Sources */, 3A1F74FA1F5ED344009A11E4 /* PushService.swift in Sources */, - 3A82187F1EDEEDF400337B05 /* AlertManager.swift in Sources */, 261F2E2E200EB0AD007D0813 /* RepliesVC+CellDelegate.swift in Sources */, A45F110620B4218D00F45004 /* MessageConfiguration.swift in Sources */, 26F5C8BE206BD49B003A7FF5 /* DefaultActionItemModel.swift in Sources */, @@ -13681,6 +13830,7 @@ F1AC0DE3207252E1001C68F7 /* Testable.swift in Sources */, A408A0BD20C174040029F54B /* ChannelsListInteractor.swift in Sources */, A458FABD20EB8B320075D55E /* MessageChannelActionsProtocol.swift in Sources */, + 4BE2C5D92142EAC500A73DD9 /* AudioPlayer.swift in Sources */, F11786EE20AC39E9007A9A1B /* Job_Spec.swift in Sources */, 0008E92420347A8E003E316E /* DBJobMessage.swift in Sources */, 850FC5EC2032F21E00832D87 /* ForwardSelectorProtocols.swift in Sources */, @@ -13695,6 +13845,7 @@ A42CE5AF20692EDB000889CC /* TypeSpec.swift in Sources */, FB0B721320907DB5003B9757 /* MessageEditService.swift in Sources */, 859B862C204820DC003272B2 /* ThemePickerPresenter.swift in Sources */, + 8509FC872158F7FC00734D93 /* DirectoryWatcher.swift in Sources */, 2603139B20A0A4BA009AC66D /* LanguageSelectorViewController.swift in Sources */, 2686D3201FC3E39C0079CB75 /* ContentNavigationVC.swift in Sources */, B7EF8EDB210C759400E0E981 /* InterpretationTypeCellModel.swift in Sources */, @@ -13706,6 +13857,7 @@ 26FA4210201821B400E6F6EC /* StarHandler.swift in Sources */, FBCE83E120E52496003B7558 /* ProfileServices.swift in Sources */, 26C0C1E42073DA3A00C530DA /* Muc+DB.swift in Sources */, + 4BE2C5DA2142EAC500A73DD9 /* AudioSessionManager.swift in Sources */, 00E98252205C26F7008BF03D /* SessionLineView.swift in Sources */, E77764B61FBDA8E30042541D /* WheelContainerDataSource.swift in Sources */, A4B544E820EFB15C00EB7B0F /* errors.swift in Sources */, @@ -13748,6 +13900,7 @@ 26AD28371FFB0AE3009E4580 /* StorageSubscriber.swift in Sources */, A49E1BCD20A9A6970074DFD3 /* BaseChatModel.swift in Sources */, 3A62B7D81F4CB9D100F45B51 /* BaseMQTTModel.swift in Sources */, + 85ADEB7B2162445200ABECBD /* InputTextStorageDelegate.swift in Sources */, 3A1DC73F1EF15B65006A8E9F /* IoHandler.swift in Sources */, E77FBDDD1FFE828400BDB255 /* AVURLAsset+Duration.swift in Sources */, 85433F26204D596D00B373A7 /* WebFullScreenWireFrame.swift in Sources */, @@ -13770,7 +13923,6 @@ 26A0CFE12005138C006F6617 /* MemberExtension+BERT.swift in Sources */, 2652D6181FA85B28005E62C7 /* ImageSelector.swift in Sources */, 26DE8D9120FE1AF500C41096 /* ChatCellFooterView.swift in Sources */, - 2631C512207A4C0C00F9AA55 /* AudioRecorder.swift in Sources */, 6D485DE51F0AD96D00E12FB1 /* Localizable.swift in Sources */, 260313C820A0BC80009AC66D /* Array+LangExtended.swift in Sources */, 26DAE5D21FFAF7EE00EDF412 /* BackgroundModeService.swift in Sources */, @@ -13802,8 +13954,6 @@ 855EF421202CC6F800541BE3 /* GetExtendedStarsModel.swift in Sources */, A45F113420B4218D00F45004 /* UnreadCell.swift in Sources */, E7ABD2FF1FC2EDBC00E233F7 /* TagTable.swift in Sources */, - 5BC1D37320D3B3D9002A44B3 /* NynjaCommunicatorService.swift in Sources */, - 859C42AA2056B05D00AE3797 /* SoundBundle.swift in Sources */, 265AEA151FE9AFA700AC4806 /* MemberHandler.swift in Sources */, E7C36C311FC4399B00740630 /* DBService.swift in Sources */, B77C11DE2109242200CCB42E /* AssigningInterpreterInteractor.swift in Sources */, @@ -13821,6 +13971,7 @@ 85D669EA20BD95FA00FBD803 /* MessagePresenter+MentionUnreadCounter.swift in Sources */, 854A4B312080D6C400759152 /* CellWithImageCellModel.swift in Sources */, FBCE841020E525A6003B7558 /* HTTPHeader+Authorization.swift in Sources */, + 4B71AC4221622A6A00E4583B /* AppNotificationsProvider.swift in Sources */, 855EF425202CCADB00541BE3 /* ExtendedStarHandler.swift in Sources */, 85BEC0E12063F91C0098C99C /* TimeZoneCellModel.swift in Sources */, 3AC321781EEAC4C10068F3C8 /* AuthModel.swift in Sources */, @@ -13854,7 +14005,7 @@ 8580BAF320BD9B8000239D9D /* String+Suffix.swift in Sources */, A43B259620AB1DFA00FF8107 /* RecordDisplayInputContent.swift in Sources */, 4B4266C1204D917800194BC1 /* ActionsView+Layout.swift in Sources */, - AF440BA5CEBE5170D082FF60 /* AuthProtocols.swift in Sources */, + AF440BA5CEBE5170D082FF60 /* LoginProtocols.swift in Sources */, 85C16C3520D2520E00EDB77E /* StickersDownloadingService.swift in Sources */, 6D6234F81F1E158600EF375F /* HistoryCell.swift in Sources */, E74EC9EF1FC2DE23007268E6 /* MemberTable.swift in Sources */, @@ -13866,7 +14017,6 @@ A43B259D20AB1DFA00FF8107 /* PhoneField.swift in Sources */, 267BE90820693DE700153FB8 /* DBManagerProtocol.swift in Sources */, 26342CAB20ECBB0100D2196B /* TranscribeNetworkService.swift in Sources */, - 3A213F7A1F0082AC006DBE91 /* VerifyNumberVC.swift in Sources */, FBCE83D020E52352003B7558 /* PaymentTableViewCell.swift in Sources */, A44B4D5820CE9BDF00CA700A /* AvatarCell.swift in Sources */, 4B8996F7204EF77100DCB183 /* FeedDAO.swift in Sources */, @@ -13877,10 +14027,10 @@ 85E1DD2720BEE961008AD211 /* ScalableCell.swift in Sources */, 5BC1D37B20D3B4A8002A44B3 /* GroupAddParticipantsCollectionViewCell.swift in Sources */, 269D9DF01FC3AF0D00324263 /* CGSizeExtension.swift in Sources */, + 4BB0EFBD2151347900704136 /* AlertManager.swift in Sources */, D30EB73829E48C0B1C1FD1C9 /* LoginViewController.swift in Sources */, A42D51BA206A361400EEB952 /* io.swift in Sources */, 3A1EB9A51F3A848A00658E93 /* HistoryHandler.swift in Sources */, - 2683F763203F36B10003181A /* ConnectionSubscriberService.swift in Sources */, 850FC5F42032F4CE00832D87 /* ForwardTargets.swift in Sources */, 85788C422044237B003600C9 /* BuildNumberViewController.swift in Sources */, A4679BA920B2DD100021FE9C /* SubscribersTableDelegate.swift in Sources */, @@ -13888,7 +14038,6 @@ 260313AC20A0A4BA009AC66D /* LanguageSettings+Helper.swift in Sources */, A45F112D20B4218D00F45004 /* MessageContentView.swift in Sources */, A42D52C3206A53AA00EEB952 /* Star_Spec.swift in Sources */, - 26D35AB81FD0EFA800A5D513 /* AudioPlayer.swift in Sources */, 853FB0702049B396000996C5 /* SupportItemsFactory.swift in Sources */, 005B0B202029ABC2000D6416 /* TimeZoneItemView.swift in Sources */, E7598F591FA1CDFD0082FBE7 /* ProfileAction.swift in Sources */, @@ -13909,9 +14058,9 @@ B74BB00321076AFA0049CD27 /* CircleMenuItem.swift in Sources */, 6D485DDF1F0ACA4700E12FB1 /* UIImageView+Rounded.swift in Sources */, 26CD3FDB2104D19D00597E62 /* AudioShortTranscribeOperation.swift in Sources */, - 40C2631343E285717633ADFA /* AuthPresenter.swift in Sources */, + 40C2631343E285717633ADFA /* LoginPresenter.swift in Sources */, A42D51A7206A361400EEB952 /* log.swift in Sources */, - DAE89B7EFAB308A6B48AF5EC /* AuthInteractor.swift in Sources */, + DAE89B7EFAB308A6B48AF5EC /* LoginInteractor.swift in Sources */, C940514A204C7FAF00D72B04 /* DataAndStorageViewController.swift in Sources */, 26C1A3E32031A95D0009F7F0 /* OtherUserProtocols.swift in Sources */, 8503B528205046A6006F0593 /* NotificationSettingsInteractor.swift in Sources */, @@ -13944,7 +14093,7 @@ 85433F25204D596D00B373A7 /* WebFullScreenInteractor.swift in Sources */, 8504DEA920693588006722AC /* MediaFullWheelItemModel.swift in Sources */, 8580BAD720BD98E700239D9D /* ChatListMessageAccessoryView.swift in Sources */, - 6F3F21025258D8071BCF95EF /* AuthWireframe.swift in Sources */, + 6F3F21025258D8071BCF95EF /* LoginWireframe.swift in Sources */, 26DCB2522064BA46001EF0AB /* ContactsInteractor.swift in Sources */, FBF0E38A20E5232E00B6FB59 /* WalletBalancesWireFrame.swift in Sources */, FBCE841120E525A6003B7558 /* URLSessionNetworkClient.swift in Sources */, @@ -14014,7 +14163,6 @@ A45F111720B4218D00F45004 /* RepliedMessageModel.swift in Sources */, F10AFEB820F7B1B000C7CE83 /* WheelChatItemPreview.swift in Sources */, FBCE841320E525A6003B7558 /* NetworkRouter.swift in Sources */, - 267C1D5A20404EDB0087808F /* AlertImageViewControllerConstraints.swift in Sources */, A45F112420B4218D00F45004 /* MessageTextView.swift in Sources */, 85D66A0420BD963C00FBD803 /* MessagePayloadBuilder.swift in Sources */, 004581212036073100F8E413 /* JobMessageTable.swift in Sources */, @@ -14026,6 +14174,7 @@ 32868DD51F31CADF0028B260 /* ChatsListProtocols.swift in Sources */, A49CC1D220E4A9C000879D41 /* InputBar+DisplayMode.swift in Sources */, A42D51B3206A361400EEB952 /* Room.swift in Sources */, + 85BDD2BA21467A9500695DE5 /* MessageFactoryProtocol.swift in Sources */, 4B06D31E2028A6D6003B275B /* MySelfItemsFactory.swift in Sources */, 26588E6720A20E49000D3E1A /* Customizable.swift in Sources */, 8541BD6B206CE3A40093EF1E /* ChatPlaceholderWheelItemModel.swift in Sources */, @@ -14061,7 +14210,6 @@ A4330A742109F0D40060BD93 /* StorageService+UserInfo.swift in Sources */, FBCE841520E525A6003B7558 /* URLRequestConvertible.swift in Sources */, 264638271FFFE7F9002590E6 /* RepliesPresenter.swift in Sources */, - F7DFBC93C800B802534D2DE1 /* WebViewProtocols.swift in Sources */, E7C36C391FC46A9E00740630 /* ServiceExtension.swift in Sources */, 8514F17620EA219E00883513 /* ContextMenuControlCell.swift in Sources */, C9DF574C2023BE92006B990A /* SelectCountryHeaderView.swift in Sources */, @@ -14093,16 +14241,15 @@ 2600DAD6203479D000A2D4F7 /* ReturnToHomeHeaderView.swift in Sources */, F117FBD520FF9DAF00BA1F82 /* MediaInfoView.swift in Sources */, A45F114B20B421E400F45004 /* ChatModel.swift in Sources */, - 27F05908F44F464BB3903C89 /* WebViewViewController.swift in Sources */, FB0AD86B20F3A07100F052CE /* ImagePreviewTransitionAnimatable.swift in Sources */, 005B0B222029ABDA000D6416 /* DateTimeItemView.swift in Sources */, 4B5A714D204F069000A551F5 /* ChatService.swift in Sources */, 3A0281F71F53794800206871 /* UIViewExtenstions.swift in Sources */, 2603139420A0A4B9009AC66D /* LanguageSelectorPresenter.swift in Sources */, 26E476591FFEE2D400C06C05 /* Modelka.swift in Sources */, + 4B749F07214FEE4F002F3A33 /* VerifyNumberProtocols.swift in Sources */, 852003FE20D46680007C0036 /* StickerPack.swift in Sources */, B723C636204DA56600884FFD /* SettingsDataAndStorageTableDelegate.swift in Sources */, - 62BCFA14D06D96AFFE53D8BE /* WebViewPresenter.swift in Sources */, 3AC07E3C1F055B3F00ADBE26 /* DoubleExtensions.swift in Sources */, 264312EC210DE4040057E8B0 /* LanguageSectionCoordinator.swift in Sources */, 85579882209322A8007050B8 /* StickerMenuDataSource.swift in Sources */, @@ -14122,6 +14269,7 @@ 85788C4A20442887003600C9 /* Bundle+Keys.swift in Sources */, 260313A820A0A4BA009AC66D /* LabeledHeaderView.swift in Sources */, 2652D6161FA82EFE005E62C7 /* EditProfileVCLayout.swift in Sources */, + 26773F2B215BE15800C09248 /* Array+Operation.swift in Sources */, A432CF1920B4347D00993AFB /* UIView+Animate.swift in Sources */, 855A393D213E76E20002B8DC /* LoadingInteractive.swift in Sources */, E70189BB1F9107AD00CA7005 /* ProximitySensorManager.swift in Sources */, @@ -14130,11 +14278,9 @@ A42D519E206A361400EEB952 /* Loc.swift in Sources */, 85249D322045B1F800B43007 /* WheelPositionItemsFactory.swift in Sources */, A42D51C0206A361400EEB952 /* Cursor.swift in Sources */, - B18705FCC88EEA39EA6DCD7E /* WebViewInteractor.swift in Sources */, F117871720ACF018007A9A1B /* DetailedTableCell.swift in Sources */, 2633EF702052130400DB3868 /* MemberDAO.swift in Sources */, E7EED2381F740D71005DAE20 /* OptionsItem.swift in Sources */, - 85F43C2BE9C631868C394BCD /* WebViewWireframe.swift in Sources */, F119E67420D2510B0043A532 /* GalleryDataSource.swift in Sources */, 2683F751203F34470003181A /* ChatBaseFactory.swift in Sources */, F117871620ACF018007A9A1B /* SwitchedTableCell.swift in Sources */, @@ -14154,6 +14300,7 @@ 263C04E92132E2FF00B8F0BE /* WrappedTaskOperation.swift in Sources */, A42D52BE206A53AA00EEB952 /* cur_Spec.swift in Sources */, A402A1CE20DE6B38005BFA20 /* BaseButton.swift in Sources */, + 4BE2C5DB2142EAC500A73DD9 /* AudioRecorder.swift in Sources */, E74597751FA2222600D3C88C /* NavigationView.swift in Sources */, 6A90F09E2B49EF13666271F8 /* AddContactProtocols.swift in Sources */, A43B25DC20AB1EE400FF8107 /* CreateChannelInfo.swift in Sources */, @@ -14162,6 +14309,7 @@ 854751492093BDD300F8D5F8 /* CollectionViewScrollProxy.swift in Sources */, 850C301B204DA87A00DB26C2 /* PrivacyListPresenter.swift in Sources */, 267BE28E1FDE9FCC00C47E18 /* SettingsGroupWireFrame.swift in Sources */, + 85BDD2B821465EFA00695DE5 /* ScrollDirection.swift in Sources */, A4679BA620B2DD0F0021FE9C /* SubscribersSelectorWireFrame.swift in Sources */, 2648C41A2069B52100863614 /* ChangeNumberViewLayout.swift in Sources */, 8514D52420EE48A30002378A /* NynjaContextMenuItemsFactory+Messages.swift in Sources */, @@ -14220,6 +14368,7 @@ 26A856242074C50D00C642EA /* ActionsView+ScheduleAction.swift in Sources */, B1B8ED3EDB12866323C9EE74 /* QRCodeGeneratorInteractor.swift in Sources */, 85F0866220D6412300A7762E /* RemoteStorageDestination.swift in Sources */, + 260E77DB215D3C7700D18789 /* ComingSoonProtocol.swift in Sources */, 8580BABF20BD981900239D9D /* MessageInteractor+Schedule.swift in Sources */, A45F111920B4218D00F45004 /* MessageContactView.swift in Sources */, 4B06D3242028B209003B275B /* WCBaseItemsFactory.swift in Sources */, @@ -14233,10 +14382,12 @@ 85018419204946C900F324A1 /* ThemeCollectionViewCell.swift in Sources */, A45F112120B4218D00F45004 /* InfoDateView.swift in Sources */, A4688DFA20650FF50013660D /* DBObserver.swift in Sources */, + 4BE2C5E72142EB5A00A73DD9 /* NynjaCommunicatorService.swift in Sources */, 263409342119CFE2002F8D8F /* RecordContainer.swift in Sources */, 853FB0682049B193000996C5 /* SupportProtocols.swift in Sources */, A42D51B1206A361400EEB952 /* Typing.swift in Sources */, A42D52D3206A53AB00EEB952 /* Test_Spec.swift in Sources */, + 4B7C73F0215A5509007924DB /* SMSCodeProvider.swift in Sources */, E7F2CFE21F5EEF1E00806E43 /* PermissionManager.swift in Sources */, 26ACC5CE212C3DDB008455E8 /* AudioTranscribeSendOperation.swift in Sources */, F6A317F954DA5B46BFD50E3C /* QRCodeGeneratorWireframe.swift in Sources */, @@ -14251,11 +14402,13 @@ B7121EB8205045F300AABBE6 /* MediaDownloadManager.swift in Sources */, FBCE840E20E525A6003B7558 /* HTTPMethod.swift in Sources */, 2648C4152069B52100863614 /* ChangeNumberStep3Interactor.swift in Sources */, + 2695F20121625CAB0095A0FA /* ProgressDisplayable.swift in Sources */, 2648C40C2069B52100863614 /* ChangeNumberStep1ViewController.swift in Sources */, E7EED2321F740A9D005DAE20 /* ActionsItem.swift in Sources */, B723C632204D9E5100884FFD /* DataAndStorageOption.swift in Sources */, FBF0E38D20E5232E00B6FB59 /* WalletBalancesPresenter.swift in Sources */, 26C0C1EE2073DE1600C530DA /* ForwardSelectorProtocols+ShareExt.swift in Sources */, + 4BB0EFBC2151347900704136 /* AlertImageViewControllerConstraints.swift in Sources */, 8511D3742034596E00B2A620 /* Collection+ViewLayout.swift in Sources */, F10B0E1720B4401500528E7A /* GalleryPresenter.swift in Sources */, B7EF8ED9210C71E800E0E981 /* InterpretationType.swift in Sources */, @@ -14329,6 +14482,7 @@ 8ED0F3CE1FBC5CF2004916AB /* GroupsListInteractor.swift in Sources */, 267BE2851FDE983400C47E18 /* SettingsGroupVC.swift in Sources */, 8580BAC720BD983400239D9D /* MentionFetchProtocols.swift in Sources */, + 4B749F09214FEE4F002F3A33 /* VerifyNumberWireFrame.swift in Sources */, 4B052CB0203614D400BC2A9B /* StringAtomExtension.swift in Sources */, A43E67EA206E855600048916 /* BadgeNumberService.swift in Sources */, 4D53FE7454959323B1CCFD96 /* ProfileViewController.swift in Sources */, @@ -14360,9 +14514,9 @@ F10B0E2C20B51CCF00528E7A /* CounterIndicatorButton.swift in Sources */, FB16E79B20EFBD57009FA203 /* CryptoCurrency.swift in Sources */, F117870E20ACF018007A9A1B /* CameraQualitySettingsWireframe.swift in Sources */, - E7718BF71FB5D8D70070B402 /* UILabel+Debug.swift in Sources */, 26DCB2422064B9B1001EF0AB /* InviteFriendHeaderView.swift in Sources */, 263D66271FE829CC00A509F8 /* RoomExtension+BERT.swift in Sources */, + 4BE2C5DE2142EAC500A73DD9 /* Sound.swift in Sources */, A45F112320B4218D00F45004 /* MessageVoiceView.swift in Sources */, A44B4D5520CE9BDF00CA700A /* SwitchCellViewModel.swift in Sources */, B74BAFFB21076AFA0049CD27 /* CircleMenu.swift in Sources */, @@ -14400,7 +14554,6 @@ 85057966206D17AB00565C60 /* ImagePickerHandler.swift in Sources */, 6D5157D01F30B36A002A27DB /* ChatView.swift in Sources */, 8572C3BB2092366100E4840C /* StickerCollectionDataSource.swift in Sources */, - E7FF40B41F96089B00810D1C /* AudioManagerDelegate.swift in Sources */, E735853D1F6C2705003354B5 /* Geometry.swift in Sources */, 85B0013421272694000C89FE /* MessageInteractor+History.swift in Sources */, F119E67720D27E990043A532 /* ImagePreviewCVCell.swift in Sources */, @@ -14440,6 +14593,7 @@ 00F7B34A202B350A00E443E1 /* TimeZoneManager.swift in Sources */, A94B03A70E016BDA759B0703 /* EditProfileViewController.swift in Sources */, 8562853920D166E5000C9739 /* CollectionPreviewState.swift in Sources */, + 4B7C73F2215A5509007924DB /* MotionManager.swift in Sources */, F11786BB20A8A63F007A9A1B /* CoordinatorProtocol.swift in Sources */, F105C6BE20A1347E0091786A /* PhotoPreviewInteractor.swift in Sources */, F18AEAFD20C15792004FE01C /* SelectAvatarCoordinator.swift in Sources */, @@ -14465,7 +14619,6 @@ 26142B1320473BFD004E5FE4 /* DBMessageLink.swift in Sources */, 68B66BDEEFD73CDC331AC840 /* EditProfilePresenter.swift in Sources */, 4B06D3082028A200003B275B /* WCItemsFactory.swift in Sources */, - C9C694FB201FA50100A57297 /* CustomPanGesture.swift in Sources */, E7AE41681FCC596300C3ED5D /* DBRoomMember.swift in Sources */, C9C69505202230DD00A57297 /* SelectCountryCell.swift in Sources */, 2648C4102069B52100863614 /* ChangeNumberStep3Wireframe.swift in Sources */, @@ -14478,7 +14631,6 @@ 8ED0F3C11FBC5CB1004916AB /* Contact+DialogCellModel.swift in Sources */, 264C808620DBF397003532FA /* DBFeatureFactory.swift in Sources */, 8EE9BC1A1FFE79FF00ECBBC7 /* GroupStorageCell.swift in Sources */, - E7FF40B01F9602C400810D1C /* AudioManager.swift in Sources */, A42D51A2206A361400EEB952 /* Desc.swift in Sources */, FBCE83CF20E52352003B7558 /* PaymentWireFrame.swift in Sources */, C940514B204C7FAF00D72B04 /* DataAndStorageProtocols.swift in Sources */, @@ -14494,6 +14646,7 @@ 2606F3BC20BFE20500CF7F15 /* MessageInteractor+Translation.swift in Sources */, ACD15567460FFE46A0AAF51E /* EditProfileInteractor.swift in Sources */, FE9E70D021175DDC0034067A /* ChatScreenAlertFactory.swift in Sources */, + 4B7C73FB215A5522007924DB /* UILabel+Debug.swift in Sources */, A4B544FF20EFC1BA00EB7B0F /* StatusCode.swift in Sources */, 001169B5201A0B02001B435F /* MapSearchCell.swift in Sources */, 26342CB420ECFAB600D2196B /* MessageInteractor+Transcription.swift in Sources */, @@ -14551,6 +14704,7 @@ A45F114820B421AB00F45004 /* Contact+BaseChatModel.swift in Sources */, 6CED2C4CE125011A3A731D62 /* AddContactViaPhoneInteractor.swift in Sources */, 260313AA20A0A4BA009AC66D /* ChatLanguageSettingsViewController.swift in Sources */, + 851105BB2163D0C800F07019 /* TranscribeResponseData.swift in Sources */, 859B862F204820DC003272B2 /* ThemePickerInteractor.swift in Sources */, 263A60AC1FB4F8F7006F9D52 /* ParticipantsDataSource.swift in Sources */, 852E8475213462F000FD3841 /* ReversedMessageCollectionViewLayout.swift in Sources */, @@ -14569,6 +14723,7 @@ 3A237BC91F30AB0F00C42B6E /* EditProfileVC.swift in Sources */, A406E39A210B457300435B3E /* DictionaryExtension.swift in Sources */, 2661D1331F373D5900F3E125 /* WheelConfiguration.swift in Sources */, + 4B749F05214FEE4F002F3A33 /* VerifyNumberPresenter.swift in Sources */, 263C04EB2132E56E00B8F0BE /* TranscribeOperation.swift in Sources */, A42CE5AD20692EDB000889CC /* StringAtom.swift in Sources */, A4CB1520210372DF00C3B68B /* JDMechanism.swift in Sources */, @@ -14597,7 +14752,6 @@ 854A4B2D2080D68200759152 /* CellWithArrowCellModel.swift in Sources */, C9405149204C7FAF00D72B04 /* DataAndStoragePresenter.swift in Sources */, 3819EAEB412EBA913146F443 /* HistoryPresenter.swift in Sources */, - E7718BF51FB5D6080070B402 /* UIView+Debug.swift in Sources */, 26245F44204EF67C00C8D3DD /* PresenterApearingProtocol.swift in Sources */, 8514F17520EA219E00883513 /* ContextMenuItemCell.swift in Sources */, 87A3D03524B9258B33726A57 /* HistoryInteractor.swift in Sources */, @@ -14622,6 +14776,7 @@ 267BE2AD1FE13AB600C47E18 /* ParticipantsProtocols.swift in Sources */, 268C341121067F1D00F1472A /* AudioLongTranscribeOperation.swift in Sources */, A4ED79B020C8041500A41F67 /* TableViewDataSourceProxy.swift in Sources */, + 26E0C44721469E9800A58ECD /* ConnectionService.swift in Sources */, A45F113E20B4218D00F45004 /* MessageInteractor+Utils.swift in Sources */, A4CB153521038A7A00C3B68B /* UIDevice+Jailbreak.swift in Sources */, 3AE0A84C1F20321A008A04F3 /* WheelItemModel.swift in Sources */, @@ -14642,13 +14797,16 @@ 4B06D30620287060003B275B /* WCDataManagerProtocol.swift in Sources */, A411D95A20AC39C6009D107C /* ConversationsProviding.swift in Sources */, 267BE90A20693F4800153FB8 /* ProfileDAOProtocol.swift in Sources */, + 4BE2C5DC2142EAC500A73DD9 /* SoundBundle.swift in Sources */, 4B8996CD204ED33400DCB183 /* StarDAOProtocol.swift in Sources */, 85D669EC20BD962800FBD803 /* MessageVCLayout.swift in Sources */, 8EDDB08A200529C6000B7EC2 /* GroupStorageCollectionVC.swift in Sources */, + 2695F1FF21625B800095A0FA /* ChangableProgress.swift in Sources */, B74BB00021076AFA0049CD27 /* UIView+Mask.swift in Sources */, A42D51CD206A361400EEB952 /* operation.swift in Sources */, 265F5D25209B6987008ACCC8 /* LocationType.swift in Sources */, 4B2D063A202DDA2000010A0C /* BackSwipable.swift in Sources */, + 4BE2C5DD2142EAC500A73DD9 /* SystemSoundManager.swift in Sources */, F10AFEBC20F7B1D200C7CE83 /* WheelPreviewFactory.swift in Sources */, 4B3F055F2043F871002E0F54 /* ScheduleMessageConfiguration.swift in Sources */, A42D51A1206A361400EEB952 /* Friend.swift in Sources */, @@ -14768,7 +14926,7 @@ 852DF26120371FB400A4F8B6 /* FileExtension.swift in Sources */, A45F110B20B4218D00F45004 /* InternetStatus.swift in Sources */, FBCE83D320E52352003B7558 /* PaymentInteractor.swift in Sources */, - 26052C7A20FCE7E000E7A6A0 /* LogService.swift in Sources */, + 26F87DF62142B40F000ED2C8 /* SenderService.swift in Sources */, 26FF00A61FCC2ED5002170B1 /* MQTTService.swift in Sources */, 26D621F42069778400595E13 /* ChatWheelItemView.swift in Sources */, E7E06C661F792AEF00BFC8FA /* LoginWheelContainerDelegate.swift in Sources */, @@ -14779,10 +14937,8 @@ 5BC1D38120D3B54B002A44B3 /* CallInfoView.swift in Sources */, 264638291FFFE835002590E6 /* RepliesInteractor.swift in Sources */, 85D66A2220BD970400FBD803 /* BBCodeElement.swift in Sources */, - 8566771C20C139A000DD4204 /* DebugLogs.swift in Sources */, B723C634204DA54200884FFD /* SettingsDataAndStorageTableDataSource.swift in Sources */, 851EBD7F20B418890065C644 /* StickersInputView.swift in Sources */, - 267C1D5920404EDB0087808F /* AlertImageViewController.swift in Sources */, 0008E91B20333A38003E316E /* JobHandler.swift in Sources */, B750EF042046D69C00A99F9C /* SpeedMesurement.swift in Sources */, 8512349221221B9E000129A2 /* Collection.swift in Sources */, @@ -14797,7 +14953,7 @@ 26B32B961FE20BAB00888A0A /* DescExtension+BERT.swift in Sources */, 4B1D7E0B2029D8CD00703228 /* GroupOptionsItemsFactory.swift in Sources */, CCF8AA193F15D4191EC99051 /* SplashProtocols.swift in Sources */, - 26342CAF20ECD16A00D2196B /* TranscribeShortResponseData.swift in Sources */, + 8509FC852158F7D100734D93 /* AppGroupFlagContainer.swift in Sources */, E743B5881FB08F0F00F72F92 /* ParticipantsAvatarCell.swift in Sources */, D883A2CBD629A340B27997EF /* SplashViewController.swift in Sources */, 26D6D229212EDADC00EA2419 /* ConvertMessageDAO.swift in Sources */, @@ -14817,7 +14973,6 @@ 85788C4620442392003600C9 /* BuildNumberInteractor.swift in Sources */, 2605312921298BEF002E1CF1 /* Logoutputcell.swift in Sources */, 6B3D349607A18D5650BF47E6 /* SplashInteractor.swift in Sources */, - 260D67D92124616A0072F11F /* LogWriter.swift in Sources */, 859B863720485F01003272B2 /* CarouselPickerViewController.swift in Sources */, 8566771E20C1579C00DD4204 /* StorageSubscriberReference.swift in Sources */, F117871420ACF018007A9A1B /* CameraSettingsWireframe.swift in Sources */, @@ -14828,6 +14983,7 @@ 26FA420E201812D600E6F6EC /* StarTableDS.swift in Sources */, F10B0E2820B519B700528E7A /* GalleryPhotoItemCollectionViewCell.swift in Sources */, E7C36C351FC448CB00740630 /* DBFeature.swift in Sources */, + 4B71AC4521622AA700E4583B /* AppNotificationsProviding.swift in Sources */, 4B06D3122028A4CF003B275B /* GroupChatsItemsFactory.swift in Sources */, 0062D93B2062EC4100B915AC /* PhoneContact.swift in Sources */, 85433F24204D596D00B373A7 /* WebFullScreenProtocols.swift in Sources */, @@ -14873,6 +15029,7 @@ 26E7D04C1FCB8A72001C69B7 /* UIImageView+SetImage.swift in Sources */, A45F111320B4218D00F45004 /* MessageVC+CellDelegate.swift in Sources */, 0008E92220347A7B003E316E /* DBJob.swift in Sources */, + 85ADEB7921621CAD00ABECBD /* InputTextStorage.swift in Sources */, 85788C4C20443366003600C9 /* BuildNumberViewControllerLayout.swift in Sources */, F10B0E1D20B4420900528E7A /* GalleryInteractor.swift in Sources */, DDDA12EC6C743547BC91276F /* ImagePreviewWireframe.swift in Sources */, @@ -14898,11 +15055,14 @@ 26F03C0D20698B0000712CB0 /* ChatWheelItemModel.swift in Sources */, E4F62F7771D4BB7FE3B48FA2 /* VideoPreviewProtocols.swift in Sources */, 8580BAEC20BD9A7100239D9D /* LinkRecognizable.swift in Sources */, + 26C8555D215123B00037F106 /* AudioPlayable.swift in Sources */, A4F3DAAF2084944900FF71C7 /* RoomModel.swift in Sources */, A44B4D5620CE9BDF00CA700A /* ArrowCell.swift in Sources */, + 4BE2C5E32142EB0F00A73DD9 /* AudioManagerDelegate.swift in Sources */, 85D66A2320BD970400FBD803 /* BBTagBuilder.swift in Sources */, A981ECC08BCDAF26E135B20D /* VideoPreviewViewController.swift in Sources */, A45F113B20B4218D00F45004 /* MessageInteractor+Fetch.swift in Sources */, + 5E0CEA9A21490663004B3F7A /* TypingStatusCache.swift in Sources */, 3A8045DA1F60E18E00AED866 /* Queue.swift in Sources */, A42D51A3206A361400EEB952 /* Test.swift in Sources */, A45F113220B4218D00F45004 /* BaseChatCellLayout.swift in Sources */, @@ -14933,6 +15093,7 @@ 8540A331211B34B4007F65AF /* MessageCollectionViewDataSource.swift in Sources */, A48C154220EF76EE002DA994 /* LinkExtension.swift in Sources */, A42D52AD206A53AA00EEB952 /* log_Spec.swift in Sources */, + 4BB0EFBB2151347900704136 /* AlertImageViewController.swift in Sources */, F6150A15F8A3E399EEB2C724 /* MapWireframe.swift in Sources */, 853E595920D711B1007799B9 /* StickerPackTable.swift in Sources */, 8514DE8C2136A5FD00718DD8 /* StarActionTable.swift in Sources */, @@ -14958,11 +15119,12 @@ 82FCF48AA4A8C04CC8B0B5B6 /* FavoritesWireframe.swift in Sources */, 26AD28391FFB0AF9009E4580 /* StorageObserver.swift in Sources */, 85D66A0120BD963C00FBD803 /* Mention.swift in Sources */, - 85EBBE032056CF97009BB269 /* SoundPlayer.swift in Sources */, E7C36C331FC441B900740630 /* DBRoster.swift in Sources */, 85D66A1020BD965300FBD803 /* MentionPanelView.swift in Sources */, + 4B7C73F1215A5509007924DB /* SMSCodeProviding.swift in Sources */, 45F60C4B14438C65076457AB /* EditUsernameProtocols.swift in Sources */, 00F7B33E2029DD4B00E443E1 /* AudioItemView.swift in Sources */, + 4B7C73F9215A5522007924DB /* DebugLogs.swift in Sources */, 0062D9412062EC4100B915AC /* InviteFriendsSelectionCell.swift in Sources */, 5A6237362268CC9BD4792230 /* EditUsernameViewController.swift in Sources */, A42D51C7206A361400EEB952 /* timeoutEvent.swift in Sources */, @@ -14990,6 +15152,7 @@ A42D51C8206A361400EEB952 /* iterator.swift in Sources */, C018D77E539F6831CFE89216 /* EditUsernameWireframe.swift in Sources */, 00102F3A202C8A5600A877A9 /* XDoneView.swift in Sources */, + 4B8AB702215CD02100C69DE1 /* SequenceExtension.swift in Sources */, 50960A9A3A3E544A494B4642 /* EditPhotoProtocols.swift in Sources */, 260313A220A0A4BA009AC66D /* ActionCell.swift in Sources */, 26E7D04A1FCB8973001C69B7 /* Amazon+FileSync.swift in Sources */, @@ -15013,7 +15176,6 @@ A43B25AF20AB1DFA00FF8107 /* ImagePlaceholderField.swift in Sources */, E7CC5AD01FD99697002746F6 /* ImageCellModel.swift in Sources */, A43B25D520AB1EE400FF8107 /* NewChannelProtocols.swift in Sources */, - 856B827821184FDF00917F90 /* AudioSessionManager.swift in Sources */, A42D51BD206A361400EEB952 /* CDR.swift in Sources */, 2689CDEA20C48AD8007816B9 /* TranslationManualView.swift in Sources */, 6C25C4720B043D98729C02C8 /* TopUpAccountPresenter.swift in Sources */, @@ -15026,6 +15188,7 @@ 2648C4182069B52100863614 /* ChangeNumberStep2Protocols.swift in Sources */, 26B32B6D1FE171F700888A0A /* BadgeButton.swift in Sources */, 85433F22204D596D00B373A7 /* WebFullScreenPresenter.swift in Sources */, + 4B71AC36216215F600E4583B /* DBRoomExtension.swift in Sources */, 8580BAC620BD983400239D9D /* MessageProtocols.swift in Sources */, 2683F77A203F38E30003181A /* UIPickerView.swift in Sources */, A4679BAB20B2DD100021FE9C /* SubscribersCollectionDataSource.swift in Sources */, @@ -15063,7 +15226,7 @@ 0B79E13E95305A80847AA99F /* MyGroupAliasViewController.swift in Sources */, B745F2E42109B9E500488A91 /* LanguagePickerDelegate.swift in Sources */, 990A25B2C84CE09B4CE64533 /* MyGroupAliasPresenter.swift in Sources */, - D839883F9B7A8CD245A85701 /* MyGroupAliasInteractor.swift in Sources */, + 4BE2C5E82142EB5A00A73DD9 /* NynjaRingingService.swift in Sources */, 4B06D3202028A9B1003B275B /* P2pChatItemsFactory.swift in Sources */, A432CF1820B4347D00993AFB /* MaterialTextField.swift in Sources */, 8514F17420EA219E00883513 /* ContextMenuNextCell.swift in Sources */, @@ -15113,6 +15276,7 @@ 85D669E420BD956000FBD803 /* Int+AnyObject.swift in Sources */, A4569873060C49904EF8C555 /* EditGroupPhotoViewController.swift in Sources */, 8502DB502061030000613C8C /* WheelPositionPickerWireFrame.swift in Sources */, + 85A3CA02214129F200E0EDD5 /* KeyboardInteractive.swift in Sources */, 854D13D8211B2E7200E139FC /* MessageCollectionViewLayout.swift in Sources */, 0062D9452062EC4100B915AC /* InviteFriendsDS.swift in Sources */, 85458CF3212D762900BA8814 /* Message+Factory.swift in Sources */, @@ -15140,11 +15304,13 @@ C493782D4488E45CB1D67DE4 /* CreateGroupViewController.swift in Sources */, A42D52C9206A53AB00EEB952 /* operation_Spec.swift in Sources */, A42D51B7206A361400EEB952 /* push.swift in Sources */, + 4B7C73FA215A5522007924DB /* UIView+Debug.swift in Sources */, E7C36C3B1FC46B4300740630 /* FeatureExtension.swift in Sources */, B723C630204D9E1500884FFD /* PickableEnum.swift in Sources */, A432CF1F20B44C0000993AFB /* MaterialTextContainer.swift in Sources */, 5AD8110B5B87B1AB9F1C5B52 /* CreateGroupPresenter.swift in Sources */, A43B259520AB1DFA00FF8107 /* InputContentProtocol.swift in Sources */, + 8509FC89215908B300734D93 /* AppGroupFlagObserver.swift in Sources */, 2625DBF620EFC52E00E01C05 /* AudioFileConvertOperation.swift in Sources */, 0062D9482062EC4100B915AC /* InviteFriendsInteractor.swift in Sources */, 8562853220D140FC000C9739 /* InputBar+ButtonType.swift in Sources */, @@ -15175,7 +15341,7 @@ 8520040720D4F436007C0036 /* StickerPreviewConfig.swift in Sources */, F1607B1D20B20F7800BDF60A /* GridView.swift in Sources */, F11786F320AC7A6E007A9A1B /* VideoPreviewView.swift in Sources */, - 26534B25210B4BE70003B9BC /* DBMessage+TypeExtension.swift in Sources */, + 26534B25210B4BE70003B9BC /* DBMessage+Extension.swift in Sources */, 2F7C7F7837BDE6F5767A3A8C /* GroupStorageViewController.swift in Sources */, 4B1D7E012029C4BE00703228 /* OptionsItemsFactory.swift in Sources */, C921738220BADAFC00519A2D /* TextInputValidationService.swift in Sources */, @@ -15195,6 +15361,7 @@ A1AD6864F4F49D9FC8997D59 /* SelectCountryPresenter.swift in Sources */, 32E5A25AD25BF752EB3864AB /* SelectCountryInteractor.swift in Sources */, A42D52DB206A53AB00EEB952 /* messageEvent_Spec.swift in Sources */, + 4B749F08214FEE4F002F3A33 /* VerifyNumberInteractor.swift in Sources */, 1A9DFA4A2ED5ACE55035FA17 /* SelectCountryWireframe.swift in Sources */, A42D51CA206A361400EEB952 /* ExtendedStar.swift in Sources */, 01AA377709C2831ACE2F08D0 /* AddContactByUsernameProtocols.swift in Sources */, @@ -15206,10 +15373,12 @@ 8562853B20D16C61000C9739 /* LongPressClosureRecognizer.swift in Sources */, 40D11B34597AE40C8B71E59C /* AddContactByUsernameInteractor.swift in Sources */, 26DCB24C2064B9CC001EF0AB /* ContactCellModel.swift in Sources */, + 4B749F06214FEE4F002F3A33 /* VerifyNumberViewController.swift in Sources */, A42D52AF206A53AA00EEB952 /* Room_Spec.swift in Sources */, 26053123212741C2002E1CF1 /* LogOutputWireFrame.swift in Sources */, A45F112B20B4218D00F45004 /* MessageContainerView.swift in Sources */, A7285B8B56BFCA857AD9BA8A /* AddContactByUsernameWireframe.swift in Sources */, + 260E77D9215D3C5000D18789 /* ComingSoonExtension.swift in Sources */, 2603139A20A0A4B9009AC66D /* LanguageSelectorTableDelegate.swift in Sources */, 853FB06A2049B193000996C5 /* SupportWireFrame.swift in Sources */, 853D0F9A20C0514E008C3684 /* UICollectionViewFlowLayout+ItemSize.swift in Sources */, @@ -15268,6 +15437,7 @@ 2605312121274133002E1CF1 /* LogOutputPresenter.swift in Sources */, E3E22BD2755EAE3DBBCE2E9D /* DateTimePickerWireframe.swift in Sources */, 850FC5F22032F33900832D87 /* ForwardSelectorViewController.swift in Sources */, + 4B7C73F4215A5509007924DB /* LogService.swift in Sources */, A42D52D0206A53AB00EEB952 /* Friend_Spec.swift in Sources */, F11786D120A98685007A9A1B /* MessageFactory.swift in Sources */, 84BB63C68EA124AA7DD21B30 /* LanguageSettingsProtocols.swift in Sources */, @@ -15283,7 +15453,6 @@ C6B308C6734EFB77892832A0 /* SecurityPresenter.swift in Sources */, A42D52B4206A53AA00EEB952 /* ok_Spec.swift in Sources */, B74BAFFD21076AFA0049CD27 /* Sector.swift in Sources */, - 260531262127455C002E1CF1 /* MotionManager.swift in Sources */, 8E54E93EA25B11D417A6100E /* SecurityInteractor.swift in Sources */, A43B259420AB1DFA00FF8107 /* InputBar.swift in Sources */, F11DF06820BD996200F3E005 /* NavigationProtocol.swift in Sources */, @@ -15487,6 +15656,7 @@ PROVISIONING_PROFILE = "2a318f9e-d0ab-41dc-968a-e1cb13de4de5"; PROVISIONING_PROFILE_SPECIFIER = ProductionBundle_AppstoreExt; SKIP_INSTALL = YES; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = RELEASE; SWIFT_COMPILATION_MODE = wholemodule; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; SWIFT_VERSION = 4.0; @@ -15621,7 +15791,7 @@ CODE_SIGN_IDENTITY = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; CODE_SIGN_STYLE = Manual; - DEBUG_INFORMATION_FORMAT = dwarf; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEFINES_MODULE = YES; DEVELOPMENT_TEAM = 9GKQ5AMF2B; ENABLE_BITCODE = NO; @@ -15631,7 +15801,7 @@ OTHER_SWIFT_FLAGS = "$(inherited) -D SQLITE_HAS_CODEC -D GRDBCIPHER $(inherited) \"-D\" \"COCOAPODS\" -Xfrontend -debug-time-function-bodies -Xfrontend -warn-long-expression-type-checking=20 -Xfrontend -warn-long-function-bodies=20"; PRODUCT_BUNDLE_IDENTIFIER = "$(BundleIdentifier)"; PRODUCT_NAME = "$(TARGET_NAME)"; - PROVISIONING_PROFILE = "dc81b7c8-7ca8-4e18-9fee-d0f512f7c8ca"; + PROVISIONING_PROFILE = "7757f70a-8690-4f76-9822-0ac1e08381ea"; PROVISIONING_PROFILE_SPECIFIER = DevBundle_Dev; SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; SWIFT_OBJC_BRIDGING_HEADER = "Nynja-Bridging-Header.h"; @@ -15945,9 +16115,9 @@ MTL_ENABLE_DEBUG_INFO = NO; PRODUCT_BUNDLE_IDENTIFIER = "$(BundleIdentifier)"; PRODUCT_NAME = "$(TARGET_NAME)"; - PROVISIONING_PROFILE = "3d9e361e-de0d-4189-be64-4bea82318684"; + PROVISIONING_PROFILE = "45a1afcf-be11-4391-825d-7cf40979bc47"; PROVISIONING_PROFILE_SPECIFIER = NynjaRC_adhoc; - SWIFT_ACTIVE_COMPILATION_CONDITIONS = ""; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = RELEASE; SWIFT_OBJC_BRIDGING_HEADER = "Nynja-Bridging-Header.h"; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; SWIFT_SWIFT3_OBJC_INFERENCE = Off; @@ -15977,9 +16147,10 @@ OTHER_SWIFT_FLAGS = "$(inherited) \"-D\" \"COCOAPODS\" -DSHARE_EXTENSION"; PRODUCT_BUNDLE_IDENTIFIER = "$(ExtensionBundleIdentifier)"; PRODUCT_NAME = "$(TARGET_NAME)"; - PROVISIONING_PROFILE = "f232c367-7000-49c5-b32c-0efb36c5f2e3"; + PROVISIONING_PROFILE = "9b8a591a-4b46-4454-a314-16a54e890933"; PROVISIONING_PROFILE_SPECIFIER = NynjaRC_adhocExt; SKIP_INSTALL = YES; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = RELEASE; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; SWIFT_VERSION = 4.0; TARGETED_DEVICE_FAMILY = "1,2"; diff --git a/Nynja/AppDelegate.swift b/Nynja/AppDelegate.swift index bd68ff5ad..7582b767f 100644 --- a/Nynja/AppDelegate.swift +++ b/Nynja/AppDelegate.swift @@ -15,9 +15,7 @@ import AWSCore import AWSS3 import UserNotifications import Firebase - -// TODO: will be moved to DependencyFactory -let connectionSubscriberService = ConnectionSubscriberService() +import Intercom @UIApplicationMain class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterDelegate { @@ -34,6 +32,9 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD private let storageService = StorageService.sharedInstance private let antiDebuggingService = AntiDebuggingService() + // FIXME: need to be removed from here when share extension won't require new mqtt connection. + private var appGroupObserver: AppGroupFlagObserver? + func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool { UNUserNotificationCenter.current().delegate = self @@ -41,7 +42,8 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD configureDependencies() - migrateFromV5toV6() + observeGroupAppChanges() + wipeStorage() configureWindow() @@ -64,11 +66,11 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD } func applicationWillEnterForeground(_ application: UIApplication) { - MQTTService.sharedInstance.reconnectWithTimer() + MQTTService.sharedInstance.reconnect() } func applicationWillTerminate(_ application: UIApplication) { - MQTTService.sharedInstance.mqtt?.disconnect() + MQTTService.sharedInstance.disconnect() } } @@ -103,6 +105,9 @@ private extension AppDelegate { setupCrashlytics() setupGoogleMaps() setupAmazon() + setupIntercom() + + try? AudioSessionManager.shared.configureDefaultSession() FirebaseApp.configure() @@ -110,26 +115,6 @@ private extension AppDelegate { FileManagerService.sharedInstance.createDirectory(dirName: Constants.Folders.downloads) } - private func migrateFromV5toV6() { - migrateWasLogined() - migrateWasRun() - } - - private func migrateWasLogined() { - let oldKey = "firstRun" - - if let wasLogined = storageService.userDefaults?.bool(forKey: oldKey), wasLogined { - storageService.userDefaults?.set(true, forKey: UserIdentifiers.wasLogined.rawValue) - storageService.userDefaults?.set(false, forKey: oldKey) - } - } - - private func migrateWasRun() { - if storageService.isUserLogined { - storageService.wasRun = true - } - } - private func wipeStorage() { if !storageService.wasRun { LogService.log(topic: .db) { return "Clear storage: AppDelegate - if it is first runs" } @@ -137,6 +122,23 @@ private extension AppDelegate { storageService.wasRun = true } } + + private func observeGroupAppChanges() { + self.appGroupObserver = AppGroupFlagObserver(fileManager: .default, appGroup: Bundle.main.appGroupName) + do { + try appGroupObserver?.prepare() + try appGroupObserver?.removeFlagIfExists(.shareExtension) + try appGroupObserver?.observe { flags in + if flags.contains(.shareExtension) { + MQTTService.sharedInstance.disconnect() + } else { + MQTTService.sharedInstance.reconnect() + } + } + } catch { + LogService.log(topic: .fileSystem) { error.localizedDescription } + } + } } // MARK: - Setup third party services @@ -181,4 +183,10 @@ private extension AppDelegate { AWSServiceManager.default().defaultServiceConfiguration = defaultConfiguration } + + private func setupIntercom() { + let intercomServiceConfig = ThirdPartyServicesFactory.intercom.serviceConfig + Intercom.setApiKey(intercomServiceConfig.apiKey, forAppId: intercomServiceConfig.appId) + } + } diff --git a/Nynja/Audio/AudioSessionManager.swift b/Nynja/Audio/AudioSessionManager.swift deleted file mode 100644 index c491b94a9..000000000 --- a/Nynja/Audio/AudioSessionManager.swift +++ /dev/null @@ -1,138 +0,0 @@ -// -// AudioSessionManager.swift -// Nynja -// -// Created by Anton Poltoratskyi on 06.08.2018. -// Copyright © 2018 TecSynt Solutions. All rights reserved. -// - -import AVFoundation - -final class AudioSessionManager { - - private let session: AVAudioSession - - private let lock = NSLock() - - enum Category { - case message - case playback - case playAndRecord - - var nativeCategory: String { - switch self { - case .message: - return AVAudioSessionCategoryAmbient - case .playback: - return AVAudioSessionCategoryPlayback - case .playAndRecord: - return AVAudioSessionCategoryPlayAndRecord - } - } - - var isAmbient: Bool { - switch self { - case .message: - return true - case .playback, .playAndRecord: - return false - } - } - - init?(string: String) { - switch string { - case AVAudioSessionCategoryAmbient: - self = .message - case AVAudioSessionCategoryPlayback: - self = .playback - case AVAudioSessionCategoryPlayAndRecord: - self = .playAndRecord - default: - // Just ignore other categories at now. - return nil - } - } - } - - private var currentCategory: Category? { - return Category(string: session.category) - } - - private var isPlaybackActive: Bool = false - - - // MARK: - Init - - static let shared = AudioSessionManager(session: .sharedInstance()) - - private init(session: AVAudioSession) { - self.session = session - NotificationCenter.default.addObserver(self, - selector: #selector(audioSessionDidInterrupted(notification:)), - name: .AVAudioSessionInterruption, - object: nil) - } - - - // MARK: - Setup - - @discardableResult - func request(category: Category, with options: AVAudioSessionCategoryOptions = []) throws -> Bool { - lock.lock() - defer { - lock.unlock() - } - do { - if canSetup(category: category) { - try setup(category: category, with: options) - return true - } - return false - - } catch { - return false - } - } - - - // MARK: - Playback - - func startPlayback() { - lock.lock() - isPlaybackActive = true - lock.unlock() - } - - func stopPlayback() { - lock.lock() - isPlaybackActive = false - lock.unlock() - } - - - // MARK: - Interruptions - - @objc private func audioSessionDidInterrupted(notification: Notification) { - stopPlayback() - } - - - // MARK: - Utils - - private func setup(category: Category, with options: AVAudioSessionCategoryOptions) throws { - try session.setCategory(category.nativeCategory, with: options) - try session.setActive(true) - } - - private func canSetup(category: Category) -> Bool { - guard isPlaybackActive else { - return true - } - - guard let currentCategory = currentCategory else { - return true - } - - return !category.isAmbient || currentCategory.isAmbient - } -} diff --git a/Nynja/AudioFileConvertOperation.swift b/Nynja/AudioFileConvertOperation.swift index 73939c6ce..b05616b3e 100755 --- a/Nynja/AudioFileConvertOperation.swift +++ b/Nynja/AudioFileConvertOperation.swift @@ -24,7 +24,7 @@ enum AudioFileConvertOperationState { class AudioFileConvertOperation: Operation { - typealias AudioFileConvertOperationHandler = (AudioFileConvertOperationState)->Void + typealias AudioFileConvertOperationHandler = (AudioFileConvertOperationState) -> Void let sourceURL: URL @@ -608,16 +608,12 @@ class AudioFileConvertOperation: Operation { } } } else { - do { - - try AVAudioSession.sharedInstance().setActive(true) - + try AudioSessionManager.shared.setActive(true) } catch let error { NSLog("AVAudioSession setActive failed with error: \(error.localizedDescription)") } - if self.state == .paused { self.semaphore.signal() } diff --git a/Nynja/ChatService/ChatService.swift b/Nynja/ChatService/ChatService.swift index 4e9442626..47aaf4315 100644 --- a/Nynja/ChatService/ChatService.swift +++ b/Nynja/ChatService/ChatService.swift @@ -6,34 +6,12 @@ // Copyright © 2018 TecSynt Solutions. All rights reserved. // -enum ReaderOwner { - case contact(String) - case room(String) -} - -enum ReaderKind: Int { - case other = 0 - case own = 1 -} - final class ChatService { private static let messageParser = MessageParser(dependencies: .init(storageService: storageService)) private static let storageService = StorageService.sharedInstance // MARK: - Fetch - static func fetchReader(for owner: ReaderOwner, kind: ReaderKind) -> MessageServerId? { - let reader: Int64? - - switch owner { - case .contact(let phoneId): - reader = ContactDAO.fetchReader(for: phoneId, kind: kind) - case .room(let roomId): - reader = RoomDAO.fetchReader(for: roomId, kind: kind) - } - - return reader - } static func fetchChatModel(from message: Message) -> ChatModel? { if let p = message.feed_id as? p2p, let target = p.opponentId, let contact = ContactDAO.findContactBy(phoneId: target) { @@ -53,8 +31,8 @@ final class ChatService { if let phoneId = message.p2pFeed?.opponentId { ContactDAO.updateReader(reader, phoneId: phoneId, kind: kind) - } else if let roomId = message.mucFeed?.name { - RoomDAO.updateReader(reader, roomId: roomId, kind: kind) + } else if let roomId = message.mucFeed?.name, let phoneId = message.from { + RoomDAO.updateReader(reader, roomId: roomId, phoneId: phoneId, kind: kind) } } @@ -150,9 +128,8 @@ final class ChatService { } static func removeMessage(_ message: Message, shouldUpdateChat: Bool = true) { - guard MessageDAO.removeMessage(using: message), - let id = message.link else { - return + guard MessageDAO.removeMessage(using: message), let id = message.linkedId else { + return } if let action = MessageActionDAO.fetchMessageAction(by: id) { @@ -168,24 +145,20 @@ final class ChatService { } private static func updateUnreadCounterAfterRemove(for chat: ChatModel, serverId: MessageServerId) { - guard let selfReader = chat.selfReader, - serverId > selfReader, - chat.unreadCount > 0 else { - return + guard let selfReader = chat.selfReader, serverId > selfReader, chat.unreadCount > 0 else { + return } - - chat.unread = chat.unreadCount - 1 + chat.unread = max(0, chat.unreadCount - 1) updateUnreadCounter(for: chat) } private static func updateLastMessageAfterRemove(for chat: ChatModel, message: Message) { - guard let fetchType = self.fetchType(from: message), + guard let fetchType = self.fetchType(from: message), let lastMessage = MessageDAO.fetchLastMessage(of: fetchType) else { return } - ChatService.updateLastMessage(lastMessage, chat: chat, shouldChangeUnread: false) { (lastMessageId) -> Bool in - return message.link == lastMessageId + return message.linkedId == lastMessageId } } @@ -202,6 +175,10 @@ final class ChatService { // MARK: - Clear History static func clearHistory(_ message: Message) { do { + guard !MessageDAO.shouldMarkMessageAsDeleted(message) else { + try hideClearHistoryMessage(message) + return + } message.isTrusted = true if let muc = message.feed_id as? muc, let name = muc.name { try MessageDAO.clearHistory(FetchType.muc(name: name)) @@ -215,6 +192,10 @@ final class ChatService { static func clearMessages(before message: Message) { do { + guard !MessageDAO.shouldMarkMessageAsDeleted(message) else { + try hideClearHistoryMessage(message) + return + } guard let messageServerId = message.id else { return } @@ -229,6 +210,13 @@ final class ChatService { } catch {} } + private static func hideClearHistoryMessage(_ message: Message) throws { + // Save message into chain, but mark as deleted in order to ignore it. + try MessageDAO.trustIfNextMessageExists(before: message) + message.localStatus = .deleted + try storageService.perform(action: .save, with: message) + } + private static func updateChatModelAfterClear(with message: Message) { do { try storageService.perform(action: .save, with: message) @@ -237,6 +225,11 @@ final class ChatService { chatModel.unread = unreadAfterClear(for: message) chatModel.last_msg = message + chatModel.selfReader = 0 + if let room = chatModel as? Room { + room.readers = [0, 0] as [AnyObject] + } + try storageService.perform(action: .save, with: chatModel) } } catch {} diff --git a/Nynja/ChatService/SenderService.swift b/Nynja/ChatService/SenderService.swift new file mode 100644 index 000000000..c82826118 --- /dev/null +++ b/Nynja/ChatService/SenderService.swift @@ -0,0 +1,73 @@ +// +// SenderService.swift +// Nynja +// +// Created by Andrey Reznik on 07.09.2018. +// Copyright © 2018 TecSynt Solutions. All rights reserved. +// + +import Foundation + +final class SenderService: InitializeInjectable { + private let mqttService: MQTTService + + private(set) var searchCompletion: SearchCompletion? + private var lastPhoneId: String? + + init(dependencies: Dependencies) { + mqttService = dependencies.mqttService + IoHandler.delegate = self + } + + + func searchContact(with phoneId: String, completion: SearchCompletion?) { + searchCompletion = completion + lastPhoneId = phoneId + if let contact = ContactDAO.findContactBy(phoneId: phoneId) { + completion?(.success(contact)) + } else { + guard let phoneNumber = Contact.phoneNumber(from: phoneId) else { + completion?(.error) + return + } + MQTTService.sharedInstance.tryFindContact(number: phoneNumber) + } + } + + func updateSubscribes() { + IoHandler.delegate = self + } +} + + +// MARK: - Inner Types + +extension SenderService { + typealias SearchCompletion = (SearchResult) -> Void + + enum SearchResult { + case success(Contact) + case error + } + + struct Dependencies { + let mqttService: MQTTService + } +} + + +// MARK: IoHandlerDelegate + +extension SenderService: IoHandlerDelegate { + + func getContactSuccess(contact: Contact) { + guard lastPhoneId == contact.phoneId else { + return + } + searchCompletion?(.success(contact)) + } + + func contactNotFound() { + searchCompletion?(.error) + } +} diff --git a/Nynja/CircleMenuControl/Core/Sector/SectionView.swift b/Nynja/CircleMenuControl/Core/Sector/SectionView.swift index e0928faed..ab87c06af 100644 --- a/Nynja/CircleMenuControl/Core/Sector/SectionView.swift +++ b/Nynja/CircleMenuControl/Core/Sector/SectionView.swift @@ -42,13 +42,15 @@ class SectionView: UIView { view.backgroundColor = .clear view.transform = CGAffineTransform(rotationAngle: self.angle) - - let shiftCenterY: CGFloat = 10.0 + + let shiftCenterY: CGFloat = 10.0 + let centerX: CGFloat = self.bounds.width/2 + let centerY: CGFloat = self.bounds.height/2 - shiftCenterY self.addSubview(view) view.snp.makeConstraints({ (make) in - make.centerX.equalToSuperview() - make.centerY.equalToSuperview().offset(-shiftCenterY) + make.centerX.equalTo(centerX) + make.centerY.equalTo(centerY) make.height.equalTo(height) }) return view diff --git a/Nynja/ContactDAO.swift b/Nynja/ContactDAO.swift index 738c269fb..d7ce86b92 100644 --- a/Nynja/ContactDAO.swift +++ b/Nynja/ContactDAO.swift @@ -174,9 +174,7 @@ class ContactDAO: ContactDAOProtocol { return } - for i in oldReader.count..<2 { - oldReader.append(0) - } + oldReader.complete(to: 2, with: 0) var newReader = oldReader.map { $0 as AnyObject} newReader[kind.rawValue] = reader as AnyObject diff --git a/Nynja/ConversationsProvider.swift b/Nynja/ConversationsProvider.swift index b5acc08a0..c7b497524 100644 --- a/Nynja/ConversationsProvider.swift +++ b/Nynja/ConversationsProvider.swift @@ -64,8 +64,8 @@ class ConversationsProvider: ConversationsProviding { // MARK: - Comparator func comparator(lhs: ChatModel, rhs: ChatModel) -> Bool { - let created1 = lhs.last_msg?.created as? Int64 ?? 0 - let created2 = rhs.last_msg?.created as? Int64 ?? 0 + let created1 = lhs.last_msg?.created ?? 0 + let created2 = rhs.last_msg?.created ?? 0 return created1 > created2 } diff --git a/Nynja/DB/Extensions/DatabaseExtension.swift b/Nynja/DB/Extensions/DatabaseExtension.swift index 702c67a1b..0d4329dfe 100644 --- a/Nynja/DB/Extensions/DatabaseExtension.swift +++ b/Nynja/DB/Extensions/DatabaseExtension.swift @@ -10,6 +10,11 @@ import GRDBCipher extension Database { + func allColumns(tableName: String) throws -> [String] { + let rows = try Row.fetchAll(self, "PRAGMA table_info('\(tableName)')") + return rows.compactMap { $0["name"] } + } + func hasColumns(_ columns: Set, tableName: String) throws -> Bool { let rows = try Row.fetchAll(self, "PRAGMA table_info('\(tableName)')") let tableColumns = Set(rows.compactMap { $0["name"] }) @@ -20,6 +25,10 @@ extension Database { return try hasColumns(columns, tableName: T.name) } + func allColumns(in table: T.Type) throws -> [String] { + return try allColumns(tableName: T.name) + } + func clearTables(_ tableNames: [String]) throws { try tableNames.forEach { name in try execute("delete from \(name)") diff --git a/Nynja/DB/Models/DBContact.swift b/Nynja/DB/Models/DBContact.swift index 3750523e5..f58a43005 100644 --- a/Nynja/DB/Models/DBContact.swift +++ b/Nynja/DB/Models/DBContact.swift @@ -22,7 +22,7 @@ class DBContact: Record, DBModelProtocol { var presence: String? var status: String? - var messageId: Int64? + var messageId: String? var rosterId: Int64? var message: DBMessage? @@ -46,7 +46,7 @@ class DBContact: Record, DBModelProtocol { if let message = contact.message { self.message = DBMessage(message: message) - self.messageId = message.id + self.messageId = message.msg_id } else { self.messageId = contact.lastMessageId } @@ -132,10 +132,11 @@ class DBContact: Record, DBModelProtocol { try message?.saveAggregate(db) if message != nil || messageId == nil { - self.messageId = message?.id + messageId = message?.localId } - try self.save(db) + try save(db) + try features.forEach { try $0.save(db) } try services.forEach { try $0.save(db) } } @@ -190,11 +191,11 @@ class DBContact: Record, DBModelProtocol { } func construct(_ db: Database) throws { - if let messageId = self.messageId { - self.message = try DBMessage.message(db, id: messageId) + if let messageId = messageId { + message = try DBMessage.message(db, localId: messageId) } - self.features = (try? DBContact.request(targetId: self.phoneId).fetchAll(db)) ?? [] + features = (try? DBContact.request(targetId: self.phoneId).fetchAll(db)) ?? [] services = try DBService.request(targetId: self.phoneId, targetType: .contact).fetchAll(db) } diff --git a/Nynja/DB/Models/DBConvertMessage.swift b/Nynja/DB/Models/DBConvertMessage.swift index 46002c48f..22c14c406 100644 --- a/Nynja/DB/Models/DBConvertMessage.swift +++ b/Nynja/DB/Models/DBConvertMessage.swift @@ -92,7 +92,21 @@ extension DBConvertMessage { case convert case upload case transcribing + case transcribingShort case transcribeProcessing case send + + var nextProcess: Process? { + switch self { + case .upload: + return .transcribing + case .transcribing: + return .transcribeProcessing + case .transcribingShort, .transcribeProcessing: + return .send + case .convert, .send: + return nil + } + } } } diff --git a/Nynja/DB/Models/DBJobMessage.swift b/Nynja/DB/Models/DBJobMessage.swift index 558373a7c..6787e6ceb 100644 --- a/Nynja/DB/Models/DBJobMessage.swift +++ b/Nynja/DB/Models/DBJobMessage.swift @@ -9,7 +9,7 @@ import Foundation import GRDBCipher -class DBJobMessage: Record, DBModelProtocol { +final class DBJobMessage: Record, DBModelProtocol { var id: Int64? var container: String? @@ -23,7 +23,7 @@ class DBJobMessage: Record, DBModelProtocol { var to: String? var created: Int64? var type: String? - var editMessage: Int64? + var link: Int64? var repliedBy: String? var mentioned: String? var status: String? @@ -43,9 +43,9 @@ class DBJobMessage: Record, DBModelProtocol { self.localId = message.msg_id self.from = message.from self.to = message.to - self.created = message.created as? Int64 + self.created = message.created self.type = message.types.joinedByCommaIfNotEmpty() - self.editMessage = message.link + self.link = message.linkedId self.status = message.statusString self.files = (message.files ?? []).compactMap { DBDesc(desc: $0, targetId: nil, targetType: .schedule) } @@ -79,7 +79,7 @@ class DBJobMessage: Record, DBModelProtocol { self.to = row[JobMessageTable.Column.to.title] self.created = row[JobMessageTable.Column.created.title] self.type = row[JobMessageTable.Column.type.title] - self.editMessage = row[JobMessageTable.Column.editMessage.title] + self.link = row[JobMessageTable.Column.editMessage.title] self.repliedBy = row[JobMessageTable.Column.repliedBy.title] self.mentioned = row[JobMessageTable.Column.mentioned.title] self.status = row[JobMessageTable.Column.status.title] @@ -100,7 +100,7 @@ class DBJobMessage: Record, DBModelProtocol { container[JobMessageTable.Column.to.title] = self.to container[JobMessageTable.Column.created.title] = self.created container[JobMessageTable.Column.type.title] = self.type - container[JobMessageTable.Column.editMessage.title] = self.editMessage + container[JobMessageTable.Column.editMessage.title] = self.link container[JobMessageTable.Column.repliedBy.title] = self.repliedBy container[JobMessageTable.Column.mentioned.title] = self.mentioned container[JobMessageTable.Column.status.title] = self.status diff --git a/Nynja/DB/Models/DBMessage.swift b/Nynja/DB/Models/DBMessage.swift index 7301113c6..635c56dc8 100644 --- a/Nynja/DB/Models/DBMessage.swift +++ b/Nynja/DB/Models/DBMessage.swift @@ -12,14 +12,13 @@ private let tableName = MessageTable.name final class DBMessage: Record, DBModelProtocol { - var id: Int64? var container: String? var feedId: Int64? var feedType: Int? var prev: Int64? var next: Int64? var serverId: Int64? - var localId: String? + var localId: String var from: String? var to: String? var created: Int64? @@ -28,25 +27,38 @@ final class DBMessage: Record, DBModelProtocol { var repliedBy: String? var mentioned: String? var status: String? + var localStatus: Message.LocalStatus? var isTrusted: Bool? var files: [DBDesc] = [] var feed: (Record & FeedProtocol)? + private var repliedMessage: DBMessage? + + // MARK: - Mapping init?(message: Message) { self.container = (message.container as? StringAtom)?.string self.prev = message.prev self.next = message.next self.serverId = message.id - self.localId = message.msg_id + + if let localId = message.msg_id { + self.localId = localId + } else if let serverId = message.id { + self.localId = String(describing: serverId) + } else { + self.localId = IdBuilder(format: .defaultId).build() + } + self.from = message.from self.to = message.to - self.created = message.created as? Int64 + self.created = message.created self.type = message.types.joinedByCommaIfNotEmpty() - self.link = message.link + self.link = message.linkedId self.status = message.statusString + self.localStatus = message.localStatus self.isTrusted = message.isTrusted self.files = (message.files ?? []).compactMap { DBDesc(desc: $0, targetId: nil, targetType: .message) } @@ -56,6 +68,8 @@ final class DBMessage: Record, DBModelProtocol { super.init() + self.repliedMessage = message.repliedMessage.flatMap { DBMessage(message: $0) } + if let p2p = message.feed_id as? p2p { self.feedType = FeedType.p2p.rawValue self.feed = DBP2p(p2p: p2p) @@ -71,7 +85,6 @@ final class DBMessage: Record, DBModelProtocol { } required init(row: Row) { - self.id = row[MessageTable.Column.id.title] self.container = row[MessageTable.Column.container.title] self.feedId = row[MessageTable.Column.feedId.title] self.feedType = row[MessageTable.Column.feedType.title] @@ -87,13 +100,14 @@ final class DBMessage: Record, DBModelProtocol { self.repliedBy = row[MessageTable.Column.repliedBy.title] self.mentioned = row[MessageTable.Column.mentioned.title] self.status = row[MessageTable.Column.status.title] + let localStatus: Int64? = row[MessageTable.Column.localStatus.title] + self.localStatus = localStatus.flatMap { Message.LocalStatus(rawValue: $0) } self.isTrusted = row[MessageTable.Column.trusted.title] super.init() } override func encode(to container: inout PersistenceContainer) { - container[MessageTable.Column.id.title] = self.id container[MessageTable.Column.container.title] = self.container container[MessageTable.Column.feedId.title] = self.feedId container[MessageTable.Column.feedType.title] = self.feedType @@ -109,41 +123,29 @@ final class DBMessage: Record, DBModelProtocol { container[MessageTable.Column.repliedBy.title] = self.repliedBy container[MessageTable.Column.mentioned.title] = self.mentioned container[MessageTable.Column.status.title] = self.status + container[MessageTable.Column.localStatus.title] = self.localStatus?.rawValue if let isTrusted = isTrusted { container[MessageTable.Column.trusted.title] = isTrusted } } - override func didInsert(with rowID: Int64, for column: String?) { - id = rowID - } - // MARK: - Query, Modification func saveAggregate(_ db: Database) throws { + try repliedMessage?.saveAggregate(db) + if let feed = self.feed { try feed.saveAggregate(db) self.feedId = feed.id } - if let id = try fetchId(db) { - self.id = id - } - if let localId = try fetchLocalId(db) { - self.localId = localId - } else { - self.localId = IdBuilder(format: .defaultId).build() - } - try save(db) - if let id = self.id, case let targetId = String(id) { - try DBDesc.deleteAll(db, targetId: targetId, targetType: .message) - - for file in self.files { - if file.serverId != nil { - file.targetId = targetId - try file.saveAggregate(db, shouldSaveLink: !isSystem) - } + try DBDesc.deleteAll(db, targetId: localId, targetType: .message) + + try files.forEach { + if $0.serverId != nil { + $0.targetId = localId + try $0.saveAggregate(db, shouldSaveLink: !isSystem) } } } @@ -187,14 +189,42 @@ final class DBMessage: Record, DBModelProtocol { return message } + static func lastDeliveredMessage(_ db: Database, + ofType fetchType: FetchType, + orderedBy orderColumn: MessageTable.Column) throws -> DBMessage? { + let sql: String + let skippedStatuses = skippedCondition() + let conditions = "and \(MessageTable.Column.serverId.title) is not null" + + switch fetchType { + case let .p2p(from, to): + sql = sqlP2p(from: from, to: to, + conditions: conditions, + ordered: .desc, + orderColumn: orderColumn, + skippedStatuses: skippedStatuses) + case let .muc(mucName): + sql = sqlMuc(name: mucName, + conditions: conditions, + ordered: .desc, + orderColumn: orderColumn, + skippedStatuses: skippedStatuses) + } + + let message = try SQLRequest(sql).asRequest(of: DBMessage.self).fetchOne(db) + try message?.construct(db) + return message + } + static func lastMessage(_ db: Database, ofType fetchType: FetchType) throws -> DBMessage? { let sql: String + let skippedStatuses = skippedCondition() switch fetchType { case let .p2p(from, to): - sql = sqlP2p(from: from, to: to, ordered: .desc, orderColumn: .created) + sql = sqlP2p(from: from, to: to, ordered: .desc, orderColumn: .created, skippedStatuses: skippedStatuses) case let .muc(mucName): - sql = sqlMuc(name: mucName, ordered: .desc, orderColumn: .created) + sql = sqlMuc(name: mucName, ordered: .desc, orderColumn: .created, skippedStatuses: skippedStatuses) } let message = try SQLRequest(sql).asRequest(of: DBMessage.self).fetchOne(db) @@ -206,12 +236,21 @@ final class DBMessage: Record, DBModelProtocol { let condition = "and \(tableName).[\(MessageTable.Column.from.title)] <> '\(phoneId)'" let sql: String + let skippedStatuses = skippedCondition() switch fetchType { case let .p2p(from, to): - sql = sqlP2p(from: from, to: to, conditions: condition, ordered: .desc, orderColumn: .created) + sql = sqlP2p(from: from, to: to, + conditions: condition, + ordered: .desc, + orderColumn: .created, + skippedStatuses: skippedStatuses) case let .muc(mucName): - sql = sqlMuc(name: mucName, conditions: condition, ordered: .desc, orderColumn: .created) + sql = sqlMuc(name: mucName, + conditions: condition, + ordered: .desc, + orderColumn: .created, + skippedStatuses: skippedStatuses) } let message = try SQLRequest(sql).asRequest(of: DBMessage.self).fetchOne(db) @@ -220,7 +259,7 @@ final class DBMessage: Record, DBModelProtocol { } static func messages(_ db: Database, fetchType: FetchType) throws -> [DBMessage] { - return try messages(db, fetchType: fetchType, conditions: "") + return try messages(db, fetchType: fetchType, conditions: "", skippedStatuses: skippedCondition()) } static func messages(_ db: Database, fetchType: FetchType, serverIds: Set) throws -> [DBMessage] { @@ -229,7 +268,8 @@ final class DBMessage: Record, DBModelProtocol { if !serverIds.isEmpty { let ids = serverIds.joinedByComma() conditions = "and \(tableName).\(MessageTable.Column.serverId.title) in (\(ids))" - return try messages(db, fetchType: fetchType, conditions: conditions) + let skippedStatuses = skippedCondition(localStatuses: nil) + return try messages(db, fetchType: fetchType, conditions: conditions, skippedStatuses: skippedStatuses) } else { return [] } @@ -237,17 +277,23 @@ final class DBMessage: Record, DBModelProtocol { static func messages(_ db: Database, fetchType: FetchType, before messageServerId: Int64) throws -> [DBMessage] { let condition = conditionBefore(messageServerId: messageServerId) - return try messages(db, fetchType: fetchType, conditions: condition) + return try messages(db, fetchType: fetchType, conditions: condition, skippedStatuses: nil) } - private static func messages(_ db: Database, fetchType: FetchType, conditions: String) throws -> [DBMessage] { + private static func messages(_ db: Database, fetchType: FetchType, conditions: String, skippedStatuses: String?) throws -> [DBMessage] { var request: AnyTypedRequest switch fetchType { case .p2p(let from, let to): - request = requestP2pMessages(from: from, to: to, conditions: conditions, orderColumn: .created) + request = requestP2pMessages(from: from, to: to, + conditions: conditions, + orderColumn: .created, + skippedStatuses: skippedStatuses) case .muc(let name): - request = requestMucMessages(name: name, conditions: conditions, orderColumn: .created) + request = requestMucMessages(name: name, + conditions: conditions, + orderColumn: .created, + skippedStatuses: skippedStatuses) } let messages = try request.fetchAll(db) @@ -261,10 +307,16 @@ final class DBMessage: Record, DBModelProtocol { return messages } + static func rawMessage(_ db: Database, serverId: MessageServerId) throws -> DBMessage? { + let serverIdColumn = Column(MessageTable.Column.serverId.title) + + return try DBMessage + .filter(serverIdColumn == serverId) + .fetchOne(db) + } + func construct(_ db: Database) throws { - if let id = self.id { - files = try DBMessage.files(targetId: String(id), db: db) - } + files = try DBMessage.files(targetId: localId, db: db) if let feedId = self.feedId, let feedType = self.feedType { let type = FeedType(rawValue: feedType) @@ -276,27 +328,6 @@ final class DBMessage: Record, DBModelProtocol { } } - func fetchId(_ db: Database) throws -> Int64? { - let idColumn = Column(MessageTable.Column.id.title) - let serverIdColumn = Column(MessageTable.Column.serverId.title) - let localIdColumn = Column(MessageTable.Column.localId.title) - - let predicate = (serverId != nil && serverIdColumn == self.serverId) || (self.localId != nil && localIdColumn == self.localId) - return try DBMessage.filter(predicate).select(idColumn).asRequest(of: Int64.self).fetchOne(db) - } - - private func fetchLocalId(_ db: Database) throws -> String? { - if let localId = self.localId { - return localId - } - - let localIdColumn = Column(MessageTable.Column.localId.title) - let serverIdColumn = Column(MessageTable.Column.serverId.title) - - let predicate = (serverId != nil && serverIdColumn == self.serverId) - return try DBMessage.filter(predicate).select(localIdColumn).asRequest(of: String.self).fetchOne(db) - } - // MARK: - Cursor static func fetchCursor(_ db: Database, @@ -307,9 +338,15 @@ final class DBMessage: Record, DBModelProtocol { switch fetchType { case let .p2p(from, to): - request = requestP2pMessages(from: from, to: to, orderedBy: order, orderColumn: orderColumn, skippedStatuses: nil) + request = requestP2pMessages(from: from, to: to, + orderedBy: order, + orderColumn: orderColumn, + skippedStatuses: nil) case let .muc(name): - request = requestMucMessages(name: name, orderedBy: order, orderColumn: orderColumn, skippedStatuses: nil) + request = requestMucMessages(name: name, + orderedBy: order, + orderColumn: orderColumn, + skippedStatuses: nil) } return try request.fetchCursor(db) @@ -318,14 +355,7 @@ final class DBMessage: Record, DBModelProtocol { // MARK: - Delete @discardableResult func deleteAggregate(_ db: Database) throws -> Bool { - let tempId: Int64? = try self.id != nil ? self.id : fetchId(db) - guard let id = tempId else { - return false - } - self.id = id - - try DBDesc.deleteAll(db, targetId: String(id), targetType: .message) - + try DBDesc.deleteAll(db, targetId: localId, targetType: .message) return try delete(db) } @@ -354,7 +384,7 @@ final class DBMessage: Record, DBModelProtocol { conditions: String = "", orderedBy order: TableOrder = .asc, orderColumn: MessageTable.Column, - skippedStatuses: String? = skippedStatuses) -> AnyTypedRequest { + skippedStatuses: String?) -> AnyTypedRequest { let sql = sqlP2p(from: from, to: to, conditions: conditions, ordered: order, orderColumn: orderColumn, skippedStatuses: skippedStatuses) return SQLRequest(sql).asRequest(of: DBMessage.self) } @@ -363,7 +393,7 @@ final class DBMessage: Record, DBModelProtocol { conditions: String = "", orderedBy order: TableOrder = .asc, orderColumn: MessageTable.Column, - skippedStatuses: String? = skippedStatuses) -> AnyTypedRequest { + skippedStatuses: String?) -> AnyTypedRequest { let sql = sqlMuc(name: name, conditions: conditions, ordered: order, orderColumn: orderColumn, skippedStatuses: skippedStatuses) return SQLRequest(sql).asRequest(of: DBMessage.self) } @@ -371,9 +401,30 @@ final class DBMessage: Record, DBModelProtocol { // MARK: - Raw SQL - static private var skippedStatuses: String { - let statusColumn = "\(tableName).\(MessageTable.Column.status.title)" - return "(\(statusColumn) is null or \(statusColumn) not in ('delete', 'edit', 'deleted'))" + private static func skippedCondition(statuses: [Message.Status]? = [.delete, .edit], + localStatuses: [Message.LocalStatus]? = [.replied, .deleted]) -> String { + var query = "" + + if let statuses = statuses, !statuses.isEmpty { + let statusColumn = "\(tableName).\(MessageTable.Column.status.title)" + let statuses = statuses.map { "'\($0.rawValue)'" }.joinedByComma() + + query.append("(\(statusColumn) is null or \(statusColumn) not in (\(statuses)))") + } + + if let localStatuses = localStatuses, !localStatuses.isEmpty { + let localStatusColumn = "\(tableName).\(MessageTable.Column.localStatus.title)" + + if !query.isEmpty { + query.append(" and ") + } + let masks = localStatuses.map { "(\(localStatusColumn) & \($0.rawValue) != \($0.rawValue))" } + let maskQuery = masks.joined(separator: " and ") + + query.append("(\(localStatusColumn) is null or (\(maskQuery)))") + } + + return query } static private func conditionBefore(messageServerId: MessageServerId) -> String { @@ -389,7 +440,7 @@ final class DBMessage: Record, DBModelProtocol { conditions: String = "", ordered: TableOrder, orderColumn: MessageTable.Column, - skippedStatuses: String? = skippedStatuses) -> String { + skippedStatuses: String?) -> String { let messageTable = tableName let p2pTable = P2pTable.name @@ -412,7 +463,7 @@ final class DBMessage: Record, DBModelProtocol { conditions: String = "", ordered: TableOrder, orderColumn: MessageTable.Column, - skippedStatuses: String? = skippedStatuses) -> String { + skippedStatuses: String?) -> String { let messageTable = tableName let mucTable = MucTable.name diff --git a/Nynja/DB/Models/DBMessageLink.swift b/Nynja/DB/Models/DBMessageLink.swift index 0e43278d2..307000ddd 100644 --- a/Nynja/DB/Models/DBMessageLink.swift +++ b/Nynja/DB/Models/DBMessageLink.swift @@ -100,7 +100,6 @@ class DBMessageLink: Record, DBModelProtocol { let descID = "\(descTable).\(DescTable.Column.targetId.title)" let descTarget = "\(descTable).\(DescTable.Column.targetType.title)" let descMime = DescTable.Column.mime.title - let msgId = "\(msgTable).\(MessageTable.Column.id.title)" let msgCreated = "\(msgTable).\(MessageTable.Column.created.title)" var localIdEqual = "" @@ -112,7 +111,7 @@ class DBMessageLink: Record, DBModelProtocol { select \(msgLocalId), \(linksValue) from \(msgTable) left join \(p2pTable) on \(msgFeedId) = \(p2pId) and \(msgFeedType) = 0 - left join \(descTable) on \(msgId) = \(descID) + left join \(descTable) on \(msgLocalId) = \(descID) inner join \(linksTable) on \(linksFeedId) = \(descID) and \(linksFeedType) = \(descTarget) where (\(msgTable).\(msgType) is NULL or \(msgTable).\(msgType) not like '%sys%') and \(descTable).\(descMime) = 'text' and \(linksValue) <> '' and (\(p2pFrom) = '\(feed.from)' and \(p2pTo) = '\(feed.to)'\(localIdEqual)) @@ -142,7 +141,6 @@ class DBMessageLink: Record, DBModelProtocol { let descID = "\(descTable).\(DescTable.Column.targetId.title)" let descTarget = "\(descTable).\(DescTable.Column.targetType.title)" let descMime = DescTable.Column.mime.title - let msgId = "\(msgTable).\(MessageTable.Column.id.title)" let msgCreated = "\(msgTable).\(MessageTable.Column.created.title)" var localIdEqual = "" @@ -154,7 +152,7 @@ class DBMessageLink: Record, DBModelProtocol { select \(msgLocalId), \(linksValue) from \(msgTable) left join \(mucTable) on \(msgFeedId) = \(mucId) and \(msgFeedType) = 1 - left join \(descTable) on \(msgId) = \(descID) + left join \(descTable) on \(msgLocalId) = \(descID) inner join \(linksTable) on \(linksFeedId) = \(descID) and \(linksFeedType) = \(descTarget) where (\(msgTable).\(msgType) is NULL or \(msgTable).\(msgType) not like '%sys%') and \(descTable).\(descMime) = 'text' and \(linksValue) <> '' and (\(mucName) = '\(feed.name)'\(localIdEqual)) diff --git a/Nynja/DB/Models/DBRoom.swift b/Nynja/DB/Models/DBRoom.swift index e8a26a904..a41b3696d 100644 --- a/Nynja/DB/Models/DBRoom.swift +++ b/Nynja/DB/Models/DBRoom.swift @@ -19,12 +19,18 @@ class DBRoom: Record, DBModelProtocol { var unread: Int64 var update: Int64 var mentions: String? - var reader: Int64? + + /// Contains the highest reader among all members except own one. + var reader: Int64? // + + /// Contains 2 highest readers among all members. + var readers: [Int64] = [] + var created: Int64 var status: String? var rosterId: Int64? - var messageId: Int64? + var messageId: String? var message: DBMessage? @@ -45,7 +51,7 @@ class DBRoom: Record, DBModelProtocol { self.tosUpdate = room.tos_update ?? 0 self.unread = room.unread ?? 0 self.mentions = room.mentions?.joinedByComma() - self.reader = (room.readers as? [Int64])?.first + self.update = room.update ?? 0 self.created = room.created ?? 0 self.status = (room.status as? StringAtom)?.string @@ -57,7 +63,7 @@ class DBRoom: Record, DBModelProtocol { // Message if let message = room.message { self.message = DBMessage(message: message) - self.messageId = room.message?.id + self.messageId = room.message?.msg_id } else { self.messageId = room.lastMessageId } @@ -73,6 +79,8 @@ class DBRoom: Record, DBModelProtocol { // Links self.links = (room.links ?? []).compactMap { DBLink(link: $0, roomId: id) } + self.readers = (room.readers as? [Int64]) ?? [] + super.init() } @@ -121,35 +129,14 @@ class DBRoom: Record, DBModelProtocol { // MARK: - Modification func saveAggregate(_ db: Database) throws { - try saveAggregate(db, withMembers: true) - } - - func saveAggregate(_ db: Database, withMembers: Bool) throws { - try message?.saveAggregate(db) - - if message != nil || messageId == nil { - messageId = message?.id - } + try saveMessage(db) + reader = calculateReader() try save(db) - - if withMembers { - try admins.forEach { - try $0.saveAggregate(db) - try DBRoomMember(roomId: id, memberId: $0.id, isAdmin: $0.isAdmin).save(db) - } - - try members.forEach { - try $0.saveAggregate(db) - try DBRoomMember(roomId: id, memberId: $0.id, isAdmin: $0.isAdmin).save(db) - } - } + try saveMembers(db) try DBDesc.deleteAll(db, targetId: id, targetType: .room) - - try files.forEach { - try $0.saveAggregate(db) - } + try files.forEach { try $0.saveAggregate(db) } try DBFeature.deleteAll(db, targetId: id, targetType: .room) try features.forEach { try $0.save(db) } @@ -158,6 +145,36 @@ class DBRoom: Record, DBModelProtocol { try links.forEach { try $0.save(db) } } + private func calculateReader() -> Int64? { + guard let selfReader = selfMember?.reader else { + return nil + } + + return readers.max(except: selfReader) + } + + private func saveMessage(_ db: Database) throws { + try message?.saveAggregate(db) + + if message != nil || messageId == nil { + messageId = message?.localId + } + } + + private func saveMembers(_ db: Database) throws { + try admins.forEach { + try $0.saveAggregate(db) + try DBRoomMember(roomId: id, memberId: $0.id, isAdmin: $0.isAdmin).save(db) + } + + try members.forEach { + try $0.saveAggregate(db) + try DBRoomMember(roomId: id, memberId: $0.id, isAdmin: $0.isAdmin).save(db) + } + } + + + @discardableResult func deleteAggregate(_ db: Database) throws -> Bool { try DBRoom.requestFeature(targetId: self.id).deleteAll(db) @@ -215,7 +232,7 @@ class DBRoom: Record, DBModelProtocol { private func construct(_ db: Database) throws { if let messageId = self.messageId { - self.message = try DBMessage.message(db, id: messageId) + self.message = try DBMessage.message(db, localId: messageId) } self.admins = try DBMember.members(from: db, roomId: id, isAdmin: true) diff --git a/Nynja/DB/Models/DBStar.swift b/Nynja/DB/Models/DBStar.swift index 5fb1f16ca..6f21437a7 100644 --- a/Nynja/DB/Models/DBStar.swift +++ b/Nynja/DB/Models/DBStar.swift @@ -14,7 +14,7 @@ final class DBStar: Record, DBModelProtocol { var clientId: String? var rosterId: Int64? var message: DBStarMessage? - var messageID: Int64? + var messageID: String? var status: String? @@ -69,7 +69,7 @@ final class DBStar: Record, DBModelProtocol { // MARK: - DBModelProtocol func saveAggregate(_ db: Database) throws { try message?.saveAggregate(db) - messageID = message?.id + messageID = message?.localId try self.save(db) } @@ -109,8 +109,8 @@ final class DBStar: Record, DBModelProtocol { } private func construct(_ db: Database) throws { - if let messageId = self.messageID { - message = try DBStarMessage.message(db, id: messageId) + if let messageId = messageID { + message = try DBStarMessage.message(db, localId: messageId) } } diff --git a/Nynja/DB/Models/DBStarMessage.swift b/Nynja/DB/Models/DBStarMessage.swift index c3143042d..00e7da110 100644 --- a/Nynja/DB/Models/DBStarMessage.swift +++ b/Nynja/DB/Models/DBStarMessage.swift @@ -11,19 +11,18 @@ import GRDBCipher final class DBStarMessage: Record, DBModelProtocol { - var id: Int64? var container: String? var feedId: Int64? var feedType: Int? var prev: Int64? var next: Int64? var serverId: Int64? - var localId: String? + var localId: String var from: String? var to: String? var created: Int64? var type: String? - var editMessage: Int64? + var link: Int64? var repliedBy: String? var mentioned: String? var status: String? @@ -60,12 +59,12 @@ final class DBStarMessage: Record, DBModelProtocol { self.prev = message.prev self.next = message.next self.serverId = message.id - self.localId = message.msg_id + self.localId = message.msg_id ?? IdBuilder(format: .defaultId).build() self.from = message.from self.to = message.to - self.created = message.created as? Int64 + self.created = message.created self.type = message.types.joinedByCommaIfNotEmpty() - self.editMessage = message.link + self.link = message.linkedId self.status = message.statusString self.files = (message.files ?? []).compactMap { DBDesc(desc: $0, targetId: nil, targetType: .star) } @@ -89,7 +88,6 @@ final class DBStarMessage: Record, DBModelProtocol { } required init(row: Row) { - self.id = row[StarMessageTable.Column.id.title] self.container = row[StarMessageTable.Column.container.title] self.feedId = row[StarMessageTable.Column.feedId.title] self.feedType = row[StarMessageTable.Column.feedType.title] @@ -101,7 +99,7 @@ final class DBStarMessage: Record, DBModelProtocol { self.to = row[StarMessageTable.Column.to.title] self.created = row[StarMessageTable.Column.created.title] self.type = row[StarMessageTable.Column.type.title] - self.editMessage = row[StarMessageTable.Column.editMessage.title] + self.link = row[StarMessageTable.Column.editMessage.title] self.repliedBy = row[StarMessageTable.Column.repliedBy.title] self.mentioned = row[StarMessageTable.Column.mentioned.title] self.status = row[StarMessageTable.Column.status.title] @@ -110,7 +108,6 @@ final class DBStarMessage: Record, DBModelProtocol { } override func encode(to container: inout PersistenceContainer) { - container[StarMessageTable.Column.id.title] = self.id container[StarMessageTable.Column.container.title] = self.container container[StarMessageTable.Column.feedId.title] = self.feedId container[StarMessageTable.Column.feedType.title] = self.feedType @@ -122,16 +119,12 @@ final class DBStarMessage: Record, DBModelProtocol { container[StarMessageTable.Column.to.title] = self.to container[StarMessageTable.Column.created.title] = self.created container[StarMessageTable.Column.type.title] = self.type - container[StarMessageTable.Column.editMessage.title] = self.editMessage + container[StarMessageTable.Column.editMessage.title] = self.link container[StarMessageTable.Column.repliedBy.title] = self.repliedBy container[StarMessageTable.Column.mentioned.title] = self.mentioned container[StarMessageTable.Column.status.title] = self.status } - override func didInsert(with rowID: Int64, for column: String?) { - id = rowID - } - // MARK: - Sender Info Setup private func setup(contact: Contact) { @@ -140,7 +133,7 @@ final class DBStarMessage: Record, DBModelProtocol { } private func setup(room: Room) { - let member = room.members?.first(where: { $0.phone_id == self.from }) + let member = room.allMembersWithoutFilter?.first(where: { $0.phone_id == self.from }) setup(room: room, member: member) } @@ -171,15 +164,6 @@ final class DBStarMessage: Record, DBModelProtocol { self.feedId = feed.id } - if let id = try fetchId(db) { - self.id = id - } - if let localId = try fetchLocalId(db) { - self.localId = localId - } else { - self.localId = IdBuilder(format: .defaultId).build() - } - try self.save(db) if let sender = self.sender { @@ -194,11 +178,9 @@ final class DBStarMessage: Record, DBModelProtocol { } } - if let id = self.id { - try self.files.forEach { desc in - desc.targetId = "\(id)" - try desc.saveAggregate(db) - } + try files.forEach { desc in + desc.targetId = localId + try desc.saveAggregate(db) } } @@ -254,26 +236,24 @@ final class DBStarMessage: Record, DBModelProtocol { } private func construct(_ db: Database) throws { - if let id = self.id, case let targetId = String(id) { - self.files = try DBDesc.descs(targetId: targetId, targetType: .star, db: db) - } + files = try DBDesc.descs(targetId: localId, targetType: .star, db: db) - guard let feedId = self.feedId, let feedType = self.feedType, let type = FeedType(rawValue: feedType) else { + guard let feedId = feedId, let feedType = feedType, let type = FeedType(rawValue: feedType) else { return } switch type { case .p2p: - self.feed = try DBP2p.fetchOne(db, key: feedId) - if let from = self.from { + feed = try DBP2p.fetchOne(db, key: feedId) + if let from = from { if let sender = try? DBContact.request(phoneId: from).fetchOne(db), let contact = sender { let contact = Contact(contact: contact) - self.setup(contact: contact) + setup(contact: contact) } } case .muc: - self.feed = try DBMuc.fetchOne(db, key: feedId) - if let from = self.from { + feed = try DBMuc.fetchOne(db, key: feedId) + if let from = from { if let f = feed as? DBMuc, let room = try DBRoom.room(from: db, id: f.name, fullModel: false) { room.files = try DBDesc.descs(targetId: room.id, targetType: .room, db: db) let member = try DBMember.member(from: db, roomId: room.id, phoneId: from) @@ -284,37 +264,10 @@ final class DBStarMessage: Record, DBModelProtocol { } } - func fetchId(_ db: Database) throws -> Int64? { - let idColumn = Column(StarMessageTable.Column.id.title) - let serverIdColumn = Column(StarMessageTable.Column.serverId.title) - let localIdColumn = Column(StarMessageTable.Column.localId.title) - - let predicate = (serverId != nil && serverIdColumn == serverId) || (localId != nil && localIdColumn == localId) - return try DBStarMessage.filter(predicate).select(idColumn).asRequest(of: Int64.self).fetchOne(db) - } - - private func fetchLocalId(_ db: Database) throws -> String? { - if let localId = localId { - return localId - } - - let localIdColumn = Column(StarMessageTable.Column.localId.title) - let serverIdColumn = Column(StarMessageTable.Column.serverId.title) - - let predicate = (serverId != nil && serverIdColumn == serverId) - return try DBStarMessage.filter(predicate).select(localIdColumn).asRequest(of: String.self).fetchOne(db) - } - // MARK: - Delete @discardableResult func deleteAggregate(_ db: Database) throws -> Bool { - let tempId: Int64? = try self.id != nil ? self.id : fetchId(db) - guard let id = tempId else { - return false - } - self.id = id - - try DBDesc.deleteAll(db, targetId: String(id), targetType: .star) + try DBDesc.deleteAll(db, targetId: localId, targetType: .star) return try delete(db) } diff --git a/Nynja/DB/Models/Extension/DBMessage+TypeExtension.swift b/Nynja/DB/Models/Extension/DBMessage+Extension.swift similarity index 71% rename from Nynja/DB/Models/Extension/DBMessage+TypeExtension.swift rename to Nynja/DB/Models/Extension/DBMessage+Extension.swift index d003ddb0d..16ea1f41f 100644 --- a/Nynja/DB/Models/Extension/DBMessage+TypeExtension.swift +++ b/Nynja/DB/Models/Extension/DBMessage+Extension.swift @@ -1,21 +1,13 @@ // -// DBMessage+TypeExtension.swift +// DBMessage+Extension.swift // Nynja // // Created by Andrey Reznik on 27.07.2018. // Copyright © 2018 TecSynt Solutions. All rights reserved. // - extension DBMessage { - var isInOwnChat: Bool { - let ownerId = StorageService.sharedInstance.phoneId - return from == ownerId && to == ownerId - } - - var isOwn: Bool { - return self.from == StorageService.sharedInstance.phoneId - } + // MARK: - Types var isForward: Bool { return type?.contains("forward") ?? false @@ -40,5 +32,11 @@ extension DBMessage { var isCursor: Bool { return type?.contains("cursor") ?? false } + + + // MARK: - Delivery Status + + var isDelivered: Bool { + return serverId != nil + } } - diff --git a/Nynja/DB/Models/Extension/DBRoomExtension.swift b/Nynja/DB/Models/Extension/DBRoomExtension.swift new file mode 100644 index 000000000..d3a7ae8cc --- /dev/null +++ b/Nynja/DB/Models/Extension/DBRoomExtension.swift @@ -0,0 +1,17 @@ +// +// DBRoomExtension.swift +// Nynja +// +// Created by Volodymyr Hryhoriev on 10/1/18. +// Copyright © 2018 TecSynt Solutions. All rights reserved. +// + +extension DBRoom { + + var selfMember: DBMember? { + let phoneId = StorageService.sharedInstance.phoneId + return members.first { $0.phoneId == phoneId } + ?? admins.first { $0.phoneId == phoneId } + } + +} diff --git a/Nynja/DB/Tables/Base/Table.swift b/Nynja/DB/Tables/Base/Table.swift index b798936cf..6a1aac0f6 100644 --- a/Nynja/DB/Tables/Base/Table.swift +++ b/Nynja/DB/Tables/Base/Table.swift @@ -36,4 +36,8 @@ extension Table { static func hasColumns(_ columns: Set, in db: Database) throws -> Bool { return try db.hasColumns(columns, in: self) } + + static func allColumns(in db: Database) throws -> [String] { + return try db.allColumns(in: self) + } } diff --git a/Nynja/DB/Tables/ContactTable.swift b/Nynja/DB/Tables/ContactTable.swift index 7c2280d77..3ac2717c6 100644 --- a/Nynja/DB/Tables/ContactTable.swift +++ b/Nynja/DB/Tables/ContactTable.swift @@ -28,7 +28,7 @@ final class ContactTable: Table { t.column(Column.presence, .text) t.column(Column.status, .text) - t.column(Column.messageId, .integer).references(MessageTable.name, onDelete: .setNull) + t.column(Column.messageId, .text).references(MessageTable.name, column: MessageTable.Column.localId.title, onDelete: .setNull) t.column(Column.rosterId, .integer).references(RosterTable.name, onDelete: .cascade) } } diff --git a/Nynja/DB/Tables/DescTable.swift b/Nynja/DB/Tables/DescTable.swift index 79b491860..fe4885e34 100644 --- a/Nynja/DB/Tables/DescTable.swift +++ b/Nynja/DB/Tables/DescTable.swift @@ -16,7 +16,7 @@ final class DescTable: Table { static func create(in db: Database) throws { try db.create(self) { t in - t.primaryKey([Column.serverId.title, Column.targetType.title], onConflict: nil) + t.primaryKey([Column.serverId.title, Column.targetId.title, Column.targetType.title], onConflict: nil) t.column(Column.serverId, .text).notNull() t.column(Column.mime, .text) t.column(Column.payload, .text) diff --git a/Nynja/DB/Tables/MessageTable.swift b/Nynja/DB/Tables/MessageTable.swift index c65e1fd62..3d0040a72 100644 --- a/Nynja/DB/Tables/MessageTable.swift +++ b/Nynja/DB/Tables/MessageTable.swift @@ -16,14 +16,13 @@ final class MessageTable: Table { static func create(in db: Database) throws { try db.create(self) { t in - t.column(Column.id, .integer).primaryKey(onConflict: nil, autoincrement: true) t.column(Column.container, .text) t.column(Column.feedId, .integer) t.column(Column.feedType, .integer) t.column(Column.prev, .integer) t.column(Column.next, .integer) t.column(Column.serverId, .integer) - t.column(Column.localId, .text) + t.column(Column.localId, .text).notNull().primaryKey(onConflict: .replace) t.column(Column.from, .text) t.column(Column.to, .text) t.column(Column.created, .integer) @@ -32,6 +31,7 @@ final class MessageTable: Table { t.column(Column.repliedBy, .text) t.column(Column.mentioned, .text) t.column(Column.status, .text) + t.column(Column.localStatus, .integer) t.column(Column.trusted, .boolean) } } @@ -41,7 +41,6 @@ final class MessageTable: Table { extension MessageTable { enum Column: Int, Describable { - case id case container case feedId case feedType @@ -57,6 +56,7 @@ extension MessageTable { case repliedBy case mentioned case status + case localStatus case trusted } } diff --git a/Nynja/DB/Tables/RoomTable.swift b/Nynja/DB/Tables/RoomTable.swift index ac54bcc4e..d18717a78 100644 --- a/Nynja/DB/Tables/RoomTable.swift +++ b/Nynja/DB/Tables/RoomTable.swift @@ -31,7 +31,7 @@ final class RoomTable: Table { t.column(Column.created, .integer).notNull().defaults(to: 0) t.column(Column.status, .text) - t.column(Column.messageId, .integer).references(MessageTable.name, onDelete: .setNull) + t.column(Column.messageId, .text).references(MessageTable.name, onDelete: .setNull) t.column(Column.rosterId, .integer).references(RosterTable.name) } } diff --git a/Nynja/DB/Tables/StarMessageTable.swift b/Nynja/DB/Tables/StarMessageTable.swift index 696f8bdad..68fede57d 100644 --- a/Nynja/DB/Tables/StarMessageTable.swift +++ b/Nynja/DB/Tables/StarMessageTable.swift @@ -16,14 +16,13 @@ final class StarMessageTable: Table { static func create(in db: Database) throws { try db.create(self) { t in - t.column(Column.id, .integer).primaryKey(onConflict: nil, autoincrement: true) t.column(Column.container, .text) t.column(Column.feedId, .integer) t.column(Column.feedType, .integer) t.column(Column.prev, .integer) t.column(Column.next, .integer) t.column(Column.serverId, .integer) - t.column(Column.localId, .text) + t.column(Column.localId, .text).primaryKey(onConflict: .replace) t.column(Column.from, .text) t.column(Column.to, .text) t.column(Column.created, .integer) @@ -40,7 +39,6 @@ final class StarMessageTable: Table { extension StarMessageTable { enum Column: Int, Describable { - case id case container case feedId case feedType @@ -56,11 +54,5 @@ extension StarMessageTable { case repliedBy case mentioned case status - - // -- removed - case feedName - case senderName - case senderAvatar - // -- } } diff --git a/Nynja/DBObserver.swift b/Nynja/DBObserver.swift index 0b7b533bc..25c3f18c5 100644 --- a/Nynja/DBObserver.swift +++ b/Nynja/DBObserver.swift @@ -84,13 +84,7 @@ class DBObserver: StorageObserver, TransactionObserver { notify(with: storageChanges, type: .job(nil)) case MessageTable.name: - if changes.count > 1, let info = changes.first, let message = changedValue(from: info) as? Message { - if let receiver = message.p2pFeed?.opponentId { - notify(with: [], type: .chat(receiver)) - } else if let roomId = message.mucFeed?.name { - notify(with: [], type: .chat(roomId)) - } - } else { + func handle(changes: [ChangeInfo]) { changes.forEach { info in let message = changedValue(from: info) as? Message @@ -103,6 +97,28 @@ class DBObserver: StorageObserver, TransactionObserver { notify(with: info.event, entity: message, type: .reply(message?.id)) } } + + if changes.count == 2, + case let firstInfo = changes[0], + case let secondInfo = changes[1], + let firstMessage = changedValue(from: firstInfo) as? Message, + let secondMessage = changedValue(from: secondInfo) as? Message, + case let isFirstReply = firstMessage.isReply && firstMessage.linkedId == secondMessage.id, + case let isSecondReply = secondMessage.isReply && secondMessage.linkedId == firstMessage.id, + isFirstReply || isSecondReply { + + // Notify only for 1 of 2 message + handle(changes: [isFirstReply ? firstInfo : secondInfo]) + + } else if changes.count > 1, let info = changes.first, let message = changedValue(from: info) as? Message { + if let receiver = message.p2pFeed?.opponentId { + notify(with: [], type: .chat(receiver)) + } else if let roomId = message.mucFeed?.name { + notify(with: [], type: .chat(roomId)) + } + } else { + handle(changes: changes) + } case ContactTable.name: if changes.count == 1, let info = changes.first { let contact = changedValue(from: info) as? DBContact diff --git a/Nynja/DatabaseManager.swift b/Nynja/DatabaseManager.swift index 9794ea67b..315b4c233 100644 --- a/Nynja/DatabaseManager.swift +++ b/Nynja/DatabaseManager.swift @@ -58,10 +58,8 @@ final class DatabaseManager: DBManagerProtocol { do { let name = "\(name).sqlite" - let isFirstEncryption = try encryptOldDatabase(with: mode, newName: name) - if let path = fileManagerService.getPathOfFile(folder: folderName, name: name) { - try setupExistedDatabase(at: path, mode: mode, isFirstEncryption: isFirstEncryption) + try setupExistedDatabase(at: path, mode: mode) } else if let path = fileManagerService.createFile(folder: folderName, name: name) { try setupNewDatabase(at: path, mode: mode) } else { @@ -75,14 +73,13 @@ final class DatabaseManager: DBManagerProtocol { } } - private func setupExistedDatabase(at path: String, mode: EncryptionMode, isFirstEncryption: Bool) throws { - let passphrase = mode.passphraseForFirstEncryption(isFirstEncryption) - let configuration = makeConfiguration(with: passphrase) + private func setupExistedDatabase(at path: String, mode: EncryptionMode) throws { + let configuration = makeConfiguration(with: mode.oldPassphrase) dbPool = try DatabasePool(path: path, configuration: configuration) try performMigration() - if !isFirstEncryption, let newPassphrase = mode.newPassphrase { + if let newPassphrase = mode.newPassphrase { try dbPool?.change(passphrase: newPassphrase) } } @@ -97,36 +94,9 @@ final class DatabaseManager: DBManagerProtocol { private func makeConfiguration(with passphrase: String?) -> Configuration { var configuration = Configuration() configuration.passphrase = passphrase - configuration.trace = { info in - LogService.log(topic: .db) { return "DB Path: \(self.dbPool?.path ?? "")" + "\n" + info } - } return configuration } - private func encryptOldDatabase(with mode: EncryptionMode, newName name: String) throws -> Bool { - let configuration = makeConfiguration(with: mode.newPassphrase) - - let oldFileName = "mainDB.sqlite" - - guard let oldPath = fileManagerService.getPathOfFile(folder: folderName, name: oldFileName), - let newPath = fileManagerService.createFile(folder: folderName, name: name) else { - return false - } - - let clearDBQueue = try DatabaseQueue(path: oldPath) - let encryptedDBQueue = try DatabaseQueue(path: newPath, configuration: configuration) - - try clearDBQueue.inDatabase { db in - try db.execute("ATTACH DATABASE ? AS encrypted KEY ?", - arguments: [encryptedDBQueue.path, configuration.passphrase]) - try db.execute("SELECT sqlcipher_export('encrypted')") - try db.execute("DETACH DATABASE encrypted") - } - - try fileManagerService.removeFiles(in: folderName, contains: oldFileName) - return true - } - private func createTables() { try? writeInTransaction{ db in try DatabaseManager.tables.forEach { @@ -140,6 +110,10 @@ final class DatabaseManager: DBManagerProtocol { } private func fillMigrationsTable(_ database: Database, values: [String] = Migration.allTitles) throws { + guard !values.isEmpty else { + return + } + let tableName = "grdb_migrations" let identifierColumn = "identifier" diff --git a/Nynja/DefaultMessageProcessingManager.swift b/Nynja/DefaultMessageProcessingManager.swift index 1b8d31f2e..ab65a399b 100644 --- a/Nynja/DefaultMessageProcessingManager.swift +++ b/Nynja/DefaultMessageProcessingManager.swift @@ -22,6 +22,7 @@ protocol DefaultMessagesProcessingManagerInterface: MessageProcessingManagerInte class DefaultMessagesProcessingManager: DefaultMessagesProcessingManagerInterface { weak var delegate: MessageProcessingDelegate? + private let types: [SendMessageType] = [.file, .video, .audio, .image] private var queueMessages = [Message]() lazy var uploadOperationQueue: OperationQueue = { @@ -33,12 +34,22 @@ class DefaultMessagesProcessingManager: DefaultMessagesProcessingManagerInterfac static var shared = DefaultMessagesProcessingManager() private let typesWithAttachment = [SendMessageType.audio, .image, .file, .video] + private let typingStatusCache: ITypingStatusCache = TypingStatusCache(config: TypingStatusCache.Config(cacheValueMinimalRenewTime: 4)) + // MARK: Utils func postSendingStatusIfNeeded(_ messageType: SendMessageType, message: Message) { - let types: [SendMessageType] = [.file, .video, .audio, .image] - - if types.contains(messageType), let typingType = TypingModelType(sendMessageType: messageType) { + guard message.from != message.to, types.contains(messageType) else { + return + } + + let date = Date() + typingStatusCache.clearDeprecatedCaсhe(at: date) + + if let typingType = TypingModelType(sendMessageType: messageType), + let messageID = message.msg_id, + typingStatusCache.isCacheValueOutdated(for: messageID, at: date) { sendTypingStatus(typingType, message: message) + typingStatusCache.renewCacheValue(for: messageID, at: date) } } } @@ -130,23 +141,10 @@ extension DefaultMessagesProcessingManager { extension DefaultMessagesProcessingManager { func handleProgress(progress: ProgressModel) { - for msg in queueMessages { - guard let link = msg.mainUrl else { - break - } - - if link == progress.url { - guard let typeString = msg.mainFile?.mime, - let type = SendMessageType(rawValue: typeString) else { - break - } - - postSendingStatusIfNeeded(type, message: msg) - - break - } + if let message = queueMessages.first(where: { $0.mainUrl == progress.url }), + let type = message.sendType { + postSendingStatusIfNeeded(type, message: message) } - notify(with: progress) } } diff --git a/Nynja/Extensions/Bundle+Keys.swift b/Nynja/Extensions/Bundle+Keys.swift index dcc1a320a..94eaffcfe 100644 --- a/Nynja/Extensions/Bundle+Keys.swift +++ b/Nynja/Extensions/Bundle+Keys.swift @@ -33,7 +33,6 @@ extension Bundle { return object(forInfoDictionaryKey: "ServerURL") as! String } - var appGroupName: String { return object(forInfoDictionaryKey: "AppGroup") as! String } @@ -41,7 +40,6 @@ extension Bundle { var serverPort: UInt16 { let serverPort = object(forInfoDictionaryKey: "ServerPort") as! String return UInt16(serverPort)! - } var modelsVersion: Int { @@ -54,6 +52,11 @@ extension Bundle { return AppConfig(rawValue: value)! } + var isServerConnectionSecure: Bool { + let value = object(forInfoDictionaryKey: "isServerConnectionSecure") as! NSString + return value.boolValue + } + var confServerAddress: String { return object(forInfoDictionaryKey: "ConfServerAddress") as! String } @@ -62,4 +65,10 @@ extension Bundle { let port = object(forInfoDictionaryKey: "ConfServerPort") as! String return UInt16(port)! } + + var confServerSecure: Bool { + let value = object(forInfoDictionaryKey: "ConfServerSecure") as! String + let secure: NSString = NSString(string:value) + return secure.boolValue + } } diff --git a/Nynja/Extensions/Date+Extension.swift b/Nynja/Extensions/Date+Extension.swift index d0509965f..a0df40d09 100644 --- a/Nynja/Extensions/Date+Extension.swift +++ b/Nynja/Extensions/Date+Extension.swift @@ -50,17 +50,19 @@ extension Date { } var zeroSeconds: Date? { - get { - let calender = Calendar.current - let dateComponents = calender.dateComponents([.year, .month, .day, .hour, .minute], from: self) - return calender.date(from: dateComponents) - } + let calendar = Calendar.current + let dateComponents = calendar.dateComponents([.year, .month, .day, .hour, .minute], from: self) + return calendar.date(from: dateComponents) } var startOfDay: Date { return Calendar.current.startOfDay(for: self) } + func adding(_ component: Calendar.Component, value: Int) -> Date? { + let calendar = Calendar.current + return calendar.date(byAdding: component, value: value, to: self) + } func isDayEqual(to date: Date) -> Bool { return Calendar.current.compare(self, to: date, toGranularity: .day) == .orderedSame diff --git a/Nynja/Extensions/Models/MemberExtension.swift b/Nynja/Extensions/Models/MemberExtension.swift index 5529417fd..9a0c1485b 100644 --- a/Nynja/Extensions/Models/MemberExtension.swift +++ b/Nynja/Extensions/Models/MemberExtension.swift @@ -56,8 +56,8 @@ extension Member { var fullName: String? { if let name = self.names, let surname = self.surnames { - if surname != "" { - return name + " " + surname + if !surname.isEmpty { + return "\(name) \(surname)" } else { return name } @@ -159,4 +159,8 @@ extension Array where Element == Member { return contacts } + + var memberContacts: [Contact] { + return self.map() { Contact(member: $0) } + } } diff --git a/Nynja/Extensions/Models/Message/Message+DB.swift b/Nynja/Extensions/Models/Message/Message+DB.swift index 891a54a70..0e8927c5a 100644 --- a/Nynja/Extensions/Models/Message/Message+DB.swift +++ b/Nynja/Extensions/Models/Message/Message+DB.swift @@ -20,14 +20,15 @@ extension Message { self.msg_id = message.localId self.from = message.from self.to = message.to - self.created = message.created as AnyObject? + self.created = message.created self.types = Set(message.type?.components(separatedBy: Constants.commaSeparator) ?? []) - self.link = message.link + self.linkedId = message.link self.repliedby = message.repliedBy?.splitIntegerIdentifiers() self.mentioned = message.mentioned?.splitIntegerIdentifiers() self.status = StringAtom(string: message.status) + self.localStatus = message.localStatus self.files = message.files.map { Desc(desc: $0) } @@ -51,9 +52,9 @@ extension Message { self.msg_id = message.localId self.from = message.from self.to = message.to - self.created = message.created as AnyObject? + self.created = message.created self.types = Set(message.type?.components(separatedBy: Constants.commaSeparator) ?? []) - self.link = message.editMessage + self.linkedId = message.link self.repliedby = message.repliedBy?.splitIntegerIdentifiers() self.mentioned = message.mentioned?.splitIntegerIdentifiers() @@ -86,9 +87,9 @@ extension Message { self.msg_id = message.localId self.from = message.from self.to = message.to - self.created = message.created as AnyObject? + self.created = message.created self.types = Set(message.type?.components(separatedBy: Constants.commaSeparator) ?? []) - self.link = message.editMessage + self.linkedId = message.link self.repliedby = message.repliedBy?.splitIntegerIdentifiers() self.mentioned = message.mentioned?.splitIntegerIdentifiers() diff --git a/Nynja/Extensions/Models/Message/Message+System.swift b/Nynja/Extensions/Models/Message/Message+System.swift index ca27c9ac2..d3a53ea3d 100644 --- a/Nynja/Extensions/Models/Message/Message+System.swift +++ b/Nynja/Extensions/Models/Message/Message+System.swift @@ -13,7 +13,7 @@ extension Message { return nil } - if self.statusString == "clear" { + if case .clear? = messageStatus { return mainFile?.payload } else if let room = chat as? Room { return systemMessage(for: room) @@ -73,14 +73,10 @@ extension Message { } private func member(for phoneId: String, in room: Room?) -> Member? { - if let room = room { - if let members = room.members, let index = members.index(where: { $0.phone_id == phoneId }) { - return members[index] - } else if let admins = room.admins, let index = admins.index(where: {$0.phone_id == phoneId } ) { - return admins[index] - } + guard let room = room else { + return nil } - return nil + return room.members?.first { $0.phone_id == phoneId } + ?? room.admins?.first { $0.phone_id == phoneId } } - } diff --git a/Nynja/Extensions/Models/StarExtension.swift b/Nynja/Extensions/Models/StarExtension.swift index a0f63c181..95cbaf53a 100644 --- a/Nynja/Extensions/Models/StarExtension.swift +++ b/Nynja/Extensions/Models/StarExtension.swift @@ -101,7 +101,7 @@ extension Star { } var timestamp: Int64 { - guard let timestamp = message?.created as? Int64 else { + guard let timestamp = message?.created else { return 0 } return timestamp diff --git a/Nynja/Extensions/Range+Extension.swift b/Nynja/Extensions/Range+Extension.swift index 632ae77ca..3d9e865f8 100644 --- a/Nynja/Extensions/Range+Extension.swift +++ b/Nynja/Extensions/Range+Extension.swift @@ -25,6 +25,11 @@ extension Range where Bound == Int { // MARK: - NSRange extension Range where Bound == Int { + + init(_ range: NSRange) { + self = range.lowerBound.. { + return Range(self) + } +} + // MARK: - Shift extension Range where Bound == Int { diff --git a/Nynja/Extensions/SwiftLibrary/Array/ArrayExtension.swift b/Nynja/Extensions/SwiftLibrary/Array/ArrayExtension.swift index 093d1a6db..ae063c708 100644 --- a/Nynja/Extensions/SwiftLibrary/Array/ArrayExtension.swift +++ b/Nynja/Extensions/SwiftLibrary/Array/ArrayExtension.swift @@ -36,12 +36,29 @@ extension Array { } return count } -} - -extension Array { mutating func move(at index: Int, to newIndex: Int) { let element = remove(at: index) insert(element, at: newIndex) } + + + // MARK: - Complete + + mutating func complete(to length: Int, with element: Element) { + guard self.count <= length else { + return + } + + for _ in self.count.. [Element] { + var temp = self + temp.complete(to: length, with: element) + return temp + } + } diff --git a/Nynja/Extensions/SwiftLibrary/Dictionary/DictionaryExtension.swift b/Nynja/Extensions/SwiftLibrary/Dictionary/DictionaryExtension.swift index ec858228e..da1aae33e 100644 --- a/Nynja/Extensions/SwiftLibrary/Dictionary/DictionaryExtension.swift +++ b/Nynja/Extensions/SwiftLibrary/Dictionary/DictionaryExtension.swift @@ -16,8 +16,15 @@ extension Dictionary { try? merge(other, uniquingKeysWith: uniquingUsingFirst) } + mutating func mergeUniquingOther(with other: [Key: Value]) { + try? merge(other, uniquingKeysWith: uniquingUsingOther) + } + private func uniquingUsingFirst(value: Value, secondValue: Value) throws -> Value { return value } + private func uniquingUsingOther(value: Value, secondValue: Value) throws -> Value { + return secondValue + } } diff --git a/Nynja/Extensions/SwiftLibrary/Sequence/SequenceExtension.swift b/Nynja/Extensions/SwiftLibrary/Sequence/SequenceExtension.swift new file mode 100644 index 000000000..e4f385c34 --- /dev/null +++ b/Nynja/Extensions/SwiftLibrary/Sequence/SequenceExtension.swift @@ -0,0 +1,19 @@ +// +// SequenceExtension.swift +// Nynja +// +// Created by Volodymyr Hryhoriev on 9/27/18. +// Copyright © 2018 TecSynt Solutions. All rights reserved. +// + +extension Sequence where Element: Comparable & Hashable { + + /// Returns max element in sequence except `element` one. + /// If sequence contains only `element`, return `element`. + func max(except element: Element) -> Element { + var set = Set(self) + set.remove(element) + return set.max() ?? element + } + +} diff --git a/Nynja/Extensions/SwiftLibrary/String/String+LocationURL.swift b/Nynja/Extensions/SwiftLibrary/String/String+LocationURL.swift index 2979e13bd..89c90e796 100644 --- a/Nynja/Extensions/SwiftLibrary/String/String+LocationURL.swift +++ b/Nynja/Extensions/SwiftLibrary/String/String+LocationURL.swift @@ -10,16 +10,22 @@ import CoreLocation.CLLocation extension String { + private enum Constants { + static let zoomValue = 13 + static let imageWidth = 400 + static let imageHeight = 400 + static let apiKey = ThirdPartyServicesFactory.google.serviceConfig.apiKey + } + var locationUrl: URL? { - let staticMapUrl: String = "http://maps.google.com/maps/api/staticmap?markers=color:red|\(self)&\("zoom=13&size=400x400")&sensor=true" - let formattedString = staticMapUrl.addingPercentEncoding(withAllowedCharacters: CharacterSet.urlQueryAllowed) - if formattedString != nil { - let mapUrl = URL(string: formattedString!) - if mapUrl != nil { - return mapUrl! - } - } - return nil + let size = "size=\(Constants.imageWidth)x\(Constants.imageHeight)" + let zoom = "zoom=\(Constants.zoomValue)" + let sensor = "sensor=true" + let key = "key=\(Constants.apiKey)" + + return "http://maps.google.com/maps/api/staticmap?markers=color:red|\(self)&\(zoom)&\(size)&\(sensor)&\(key)" + .addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed) + .flatMap { URL(string: $0) } } var coordinate: CLLocationCoordinate2D? { diff --git a/Nynja/Files/AppGroupFlagContainer.swift b/Nynja/Files/AppGroupFlagContainer.swift new file mode 100644 index 000000000..f7341dca5 --- /dev/null +++ b/Nynja/Files/AppGroupFlagContainer.swift @@ -0,0 +1,66 @@ +// +// AppGroupFlagContainer.swift +// Nynja +// +// Created by Anton Poltoratskyi on 24.09.2018. +// Copyright © 2018 TecSynt Solutions. All rights reserved. +// + +import Foundation + +class AppGroupFlagContainer { + + enum Flag: String { + case shareExtension = "share-extension.lock" + + var fileName: String { + return rawValue + } + } + + let fileManager: FileManager + + let containerURL: URL + + var flagsDirectoryURL: URL { + return containerURL.appendingPathComponent("flags") + } + + + // MARK: - Init + + init?(fileManager: FileManager, appGroup: String) { + self.fileManager = fileManager + + guard let containerURL = fileManager.containerURL(forSecurityApplicationGroupIdentifier: appGroup) else { + return nil + } + self.containerURL = containerURL + } + + func prepare() throws { + if !fileManager.fileExists(atPath: flagsDirectoryURL.path) { + try fileManager.createDirectory(at: flagsDirectoryURL, withIntermediateDirectories: true, attributes: nil) + } + } + + + // MARK: - Flags + + func setFlag(_ flag: Flag) throws { + let flagURL = flagsDirectoryURL.appendingPathComponent(flag.fileName) + + // From Apple docs: + // If a file already exists at path, this method overwrites the contents of that file + // if the current process has the appropriate privileges to do so. + fileManager.createFile(atPath: flagURL.path, contents: nil, attributes: nil) + } + + func removeFlagIfExists(_ flag: Flag) throws { + let flagURL = flagsDirectoryURL.appendingPathComponent(flag.fileName) + guard fileManager.fileExists(atPath: flagURL.path) else { + return + } + try fileManager.removeItem(atPath: flagURL.path) + } +} diff --git a/Nynja/Files/AppGroupFlagObserver.swift b/Nynja/Files/AppGroupFlagObserver.swift new file mode 100644 index 000000000..d934c1f35 --- /dev/null +++ b/Nynja/Files/AppGroupFlagObserver.swift @@ -0,0 +1,40 @@ +// +// AppGroupFlagObserver.swift +// Nynja +// +// Created by Anton Poltoratskyi on 24.09.2018. +// Copyright © 2018 TecSynt Solutions. All rights reserved. +// + +import Foundation + +final class AppGroupFlagObserver: AppGroupFlagContainer { + + private var directoryWatcher: DirectoryWatcher? + + + // MARK: - Init + + deinit { + stopObserving() + } + + func observe(callback: @escaping ([Flag]) -> Void) throws { + try prepare() + + directoryWatcher = DirectoryWatcher.makeDirectoryWatcher(withPath: flagsDirectoryURL.path) { [weak self] watcher in + guard let `self` = self else { + return + } + do { + let content = try self.fileManager.contentsOfDirectory(atPath: self.flagsDirectoryURL.path) + let flags = content.compactMap { Flag(rawValue: $0) } + callback(flags) + } catch { } + } + } + + func stopObserving() { + directoryWatcher?.stop() + } +} diff --git a/Nynja/Files/DirectoryWatcher.swift b/Nynja/Files/DirectoryWatcher.swift new file mode 100644 index 000000000..f6950e471 --- /dev/null +++ b/Nynja/Files/DirectoryWatcher.swift @@ -0,0 +1,96 @@ +// +// DirectoryWatcher.swift +// Nynja +// +// Created by Anton Poltoratskyi on 24.09.2018. +// Copyright © 2018 TecSynt Solutions. All rights reserved. +// + +import Foundation + +public class DirectoryWatcher { + + public typealias Callback = (DirectoryWatcher) -> Void + + private var directoryFileDescriptor: Int32 = -1 { + didSet { + if oldValue != -1 { + close(oldValue) + } + } + } + + private var dispatchSource: DispatchSourceFileSystemObject? + + private let queue: DispatchQueue + + + // MARK: - Init + + private init(queue: DispatchQueue) { + self.queue = queue + } + + deinit { + stop() + } + + public static func makeDirectoryWatcher(withPath path: String, + onQueue queue: DispatchQueue = .main, + callback: @escaping Callback) -> DirectoryWatcher? { + let directoryWatcher = DirectoryWatcher(queue: queue) + + if !directoryWatcher.watch(path: path, callback: callback) { + assertionFailure() + return nil + } + + return directoryWatcher + } + + + // MARK: - Watch + + private func watch(path: String, callback: @escaping Callback) -> Bool { + // Open the directory + directoryFileDescriptor = open(path, O_EVTONLY) + if directoryFileDescriptor < 0 { + return false + } + + // Create and configure a DispatchSource to monitor it + let dispatchSource = DispatchSource.makeFileSystemObjectSource(fileDescriptor: directoryFileDescriptor, + eventMask: .write, + queue: queue) + dispatchSource.setEventHandler { [unowned self] in + callback(self) + } + dispatchSource.setCancelHandler { [unowned self] in + self.directoryFileDescriptor = -1 + } + self.dispatchSource = dispatchSource + + // Start monitoring + dispatchSource.resume() + + // Success + return true + } + + public func stop() { + // Leave if not monitoring + guard let dispatchSource = dispatchSource else { + return + } + + // Don't listen to more events + dispatchSource.setEventHandler(handler: nil) + + // Cancel the source (this will also close the directory) + dispatchSource.cancel() + + dispatchSource.setCancelHandler(handler: nil) + + self.dispatchSource = nil + } +} diff --git a/Nynja/HomeItemsFactory.swift b/Nynja/HomeItemsFactory.swift index e46843c23..966285952 100644 --- a/Nynja/HomeItemsFactory.swift +++ b/Nynja/HomeItemsFactory.swift @@ -26,8 +26,12 @@ class HomeItemsFactory: WCBaseItemsFactory { navigateDelegate?.call(indexPath: indexPath) }) call.subitems = [videoCall, voiceCall] + + let help = ImageActionItemModel(navItem: .help, isSelectable: false, action: { [weak navigateDelegate] (item, indexPath) in + navigateDelegate?.helpFeedBack(indexPath: indexPath) + }) - return [call, edit, myQR] + return [call, edit, myQR, help] } // MARK: - Calls diff --git a/Nynja/Debug/DebugLogs.swift b/Nynja/Library/Debug/DebugLogs.swift similarity index 100% rename from Nynja/Debug/DebugLogs.swift rename to Nynja/Library/Debug/DebugLogs.swift diff --git a/Nynja/Debug/UILabel+Debug.swift b/Nynja/Library/Debug/UILabel+Debug.swift similarity index 100% rename from Nynja/Debug/UILabel+Debug.swift rename to Nynja/Library/Debug/UILabel+Debug.swift diff --git a/Nynja/Debug/UIView+Debug.swift b/Nynja/Library/Debug/UIView+Debug.swift similarity index 100% rename from Nynja/Debug/UIView+Debug.swift rename to Nynja/Library/Debug/UIView+Debug.swift diff --git a/Nynja/Library/MessageFactory/MessageFactory.swift b/Nynja/Library/MessageFactory/MessageFactory.swift index ef4b71c75..0326fd7c6 100644 --- a/Nynja/Library/MessageFactory/MessageFactory.swift +++ b/Nynja/Library/MessageFactory/MessageFactory.swift @@ -9,57 +9,10 @@ import AVFoundation import CoreLocation -protocol MessageFactoryProtocol: class { - func makeTextMessage(inputText: InputTextMessage, contact: Contact?, room: Room?) -> Message - - func makeTextMessageEdited(message: Message, newInputText: InputTextMessage) -> Message - func makeTextMessageEdited(message: Message, action: DBMessageEditAction) -> Message - - func makeMediaMessage(media: Media, contact: Contact?, room: Room?) -> Message - func makeImageMessage(imageURL: URL, contact: Contact?, room: Room?) -> Message - func makeVideoMessage(videoURL: URL, contact: Contact?, room: Room?) -> Message - - func makeLocationMessage(coordinate: CLLocationCoordinate2D, contact: Contact?, room: Room?) -> Message - func makeLocationMessage(coordinate: String, contact: Contact?, room: Room?) -> Message - - func makePlaceMessage(place: Place, contact: Contact?, room: Room?) -> Message - func makeAudioMessage(withUrl url: URL, language: String?, contact: Contact?, room: Room?) -> Message - func makeContactMessage(sendingContact: Contact, contact: Contact?, room: Room?) -> Message - - func makeFileMessage(withUrl url: URL, contact: Contact?, room: Room?) -> Message - func makeStickerMessage(sticker: Sticker, contact: Contact?, room: Room?) -> Message - - func makeTranslatedMessage(message: Message, text: String, translatedText: String, lang: String) -> Message - func makeUntranslatedMessage(message: Message, translationId: String) -> Message - func autotranslationDesc(text: String, translatedText: String, lang: String) -> Desc - - func makeTranscribedMessage(message: Message, text: String, lang: String) -> Message - func makeUntranscribedMessage(message: Message, transcriptionId: String, translationId: String?) -> Message - - func makePaymentMessage(inputText: String, contact: Contact) -> Message - - func makeCallMessage(members: [String], room: Room?) -> Message - - func makeMessageForDelete(message: Message, seenBy: [AnyObject]) -> Message -} - -extension MessageFactoryProtocol { - func makeAudioMessage(withUrl url: URL, language: String? = nil, contact: Contact?, room: Room?) -> Message { - return makeAudioMessage(withUrl: url, - language: language, - contact: contact, - room: room) - } - - func makeUntranscribedMessage(message: Message, transcriptionId: String, translationId: String? = nil) -> Message { - return makeUntranscribedMessage(message: message, - transcriptionId: transcriptionId, - translationId: translationId) - } -} - final class MessageFactory: MessageFactoryProtocol, InitializeInjectable { + // MARK: - Dependencies + private let storageService: StorageService private let payloadBuilder: MessagePayloadBuilderInput @@ -68,6 +21,9 @@ final class MessageFactory: MessageFactoryProtocol, InitializeInjectable { let payloadBuilder: MessagePayloadBuilderInput } + + // MARK: - Init + init(dependencies: Dependencies) { storageService = dependencies.storageService payloadBuilder = dependencies.payloadBuilder @@ -114,7 +70,7 @@ final class MessageFactory: MessageFactoryProtocol, InitializeInjectable { } else { message.files = [newFile] } - message.status = StringAtom(string: "edit") + message.messageStatus = .edit message.markAsEdited() message.mentioned = newInputText.mentions.map { $0.memberId }.uniqueWithPreservedOrder() as [AnyObject]? @@ -127,7 +83,7 @@ final class MessageFactory: MessageFactoryProtocol, InitializeInjectable { let mimes: [DescMime] = [.base(mime: .text, payload: action.payload)] message.files = mimes.map { Desc(mime: $0) } - message.status = StringAtom(string: "edit") + message.messageStatus = .edit message.markAsEdited() message.mentioned = action.mentioned?.splitByComma(Int64.self)?.uniqueWithPreservedOrder() as [AnyObject]? @@ -240,7 +196,7 @@ final class MessageFactory: MessageFactoryProtocol, InitializeInjectable { desc.data = Feature.translateRepresentation(language: lang, translation: translatedText, users: nil) message.files = [desc] - message.status = StringAtom(string: "update") + message.messageStatus = .update return message } @@ -252,7 +208,7 @@ final class MessageFactory: MessageFactoryProtocol, InitializeInjectable { desc.mime = SendMessageType.translate.rawValue message.files = [desc] - message.status = StringAtom(string: "update") + message.messageStatus = .update return message } @@ -276,7 +232,7 @@ final class MessageFactory: MessageFactoryProtocol, InitializeInjectable { desc.data = Feature.transcribeRepresentation(language: lang, users: nil) message.files = [desc] - message.status = StringAtom(string: "update") + message.messageStatus = .update return message } @@ -297,7 +253,7 @@ final class MessageFactory: MessageFactoryProtocol, InitializeInjectable { } message.files = files - message.status = StringAtom(string: "update") + message.messageStatus = .update return message } @@ -313,7 +269,7 @@ final class MessageFactory: MessageFactoryProtocol, InitializeInjectable { let message = Message(message: message) message.from = storageService.phoneId message.seenby = seenBy - message.status = StringAtom(string: "delete") + message.messageStatus = .delete return message } } @@ -322,9 +278,9 @@ final class MessageFactory: MessageFactoryProtocol, InitializeInjectable { private extension MessageFactory { func makeMessage(descs: [Desc], mentioned: [Int64]? = nil, phoneId: String?, contact: Contact?, room: Room?) -> Message { let message = Message(phoneId: phoneId, contact: contact, room: room) - message.created = Date.currentTimestamp as AnyObject + message.created = Date.currentTimestamp message.files = descs - message.status = nil + message.messageStatus = nil message.mentioned = mentioned as [AnyObject]? return message diff --git a/Nynja/Library/MessageFactory/MessageFactoryProtocol.swift b/Nynja/Library/MessageFactory/MessageFactoryProtocol.swift new file mode 100644 index 000000000..5fde105e4 --- /dev/null +++ b/Nynja/Library/MessageFactory/MessageFactoryProtocol.swift @@ -0,0 +1,59 @@ +// +// MessageFactoryProtocol.swift +// Nynja +// +// Created by Anton Poltoratskyi on 10.09.2018. +// Copyright © 2018 TecSynt Solutions. All rights reserved. +// + +import Foundation +import CoreLocation + +protocol MessageFactoryProtocol: class { + func makeTextMessage(inputText: InputTextMessage, contact: Contact?, room: Room?) -> Message + + func makeTextMessageEdited(message: Message, newInputText: InputTextMessage) -> Message + func makeTextMessageEdited(message: Message, action: DBMessageEditAction) -> Message + + func makeMediaMessage(media: Media, contact: Contact?, room: Room?) -> Message + func makeImageMessage(imageURL: URL, contact: Contact?, room: Room?) -> Message + func makeVideoMessage(videoURL: URL, contact: Contact?, room: Room?) -> Message + + func makeLocationMessage(coordinate: CLLocationCoordinate2D, contact: Contact?, room: Room?) -> Message + func makeLocationMessage(coordinate: String, contact: Contact?, room: Room?) -> Message + + func makePlaceMessage(place: Place, contact: Contact?, room: Room?) -> Message + func makeAudioMessage(withUrl url: URL, language: String?, contact: Contact?, room: Room?) -> Message + func makeContactMessage(sendingContact: Contact, contact: Contact?, room: Room?) -> Message + + func makeFileMessage(withUrl url: URL, contact: Contact?, room: Room?) -> Message + func makeStickerMessage(sticker: Sticker, contact: Contact?, room: Room?) -> Message + + func makeTranslatedMessage(message: Message, text: String, translatedText: String, lang: String) -> Message + func makeUntranslatedMessage(message: Message, translationId: String) -> Message + func autotranslationDesc(text: String, translatedText: String, lang: String) -> Desc + + func makeTranscribedMessage(message: Message, text: String, lang: String) -> Message + func makeUntranscribedMessage(message: Message, transcriptionId: String, translationId: String?) -> Message + + func makePaymentMessage(inputText: String, contact: Contact) -> Message + + func makeCallMessage(members: [String], room: Room?) -> Message + + func makeMessageForDelete(message: Message, seenBy: [AnyObject]) -> Message +} + +extension MessageFactoryProtocol { + func makeAudioMessage(withUrl url: URL, language: String? = nil, contact: Contact?, room: Room?) -> Message { + return makeAudioMessage(withUrl: url, + language: language, + contact: contact, + room: room) + } + + func makeUntranscribedMessage(message: Message, transcriptionId: String, translationId: String? = nil) -> Message { + return makeUntranscribedMessage(message: message, + transcriptionId: transcriptionId, + translationId: translationId) + } +} diff --git a/Nynja/Library/UI/AlertManager.swift b/Nynja/Library/UI/AlertManager.swift index 52b3dab16..f1debc751 100644 --- a/Nynja/Library/UI/AlertManager.swift +++ b/Nynja/Library/UI/AlertManager.swift @@ -6,12 +6,11 @@ // Copyright © 2017 TecSynt Solutions. All rights reserved. // -import Foundation import UIKit class AlertManager { - static let sharedInstance : AlertManager = { + static let sharedInstance: AlertManager = { let instance = AlertManager() return instance }() @@ -29,6 +28,14 @@ class AlertManager { presentingController?.present(alert, animated: true, completion: nil) } + func showAlert(title: String, dismissInterval: TimeInterval) { + let alert = UIAlertController(title: "\n\(title)\n ", message: "", preferredStyle: .alert) + dispatchAsyncMainAfter(dismissInterval) { + alert.dismiss(animated: true, completion: nil) + } + presentingController?.present(alert, animated: true, completion: nil) + } + func showAlertOk(message: String, completion:(()->Void)? = nil) { showAlertOk(title: "", message: message, completion: completion) } @@ -190,9 +197,6 @@ class AlertManager { func showNativeShare(with activityItems: [Any]) { let activityViewController = UIActivityViewController(activityItems: activityItems, applicationActivities: nil) - activityViewController.completionWithItemsHandler = { (type, flag, objects, error) in - MQTTService.sharedInstance.reconnectWithTimer() - } presentingController?.present(activityViewController, animated: true, completion: nil) } diff --git a/Nynja/Library/UI/BaseVC/BaseVC.swift b/Nynja/Library/UI/BaseVC/BaseVC.swift index e7e8ec703..1ce662e8f 100644 --- a/Nynja/Library/UI/BaseVC/BaseVC.swift +++ b/Nynja/Library/UI/BaseVC/BaseVC.swift @@ -9,8 +9,6 @@ import Foundation import UIKit -let viewShoudEndEditing = Notification.Name("viewShoudEndEditing") - class BaseVC: UIViewController, UIGestureRecognizerDelegate, BaseViewProtocol, UserSettingsRespondable { typealias GradientConfiguration = (height: Double, bottomToView: UIView) @@ -60,10 +58,12 @@ class BaseVC: UIViewController, UIGestureRecognizerDelegate, BaseViewProtocol, U } } - // MARK: Views + + // MARK: - Views + private var spinner = UIActivityIndicatorView() - lazy var navigationView: NavigationView = { + private(set) lazy var navigationView: NavigationView = { let navView = NavigationView() navView.clipsToBounds = true @@ -86,7 +86,7 @@ class BaseVC: UIViewController, UIGestureRecognizerDelegate, BaseViewProtocol, U return navView }() - lazy var backImage: UIImageView = { + private(set) lazy var backImage: UIImageView = { let img = UIImageView() img.isUserInteractionEnabled = true setupBackground(for: img) @@ -111,8 +111,10 @@ class BaseVC: UIViewController, UIGestureRecognizerDelegate, BaseViewProtocol, U return grView }() + - // MARK: View lifecycle + // MARK: - View lifecycle + override func viewDidLoad() { super.viewDidLoad() backImage.isHidden = false @@ -136,9 +138,9 @@ class BaseVC: UIViewController, UIGestureRecognizerDelegate, BaseViewProtocol, U override func viewWillAppear(_ animated: Bool) { super.viewWillAppear(animated) - NotificationCenter.default.addObserver(self,selector: #selector(self.keyboardNotification),name: NSNotification.Name.UIKeyboardWillChangeFrame,object: nil) self.view.bringSubview(toFront: navigationView) _presenter?.screenBecomeActive() + registerForKeyboardNotifications() } override func viewDidAppear(_ animated: Bool) { @@ -146,50 +148,30 @@ class BaseVC: UIViewController, UIGestureRecognizerDelegate, BaseViewProtocol, U _presenter?.screenFinishedDisplaying() _presenter?.viewAppeared() } - + override func viewWillDisappear(_ animated: Bool) { super.viewWillDisappear(animated) + // unregister keyboard + NotificationCenter.default.removeObserver(self) + prepareForDissappear() } override func viewDidDisappear(_ animated: Bool) { super.viewDidDisappear(animated) - NotificationCenter.default.removeObserver(self) _presenter?.screenWillHide() } - // MARK: Gestures & Notifications - func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldReceive touch: UITouch) -> Bool { - if let flag = touch.view?.isKind(of: UIControl.self), flag == true { - return false - } - return true - } - - @objc func tapOnScreen(recognizer: UITapGestureRecognizer) { - let notification = Notification(name: viewShoudEndEditing) - NotificationCenter.default.post(notification) - } - - @objc func keyboardNotification(notification: NSNotification) { - if let userInfo = notification.userInfo as? [String:Any] { - if let endFrame = (userInfo["UIKeyboardFrameEndUserInfoKey"] as? NSValue)?.cgRectValue { - let duration: TimeInterval = (userInfo["UIKeyboardAnimationDurationUserInfoKey"] as? NSNumber)?.doubleValue ?? 0 - let animationCurveRawNSN = userInfo["UIKeyboardAnimationCurveUserInfoKey"] as? NSNumber - let animationCurveRaw = animationCurveRawNSN?.uintValue ?? UIViewAnimationOptions.curveEaseOut.rawValue - let animationCurve:UIViewAnimationOptions = UIViewAnimationOptions(rawValue: animationCurveRaw) - self.keyboardNotified(endFrame: endFrame) - - UIView.animate(withDuration: duration, delay: TimeInterval(0), options: animationCurve, animations: { - self.view.layoutIfNeeded() - }, completion: nil) - } - } - } - // MARK: - BaseVC + func initialize() { } + + func prepareForDissappear() { + } + + @objc func tapOnScreen(recognizer: UITapGestureRecognizer) { + } deinit { _presenter?.prepareToRemoving() @@ -197,13 +179,27 @@ class BaseVC: UIViewController, UIGestureRecognizerDelegate, BaseViewProtocol, U deinited(self) } - func keyboardNotified(endFrame: CGRect) {} - func isKeyboardGoingToHide(_ endFrame: CGRect) -> Bool { return endFrame.origin.y >= UIScreen.main.bounds.size.height } + + func endEditing() { + view.endEditing(true) + } + + + // MARK: - Gestures + + func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldReceive touch: UITouch) -> Bool { + if let flag = touch.view?.isKind(of: UIControl.self), flag == true { + return false + } + return true + } + // MARK: - Spinner + private func configureSpinner() { spinner.hidesWhenStopped = true spinner.translatesAutoresizingMaskIntoConstraints = false @@ -251,7 +247,9 @@ class BaseVC: UIViewController, UIGestureRecognizerDelegate, BaseViewProtocol, U isSpinnerShown = false } + // MARK: - UserSettingsRespondable + func userSettingsDidChange(_ newSettings: UserSettings) { setupBackground(for: backImage, theme: newSettings.theme) currentTheme = newSettings.theme @@ -261,6 +259,7 @@ class BaseVC: UIViewController, UIGestureRecognizerDelegate, BaseViewProtocol, U imageView.image = UIImage(named: theme.backgroundName) } + // MARK: - Private methods private func adjustNavigationView() { @@ -290,5 +289,4 @@ class BaseVC: UIViewController, UIGestureRecognizerDelegate, BaseViewProtocol, U gradientView.isHidden = false } - } diff --git a/Nynja/Library/UI/Buttons/TopSwipable/ScheduleButton.swift b/Nynja/Library/UI/Buttons/TopSwipable/ScheduleButton.swift index 9791d8f37..866556900 100644 --- a/Nynja/Library/UI/Buttons/TopSwipable/ScheduleButton.swift +++ b/Nynja/Library/UI/Buttons/TopSwipable/ScheduleButton.swift @@ -6,10 +6,11 @@ // Copyright © 2018 TecSynt Solutions. All rights reserved. // -class ScheduleButton: UIButton, TopSwipable { +import SnapKit + +final class ScheduleButton: UIButton, TopSwipable { - /// Default: 85.0 - var bottomInset = 85.0.adjustedByWidth + private let bottomInset: CGFloat = CGFloat(12.0).adjustedByWidth var rightInset = ScheduleView.Constraints.upArrowScheduleView.right { didSet { @@ -19,7 +20,9 @@ class ScheduleButton: UIButton, TopSwipable { var recognizer: GestureRecognizerClosure! + // MARK: - Actions + var scheduleAction: (() -> Void)? var shouldBeginAction: (() -> Bool)? @@ -30,22 +33,28 @@ class ScheduleButton: UIButton, TopSwipable { var cancelAction: TopSwipeAction? + // MARK: - Views - lazy var scheduleView: ScheduleView = { + + private var scheduleView: ScheduleView? + + private func makeScheduleView(in window: UIWindow?) -> ScheduleView { + self.scheduleView?.removeFromSuperview() + let scheduleView = ScheduleView() - if let window = UIApplication.shared.keyWindow { - window.addSubview(scheduleView) - scheduleView.snp.makeConstraints({ (make) in - make.left.right.top.equalToSuperview() - make.bottom.equalTo(-bottomInset) - }) + window?.addSubview(scheduleView) + scheduleView.snp.makeConstraints { maker in + maker.left.right.top.equalToSuperview() + maker.bottom.equalTo(self.snp.top).offset(-bottomInset) } return scheduleView - }() + } + // MARK: - Init + override init(frame: CGRect) { super.init(frame: frame) baseSetup() @@ -56,97 +65,56 @@ class ScheduleButton: UIButton, TopSwipable { baseSetup() } - // MARK: - Layout + + // MARK: - Life Cycle + + override func didMoveToWindow() { + super.didMoveToWindow() + if window == nil { + hideScheduleView() + } + } + override func layoutSubviews() { super.layoutSubviews() - updateRightInset() + scheduleView?.rightInset = rightInset } - // MARK: - Base Setup + + // MARK: - Setup + private func baseSetup() { setup() - beginAction = { - self.showScheduleView() + beginAction = { [weak self] in + self?.showScheduleView() } - swipeAction = { + swipeAction = { [weak self] in + guard let `self` = self else { return } self.scheduleAction?() self.hideScheduleView() } - cancelAction = { - self.hideScheduleView() + cancelAction = { [weak self] in + self?.hideScheduleView() } - - scheduleView.isHidden = true - - //prepare bottom inset base on safe area of the super view - updateBottomInset(bottomInset + safeAreaBottomInset()) - - NotificationCenter.default.addObserver(self, selector: #selector(keyboardNotified(_:)), name: .UIKeyboardWillChangeFrame, object: nil) } + - deinit { - NotificationCenter.default.removeObserver(self) - } + // MARK: - Show / Hide - // MARK: - Show/hide private func showScheduleView() { - self.scheduleView.isHidden = false - self.scheduleView.isUserInteractionEnabled = true - self.scheduleView.layer.zPosition = 1 - self.scheduleView.baseSetup() + scheduleView = makeScheduleView(in: window) + scheduleView?.rightInset = rightInset + scheduleView?.isHidden = false + scheduleView?.isUserInteractionEnabled = true + scheduleView?.layer.zPosition = 1 + scheduleView?.baseSetup() } private func hideScheduleView() { - self.scheduleView.isHidden = true - } - - // MARK: - Keyboard notification - @objc private func keyboardNotified(_ notification: Notification) { - // NOTE: this logic is copied from the BaseVC - if let userInfo = notification.userInfo as? [String:Any] { - if let endFrame = (userInfo["UIKeyboardFrameEndUserInfoKey"] as? NSValue)?.cgRectValue { - let duration: TimeInterval = (userInfo["UIKeyboardAnimationDurationUserInfoKey"] as? NSNumber)?.doubleValue ?? 0 - let animationCurveRawNSN = userInfo["UIKeyboardAnimationCurveUserInfoKey"] as? NSNumber - let animationCurveRaw = animationCurveRawNSN?.uintValue ?? UIViewAnimationOptions.curveEaseOut.rawValue - let animationCurve:UIViewAnimationOptions = UIViewAnimationOptions(rawValue: animationCurveRaw) - - UIView.animate(withDuration: duration, delay: TimeInterval(0), options: animationCurve, animations: { - self.updateScheduleMessageInterfacePosition(endFrame: endFrame) - self.layoutIfNeeded() - }, completion: nil) - } - } - } - - private func updateScheduleMessageInterfacePosition(endFrame: CGRect) { - if let _ = scheduleView.superview { - var bottomInset = self.bottomInset + safeAreaBottomInset() - if endFrame.origin.y < UIScreen.main.bounds.size.height { - bottomInset = self.bottomInset + Double(endFrame.height) - } - updateBottomInset(bottomInset) - } - } - - private func updateBottomInset(_ inset: Double) { - scheduleView.snp.updateConstraints { $0.bottom.equalTo(-inset) } - } - - private func updateRightInset() { - self.scheduleView.rightInset = rightInset + scheduleView?.removeFromSuperview() + scheduleView = nil } - - private func safeAreaBottomInset() -> Double { - var safeArea : Double = 0.0 - if let s = scheduleView.superview { - if #available(iOS 11.0, *) { - safeArea = Double(s.safeAreaInsets.bottom) - } - } - return safeArea - } - } diff --git a/Nynja/Library/UI/Buttons/TopSwipable/TopSwipable.swift b/Nynja/Library/UI/Buttons/TopSwipable/TopSwipable.swift index b6426ef6a..371b01a21 100644 --- a/Nynja/Library/UI/Buttons/TopSwipable/TopSwipable.swift +++ b/Nynja/Library/UI/Buttons/TopSwipable/TopSwipable.swift @@ -18,13 +18,17 @@ protocol TopSwipable: class { var cancelAction: TopSwipeAction? { get set } func setup() - } extension TopSwipable where Self: UIView { func setup() { - recognizer = GestureRecognizerClosure(action: { [weak self] recognizer in + isUserInteractionEnabled = true + + guard recognizer == nil else { + return + } + recognizer = GestureRecognizerClosure { [weak self] recognizer in let shouldBegin = self?.shouldBeginAction?() ?? true guard shouldBegin == true else { return } @@ -38,15 +42,12 @@ extension TopSwipable where Self: UIView { default: break } - }) - - self.isUserInteractionEnabled = true - self.addGestureRecognizer(recognizer.recognizer) + } + addGestureRecognizer(recognizer.recognizer) } - } -class GestureRecognizerClosure { +final class GestureRecognizerClosure { typealias RecognizerAction = (Recognizer) -> Void let recognizer: Recognizer @@ -58,9 +59,7 @@ class GestureRecognizerClosure { self.recognizer.addTarget(self, action: #selector(fired(_:))) } - @objc func fired(_ recognizer: UIGestureRecognizer) { + @objc private func fired(_ recognizer: UIGestureRecognizer) { self.action(recognizer as! Recognizer) } - } - diff --git a/Nynja/Library/UI/Extensions/DateExtensions.swift b/Nynja/Library/UI/Extensions/DateExtensions.swift index dc4f0f76f..fe6b673a8 100644 --- a/Nynja/Library/UI/Extensions/DateExtensions.swift +++ b/Nynja/Library/UI/Extensions/DateExtensions.swift @@ -20,7 +20,15 @@ extension Date { } static var currentTimestamp: Int64 { - return Int64(Date().timeIntervalSince1970 * 1000) + return Date().timestamp + } + + static var currentDate: Date { + return Date() + } + + var timestamp: Int64 { + return Int64(timeIntervalSince1970 * 1_000) } var messageString: String { @@ -49,4 +57,18 @@ extension Date { if let second = difference.second, second > 0 { return seconds } return "" } + + func callFormattedTime() -> String { + var seconds = Int(Date().timeIntervalSince(self)) + let hours = seconds / 3600 + seconds = seconds - hours*3600 + let minutes = seconds / 60 + seconds = seconds - minutes * 60 + + if hours > 0 { + return String.localizedStringWithFormat("%02u:%02u:%02u", hours, minutes, seconds) + } else { + return String.localizedStringWithFormat("%02u:%02u", minutes, seconds) + } + } } diff --git a/Nynja/Library/UI/Extensions/UI/UIImageView/UIImageView+SetImage.swift b/Nynja/Library/UI/Extensions/UI/UIImageView/UIImageView+SetImage.swift index b46d8dc14..63cdf2b44 100644 --- a/Nynja/Library/UI/Extensions/UI/UIImageView/UIImageView+SetImage.swift +++ b/Nynja/Library/UI/Extensions/UI/UIImageView/UIImageView+SetImage.swift @@ -9,20 +9,24 @@ import Foundation import SDWebImage -typealias ImageCompletion = (URL?, UIImage?) -> Void - extension UIImageView { + + typealias ImageCompletion = (URL?, UIImage?, Error?) -> Void + func setImage(url: URL?, placeHolder: UIImage? = nil, accessibilityPrefix: String? = nil, completion: ImageCompletion? = nil) { setInitialAccesibilityIdentifier(accessibilityPrefix: accessibilityPrefix) + if let localURL = url, localURL.isLocalURL { image = UIImage(fileUrl: localURL) - - completion?(url, image) + setLoadedAccesibilityIdentifier(accessibilityPrefix: accessibilityPrefix) + completion?(url, image, nil) + } else { - sd_setImage(with: url, placeholderImage: placeHolder, options: []) { [weak self] (image, error, cache, url) in - self?.setLoadedAccesibilityIdentifier(accessibilityPrefix: accessibilityPrefix) - - completion?(url, image) + sd_setImage(with: url, placeholderImage: placeHolder, options: []) { [weak self] image, error, cache, url in + if error == nil, image != nil { + self?.setLoadedAccesibilityIdentifier(accessibilityPrefix: accessibilityPrefix) + } + completion?(url, image, error) } } } @@ -34,17 +38,4 @@ extension UIImageView { private func setLoadedAccesibilityIdentifier(accessibilityPrefix: String?) { accessibilityIdentifier = (accessibilityPrefix ?? "") + "_" + "loaded_image" } - - - private struct AssociatedKeys { - static var imageURL = "imageURL" - } - - private var imageURL: URL? { - get { - return objc_getAssociatedObject(self, &AssociatedKeys.imageURL) as? URL - } set { - objc_setAssociatedObject(self, &AssociatedKeys.imageURL, newValue as Any, .OBJC_ASSOCIATION_RETAIN_NONATOMIC) - } - } } diff --git a/Nynja/Library/UI/Extensions/URLExtensions.swift b/Nynja/Library/UI/Extensions/URLExtensions.swift index b94d59f2e..569ace4b7 100644 --- a/Nynja/Library/UI/Extensions/URLExtensions.swift +++ b/Nynja/Library/UI/Extensions/URLExtensions.swift @@ -30,4 +30,12 @@ extension URL { } return URL(fileURLWithPath: path) } + + var mediaURL: URL? { + let fileName = String(self.lastPathComponent) + guard let path = FileManagerService.sharedInstance.getPathOfFile(folder: FileManagerService.Folders.downloads.rawValue, name: fileName) else { + return nil + } + return URL(fileURLWithPath: path) + } } diff --git a/Nynja/Library/UI/ImagePreviewTransitionController/ImagePreviewTransitionController.swift b/Nynja/Library/UI/ImagePreviewTransitionController/ImagePreviewTransitionController.swift index f97a4ebc4..02bebe1c0 100644 --- a/Nynja/Library/UI/ImagePreviewTransitionController/ImagePreviewTransitionController.swift +++ b/Nynja/Library/UI/ImagePreviewTransitionController/ImagePreviewTransitionController.swift @@ -208,6 +208,9 @@ UIViewControllerAnimatedTransitioning, UIViewControllerInteractiveTransitioning } let imageView = imagePreviewViewController.hostImageView + if let contentMode = transitionInfo.endingView?.contentMode { + imageView.contentMode = contentMode + } let imageViewFrame = imageView.frame let presentersViewRemoved = from.presentationController?.shouldRemovePresentersView ?? false @@ -436,7 +439,7 @@ UIViewControllerAnimatedTransitioning, UIViewControllerInteractiveTransitioning } imageView.center.y = self.imageViewInitialCenter.y - + self.fadeView.alpha = 1 } diff --git a/Nynja/Library/UI/KeyboardLayoutGuide/KeyboardInteractive.swift b/Nynja/Library/UI/KeyboardLayoutGuide/KeyboardInteractive.swift new file mode 100644 index 000000000..85f75ef46 --- /dev/null +++ b/Nynja/Library/UI/KeyboardLayoutGuide/KeyboardInteractive.swift @@ -0,0 +1,73 @@ +// +// KeyboardInteractive.swift +// Nynja +// +// Created by Anton Poltoratskyi on 06.09.2018. +// Copyright © 2018 TecSynt Solutions. All rights reserved. +// + +import UIKit + +public protocol KeyboardInteractive: class { + func handleKeyboard(notification: Notification) + func keyboardNotified(endFrame: CGRect) + func animateKeyboardLayout(endFrame: CGRect) +} + +extension KeyboardInteractive where Self: UIViewController { + + public func handleKeyboard(notification: Notification) { + guard let userInfo = notification.userInfo as? [String: Any], + let frameEndValue = userInfo["UIKeyboardFrameEndUserInfoKey"] as? NSValue else { + return + } + let endFrame = frameEndValue.cgRectValue + let duration = (userInfo["UIKeyboardAnimationDurationUserInfoKey"] as? NSNumber)?.doubleValue ?? 0 + let animationCurveRawNSN = userInfo["UIKeyboardAnimationCurveUserInfoKey"] as? NSNumber + let animationCurveRaw = animationCurveRawNSN?.uintValue ?? UIViewAnimationOptions.curveEaseOut.rawValue + let animationCurve = UIViewAnimationOptions(rawValue: animationCurveRaw) + + UIView.performWithoutAnimation { + self.updateLayout() + } + keyboardNotified(endFrame: endFrame) + + UIView.animate(withDuration: duration, delay: 0, options: animationCurve, animations: { + self.animateKeyboardLayout(endFrame: endFrame) + }, completion: nil) + } + + func animateKeyboardLayout(endFrame: CGRect) { + updateLayout() + } + + private func updateLayout() { + view.layoutIfNeeded() + } +} + +extension UIViewController { + + public func registerForKeyboardNotifications() { + guard self is KeyboardInteractive else { + return + } + let center = NotificationCenter.default + center.addObserver(self, + selector: #selector(keyboardWillChangeFrame(notification:)), + name: .UIKeyboardWillChangeFrame, + object: nil) + } + + public func unregisterForKeyboardNotifications() { + guard self is KeyboardInteractive else { + return + } + let center = NotificationCenter.default + center.removeObserver(self, name: .UIKeyboardWillChangeFrame, object: nil) + } + + @objc private func keyboardWillChangeFrame(notification: Notification) { + (self as? KeyboardInteractive)?.handleKeyboard(notification: notification) + } +} diff --git a/Nynja/Library/UI/KeyboardLayoutGuide/KeyboardLayoutGuide.swift b/Nynja/Library/UI/KeyboardLayoutGuide/KeyboardLayoutGuide.swift index 2b74cbbcc..87da1c305 100644 --- a/Nynja/Library/UI/KeyboardLayoutGuide/KeyboardLayoutGuide.swift +++ b/Nynja/Library/UI/KeyboardLayoutGuide/KeyboardLayoutGuide.swift @@ -17,25 +17,26 @@ extension UIView { /// A layout guide representing the inset for the keyboard. /// Use this layout guide’s top anchor to create constraints pinning to the top of the keyboard. var keyboardLayoutGuide: KeyboardLayoutGuide { - get { - if let obj = objc_getAssociatedObject(self, &AssociatedKeys.keyboardLayoutGuide) as? KeyboardLayoutGuide { - return obj - } - let new = KeyboardLayoutGuide() - addLayoutGuide(new) - new.setUp() - objc_setAssociatedObject(self, &AssociatedKeys.keyboardLayoutGuide, new as Any, .OBJC_ASSOCIATION_RETAIN_NONATOMIC) - return new + if let obj = objc_getAssociatedObject(self, &AssociatedKeys.keyboardLayoutGuide) as? KeyboardLayoutGuide { + return obj } + let guide = KeyboardLayoutGuide() + addLayoutGuide(guide) + guide.setup() + objc_setAssociatedObject(self, &AssociatedKeys.keyboardLayoutGuide, guide as Any, .OBJC_ASSOCIATION_RETAIN_NONATOMIC) + return guide } } final class KeyboardLayoutGuide: UILayoutGuide { - final class Keyboard { + private final class Keyboard { static let shared = Keyboard() var currentHeight: CGFloat = 0 } + + + // MARK: - Init required init?(coder aDecoder: NSCoder) { fatalError("init(coder:) has not been implemented") @@ -51,14 +52,24 @@ final class KeyboardLayoutGuide: UILayoutGuide { name: .UIKeyboardWillChangeFrame, object: nil) } + + deinit { + NotificationCenter.default.removeObserver(self) + } + + + // MARK: - Setup - fileprivate func setUp() { - guard let view = owningView else { return } + fileprivate func setup() { + guard let view = owningView else { + return + } NSLayoutConstraint.activate([ heightAnchor.constraint(equalToConstant: Keyboard.shared.currentHeight), leftAnchor.constraint(equalTo: view.leftAnchor), rightAnchor.constraint(equalTo: view.rightAnchor), - ]) + ] + ) let viewBottomAnchor: NSLayoutYAxisAnchor if #available(iOS 11.0, *) { viewBottomAnchor = view.safeAreaLayoutGuide.bottomAnchor @@ -68,35 +79,45 @@ final class KeyboardLayoutGuide: UILayoutGuide { bottomAnchor.constraint(equalTo: viewBottomAnchor).isActive = true } - @objc - private func keyboardWillChangeFrame(_ note: Notification) { - if var height = note.keyboardHeight { - if #available(iOS 11.0, *), height > 0 { - height -= (owningView?.safeAreaInsets.bottom)! - } - heightConstraint?.constant = height - animate(note) - Keyboard.shared.currentHeight = height + + // MARK: - Notifications + + @objc private func keyboardWillChangeFrame(_ notification: Notification) { + guard var height = notification.keyboardHeight else { + return + } + if #available(iOS 11.0, *), height > 0, let owningView = owningView { + height -= owningView.safeAreaInsets.bottom + } + if let constraint = heightConstraint, case let oldHeight = constraint.constant, oldHeight != height { + animate(notification) { constraint.constant = height } } + Keyboard.shared.currentHeight = height } - private func animate(_ note: Notification) { - if self.owningView!.isVisible() { - self.owningView?.layoutIfNeeded() + private func animate(_ notification: Notification, layout: () -> Void) { + guard let owningView = owningView else { + return + } + if owningView.isVisible() { + UIView.performWithoutAnimation { + owningView.layoutIfNeeded() + } + layout() + owningView.layoutIfNeeded() } else { + layout() UIView.performWithoutAnimation { - self.owningView?.layoutIfNeeded() + owningView.layoutIfNeeded() } } } - - deinit { - NotificationCenter.default.removeObserver(self) - } } + // MARK: - Helpers -extension UILayoutGuide { + +private extension UILayoutGuide { var heightConstraint: NSLayoutConstraint? { guard let target = owningView else { return nil } for c in target.constraints { @@ -108,7 +129,7 @@ extension UILayoutGuide { } } -extension Notification { +private extension Notification { var keyboardHeight: CGFloat? { guard let v = userInfo?[UIKeyboardFrameEndUserInfoKey] as? NSValue else { return nil } @@ -118,11 +139,15 @@ extension Notification { } } -extension UIView { +private extension UIView { func isVisible() -> Bool { func isVisible(inView: UIView?) -> Bool { - guard let inView = inView else { return true } - let viewFrame = inView.convert(self.bounds, from: self) + guard let inView = inView else { + return true + } + let frame = self.layer.presentation()?.frame ?? self.frame + let viewFrame = inView.convert(frame, from: self.superview) + if viewFrame.intersects(inView.bounds) { return isVisible(inView: inView.superview) } @@ -132,4 +157,3 @@ extension UIView { return isVisible(inView: self.superview) } } - diff --git a/Nynja/Library/UI/Lists/TableView/Cells/ChatListMessageCell/Cell/ChatListMessageTableViewCell.swift b/Nynja/Library/UI/Lists/TableView/Cells/ChatListMessageCell/Cell/ChatListMessageTableViewCell.swift index 444506298..9c2f9695a 100644 --- a/Nynja/Library/UI/Lists/TableView/Cells/ChatListMessageCell/Cell/ChatListMessageTableViewCell.swift +++ b/Nynja/Library/UI/Lists/TableView/Cells/ChatListMessageCell/Cell/ChatListMessageTableViewCell.swift @@ -15,12 +15,11 @@ final class ChatListMessageTableViewCell: UITableViewCell { private(set) lazy var avatarImageView: UIImageView = { let size = Constraints.avatarImageView.size.adjustedByWidth - let borderColor = Constants.colors.black.getColor() let imageView = UIImageView() imageView.contentMode = .scaleAspectFill - imageView.roundCornersImage(borderWidth: 2, cornerRadius: size / 2, borderColor: borderColor) - + applyCorners(to: imageView, radius: size / 2) + contentView.addSubview(imageView) imageView.snp.makeConstraints { maker in maker.width.height.equalTo(size) @@ -109,12 +108,25 @@ final class ChatListMessageTableViewCell: UITableViewCell { // MARK: - Life Cycle + override func layoutSubviews() { + super.layoutSubviews() + applyCorners(to: avatarImageView, radius: Constraints.avatarImageView.cornerRadius.adjustedByWidth) + } + override func prepareForReuse() { super.prepareForReuse() avatarImageView.image = nil messageContentView.reset() messageAccessoryView.reset() } + + + // MARK: - Layout + + private func applyCorners(to imageView: UIImageView, radius: CGFloat) { + let borderColor = Constants.colors.black.getColor() + imageView.roundCornersImage(borderWidth: 2, cornerRadius: radius, borderColor: borderColor) + } } // MARK: - Layout @@ -127,6 +139,7 @@ extension ChatListMessageTableViewCell { enum avatarImageView { static let size: CGFloat = 48.0 + static let cornerRadius: CGFloat = size / 2 static let verticalInset: CGFloat = 8.0 static let leftInset: CGFloat = 16.0 diff --git a/Nynja/Modules/Auth/View/LoginView.swift b/Nynja/Library/UI/LoginView/LoginView.swift similarity index 96% rename from Nynja/Modules/Auth/View/LoginView.swift rename to Nynja/Library/UI/LoginView/LoginView.swift index dbbd05b57..d76b50442 100644 --- a/Nynja/Modules/Auth/View/LoginView.swift +++ b/Nynja/Library/UI/LoginView/LoginView.swift @@ -34,7 +34,6 @@ extension LoginViewModifiable { class LoginView: UIView, UserSettingsRespondable { weak var textViewDelegate: UITextViewDelegate? - weak var countryFieldDelegate: CountryFieldDelegate? weak var phoneFieldDelegate: PhoneFieldDelegate? weak var codeFieldDelegate: CodeFieldDelegate? @@ -65,10 +64,11 @@ class LoginView: UIView, UserSettingsRespondable { let height = Constraints.countryField.height.adjustedByHeight let cf = CountryField(frame:CGRect(x: 0, y: 0, width: height * 6.54, height: height)) - cf.countryDelegate = self.countryFieldDelegate - cf.iconTapped = { view in - self.iconTapped(view: view) + + cf.iconTapped = { [weak self] view in + self?.iconTapped(view: view) } + cf.input.font = self.inputFont cf.input.snp.remakeConstraints { (make) in make.right.bottom.top.equalTo(cf.content) @@ -117,8 +117,8 @@ class LoginView: UIView, UserSettingsRespondable { let height = Constraints.countryField.height.adjustedByHeight let cf = CodeField(frame:CGRect(x: 0, y: 0, width: height * 6.54, height: height)) cf.delegate = self.codeFieldDelegate - cf.iconTapped = { view in - self.iconTapped(view: view) + cf.iconTapped = { [weak self] view in + self?.iconTapped(view: view) } self.addSubview(cf) let topPading = UIScreen.main.bounds.height * 0.044 @@ -199,7 +199,6 @@ class LoginView: UIView, UserSettingsRespondable { if code.count == 3 { code = "" } - } diff --git a/Nynja/Modules/Auth/View/LoginViewLayout.swift b/Nynja/Library/UI/LoginView/LoginViewLayout.swift similarity index 100% rename from Nynja/Modules/Auth/View/LoginViewLayout.swift rename to Nynja/Library/UI/LoginView/LoginViewLayout.swift diff --git a/Nynja/Library/UI/ReturnToCallContentView.swift b/Nynja/Library/UI/ReturnToCallContentView.swift index 1c481d68c..2531dc4a0 100644 --- a/Nynja/Library/UI/ReturnToCallContentView.swift +++ b/Nynja/Library/UI/ReturnToCallContentView.swift @@ -77,7 +77,6 @@ class ReturnToCallContentView: UIView { lazy var time: UILabel = { let lbl = UILabel() - lbl.text = "00:00" lbl.textAlignment = .right lbl.font = UIFont(name: Constants.fonts.regular, size: 12) lbl.adjustsFontSizeToFitWidth = true @@ -100,6 +99,8 @@ class ReturnToCallContentView: UIView { img.isUserInteractionEnabled = true img.contentMode = .scaleAspectFill img.backgroundColor = .clear + img.layer.cornerRadius = CGFloat(Constraints.imageSize/2) + img.clipsToBounds = true self.addSubview(img) img.snp.makeConstraints({ (make) in @@ -110,74 +111,58 @@ class ReturnToCallContentView: UIView { return img }() - func updateImageConstraintWithSize(imgSize: Float) { - - contactImage.snp.updateConstraints { (make) in - make.width.height.equalTo(imgSize) - } - - updateConstraints() - } - func setup(call: NYNCall) { self.nynCall = call - //self.startTimer() self.backgroundColor = Constants.colors.greenForReturnToCallColor.getColor() - img.isHidden = false hing.isHidden = false - time.isHidden = true time.text = "" + setupNameAndImage(text: nil, imageUrl: nil) - if call.isConference() { - contactImage.isHidden = true - contactImage.image = nil - if let room = RoomDAO.findRoom(by: call.externalInfo), let rn = room.name { - labelName.isHidden = false - labelName.text = rn - } else { - labelName.isHidden = true - labelName.text = "" - } - updateImageConstraintWithSize(imgSize: 0) + if call.isConference(), let room = RoomDAO.findRoom(by: call.externalInfo), let rn = room.name { + setupNameAndImage(text: rn, imageUrl: room.avatarUrl) } else { - if let ctc = ContactDAO.findContactBy(phoneId: call.callee) { - labelName.isHidden = false - contactImage.isHidden = false - contactImage.setImage(url: ctc.avatarUrl, placeHolder: UIImage(named: "ava_placeholder")) - updateImageConstraintWithSize(imgSize: Float(Constraints.imageSize)) - contactImage.layer.cornerRadius = CGFloat(Constraints.imageSize/2) - contactImage.clipsToBounds = true - labelName.text = ctc.fullName ?? "" - } else { - labelName.isHidden = true - contactImage.isHidden = true - contactImage.image = nil - updateImageConstraintWithSize(imgSize: 0) - labelName.text = "" + let contactId = call.isOutgoing() ? call.callee : call.caller + if let ctc = ContactDAO.findContactBy(phoneId: contactId) { + setupNameAndImage(text: ctc.fullName, imageUrl: ctc.avatarUrl) } } + + self.startTimer() } - //TODO: ASK ANGEL + private func setupNameAndImage(text: String?, imageUrl: URL?) { + labelName.text = text + labelName.isHidden = (text == nil) + if let url = imageUrl { + contactImage.setImage(url: url, placeHolder: UIImage(named: "ava_placeholder")) + } else { contactImage.image = UIImage(named: "ava_placeholder") } + } - // func startTimer() { - // timer = Timer.scheduledTimer(timeInterval: 1, target: self, selector: #selector(runTimedCode), userInfo: nil, repeats: true) - // } - // - // @objc func runTimedCode() { - // if let call = self.nynCall { - // let durationInt = Int(call.duration()) - // let minutes = durationInt / 60 - // let seconds = durationInt % 60 - // - // if let status = call.callStatus { - // if status == .ongoing { - // self.time.text = String.localizedStringWithFormat("%02u:%02u", minutes,seconds) - // } else { - // self.time.text = status.rawValue - // } - // } - // } - // } + func tearDown() + { + self.killTimer() + self.nynCall = nil + self.time.text = "" + self.time.isHidden = true + } + + private func startTimer() { + self.killTimer() + timer = Timer.scheduledTimer(timeInterval: 1, target: self, selector: #selector(runTimedCode), userInfo: nil, repeats: true) + } + + private func killTimer() { + if timer?.isValid ?? false { + timer?.invalidate() + } + timer = nil + } + + @objc func runTimedCode() { + guard let call = self.nynCall else { return } + if let refTime = call.isConference() ? call.startTime : call.acceptTime { + self.time.text = refTime.callFormattedTime() + } + } } diff --git a/Nynja/Library/UI/SwipeBackHelper/CustomPanGesture.swift b/Nynja/Library/UI/SwipeBackHelper/CustomPanGesture.swift deleted file mode 100644 index 3b01ca2e9..000000000 --- a/Nynja/Library/UI/SwipeBackHelper/CustomPanGesture.swift +++ /dev/null @@ -1,22 +0,0 @@ -// -// CustomPanGesture.swift -// Nynja -// -// Created by Roma Chopovenko on 1/29/18. -// Copyright © 2018 TecSynt Solutions. All rights reserved. -// - -import UIKit - -///Regular UIPanGestureRecognizer extended with targetAction closure -class CustomPanGesture: UIPanGestureRecognizer { - @objc var targetAction: (() -> Void)? { - didSet { - self.addTarget(self, action: #selector(self.targetSelector)) - } - } - - @objc func targetSelector() { - self.targetAction?() - } -} diff --git a/Nynja/Library/UI/SwipeBackHelper/SwipeBackHelper.swift b/Nynja/Library/UI/SwipeBackHelper/SwipeBackHelper.swift index 0861a8fab..49ade5317 100644 --- a/Nynja/Library/UI/SwipeBackHelper/SwipeBackHelper.swift +++ b/Nynja/Library/UI/SwipeBackHelper/SwipeBackHelper.swift @@ -10,10 +10,12 @@ import UIKit typealias SwipeBackCompletion = ()->() -class SwipeBackHelper: NSObject { +final class SwipeBackHelper: NSObject { - var percentDrivenInteractiveTransition: UIPercentDrivenInteractiveTransition? - var panGestureRecognizer: CustomPanGesture! + private var percentDrivenInteractiveTransition: UIPercentDrivenInteractiveTransition? + private var panGestureRecognizer: UIPanGestureRecognizer! + + var isSwipeActive: Bool = false weak var vc: UIViewController? var gestureCompletion: SwipeBackCompletion? @@ -21,7 +23,9 @@ class SwipeBackHelper: NSObject { fileprivate weak var mainDelegate: UINavigationControllerDelegate? private var navigationController: UINavigationController? + // MARK: - Init + init(with vc: UIViewController, gestureCompletion: SwipeBackCompletion? = nil) { self.vc = vc if let completion = gestureCompletion { @@ -29,29 +33,26 @@ class SwipeBackHelper: NSObject { } } + // MARK: - Gesture + func addGesture() { - guard let controllersCount: Int = self.vc?.navigationController?.viewControllers.count, controllersCount > 1 else { + guard let controllersCount = self.vc?.navigationController?.viewControllers.count, controllersCount > 1 else { return } + panGestureRecognizer = UIPanGestureRecognizer(target: self, action: #selector(handlePanGesture(_:))) - let backPanGesture: CustomPanGesture = CustomPanGesture() - - backPanGesture.targetAction = { [weak self] in - self?.handlePanGesture(backPanGesture) - } - - panGestureRecognizer = backPanGesture self.vc?.view.addGestureRecognizer(panGestureRecognizer) } - func handlePanGesture(_ panGesture: UIPanGestureRecognizer) { + @objc private func handlePanGesture(_ panGesture: UIPanGestureRecognizer) { guard let vc = self.vc else { return } let percent = max(panGesture.translation(in: vc.view).x, 0) / vc.view.frame.width switch panGesture.state { case .began: + isSwipeActive = true if let completion = self.gestureCompletion { completion() } @@ -71,19 +72,20 @@ class SwipeBackHelper: NSObject { percentDrivenInteractiveTransition?.cancel() rollback() } + isSwipeActive = false case .cancelled, .failed: percentDrivenInteractiveTransition?.cancel() rollback() + isSwipeActive = false default: + isSwipeActive = false break } - -// if let completion = self.gestureCompletion { -// completion() -// } } + // MARK: - Helpers + private func rollback() { if let nav = navigationController, let vc = vc { mainDelegate?.navigationController?(nav, didShow: vc, animated: true) @@ -92,6 +94,8 @@ class SwipeBackHelper: NSObject { } } +// MARK: - UINavigationControllerDelegate + extension SwipeBackHelper: UINavigationControllerDelegate { func navigationController(_ navigationController: UINavigationController, animationControllerFor operation: UINavigationControllerOperation, from fromVC: UIViewController, to toVC: UIViewController) -> UIViewControllerAnimatedTransitioning? { diff --git a/Nynja/Library/UI/TextInput/InputBar/InputBar.swift b/Nynja/Library/UI/TextInput/InputBar/InputBar.swift index 464964320..92ddb6b02 100644 --- a/Nynja/Library/UI/TextInput/InputBar/InputBar.swift +++ b/Nynja/Library/UI/TextInput/InputBar/InputBar.swift @@ -8,20 +8,18 @@ import Foundation import SnapKit -final class InputBar: UIView { - typealias InputContentView = UIView & InputContentProtocol - - typealias SendHandler = (InputContent) -> Void - - typealias NewSendHandler = SendHandler - typealias EditSendHandler = SendHandler - typealias ScheduleSendHandler = SendHandler - typealias SendTypingHandler = (TypingModelType) -> Void +protocol InputBarDelegate: class { + func didPlayTapped(_ model: AudioPlayable) + func didPauseTapped(_ model: AudioPlayable) + func didChangeProgress(_ model: AudioPlayable, progress: Double) + func isCanChangeProgress(_ model: AudioPlayable) -> Bool - typealias ChangeHandler = (UITextView) -> Void - typealias TextRangeReplaceHandler = (UITextView, NSRange, _ replacementText: String) -> Bool + func tryToStartRecording() +} + +final class InputBar: UIView { - typealias ChangesHeightHandler = (CGFloat) -> Void + weak var delegate: (InputBarDelegate & InputTextStorageDelegate)? var willSendHandler: NewSendHandler? var newSendHandler: NewSendHandler? @@ -348,7 +346,7 @@ final class InputBar: UIView { private func makeRecordDisplayContent(with url: URL) -> InputContentView { let content = RecordDisplayInputContent() - + content.delegate = self content.setURLToWaveForm(url: url) content.didRemovedRecordHandler = { [weak self] in self?.buttonType = .voice @@ -596,6 +594,7 @@ final class InputBar: UIView { @objc private func microphoneLongPress(_ sender: UILongPressGestureRecognizer) { switch sender.state { case .began: + delegate?.tryToStartRecording() if permissionManager.isMicrophoneGranted { contentType = .recording } else { @@ -758,6 +757,10 @@ extension InputBar: ALTextInputBarDelegate { containerHeightConstraint = make.height.equalTo(height).constraint } } + + func inputTextStorage(_ textStorage: InputTextStorage, modifiedAttributesFor proposedAttributes: TextAttributes?, range: NSRange) -> TextAttributes? { + return delegate?.inputTextStorage(textStorage, modifiedAttributesFor: proposedAttributes, range: range) ?? proposedAttributes + } } @@ -787,8 +790,48 @@ extension InputBar { } } +// MARK: - RecordDisplayInputContentDelegate + +extension InputBar: RecordDisplayInputContentDelegate { + func didPlayTapped(_ model: AudioPlayable) { + delegate?.didPlayTapped(model) + } + + func didPauseTapped(_ model: AudioPlayable) { + delegate?.didPauseTapped(model) + } + + func didChangeProgress(_ model: AudioPlayable, progress: Double) { + delegate?.didChangeProgress(model, progress: progress) + } + + func isCanChangeProgress(_ model: AudioPlayable) -> Bool { + return delegate?.isCanChangeProgress(model) ?? false + } +} + + +// MARK: - Inner types + +extension InputBar { + typealias InputContentView = UIView & InputContentProtocol + + typealias SendHandler = (InputContent) -> Void + + typealias NewSendHandler = SendHandler + typealias EditSendHandler = SendHandler + typealias ScheduleSendHandler = SendHandler + typealias SendTypingHandler = (TypingModelType) -> Void + + typealias ChangeHandler = (UITextView) -> Void + typealias TextRangeReplaceHandler = (UITextView, NSRange, _ replacementText: String) -> Bool + + typealias ChangesHeightHandler = (CGFloat) -> Void +} + + +// MARK: - Testable -//MARK: - Testable extension InputBar: TestableViewProtocol { private enum Keys: String { case sendButton = "input_bar_send_button" diff --git a/Nynja/Library/UI/TextInput/InputBar/InputContent/RecordDisplayInputContent.swift b/Nynja/Library/UI/TextInput/InputBar/InputContent/RecordDisplayInputContent.swift index bc6b59a99..83a5f4605 100644 --- a/Nynja/Library/UI/TextInput/InputBar/InputContent/RecordDisplayInputContent.swift +++ b/Nynja/Library/UI/TextInput/InputBar/InputContent/RecordDisplayInputContent.swift @@ -7,6 +7,7 @@ // import UIKit +import AVFoundation enum RecordStatus: Int { case playing @@ -20,19 +21,31 @@ enum RecordStatus: Int { self = .playing } } + + init(status: PlayStatus) { + switch status { + case .play: + self = .playing + default: + self = .stop + } + } +} + +protocol RecordDisplayInputContentDelegate: class { + func didPlayTapped(_ model: AudioPlayable) + func didPauseTapped(_ model: AudioPlayable) + func didChangeProgress(_ model: AudioPlayable, progress: Double) + func isCanChangeProgress(_ model: AudioPlayable) -> Bool } final class RecordDisplayInputContent: UIView, InputContentProtocol { - var didRemovedRecordHandler: (() -> Void)? + weak var delegate: RecordDisplayInputContentDelegate? - var audioUrl: URL? { - get { - return audioPlayer?.url - } - } + var didRemovedRecordHandler: (() -> Void)? - private var audioPlayer: AudioPlayer? + var audioModel: AudioPlayable? private var status: RecordStatus = .stop { didSet { @@ -45,77 +58,13 @@ final class RecordDisplayInputContent: UIView, InputContentProtocol { } } - // MARK: - Subviews - - private lazy var deleteButton: UIButton = { - let btn = UIButton() - self.addSubview(btn) - btn.isMultipleTouchEnabled = false - btn.setImage(#imageLiteral(resourceName: "ic_delete"), for: .normal) - btn.addTarget(self, action: #selector(deleteButtonTapped(_:)), for: .touchUpInside) - - btn.snp.makeConstraints({ (make) in - make.left.equalToSuperview().offset(Constraints.DeleteButton.left) - make.right.equalTo(playButton.snp.left).offset(-Constraints.PlayButton.left) - make.size.equalTo(Constraints.DeleteButton.size) - make.centerY.equalTo(timeLabel.snp.centerY) - }) - - return btn - }() - - private lazy var playButton: UIButton = { - let btn = UIButton() - self.addSubview(btn) - - btn.setImage(#imageLiteral(resourceName: "ic_play"), for: .normal) - btn.addTarget(self, action: #selector(playButtonTapped(_:)), for: .touchUpInside) - - btn.snp.makeConstraints({ (make) in - make.right.equalTo(timeLabel.snp.left).offset(-Constraints.TimeLabel.left) - make.size.equalTo(Constraints.PlayButton.size) - make.centerY.equalTo(timeLabel.snp.centerY) - }) - - return btn - }() - - private lazy var timeLabel: UILabel = { - let lbl = UILabel() - lbl.textAlignment = .center - lbl.font = UIFont(name: Constants.fonts.medium, size: 12) - lbl.textColor = Constants.colors.white.getColor() - lbl.numberOfLines = 1 - lbl.adjustsFontSizeToFitWidth = true - lbl.baselineAdjustment = .alignCenters - lbl.lineBreakMode = .byClipping - lbl.text = "00:00" - - self.addSubview(lbl) - lbl.snp.makeConstraints({ (make) in - make.width.equalTo(Constraints.TimeLabel.width) - make.right.equalTo(waveform.snp.left).offset(-Constraints.Waveform.left) - make.centerY.equalTo(waveform.snp.centerY) - }) - - return lbl - }() - private lazy var waveform: DrawableAudioWaveform = { - var waveform = DrawableAudioWaveform() - waveform.mainColor = .white - waveform.delegate = self - self.addSubview(waveform) + // MARK: - Subviews - - waveform.snp.makeConstraints({ (make) in - make.top.equalToSuperview().offset(Constraints.Waveform.top) - make.height.equalTo(Constraints.Waveform.height) - make.right.equalTo(self.snp.right).offset(-Constraints.Waveform.right) - }) - - return waveform - }() + private lazy var deleteButton: UIButton = makeDeleteButton() + private lazy var playButton: UIButton = makePlayButton() + private lazy var timeLabel: UILabel = makeTimeLabel() + private lazy var waveform: DrawableAudioWaveform = makeWaveform() // MARK: - Accessibility @@ -156,63 +105,171 @@ final class RecordDisplayInputContent: UIView, InputContentProtocol { let numberOfPoints = Int(Constraints.Waveform.width/CGFloat(DrawableAudioWaveform.Default.Dimensions.maxPikeWidth+DrawableAudioWaveform.Default.Dimensions.separatorWidth)) waveform.amplitudePoints = DrawableAudioWaveform.getNormalizeAmplitudePoints(DrawableAudioWaveform.getChannelDataAudioFile(audioFile), numberOfPoints: numberOfPoints) - audioPlayer = AudioPlayer(url: url, delegate: self) + let model = AudioPlayableModel(fileUrl: url, + audioDuration: AVURLAsset.duration(from: url), + audioCurrentTime: nil) + + model.audioStateHandler = { [weak self, unowned model] in + self?.setupUI(for: model) + } + + setupUI(for: model) + audioModel = model } // MARK: - Actions @objc private func playButtonTapped(_ sender: Any) { + guard let model = audioModel else { + return + } status.switch() - audioPlayer?.didTapPlay() + switch status { + case .playing: + delegate?.didPlayTapped(model) + case .stop: + delegate?.didPauseTapped(model) + } } @objc private func deleteButtonTapped(_ sender: Any) { - audioPlayer?.stopPlaying() - audioPlayer?.didTapRemove() didRemovedRecordHandler?() - } -} - -// MARK: - AudioPlayerDelegate -extension RecordDisplayInputContent: AudioPlayerDelegate { - - func setPlayTime(_ time: TimeInterval) { - timeLabel.text = time.string - } - - func setDurationTime(_ time: TimeInterval) { - timeLabel.text = time.string + guard let url = audioModel?.fileUrl else { + return + } + try? FileManager.default.removeItem(at: url) } - func setPlayProgress(percent: Double) { - waveform.updateProgress(percent) + private func setupUI(for model: AudioPlayable?) { + guard let model = model else { + return + } + + status = .init(status: model.playStatus) + + if let total = model.audioDuration, let current = model.audioCurrentTime { + let percent = current / total * 100.0 + waveform.updateProgress(percent) + } else { + waveform.updateProgress(0.0) + } + setupDurationLabel(with: model) } - func audioPlayerDidReachEnd() { - status = .stop - setPlayProgress(percent: 0) + private func setupDurationLabel(with model: AudioPlayable) { + guard let duration = model.audioDuration else { return } + timeLabel.text = AudioDurationFormatter.string(duration: duration, currentTime: model.audioCurrentTime) } } // MARK: - DrawableAudioWaveformDelegate + extension RecordDisplayInputContent: DrawableAudioWaveformDelegate { func audioWaveformDidBeginDragging(_ waveform: DrawableAudioWaveform) { + guard let model = audioModel else { + return + } status = .stop - audioPlayer?.prepareToPause() + delegate?.didPauseTapped(model) } func audioWaveform(_ waveform: DrawableAudioWaveform, didChangeProgress progress: Double) { - audioPlayer?.changedProgress(progress) + guard let model = audioModel else { + return + } + status = .stop + delegate?.didChangeProgress(model, progress: progress) } func isAudioWaveformDraggable(_ waveform: DrawableAudioWaveform) -> Bool { - return audioPlayer?.url != nil + guard let model = audioModel else { + return false + } + return delegate?.isCanChangeProgress(model) ?? false + } +} + + +// MARK: - Subview maker + +private extension RecordDisplayInputContent { + + func makeDeleteButton() -> UIButton { + let btn = UIButton() + self.addSubview(btn) + btn.isMultipleTouchEnabled = false + btn.setImage(#imageLiteral(resourceName: "ic_delete"), for: .normal) + btn.addTarget(self, action: #selector(deleteButtonTapped(_:)), for: .touchUpInside) + + btn.snp.makeConstraints({ (make) in + make.left.equalToSuperview().offset(Constraints.DeleteButton.left) + make.right.equalTo(playButton.snp.left).offset(-Constraints.PlayButton.left) + make.size.equalTo(Constraints.DeleteButton.size) + make.centerY.equalTo(timeLabel.snp.centerY) + }) + + return btn + } + + func makePlayButton() -> UIButton { + let btn = UIButton() + self.addSubview(btn) + + btn.setImage(#imageLiteral(resourceName: "ic_play"), for: .normal) + btn.addTarget(self, action: #selector(playButtonTapped(_:)), for: .touchUpInside) + + btn.snp.makeConstraints({ (make) in + make.right.equalTo(timeLabel.snp.left).offset(-Constraints.TimeLabel.left) + make.size.equalTo(Constraints.PlayButton.size) + make.centerY.equalTo(timeLabel.snp.centerY) + }) + + return btn + } + + func makeTimeLabel() -> UILabel { + let lbl = UILabel() + lbl.textAlignment = .center + lbl.font = UIFont(name: Constants.fonts.medium, size: 12) + lbl.textColor = Constants.colors.white.getColor() + lbl.numberOfLines = 1 + lbl.adjustsFontSizeToFitWidth = true + lbl.baselineAdjustment = .alignCenters + lbl.lineBreakMode = .byClipping + lbl.text = "00:00" + + self.addSubview(lbl) + lbl.snp.makeConstraints({ (make) in + make.width.equalTo(Constraints.TimeLabel.width) + make.right.equalTo(waveform.snp.left).offset(-Constraints.Waveform.left) + make.centerY.equalTo(waveform.snp.centerY) + }) + + return lbl + } + + func makeWaveform() -> DrawableAudioWaveform { + let waveform = DrawableAudioWaveform() + waveform.mainColor = .white + waveform.delegate = self + self.addSubview(waveform) + + + waveform.snp.makeConstraints({ (make) in + make.top.equalToSuperview().offset(Constraints.Waveform.top) + make.height.equalTo(Constraints.Waveform.height) + make.right.equalTo(self.snp.right).offset(-Constraints.Waveform.right) + }) + + return waveform } } + // MARK: - Layout + extension RecordDisplayInputContent { static let waveformPoints = 55 diff --git a/Nynja/Library/UI/TextInput/InputField/DrawableAudioWaveform.swift b/Nynja/Library/UI/TextInput/InputField/DrawableAudioWaveform.swift index 6f591e028..6d6c6bd32 100644 --- a/Nynja/Library/UI/TextInput/InputField/DrawableAudioWaveform.swift +++ b/Nynja/Library/UI/TextInput/InputField/DrawableAudioWaveform.swift @@ -33,19 +33,24 @@ final class DrawableAudioWaveform: UIView { private var mainWaveformLayer: CAShapeLayer? private var progressWaveformLayer: CAShapeLayer? - var mainColor: UIColor! - var progressColor: UIColor! + private var progress: Double = 0 - private var lineWidth: Float! - private var seporatorWidth: Float! + var mainColor: UIColor = Default.Color.mainColor + var progressColor: UIColor = Default.Color.progressColor var amplitudePoints: [CGFloat] = [] { didSet { drawWaveform(in: bounds) } } - - private var progress: Double = 0 + + override var bounds: CGRect { + didSet { + if oldValue != bounds { + drawWaveform(in: bounds) + } + } + } // MARK: - Init @@ -61,19 +66,15 @@ final class DrawableAudioWaveform: UIView { } - // MARK: - Life Cycle - - override func layoutSubviews() { - super.layoutSubviews() - drawWaveform(in: bounds) - } - - // MARK: - Actions func updateProgress(_ progress: Double) { self.progress = progress - drawProgressWaveform(in: bounds) + + CATransaction.begin() + CATransaction.setDisableActions(true) + progressWaveformLayer?.frame = progressWaveformRect(in: bounds, progress: progress) + CATransaction.commit() } @objc private func handlePanGesture(_ panGesture: UIPanGestureRecognizer) { @@ -84,9 +85,9 @@ final class DrawableAudioWaveform: UIView { case .began: delegate.audioWaveformDidBeginDragging(self) case .changed: - let offset = max(panGesture.location(in: self).x, 0) / frame.width - let percent = min(offset, 100) - let progress = Double(percent * 100) + let offset = max(panGesture.location(in: self).x, 0) / bounds.width + let percent = min(offset, 1.0) + let progress = Double(percent * 100.0) updateProgress(progress) delegate.audioWaveform(self, didChangeProgress: progress) case .ended, .cancelled, .failed: @@ -109,7 +110,7 @@ final class DrawableAudioWaveform: UIView { mainLayer.frame = rect mainLayer.path = wformPath.cgPath mainLayer.lineWidth = pikeWidth - mainLayer.strokeColor = (mainColor ?? Default.Color.mainColor).cgColor + mainLayer.strokeColor = mainColor.cgColor mainLayer.masksToBounds = true mainWaveformLayer?.removeFromSuperlayer() @@ -128,13 +129,12 @@ final class DrawableAudioWaveform: UIView { private func drawProgressWaveform(`in` rect: CGRect, pikeWidth : CGFloat) { let wformPath = waveformPath(in: rect, pikeWidth: pikeWidth) - let width = max(0, rect.width * CGFloat(progress / 100.0)) let progressLayer = CAShapeLayer() - progressLayer.frame = CGRect(x: 0, y: 0, width: width, height: rect.height) + progressLayer.frame = progressWaveformRect(in: rect, progress: progress) progressLayer.path = wformPath.cgPath progressLayer.lineWidth = pikeWidth - progressLayer.strokeColor = (progressColor ?? Default.Color.progressColor).cgColor + progressLayer.strokeColor = progressColor.cgColor progressLayer.masksToBounds = true progressWaveformLayer?.removeFromSuperlayer() @@ -162,6 +162,11 @@ final class DrawableAudioWaveform: UIView { return aPath } + + private func progressWaveformRect(in rect: CGRect, progress: Double) -> CGRect { + let width = max(0, rect.width * CGFloat(progress / 100.0)) + return CGRect(x: 0, y: 0, width: width, height: rect.height) + } } // MARK: - Layout @@ -175,6 +180,7 @@ extension DrawableAudioWaveform { enum Dimensions { static let maxPikeWidth = 3.0 + static let minPikeWidth = 3.0 static let separatorWidth = 1.0 } diff --git a/Nynja/Library/UI/TextInput/InputField/TextField/TextField.swift b/Nynja/Library/UI/TextInput/InputField/TextField/TextField.swift index 8173517c4..56b144fad 100644 --- a/Nynja/Library/UI/TextInput/InputField/TextField/TextField.swift +++ b/Nynja/Library/UI/TextInput/InputField/TextField/TextField.swift @@ -39,6 +39,9 @@ open class TextField: UITextField { } } + /// Holds action which will be executed after text resets + public var didResetHandler: (() -> Void)? + /// Set as `text` if `shouldResetAfterBackground` is true /// and app enters bakcground public var initialText: String? @@ -109,6 +112,7 @@ open class TextField: UITextField { @objc private func handleBackgroundNotification() { text = initialText selectedTextRange = textRange(from: beginningOfDocument, to: beginningOfDocument) + didResetHandler?() } } diff --git a/Nynja/Library/UI/TextInput/InputField/TextInputBar/ALTextInputBar.swift b/Nynja/Library/UI/TextInput/InputField/TextInputBar/ALTextInputBar.swift index 1a6b4e24d..208f2f9c7 100755 --- a/Nynja/Library/UI/TextInput/InputField/TextInputBar/ALTextInputBar.swift +++ b/Nynja/Library/UI/TextInput/InputField/TextInputBar/ALTextInputBar.swift @@ -8,32 +8,7 @@ import UIKit -let showInputViewNotification = Notification.Name("showInputViewNotification") -let hideInputViewNotification = Notification.Name("hideInputViewNotification") - - -protocol TextInputProtocol: class { - - func showInputView() - func hideInputView() -} - -extension TextInputProtocol { - - func showInputView() { - let notification = Notification(name: showInputViewNotification) - NotificationCenter.default.post(notification) - } - - func hideInputView() { - let notification = Notification(name: hideInputViewNotification) - NotificationCenter.default.post(notification) - } -} - -public typealias TextAttributes = [NSAttributedStringKey: Any] - -open class ALTextInputBar: UIView, ALTextViewDelegate { +open class ALTextInputBar: UIView, ALTextViewDelegate, InputTextStorageDelegate { public weak var delegate: ALTextInputBarDelegate? public weak var keyboardObserver: ALKeyboardObservingView? @@ -159,7 +134,7 @@ open class ALTextInputBar: UIView, ALTextViewDelegate { } /// The text view instance - public let textView: ALTextView = { + public private(set) lazy var textView: ALTextView = { let _textView = ALTextView() _textView.textContainerInset = UIEdgeInsets(top: 7, left: 8, bottom: 7, right: 8) @@ -175,6 +150,8 @@ open class ALTextInputBar: UIView, ALTextViewDelegate { return _textView }() + private let textStorage = InputTextStorage() + private var showRightButton = false private var showLeftButton = false @@ -207,7 +184,6 @@ open class ALTextInputBar: UIView, ALTextViewDelegate { } private func commonInit() { - textViewBorderView = createBorderView() addSubview(textViewBorderView) @@ -216,6 +192,8 @@ open class ALTextInputBar: UIView, ALTextViewDelegate { textViewBorderView.isHidden = !showTextViewBorder textView.textViewDelegate = self + textStorage.inputDelegate = self + backgroundColor = UIColor.groupTableViewBackground } @@ -315,7 +293,8 @@ open class ALTextInputBar: UIView, ALTextViewDelegate { } } - // MARK: - ALTextViewDelegate - + + // MARK: - ALTextViewDelegate public final func textViewHeightChanged(textView: ALTextView, newHeight: CGFloat) { @@ -334,10 +313,8 @@ open class ALTextInputBar: UIView, ALTextViewDelegate { ko.updateHeight(height: height) } - if let d = delegate, let m = d.inputBarDidChangeHeight { - m(height) - } - + delegate?.inputBarDidChangeHeight(height: height) + textView.frame.size.height = newHeight } @@ -352,33 +329,42 @@ open class ALTextInputBar: UIView, ALTextViewDelegate { updateViews(animated: true) } - - if let d = delegate, let m = d.textViewDidChange { - m(self.textView) - } + delegate?.textViewDidChange(textView: self.textView) } public func textViewShouldBeginEditing(_ textView: UITextView) -> Bool { - return delegate?.textViewShouldBeginEditing?(textView: self.textView) ?? true + return delegate?.textViewShouldBeginEditing(textView: self.textView) ?? true } public func textViewShouldEndEditing(_ textView: UITextView) -> Bool { - return delegate?.textViewShouldEndEditing?(textView: self.textView) ?? true + return delegate?.textViewShouldEndEditing(textView: self.textView) ?? true } public func textViewDidBeginEditing(_ textView: UITextView) { - delegate?.textViewDidBeginEditing?(textView: self.textView) + delegate?.textViewDidBeginEditing(textView: self.textView) } public func textViewDidEndEditing(_ textView: UITextView) { - delegate?.textViewDidEndEditing?(textView: self.textView) + delegate?.textViewDidEndEditing(textView: self.textView) } public func textViewDidChangeSelection(_ textView: UITextView) { - delegate?.textViewDidChangeSelection?(textView: self.textView) + delegate?.textViewDidChangeSelection(textView: self.textView) } public func textView(_ textView: UITextView, shouldChangeTextIn range: NSRange, replacementText text: String) -> Bool { - return delegate?.textView?(textView: self.textView, shouldChangeTextInRange: range, replacementText: text) ?? true + return delegate?.textView(textView: self.textView, shouldChangeTextInRange: range, replacementText: text) ?? true + } + + + // MARK: - InputTextStorageDelegate + + public func inputTextStorage(_ textStorage: InputTextStorage, + modifiedAttributesFor proposedAttributes: TextAttributes?, + range: NSRange) -> TextAttributes? { + guard let delegate = delegate else { + return proposedAttributes + } + return delegate.inputTextStorage(textStorage, modifiedAttributesFor: proposedAttributes, range: range) } } diff --git a/Nynja/Library/UI/TextInput/InputField/TextInputBar/ALTextInputBarDelegate.swift b/Nynja/Library/UI/TextInput/InputField/TextInputBar/ALTextInputBarDelegate.swift index 9dfb07051..695bc45f9 100755 --- a/Nynja/Library/UI/TextInput/InputField/TextInputBar/ALTextInputBarDelegate.swift +++ b/Nynja/Library/UI/TextInput/InputField/TextInputBar/ALTextInputBarDelegate.swift @@ -8,18 +8,48 @@ import UIKit -@objc -public protocol ALTextInputBarDelegate: NSObjectProtocol { - @objc optional func textViewShouldBeginEditing(textView: ALTextView) -> Bool - @objc optional func textViewShouldEndEditing(textView: ALTextView) -> Bool +public protocol ALTextInputBarDelegate: InputTextStorageDelegate { - @objc optional func textViewDidBeginEditing(textView: ALTextView) - @objc optional func textViewDidEndEditing(textView: ALTextView) + func textViewShouldBeginEditing(textView: ALTextView) -> Bool + func textViewShouldEndEditing(textView: ALTextView) -> Bool - @objc optional func textViewDidChange(textView: ALTextView) - @objc optional func textViewDidChangeSelection(textView: ALTextView) + func textViewDidBeginEditing(textView: ALTextView) + func textViewDidEndEditing(textView: ALTextView) - @objc optional func textView(textView: ALTextView, shouldChangeTextInRange range: NSRange, replacementText text: String) -> Bool + func textViewDidChange(textView: ALTextView) + func textViewDidChangeSelection(textView: ALTextView) - @objc optional func inputBarDidChangeHeight(height: CGFloat) + func textView(textView: ALTextView, shouldChangeTextInRange range: NSRange, replacementText text: String) -> Bool + + func inputBarDidChangeHeight(height: CGFloat) +} + +extension ALTextInputBarDelegate { + + func textViewShouldBeginEditing(textView: ALTextView) -> Bool { + return true + } + + func textViewShouldEndEditing(textView: ALTextView) -> Bool { + return true + } + + func textViewDidBeginEditing(textView: ALTextView) { + } + + func textViewDidEndEditing(textView: ALTextView) { + } + + func textViewDidChange(textView: ALTextView) { + } + + func textViewDidChangeSelection(textView: ALTextView) { + } + + func textView(textView: ALTextView, shouldChangeTextInRange range: NSRange, replacementText text: String) -> Bool { + return true + } + + func inputBarDidChangeHeight(height: CGFloat) { + } } diff --git a/Nynja/MQTTModels/MessageExtension+BERT.swift b/Nynja/MQTTModels/MessageExtension+BERT.swift index 970f0ec64..b8fe8b296 100644 --- a/Nynja/MQTTModels/MessageExtension+BERT.swift +++ b/Nynja/MQTTModels/MessageExtension+BERT.swift @@ -41,18 +41,23 @@ extension Message { _seenBy = BertList(fromElements: obj) } - var _feed: BertObject = BertNil() + let _feed: BertObject if let p2p = self.feed_id as? p2p { _feed = p2p.getBert() - } - if let muc = self.feed_id as? muc { + } else if let muc = self.feed_id as? muc { _feed = muc.getBert() + } else { + _feed = BertNil() } - var _created: BertObject = BertNil() - if let c = self.created as? Int64 { + + let _created: BertObject + if let c = self.created { _created = BertNumber(fromInt64: c) + } else { + _created = BertNil() } - var attachments: BertObject = BertNil() + + let attachments: BertObject if let descs = self.files, descs.count > 0 { var att = [BertObject]() for i in descs { @@ -60,25 +65,36 @@ extension Message { att.append(desc) } attachments = BertList(fromElements: att) + } else { + attachments = BertNil() } - var types: BertObject = BertNil() + let types: BertObject if !self.types.isEmpty { let arr = self.types.map { BertAtom(fromString: $0) } types = BertList(fromElements: arr) + } else { + types = BertNil() } - let link = Bert.getBin(self.link) + let link: BertObject + if let linkedId = self.linkedId { + link = Bert.getBin(linkedId) + } else { + link = BertNil() + } - var status: BertObject = BertNil() + let status: BertObject if let string = self.status as? String { status = BertAtom(fromString: string) } else if let atom = self.status as? StringAtom, let string = atom.string { status = BertAtom(fromString: string) + } else { + status = BertNil() } - var mentioned: BertObject - if let objects = (self.mentioned as? [Int64])?.compactMap({ Bert.getBin($0) as? BertNumber }) { + let mentioned: BertObject + if let objects = (self.mentioned as? [Int64])?.compactMap({ Bert.getBin($0) as? BertNumber }), !objects.isEmpty { mentioned = BertList(fromElements: objects) } else { mentioned = BertNil() diff --git a/Nynja/MediaDownloadManager.swift b/Nynja/MediaDownloadManager.swift index 3d2cf2106..36995b7a9 100644 --- a/Nynja/MediaDownloadManager.swift +++ b/Nynja/MediaDownloadManager.swift @@ -23,8 +23,7 @@ class MediaDownloadManager { public static func setupAppDataUsageSettingsIfNeeded() { MediaDownloadManager.setDefaultValues(with: .wifi, options: DataAndStorageOption.allValues.map { ($0, true) }) MediaDownloadManager.setDefaultValues(with: .mobileData, - options: [(DataAndStorageOption.voiceMessages, true), - (DataAndStorageOption.videoMessages, true)]) + options: [(DataAndStorageOption.voiceMessages, true)]) } public static func setNew(with usageMode: DataDownloadAndUsageMode, storageOptions: [SelectDataOption]) { @@ -82,9 +81,6 @@ struct MimeAdapter { case "audio" : return .voiceMessages case "video" : return .videos case "file" : return .files - case "videoMessages" : return .videoMessages - case "music" : return .music - case "gifs" : return .gifs default : return nil } } diff --git a/Nynja/MessageDAO.swift b/Nynja/MessageDAO.swift index 4c96dbd33..30fff554d 100644 --- a/Nynja/MessageDAO.swift +++ b/Nynja/MessageDAO.swift @@ -8,20 +8,26 @@ import GRDBCipher -class MessageDAO: MessageDAOProtocol { +final class MessageDAO: MessageDAOProtocol { + // MARK: - Save + static func saveMessages(_ messages: [Message]) throws { try dbManager.perform(action: .save, with: messages) } + // MARK: - Update + static func updateColumns(_ columns: Set, message: Message) { let columns = Set(columns.map { $0.title }) try? dbManager.perform(action: .updateColumns(columns), with: message) } + // MARK: - Fetch + // MARK: -- Message static func messageExists(serverId: MessageServerId) throws -> Bool { @@ -32,6 +38,13 @@ class MessageDAO: MessageDAOProtocol { ) } + /// Return message without descs. + static func rawMessage(serverId: MessageServerId) throws -> DBMessage? { + return dbManager.fetch { db in + return try DBMessage.rawMessage(db, serverId: serverId) + } + } + static func fetchMessage(by rowId: Int64) -> Message? { return fetchMessage { db in return try DBMessage.message(from: db, rowId: rowId) @@ -68,16 +81,17 @@ class MessageDAO: MessageDAOProtocol { } } - private static func fetchMessage(using closure: (Database) throws -> DBMessage?) -> Message? { - guard let message = dbManager.fetch({ db in - return try closure(db) - }) else { - return nil + static func fetchLastDeliveredMessage(ofType fetchType: FetchType, orderedBy orderColumn: MessageTable.Column) -> Message? { + return fetchMessage { db in + return try DBMessage.lastDeliveredMessage(db, ofType: fetchType, orderedBy: orderColumn) } - - return Message(message: message) } + private static func fetchMessage(using closure: (Database) throws -> DBMessage?) -> Message? { + return dbManager.fetch(closure).flatMap { Message(message: $0) } + } + + // MARK: -- Unsent static func fetchUnsentMessages(from: String) -> [Message] { let messages = dbManager.fetch { db in @@ -87,7 +101,9 @@ class MessageDAO: MessageDAOProtocol { return messages.map { Message(message: $0)} } + // MARK: -- Messages + static func fetchMessages(to: String) -> [Message] { guard let from = StorageService.sharedInstance.phoneId else { return [] } @@ -95,23 +111,11 @@ class MessageDAO: MessageDAOProtocol { return fetchMessages(.p2p(from: ids[0], to: ids[1])) } - - static func fetchMessages(to: String, serversIds: Set) -> [Message] { - guard let from = StorageService.sharedInstance.phoneId else { return [] } - - let ids = [from, to].sorted() - - return fetchMessages(.p2p(from: ids[0], to: ids[1]), serverIds: serversIds) - } - + static func fetchMessages(name: String) -> [Message] { return fetchMessages(.muc(name: name)) } - - static func fetchMessages(name: String, serversIds: Set) -> [Message] { - return fetchMessages(.muc(name: name), serverIds: serversIds) - } - + static func fetchMessages(_ type: FetchType) -> [Message] { let messages = dbManager.fetch { db in return try DBMessage.messages(db, fetchType: type) @@ -128,11 +132,16 @@ class MessageDAO: MessageDAOProtocol { return messages.map { Message(message: $0) } } + // MARK: - Cursor - static func dropFirst(for fetchType: FetchType, closure: (DBMessage) -> Void) throws { + static func dropFirst(for fetchType: FetchType, + orderedBy order: TableOrder, + orderColumn: MessageTable.Column, + closure: (DBMessage) -> Void) throws { + dbManager.fetch { db in - let cursor = try DBMessage.fetchCursor(db, fetchType: fetchType, orderedBy: .desc, orderColumn: .serverId).dropFirst() + let cursor = try DBMessage.fetchCursor(db, fetchType: fetchType, orderedBy: order, orderColumn: orderColumn).dropFirst() while let message = try cursor.next() { closure(message) @@ -159,11 +168,24 @@ class MessageDAO: MessageDAOProtocol { } static func trustIfNextMessageExists(before message: Message) throws { + guard let repliedMessage = message.repliedMessage, let repliedId = repliedMessage.id else { + try trustIfExists(before: message) + return + } + if message.next == repliedId { + message.isTrusted = true + } else { + try trustIfExists(before: message) + } + try trustIfExists(before: repliedMessage) + } + + private static func trustIfExists(before message: Message) throws { var isTrusted: Bool? if message.next == nil { isTrusted = true - } else if let serverId = message.next, try MessageDAO.messageExists(serverId: serverId) { + } else if let serverId = message.next, try messageExists(serverId: serverId) { isTrusted = true } @@ -173,6 +195,29 @@ class MessageDAO: MessageDAOProtocol { } + // MARK: - Local Status + + static func localStatusForRepliedMessage(_ repliedMessage: Message) throws -> Message.LocalStatus? { + func localStatus(for serverId: MessageServerId) throws -> Message.LocalStatus? { + if let localRepliedMessage = try MessageDAO.rawMessage(serverId: serverId) { + return localRepliedMessage.localStatus + } else { + return .replied + } + } + + guard let repliedId = repliedMessage.id else { + return nil + } + let isDeleted = self.isDeleted(repliedMessage) + if let repliedStatus = try localStatus(for: repliedId) { + return isDeleted ? repliedStatus.union(.deleted) : repliedStatus + } else { + return isDeleted ? .deleted : nil + } + } + + // MARK: - Clear History static func clearHistory(_ type: FetchType) throws { @@ -191,44 +236,58 @@ class MessageDAO: MessageDAOProtocol { try dbManager.perform(action: .delete, with: messages) } + // MARK: - Remove static func removeMessage(using message: Message) -> Bool { - guard let id = message.link, - let deletedMessage = fetchMessage(serverId: id) else { - return false - } - - let shouldMarkMessageAsDelete = self.shouldMarkMessageAsDelete(message) - if shouldMarkMessageAsDelete { - deletedMessage.status = StringAtom(string: "deleted") + guard let id = message.linkedId else { + return false } - deletedMessage.seenby = message.seenby + let shouldMarkMessageAsDelete = shouldMarkMessageAsDeleted(message) - do { - try dbManager.perform(action: .save, with: deletedMessage) - } catch { - return false + if let deletedMessage = fetchMessage(serverId: id) { + + if shouldMarkMessageAsDelete { + if let currentLocalStatus = deletedMessage.localStatus { + deletedMessage.localStatus = currentLocalStatus.union(.deleted) + } else { + deletedMessage.localStatus = .deleted + } + } + deletedMessage.seenby = message.seenby + + do { + try dbManager.perform(action: .save, with: deletedMessage) + } catch { + return false + } } return shouldMarkMessageAsDelete } //MARK: - Primary Key - static func fetchMessagePrimaryKey(with serverId: Int64) -> Int64? { + + static func fetchMessagePrimaryKey(with serverId: Int64) -> String? { let dbMessage = dbManager.fetch { db in return try DBMessage.message(db, serverId: serverId) } - return dbMessage?.id + return dbMessage?.localId } - - private static func shouldMarkMessageAsDelete(_ message: Message) -> Bool { + + private static func isDeleted(_ message: Message) -> Bool { + guard let seenBy = message.seenby, !seenBy.isEmpty else { + return false + } + return shouldMarkMessageAsDeleted(message) + } + + static func shouldMarkMessageAsDeleted(_ message: Message) -> Bool { let stringIds = message.seenby as? [String] ?? [] let intIds = message.seenby as? [Int64] ?? [] - guard !stringIds.contains("-1"), - !intIds.contains(-1) else { - return true + guard !stringIds.contains("-1"), !intIds.contains(-1) else { + return true } guard let phoneId = StorageService.sharedInstance.phoneId else { @@ -236,29 +295,18 @@ class MessageDAO: MessageDAOProtocol { return false } - if let _ = message.p2pFeed { + if message.p2pFeed != nil { return stringIds.contains(phoneId) + } else if let roomId = message.mucFeed?.name, - let _ = MemberDAO.findMemberBy(roomId: roomId, phoneId: phoneId) { - return true + let member = MemberDAO.findMemberBy(roomId: roomId, phoneId: phoneId), + let memberId = member.id { + return intIds.contains(memberId) } return false } - private static func findLocalId(serverId: MessageServerId) -> String? { - let sql = """ - select \(MessageTable.Column.localId) - from \(MessageTable.name) - where \(MessageTable.Column.serverId) = '\(serverId)' - """ - let request = SQLRequest(sql).asRequest(of: MessageLocalId.self) - - return dbManager.fetch { db in - return try request.fetchOne(db) - } - } - // MARK: - Helpers diff --git a/Nynja/MessageDAOProtocol.swift b/Nynja/MessageDAOProtocol.swift index b703ed2ad..2b29bb60e 100644 --- a/Nynja/MessageDAOProtocol.swift +++ b/Nynja/MessageDAOProtocol.swift @@ -21,43 +21,51 @@ protocol MessageDAOProtocol: DAOProtocol { // MARK: - Fetch // MARK: -- Message + + static func messageExists(serverId: MessageServerId) throws -> Bool + + static func rawMessage(serverId: MessageServerId) throws -> DBMessage? + static func fetchMessage(by rowId: Int64) -> Message? static func fetchMessage(primaryId: Int64) -> Message? - static func fetchMessage(serverId: Int64) -> Message? - static func fetchMessage(localId: String) -> Message? + static func fetchMessage(serverId: MessageServerId) -> Message? + static func fetchMessage(localId: MessageLocalId) -> Message? static func fetchLastMessage(of type: FetchType) -> Message? static func fetchLastOponnentMessage(of type: FetchType, phoneId: String) -> Message? + static func fetchLastDeliveredMessage(ofType fetchType: FetchType, orderedBy orderColumn: MessageTable.Column) -> Message? // MARK: -- Unsent static func fetchUnsentMessages(from: String) -> [Message] // MARK: -- Messages static func fetchMessages(to: String) -> [Message] - static func fetchMessages(to: String, serversIds: Set) -> [Message] - static func fetchMessages(name: String) -> [Message] - static func fetchMessages(name: String, serversIds: Set) -> [Message] static func fetchMessages(_ type: FetchType) -> [Message] - static func fetchMessages(_ type: FetchType, serverIds: Set) -> [Message] + static func fetchMessages(_ type: FetchType, serverIds: Set) -> [Message] // MARK: - Cursor - static func dropFirst(for fetchType: FetchType, closure: (DBMessage) -> Void) throws + static func dropFirst(for fetchType: FetchType, + orderedBy order: TableOrder, + orderColumn: MessageTable.Column, + closure: (DBMessage) -> Void) throws // MARK: - Trusted - static func trustMessages(before serverId: MessageServerId, in fetchType: FetchType) throws static func trustIfNextMessageExists(before message: Message) throws + // MARK: - Local Status + static func localStatusForRepliedMessage(_ repliedMessage: Message) throws -> Message.LocalStatus? + // MARK: - Clear History static func clearHistory(_ type: FetchType) throws static func clearMessages(_ type: FetchType, before messageServerId: Int64) throws // MARK: - Remove static func removeMessage(using message: Message) -> Bool - + static func shouldMarkMessageAsDeleted(_ message: Message) -> Bool //MARK: - Primary Key - static func fetchMessagePrimaryKey(with serverId: Int64) -> Int64? + static func fetchMessagePrimaryKey(with serverId: Int64) -> String? } diff --git a/Nynja/MigrationManager.swift b/Nynja/MigrationManager.swift index 643ebe939..573d67213 100644 --- a/Nynja/MigrationManager.swift +++ b/Nynja/MigrationManager.swift @@ -8,35 +8,12 @@ import GRDBCipher -enum Migration: Int, Describable { - case createConvertMessageTable - case createRecentStickersTable - case createStickerPackTable - case updateDescSticker - case updateContactReader - case removeVoxIdEmailContact - case removeVoxIdEmailMember - case serviceTableWalletMigration - case renameLinksToMessageLink - case createLinkForRoom - case renameChannelFeatureKeys - case updateMessageIdForeignKeyOnContactRoomTables - case primaryKeyMessageAction - case addTrustedColumnForMessage - case createStarActionTable +enum Migration: Describable { static var allTitles: [String] = { - var i = 0 - - var strings: [String] = [] - - while let migration = Migration(rawValue: i) { - i += 1 - strings.append(migration.title) - } - - return strings + return [] }() + } final class MigrationManager { @@ -48,7 +25,9 @@ final class MigrationManager { registerMigrations() } + // MARK: - Migrate + func migrate(to migration: Migration?, poolOrQueue: DatabaseWriter) throws { do { if let migration = migration { @@ -61,217 +40,22 @@ final class MigrationManager { } } + // MARK: - Private methods + private func registerMigrations() { - migrator.registerMigration(.createConvertMessageTable) { db in - try ConvertMessageTable.createIfNotExists(in: db) - } - - migrator.registerMigration(.createRecentStickersTable) { db in - try RecentStickerTable.createIfNotExists(in: db) - } - - migrator.registerMigration(.createStickerPackTable) { db in - try StickerPackTable.createIfNotExists(in: db) - } - - migrator.registerMigration(.updateDescSticker) { db in - try DescTable.rename(to: "OldDesc", in: db) - try DescTable.create(in: db) - - let rows = try Row.fetchCursor(db, "Select * from OldDesc") - while let row = try rows.next() { - guard let newDesc = DBDesc(serverId: row["serverId"], - mime: row["mime"], - payload: row["payload"], - size: row["size"], - filename: row["filename"], - info: row["info"], - targetId: row["targetId"], - targetType: row["targetType"]) else { break } - try? newDesc.saveAggregate(db) - } - try db.drop(table: "OldDesc") - } - - migrator.registerMigration(.updateContactReader) { db in - try ContactTable.rename(to: "OldContact", in: db) - try ContactTable.create(in: db) - - let rows = try Row.fetchCursor(db, "Select * from OldContact") - while let row = try rows.next() { - let newContact = DBContact(oldReaderRow: row) - try? newContact.saveAggregate(db) - } - try db.drop(table: "OldContact") - } - - migrator.registerMigration(.removeVoxIdEmailContact) { (db) in - let emailNameColumn = "email" - let voxIdNameColumn = "voxId" - - if try ContactTable.hasColumns([emailNameColumn, voxIdNameColumn], in: db) { - let contacts = try DBContact.fetchAll(db) - try contacts.forEach { - try $0.construct(db) - } - - try contacts.forEach { - try $0.deleteAggregate(db) - } - - try ContactTable.drop(in: db) - try ContactTable.create(in: db) - - try contacts.forEach { - try $0.saveAggregate(db) - } - } - } - - migrator.registerMigration(.removeVoxIdEmailMember) { (db) in - let emailNameColumn = "email" - let voxIdNameColumn = "voxId" - - if try MemberTable.hasColumns([emailNameColumn, voxIdNameColumn], in: db) { - let roomMembers = try DBRoomMember.fetchAll(db) - - let members = try DBMember.all(from: db) - - try members.forEach { - try $0.deleteAggregate(db) - } - - try MemberTable.drop(in: db) - try MemberTable.create(in: db) - - try members.forEach { - try $0.saveAggregate(db) - } - - try roomMembers.forEach { - try $0.save(db) - } - } - } - - migrator.registerMigration(.serviceTableWalletMigration) { db in - let targetIdColumn = ServiceTable.Column.targetId.title - let targetTypeColumn = ServiceTable.Column.targetType.title - - guard try !ServiceTable.hasColumns([targetIdColumn, targetTypeColumn], in: db) else { return } - - let serviceOldKey = "oldService" - - try ServiceTable.rename(to: serviceOldKey, in: db) - try ServiceTable.create(in: db) - - let rows = try Row.fetchCursor(db, "Select * from \(serviceOldKey)") - while let row = try rows.next() { - let newService = DBService(id: row["id"], - type: row["type"], - data: row["data"], - login: row["login"], - password: row["password"], - expiration: row["expiration"], - status: row["status"], - targetId: row["profileId"], - targetType: .profile) - - try? newService.save(db) - } - - try db.drop(table: serviceOldKey) - } - - migrator.registerMigration(.renameLinksToMessageLink) { db in - let oldName = "links" - let newName = MessageLinkTable.name - - let isOldExisted = try db.tableExists(oldName) - let isNewNotExisted = try !db.tableExists(newName) - - let shouldRenameTable = isOldExisted && isNewNotExisted - - if shouldRenameTable { - try db.rename(table: oldName, to: newName) - } - } - - migrator.registerMigration(.createLinkForRoom) { db in - try LinkTable.createIfNotExists(in: db) - } - - migrator.registerMigration(.renameChannelFeatureKeys) { db in - try self.replaceFeatureKey("SubscribersCount", newKey: .subscribersCount, db: db) - try self.replaceFeatureKey("AdminsCount", newKey: .adminsCount, db: db) - } - - migrator.registerMigration(.updateMessageIdForeignKeyOnContactRoomTables) { db in - let rooms = try DBRoom.fetchAll(db) - let contacts = try DBContact.fetchAll(db) - let roomMembers = try DBRoomMember.fetchAll(db) - let links = try DBLink.fetchAll(db) - - try LinkTable.drop(in: db) - try RoomTable.drop(in: db) - try ContactTable.drop(in: db) - - try RoomTable.create(in: db) - try ContactTable.create(in: db) - try LinkTable.create(in: db) - - try rooms.forEach { try $0.save(db) } - try contacts.forEach { try $0.save(db) } - try links.forEach { try $0.save(db) } - try roomMembers.forEach { try $0.save(db) } - } - - migrator.registerMigration(.primaryKeyMessageAction) { db in - let actions = try DBMessageAction.fetchAll(db) - try MessageActionTable.drop(in: db) - try MessageActionTable.create(in: db) - try actions.forEach { try $0.saveAggregate(db) } - } - - migrator.registerMigration(.addTrustedColumnForMessage) { db in - let column = MessageTable.Column.trusted.title - guard try !MessageTable.hasColumns([column], in: db) else { - return - } - try MessageTable.alter(in: db) { t in - t.add(column: column, .boolean) - } - - let sql = """ - UPDATE \(MessageTable.name) - SET \(column) = ? - """ - - try db.execute(sql, arguments: [true]) - } - - migrator.registerMigration(.createStarActionTable) { db in - try StarActionTable.createIfNotExists(in: db) - } + // NOTE: Migrations will be here. } - private func replaceFeatureKey(_ key: String, newKey: Room.FeatureKey, db: Database) throws { - let keyColumn = "[\(FeatureTable.Column.key.title)]" - let sqlString = """ - update \(FeatureTable.name) - set \(keyColumn) = '\(newKey.rawValue)' - where \(keyColumn) = '\(key)' - """ - - try db.execute(sqlString) - } } + // MARK: - DatabaseMigrator private extension DatabaseMigrator { + mutating func registerMigration(_ migration: Migration, migrate: @escaping (Database) throws -> Void) { registerMigration(migration.title, migrate: migrate) } + } diff --git a/Nynja/Modules/AddContactByUsername/AddContactByUsernameProtocols.swift b/Nynja/Modules/AddContactByUsername/AddContactByUsernameProtocols.swift index ee8ce74a0..b455f0e6a 100644 --- a/Nynja/Modules/AddContactByUsername/AddContactByUsernameProtocols.swift +++ b/Nynja/Modules/AddContactByUsername/AddContactByUsernameProtocols.swift @@ -40,6 +40,7 @@ protocol AddContactByUsernamePresenterProtocol: BasePresenterProtocol { */ func search(_ name:String) + func showAlertWith(title: String, message: String) } protocol AddContactByUsernameInteractorOutputProtocol: LoadingInteractive { diff --git a/Nynja/Modules/AddContactByUsername/Presenter/AddContactByUsernamePresenter.swift b/Nynja/Modules/AddContactByUsername/Presenter/AddContactByUsernamePresenter.swift index c7e1307d1..4cc36ee78 100644 --- a/Nynja/Modules/AddContactByUsername/Presenter/AddContactByUsernamePresenter.swift +++ b/Nynja/Modules/AddContactByUsername/Presenter/AddContactByUsernamePresenter.swift @@ -39,4 +39,8 @@ final class AddContactByUsernamePresenter: BasePresenter, AddContactByUsernamePr func hideHUD() { view.hideSpinner() } + + func showAlertWith(title: String, message: String) { + AlertManager.sharedInstance.showAlertOk(title: title, message: message) + } } diff --git a/Nynja/Modules/AddContactByUsername/View/AddContactByUsernameViewController.swift b/Nynja/Modules/AddContactByUsername/View/AddContactByUsernameViewController.swift index 413183ce3..3bc548aa6 100644 --- a/Nynja/Modules/AddContactByUsername/View/AddContactByUsernameViewController.swift +++ b/Nynja/Modules/AddContactByUsername/View/AddContactByUsernameViewController.swift @@ -79,8 +79,12 @@ final class AddContactByUsernameViewController: BaseVC, AddContactByUsernameView @objc private func searchTapped() { if valid() { - self.view.endEditing(true) - self.presenter.search(userNameField.input.text ?? "") + view.endEditing(true) + presenter.search(userNameField.input.text ?? "") + } else { + let title = Strings.incorrectUsername.localized + let message = Strings.mayContainEnglishLetters.localized + presenter.showAlertWith(title: title, message: message) } } diff --git a/Nynja/Modules/AddContactByUsername/WireFrame/AddContactByUsernameWireframe.swift b/Nynja/Modules/AddContactByUsername/WireFrame/AddContactByUsernameWireframe.swift index dc3f7fab6..c2fc88038 100644 --- a/Nynja/Modules/AddContactByUsername/WireFrame/AddContactByUsernameWireframe.swift +++ b/Nynja/Modules/AddContactByUsername/WireFrame/AddContactByUsernameWireframe.swift @@ -38,5 +38,4 @@ final class AddContactByUsernameWireFrame: AddContactByUsernameWireFrameProtocol func showMyProfile() { main?.showProfile() } - } diff --git a/Nynja/Modules/AddParticipants/AddParticipantsProtocols.swift b/Nynja/Modules/AddParticipants/AddParticipantsProtocols.swift index 9cdfd4b04..2fa7f664f 100644 --- a/Nynja/Modules/AddParticipants/AddParticipantsProtocols.swift +++ b/Nynja/Modules/AddParticipants/AddParticipantsProtocols.swift @@ -78,6 +78,7 @@ protocol AddParticipantsInteractorInputProtocol: BaseInteractorProtocol { func createRoom(name: String, avatar: UIImage?, members: [Member]) func getMySelf() -> Contact? func allocateConference() + func discardAllocatedConference() } protocol AddParticipantsInteractorOutputProtocol: class { diff --git a/Nynja/Modules/AddParticipants/Interactor/AddParticipantsInteractor.swift b/Nynja/Modules/AddParticipants/Interactor/AddParticipantsInteractor.swift index 7b222043c..30271cbc4 100644 --- a/Nynja/Modules/AddParticipants/Interactor/AddParticipantsInteractor.swift +++ b/Nynja/Modules/AddParticipants/Interactor/AddParticipantsInteractor.swift @@ -58,6 +58,15 @@ class AddParticipantsInteractor: BaseInteractor, AddParticipantsInteractorInputP subject: room?.name ?? "Unnamed", roomId: room?.id ?? localId) } + + func discardAllocatedConference() { + guard let confId = self.allocateId else {return} + + callService.endConference(requestId: UUID().uuidString, conferenceId: confId) + callService.removeConference(byId: confId) + + self.allocateId = nil + } override func loadData() { super.loadData() @@ -77,14 +86,22 @@ class AddParticipantsInteractor: BaseInteractor, AddParticipantsInteractorInputP func fetchParticipants(`for` mode: ParticipantsModeType) { var contacts = ContactDAO.fetchContacts(with: [.friend]) - if mode == .delete || mode == .admins || mode == .createGroupCall { + if mode == .delete || mode == .admins { if let members = members { contacts = members.contacts } else { contacts = [] } + } else if mode == .createGroupCall { + + if let members = members { + contacts = members.memberContacts + } else { + contacts = [] + } + } else if mode == .updateGroupCall { - if let roomContacts = self.room?.allMembers?.contacts { + if let roomContacts = self.room?.allMembers?.memberContacts { contacts = roomContacts } diff --git a/Nynja/Modules/AddParticipants/Presenter/AddParticipantsPresenter.swift b/Nynja/Modules/AddParticipants/Presenter/AddParticipantsPresenter.swift index 3e53f80dd..606214421 100644 --- a/Nynja/Modules/AddParticipants/Presenter/AddParticipantsPresenter.swift +++ b/Nynja/Modules/AddParticipants/Presenter/AddParticipantsPresenter.swift @@ -11,7 +11,15 @@ class AddParticipantsPresenter: BasePresenter, AddParticipantsPresenterProtocol, var room: Room? var roomToCreate = Room() var contacts: [Contact]! - + + private var shouldDiscartAllocatedConference: Bool = true + + deinit { + if shouldDiscartAllocatedConference { + interactor.discardAllocatedConference() + } + } + override var itemsFactory: WCItemsFactory? { if participantsMode == .create { return CreateGroupItemsFactory() @@ -117,19 +125,25 @@ class AddParticipantsPresenter: BasePresenter, AddParticipantsPresenterProtocol, wireFrame.hide(with: .cancel) } case .createGroupCall: + shouldDiscartAllocatedConference = false if contacts.isEmpty { + interactor.discardAllocatedConference() wireFrame.hide(with: .cancel) } else { wireFrame.hide(with: .createGroupCall(callId: interactor.allocateId, contacts: contacts, room: room)) } case .createConferenceCall: + shouldDiscartAllocatedConference = false if contacts.isEmpty { + interactor.discardAllocatedConference() wireFrame.hide(with: .cancel) } else { wireFrame.hide(with: .createConferenceCall(callId: interactor.allocateId, contacts: contacts, room:roomToCreate)) } case .updateGroupCall: + shouldDiscartAllocatedConference = false if contacts.isEmpty { + interactor.discardAllocatedConference() wireFrame.hide(with: .cancel) } else { wireFrame.hide(with: .updateGroupCall(contacts: contacts)) diff --git a/Nynja/Modules/AddParticipants/View/AddParticipantsViewController.swift b/Nynja/Modules/AddParticipants/View/AddParticipantsViewController.swift index c2c48ad07..50957ffcc 100644 --- a/Nynja/Modules/AddParticipants/View/AddParticipantsViewController.swift +++ b/Nynja/Modules/AddParticipants/View/AddParticipantsViewController.swift @@ -8,7 +8,7 @@ import UIKit -class AddParticipantsViewController: BaseVC, AddParticipantsViewProtocol { +class AddParticipantsViewController: BaseVC, AddParticipantsViewProtocol, KeyboardInteractive { var presenter: AddParticipantsPresenterProtocol! { didSet { @@ -111,6 +111,44 @@ class AddParticipantsViewController: BaseVC, AddParticipantsViewProtocol { return tblView }() + + private lazy var imgView: UIImageView = { + let imageView = UIImageView() + imageView.contentMode = .scaleAspectFit + imageView.image = UIImage.init(named: "ic_contacts_empty") + + let labelHeight:CGFloat = UIFont.systemFont(ofSize: 18).lineHeight + self.view.addSubview(imageView) + + imageView.snp.makeConstraints { make in + make.centerX.equalToSuperview() + make.centerY.equalToSuperview().offset((-labelHeight - Constraints.mainView.offset)/2) + + make.width.width.equalTo(Constraints.imageView.width.adjustedByWidth) + make.width.height.equalTo(Constraints.imageView.height.adjustedByWidth) + } + + return imageView + }() + + private lazy var label: UILabel = { + let label = UILabel() + + label.text = "add_participants_no_contacts_to_select".localized + label.adjustsFontSizeToFitWidth = true + label.font = UIFont.systemFont(ofSize: 18) + label.numberOfLines = 1 + label.textColor = Constants.colors.darkGray.getColor() + + self.view.addSubview(label) + + label.snp.makeConstraints { (make) in + make.top.equalTo(imgView.snp.bottom).offset(Constraints.mainView.offset) + make.centerX.equalToSuperview() + } + + return label + }() private lazy var controlContainerView: NynjaControlContainerView = { let containerView = NynjaControlContainerView(contentView: searchField) @@ -288,6 +326,25 @@ class AddParticipantsViewController: BaseVC, AddParticipantsViewProtocol { doneButton.isEnabled = (participantsDataSource.selectedParticipants.count > 0) } } + + private func updateNoParticipantViews() { + + let hasContacts:Bool = (participantsDataSource.sortedLetters.count > 0) + + imgView.isHidden = hasContacts + label.isHidden = hasContacts + + if hasContacts { + + self.view.sendSubview(toBack: imgView) + self.view.sendSubview(toBack: label) + + } else { + + self.view.bringSubview(toFront: imgView) + self.view.bringSubview(toFront: label) + } + } // MARK: - Actions @objc private func doneTapped(_ button: UIButton) { @@ -365,7 +422,7 @@ class AddParticipantsViewController: BaseVC, AddParticipantsViewProtocol { // MARK: - Keyboard - override func keyboardNotified(endFrame: CGRect) { + func keyboardNotified(endFrame: CGRect) { if endFrame.origin.y >= UIScreen.main.bounds.size.height { updateToHide(view: controlContainerView, offset: -bottomInset) } else { @@ -383,6 +440,9 @@ class AddParticipantsViewController: BaseVC, AddParticipantsViewProtocol { func setupParticipants(_ participants: GroupedParticipants) { participantsDataSource.groupedParticipants = participants + + updateNoParticipantViews() + var selectedParticipants = participants.flatMap({ (key, value) -> [Participant] in return value.filter({ (participant) -> Bool in return participant.isSelected diff --git a/Nynja/Modules/AddParticipants/View/AddPaticipantsViewControllerLayout.swift b/Nynja/Modules/AddParticipants/View/AddPaticipantsViewControllerLayout.swift index d452402ad..0c0c38daf 100644 --- a/Nynja/Modules/AddParticipants/View/AddPaticipantsViewControllerLayout.swift +++ b/Nynja/Modules/AddParticipants/View/AddPaticipantsViewControllerLayout.swift @@ -21,6 +21,11 @@ extension AddParticipantsViewController { static let topInset = 16.0 } + enum imageView { + static let width: CGFloat = 64.0 + static let height: CGFloat = 64.0 + } + enum separator { static let topInset = 8.0 } @@ -29,6 +34,11 @@ extension AddParticipantsViewController { static let bottomInset: CGFloat = 28.0 } + enum mainView { + static let offset: CGFloat = 10.0 + } + + enum doneButton { static let width: CGFloat = 82.0 static let height: CGFloat = 44.0 diff --git a/Nynja/Modules/Auth/AuthProtocols.swift b/Nynja/Modules/Auth/AuthProtocols.swift deleted file mode 100644 index 217494c6b..000000000 --- a/Nynja/Modules/Auth/AuthProtocols.swift +++ /dev/null @@ -1,102 +0,0 @@ -// -// AuthAuthProtocols.swift -// Nynja -// -// Created by Anton Makarov on 31/05/2017. -// Copyright © 2017 TecSynt Solutions. All rights reserved. -// - -import UIKit - -protocol LoginWheelContainerViewProtocol: class { - - func getWheelContainerDS() -> LoginWheelContainerDataSource - func getBorderView() -> BorderView - func getWheelContainer() -> WheelContainer - func selectCountry(_ country: CountryModel, at index: Int) -} - -protocol AuthWireFrameProtocol: class { - - func presentAuth(navigation: UINavigationController) - - /** - * Add here your methods for communication PRESENTER -> WIREFRAME - */ - func showTermOfUse() - func showMainScreen() - func showProfileScreen(contact: Contact) -} - -protocol LoginViewProtocol: class { - - var presenter: LoginPresenterProtocol! { get set } - - /** - * Add here your methods for communication PRESENTER -> VIEW - */ - func removeSpinner() -} - -protocol VerifyViewProtocol: class { - - var presenter: VerifyPresenterProtocol! { get set } - var phone: String! { get set } - func showCode(_ code: String) - /** - * Add here your methods for communication PRESENTER -> VIEW - */ - func showWrongCodeAlert() -} - -protocol VerifyPresenterProtocol: BasePresenterProtocol { - - var verifyView: VerifyViewProtocol! { get set } - func resendSMS() - func voiceCall() - func handleEnteredPassword(code: String) - func handleNextTapped() -} - -protocol UpdatePresenterProtocol: class { - var profileView: ProfileViewProtocol! { get set } -} - -protocol LoginPresenterProtocol: class { - - var view: LoginViewProtocol! { get set } - var interactor: AuthInteractorInputProtocol! { get set } - var wireFrame: AuthWireFrameProtocol! { get set } - - /** - * Add here your methods for communication VIEW -> PRESENTER - */ - func login(phone: String) - func termOfUse() -} - -protocol AuthInteractorOutputProtocol: class { - - /** - * Add here your methods for communication INTERACTOR -> PRESENTER - */ - func smsSent() - func logined() - func registered(contact: Contact) - func invalidCode() - func numberNotAllowed() -} - -protocol AuthInteractorInputProtocol: BaseInteractorProtocol { - - var presenter: AuthInteractorOutputProtocol! { get set } - func login(phone: String) - func checkPassword() - func resendSMS(phone: String) - func voiceCall() - func getSMSCode(result: ((String)->())?) - func saveEnteredPassword(code: String) - /** - * Add here your methods for communication PRESENTER -> INTERACTOR - */ -} diff --git a/Nynja/Modules/Auth/Login/Interactor/LoginInteractor.swift b/Nynja/Modules/Auth/Login/Interactor/LoginInteractor.swift new file mode 100644 index 000000000..c0e1a555b --- /dev/null +++ b/Nynja/Modules/Auth/Login/Interactor/LoginInteractor.swift @@ -0,0 +1,116 @@ +// +// LoginInteractor.swift +// Nynja +// +// Created by Anton Makarov on 31/05/2017. +// Copyright © 2017 TecSynt Solutions. All rights reserved. +// + +class LoginInteractor: BaseInteractor, LoginInteractorInputProtocol, IoHandlerDelegate, MQTTServiceDelegate, SetInjectable { + + weak var presenter: LoginInteractorOutputProtocol! + + private var mqttService: MQTTService! + + private var phone: String? + + private var isBadProtocolVersion: Bool = false + + private var connectionTimerHandler: TimerHandler? + + + // MARK: - Configure + + func configure() { + IoHandler.delegate = self + mqttService.addSubscriber(self) + mqttService.tryReconnect() + } + + deinit { + mqttService.removeSubscriber(self) + } + + + // MARK: - LoginInteractorInputProtocol + + func login(phone: String) { + guard !isBadProtocolVersion else { + presenter.receiveBadProtocolVersion() + return + } + + self.phone = phone + mqttService.registration(number: phone) + } + + + // MARK: - IoHandlerDelegate + + func smsSent() { + presenter.smsSent() + } + + func numberNotAllowed() { + presenter.numberNotAllowed() + } + + + // MARK: - MQTTServiceDelegate + + func mqttServiceDidReceiveWrongServerVersion() { + if !isBadProtocolVersion { + isBadProtocolVersion = true + presenter.receiveBadProtocolVersion() + } + } + + func mqttServiceDidConnect(_ mqttService: MQTTService) { + invalidateConnectionTimer() + + if let phone = phone { + login(phone: phone) + } + } + + func mqttServiceDidDisconnect(_ mqttService: MQTTService) { + launchConnectionTimer() + } + + + // MARK: - Connection Timer + + private func launchConnectionTimer() { + connectionTimerHandler = TimerHandler.init(interval: 60, repeats: false, action: { [weak self] _ in + guard let `self` = self else { + return + } + + self.phone = nil + self.presenter.handleServerConnectionFailure() + self.invalidateConnectionTimer() + }) + } + + private func invalidateConnectionTimer() { + connectionTimerHandler?.invalidate() + } + +} + + +// MARK: - SetInjectable + +extension LoginInteractor { + + struct Dependencies { + let presenter: LoginPresenter + let mqttService: MQTTService + } + + func inject(dependencies: LoginInteractor.Dependencies) { + presenter = dependencies.presenter + mqttService = dependencies.mqttService + } + +} diff --git a/Nynja/Modules/Auth/Interactor/Modelka.swift b/Nynja/Modules/Auth/Login/Interactor/Modelka.swift similarity index 100% rename from Nynja/Modules/Auth/Interactor/Modelka.swift rename to Nynja/Modules/Auth/Login/Interactor/Modelka.swift diff --git a/Nynja/Modules/Auth/Login/LoginProtocols.swift b/Nynja/Modules/Auth/Login/LoginProtocols.swift new file mode 100644 index 000000000..855591294 --- /dev/null +++ b/Nynja/Modules/Auth/Login/LoginProtocols.swift @@ -0,0 +1,81 @@ +// +// LoginProtocols.swift +// Nynja +// +// Created by Anton Makarov on 31/05/2017. +// Copyright © 2017 TecSynt Solutions. All rights reserved. +// + +import UIKit + +protocol LoginWheelContainerViewProtocol: class { + + func getWheelContainerDS() -> LoginWheelContainerDataSource + func getBorderView() -> BorderView + func getWheelContainer() -> WheelContainer + func selectCountry(_ country: CountryModel, at index: Int) +} + +protocol LoginWireFrameProtocol: class { + + func presentLogin(navigation: UINavigationController) + + /** + * Add here your methods for communication PRESENTER -> WIREFRAME + */ + func showTermOfUse() + func showVerifyNumber(with phone: String) +} + +protocol LoginViewProtocol: class { + + var presenter: LoginPresenterProtocol! { get set } + + /** + * Add here your methods for communication PRESENTER -> VIEW + */ + var isSpinnerShown: Bool { get } + func removeSpinner() +} + + +protocol LoginPresenterProtocol: class { + + var view: LoginViewProtocol! { get set } + var interactor: LoginInteractorInputProtocol! { get set } + var wireFrame: LoginWireFrameProtocol! { get set } + + /** + * Add here your methods for communication VIEW -> PRESENTER + */ + + func showed() + + func login(phone: String) + func termOfUse() +} + +protocol LoginInteractorOutputProtocol: class { + + /** + * Add here your methods for communication INTERACTOR -> PRESENTER + */ + + func smsSent() + func numberNotAllowed() + func handleServerConnectionFailure() + func receiveBadProtocolVersion() +} + +protocol LoginInteractorInputProtocol: BaseInteractorProtocol { + + var presenter: LoginInteractorOutputProtocol! { get set } + + /** + * Add here your methods for communication PRESENTER -> INTERACTOR + */ + + func configure() + + func login(phone: String) +} diff --git a/Nynja/Modules/Auth/Login/Presenter/LoginPresenter.swift b/Nynja/Modules/Auth/Login/Presenter/LoginPresenter.swift new file mode 100644 index 000000000..a0284dd3b --- /dev/null +++ b/Nynja/Modules/Auth/Login/Presenter/LoginPresenter.swift @@ -0,0 +1,103 @@ +// +// LoginPresenter.swift +// Nynja +// +// Created by Anton Makarov on 31/05/2017. +// Copyright © 2017 TecSynt Solutions. All rights reserved. +// + +import UIKit + +class LoginPresenter: BasePresenter, LoginPresenterProtocol, LoginInteractorOutputProtocol, SetInjectable { + + weak var view: LoginViewProtocol! + var wireFrame: LoginWireFrameProtocol! + var interactor: LoginInteractorInputProtocol! { + didSet { + _interactor = interactor + } + } + + private var phone: String? + + private var alertManager: AlertManager! + + + // MARK: - LoginPresenterProtocol + + func showed() { + interactor.configure() + } + + func login(phone: String) { + self.phone = phone + interactor.login(phone: phone) + } + + func termOfUse() { + wireFrame.showTermOfUse() + } + + + // MARK: - LoginInteractorOutputProtocol + + func smsSent() { + view.removeSpinner() + wireFrame.showVerifyNumber(with: phone ?? "") + } + + func numberNotAllowed() { + view.removeSpinner() + alertManager.showAlertOk(message: Strings.notAllowedToLogin.localized) + } + + func handleServerConnectionFailure() { + guard view.isSpinnerShown == true else { + return + } + + view.removeSpinner() + alertManager.showAlertOk(message: Strings.connectionFailed.localized) + } + + func receiveBadProtocolVersion() { + view.removeSpinner() + alertManager.showAlertOk(message: "wrongVersion".localized) + } + +} + + +// MARK: - Strings + +extension LoginPresenter { + + enum Strings: String { + case notAllowedToLogin = "You_are_not_allowed_to_login" + case connectionFailed = "connection_to_server_failed" + } + +} + + +// MARK: - SetInjectable + +extension LoginPresenter { + + struct Dependencies { + let view: LoginViewProtocol + let interactor: LoginInteractorInputProtocol + let wireFrame: LoginWireFrameProtocol + + let alertManager: AlertManager + } + + func inject(dependencies: LoginPresenter.Dependencies) { + view = dependencies.view + interactor = dependencies.interactor + wireFrame = dependencies.wireFrame + + alertManager = dependencies.alertManager + } + +} diff --git a/Nynja/Modules/Auth/View/ViewController/LoginViewController.swift b/Nynja/Modules/Auth/Login/View/LoginViewController.swift similarity index 93% rename from Nynja/Modules/Auth/View/ViewController/LoginViewController.swift rename to Nynja/Modules/Auth/Login/View/LoginViewController.swift index 3848dbfea..448b008da 100644 --- a/Nynja/Modules/Auth/View/ViewController/LoginViewController.swift +++ b/Nynja/Modules/Auth/Login/View/LoginViewController.swift @@ -1,5 +1,5 @@ // -// AuthAuthViewController.swift +// LoginViewController.swift // Nynja // // Created by Anton Makarov on 31/05/2017. @@ -9,7 +9,7 @@ import UIKit import libPhoneNumber_iOS -class LoginViewController: BaseVC, LoginViewProtocol, LoginWheelContainerViewProtocol, CountryFieldDelegate, PhoneFieldDelegate, UITextViewDelegate { +class LoginViewController: BaseVC, LoginViewProtocol, LoginWheelContainerViewProtocol, PhoneFieldDelegate, UITextViewDelegate, KeyboardInteractive, SetInjectable { var presenter: LoginPresenterProtocol! var region = "" @@ -35,7 +35,6 @@ class LoginViewController: BaseVC, LoginViewProtocol, LoginWheelContainerViewPro let lv = LoginView() lv.textViewDelegate = self - lv.countryFieldDelegate = self lv.phoneFieldDelegate = self self.view.addSubview(lv) @@ -89,6 +88,8 @@ class LoginViewController: BaseVC, LoginViewProtocol, LoginWheelContainerViewPro let tapGesture = UITapGestureRecognizer(target: self, action: #selector(self.handleTap(_:))) tapGesture.delegate = self self.view.addGestureRecognizer(tapGesture) + + presenter.showed() } override func viewDidAppear(_ animated: Bool) { @@ -134,7 +135,7 @@ class LoginViewController: BaseVC, LoginViewProtocol, LoginWheelContainerViewPro } // MARK: BaseVC - override func keyboardNotified(endFrame: CGRect) { + func keyboardNotified(endFrame: CGRect) { let bottomInset = LoginView.Constraints.nextButton.bottomInset // Check if keyboard is shown @@ -168,7 +169,7 @@ class LoginViewController: BaseVC, LoginViewProtocol, LoginWheelContainerViewPro let phone = code + self.loginView.phoneField.input.text!.replacingOccurrences(of: " ", with: "") if self.validate(number: phone, region: self.region) { self.showSpinner() - self.presenter!.login(phone: phone) + self.presenter.login(phone: phone) } } else { self.hideSpinner() @@ -249,22 +250,6 @@ class LoginViewController: BaseVC, LoginViewProtocol, LoginWheelContainerViewPro return false } - // MARK: CountryFieldDelegate - func updateCode(countryModel: CountryModel) { - /* self.isRegionSelected = true */ - /* if countryModel.code == "" { - self.loginView.phoneField.code.text = "" - self.region = "" - return - } - self.loginView.phoneField.code.text = "+" + countryModel.code - self.region = countryModel.ISO - self.loginView.phoneField.input.attributedPlaceholder = NSAttributedString(string: countryModel.placeHolder, - attributes: [NSForegroundColorAttributeName: Constants.colors.white.getColor()]) */ - } - - func ready() { } - // MARK: PhoneFieldDelegate func phoneChanged(text: String) -> Bool { if !isRegionSelected { @@ -376,3 +361,16 @@ class LoginViewController: BaseVC, LoginViewProtocol, LoginWheelContainerViewPro self.wheelContainerDS.selectCountry(at: index) } } + + +// MARK: - SetInjectable + +extension LoginViewController { + struct Dependencies { + let presenter: LoginPresenterProtocol + } + + func inject(dependencies: LoginViewController.Dependencies) { + presenter = dependencies.presenter + } +} diff --git a/Nynja/Modules/Auth/View/LoginWheelContainerDataSource.swift b/Nynja/Modules/Auth/Login/View/LoginWheelContainerDataSource.swift similarity index 100% rename from Nynja/Modules/Auth/View/LoginWheelContainerDataSource.swift rename to Nynja/Modules/Auth/Login/View/LoginWheelContainerDataSource.swift diff --git a/Nynja/Modules/Auth/View/LoginWheelContainerDelegate.swift b/Nynja/Modules/Auth/Login/View/LoginWheelContainerDelegate.swift similarity index 100% rename from Nynja/Modules/Auth/View/LoginWheelContainerDelegate.swift rename to Nynja/Modules/Auth/Login/View/LoginWheelContainerDelegate.swift diff --git a/Nynja/Modules/Auth/Login/WireFrame/LoginWireframe.swift b/Nynja/Modules/Auth/Login/WireFrame/LoginWireframe.swift new file mode 100644 index 000000000..bb024fd52 --- /dev/null +++ b/Nynja/Modules/Auth/Login/WireFrame/LoginWireframe.swift @@ -0,0 +1,61 @@ +// +// LoginWireframe.swift +// Nynja +// +// Created by Anton Makarov on 31/05/2017. +// Copyright © 2017 TecSynt Solutions. All rights reserved. +// + +import UIKit + +class LoginWireFrame: LoginWireFrameProtocol, NavigableWireframeProtocol { + + weak var navigation: UINavigationController? + + func presentLogin(navigation: UINavigationController) { + self.navigation = navigation + + // Dependencies + let serviceFactory = ServiceFactory() + let mqttService = serviceFactory.makeMQTTService() + let alertManager = serviceFactory.makeAlertManager() + + // Module components + let view = LoginViewController() + let presenter = LoginPresenter() + let interactor = LoginInteractor() + + // Connecting + view.inject( + dependencies: .init(presenter: presenter)) + presenter.inject( + dependencies: .init( + view: view, + interactor: interactor, + wireFrame: self, + alertManager: alertManager)) + interactor.inject( + dependencies: .init( + presenter: presenter, + mqttService: mqttService)) + + navigation.pushViewController(view, animated: true) + navigation.viewControllers = [view] + } + + func showTermOfUse() { + tryToPresentModule { (navigation) in + let supportConfig = ThirdPartyServicesFactory.support.serviceConfig + WebFullScreenWireFrame().presentWebFullScreen(navigation: navigation, + title: "support terms title".localized, + inputURL: supportConfig.terms) + } + } + + func showVerifyNumber(with phone: String) { + tryToPresentModule { (navigation) in + VerifyNumberWireFrame().presentVerifyNumber(navigation: navigation, phone: phone) + } + } + +} diff --git a/Nynja/Modules/Auth/Presenter/AuthPresenter.swift b/Nynja/Modules/Auth/Presenter/AuthPresenter.swift deleted file mode 100644 index 17f810e85..000000000 --- a/Nynja/Modules/Auth/Presenter/AuthPresenter.swift +++ /dev/null @@ -1,90 +0,0 @@ -// -// AuthAuthPresenter.swift -// Nynja -// -// Created by Anton Makarov on 31/05/2017. -// Copyright © 2017 TecSynt Solutions. All rights reserved. -// - -import UIKit - -class AuthPresenter: BasePresenter, LoginPresenterProtocol, VerifyPresenterProtocol, AuthInteractorOutputProtocol, UpdatePresenterProtocol { - - weak var view: LoginViewProtocol! - var verifyView: VerifyViewProtocol! - var profileView: ProfileViewProtocol! - - var wireFrame: AuthWireFrameProtocol! - var interactor: AuthInteractorInputProtocol! { - didSet { - _interactor = interactor - } - } - - var phone: String? - - func login(phone: String) { - self.phone = phone - //let phoneUpdated = phone.stringAsPhone() - verifyView.phone = "+\(phone)" - self.interactor.login(phone: phone) - } - - func smsSent() { - if let nav = (self.view as? UIViewController)?.navigationController { - if nav.visibleViewController != verifyView as? UIViewController { - self.view.removeSpinner() - nav.pushViewController(verifyView as! UIViewController, animated: true) - } - } - self.interactor?.getSMSCode() { result in - if let res = result as? String { - self.verifyView.showCode(res) - } - } - } - - func termOfUse() { - self.wireFrame.showTermOfUse() - } - - func resendSMS() { - self.interactor.resendSMS(phone: self.phone ?? "") - } - - func logined() { - if let vc = (self.view as? UIViewController) { - vc.view.endEditing(true) - } - self.wireFrame.showMainScreen() - } - - func registered(contact: Contact) { - self.wireFrame.showProfileScreen(contact: contact) - } - - func invalidCode() { - self.view.removeSpinner() - self.verifyView.showWrongCodeAlert() - } - - func voiceCall() { - self.interactor.voiceCall() - } - - func numberNotAllowed() { - dispatchAsyncMain { [weak self] in - self?.view.removeSpinner() - } - AlertManager.sharedInstance.showAlertOk(message: "You_are_not_allowed_to_login".localized) - } - - func handleEnteredPassword(code: String) { - self.interactor.saveEnteredPassword(code: code) - } - - func handleNextTapped() { - self.interactor.checkPassword() - } - -} diff --git a/Nynja/Modules/Auth/VerifyNumber/Interactor/VerifyNumberInteractor.swift b/Nynja/Modules/Auth/VerifyNumber/Interactor/VerifyNumberInteractor.swift new file mode 100644 index 000000000..85826e38f --- /dev/null +++ b/Nynja/Modules/Auth/VerifyNumber/Interactor/VerifyNumberInteractor.swift @@ -0,0 +1,199 @@ +// +// VerifyNumberInteractor.swift +// Nynja +// +// Created by Volodymyr Hryhoriev on 9/17/18. +// Copyright © 2018 TecSynt Solutions. All rights reserved. +// + +import Foundation + + +final class VerifyNumberInteractor: BaseInteractor, VerifyNumberInteractorInputProtocol, IoHandlerDelegate, MQTTServiceDelegate, SetInjectable { + + override var subscribes: [SubscribeType]? { + return [.roster(nil)] + } + + weak var presenter: VerifyNumberInteractorOutputProtocol! + + private var mqttService: MQTTService! + private var smsCodeProvider: SMSCodeProviding! + private let appNotificationsProvider = AppNotificationsProvider() + + var enteredCode: String? + + var phone: String { + return _phone + } + + private var _phone: String! + + private var timerHandler: TimerHandler? + private var count = 60 + private var lastActiveTimestamp: TimeInterval? + + + // MARK: - Config + + func configure() { + IoHandler.delegate = self + mqttService.addSubscriber(self) + setupObservers() + } + + deinit { + mqttService.removeSubscriber(self) + } + + private func setupObservers() { + appNotificationsProvider.didEnterBackgroundHandler = { [weak self] in + self?.lastActiveTimestamp = Date().timeIntervalSince1970 + self?.cancelTimer() + } + + appNotificationsProvider.willEnterForegroundHandler = { [weak self] in + self?.adjustTimer() + } + } + + private func adjustTimer() { + guard let lastActiveTimestamp = self.lastActiveTimestamp else { + startCodeTimer() + return + } + + let currentTimestamp = Date().timeIntervalSince1970 + let passedSeconds = Int(currentTimestamp - lastActiveTimestamp) + count = max(count - passedSeconds, 0) + + if count > 0 { + startCodeTimer() + } else { + timerAction() + } + } + + + // MARK: - VerifyNumberInteractorInputProtocol + + func startCodeTimer() { + timerHandler = TimerHandler(interval: 1, repeats: true) { [weak self] _ in + self?.timerAction() + } + } + + func checkPassword() { + guard let code = enteredCode else { + return + } + mqttService.checkSMS(code: code, phone: phone) + } + + func resendSMS() { + mqttService.resendSMS(number: phone) + } + + func voiceCall() { + mqttService.voiceCall(phone: phone) + } + + func fetchSMSCode() { + smsCodeProvider.fetchSMSCode(for: phone) { [weak self] code in + guard let code = code else { + return + } + + self?.presenter.smsCodeFetched(code) + } + } + + + // MARK: - Timer + + private func timerAction() { + if count > 1 { + count -= 1 + presenter.timerTick(with: count) + } else { + cancelTimer() + count = 60 + presenter.timerEnd() + } + } + + func cancelTimer() { + timerHandler?.invalidate() + } + + + // MARK: - IoHandlerDelegate + + func sessionNotFound() { + mqttService.removeTokenAndLogin(number: phone) + } + + func wrongCode() { + presenter.showError(with: "auth_sms_code_is_wrong".localized) + } + + func attemptsExpired() { + presenter.showError(with: "auth_attempts_expired".localized) + } + + func mismatchUserData() { + mqttService.removeTokenAndLogin(number: phone) + } + + + // MARK: - MQTTServiceDelegate + + func mqttServiceDidConnect(_ mqttService: MQTTService) { + checkPassword() + } + + + // MARK: - StorageSubscriber + + override func update(with changes: [StorageChange], type: SubscribeType) { + guard case .roster = type, + let info = changes.first, + let dbRoster = info.entity as? DBRoster else { + return + } + + let roster = Roster(roster: dbRoster) + + if info.kind == .insert { + if (roster.names ?? "").isEmpty, let contact = roster.myContact { + cancelTimer() + presenter.registered(contact: contact) + } else { + cancelTimer() + presenter.logined() + } + } + } + +} + + +// MARK: - SetInjectable + +extension VerifyNumberInteractor { + + struct Dependencies { + let presenter: VerifyNumberPresenter + let mqttService: MQTTService + let phone: String + } + + func inject(dependencies: VerifyNumberInteractor.Dependencies) { + presenter = dependencies.presenter + mqttService = dependencies.mqttService + _phone = dependencies.phone + + smsCodeProvider = SMSCodeProvider() + } + +} diff --git a/Nynja/Modules/Auth/VerifyNumber/Presenter/VerifyNumberPresenter.swift b/Nynja/Modules/Auth/VerifyNumber/Presenter/VerifyNumberPresenter.swift new file mode 100644 index 000000000..e5b4653ef --- /dev/null +++ b/Nynja/Modules/Auth/VerifyNumber/Presenter/VerifyNumberPresenter.swift @@ -0,0 +1,107 @@ +// +// VerifyNumberPresenter.swift +// Nynja +// +// Created by Volodymyr Hryhoriev on 9/17/18. +// Copyright © 2018 TecSynt Solutions. All rights reserved. +// + +import Foundation + +final class VerifyNumberPresenter: BasePresenter, VerifyNumberPresenterProtocol, VerifyNumberInteractorOutputProtocol, SetInjectable { + + weak var view: VerifyNumberViewProtocol! + var wireFrame: VerifyNumberWireFrameProtocol! + var interactor: VerifyNumberInteractorInputProtocol! { + didSet { + _interactor = interactor + } + } + + + // MARK: - VerifyNumberPresenterProtocol + + func showed() { + view.setup(with: "+\(interactor.phone)") + interactor.configure() + interactor.fetchSMSCode() + interactor.startCodeTimer() + } + + func resendSMS() { + interactor.resendSMS() + interactor.startCodeTimer() + } + + func voiceCall() { + interactor.voiceCall() + interactor.startCodeTimer() + } + + func resetCode() { + view.isNextEnabled = false + interactor.enteredCode = nil + } + + func codeEntered(_ code: String?) { + view.isNextEnabled = code != nil + interactor.enteredCode = code + } + + func nextTapped() { + view.isNextEnabled = false + interactor.checkPassword() + } + + func timerTick(with count: Int) { + let text = "code_send".localized + "\n \("You_should_receive_it_within".localized) \(count) \("seconds".localized)." + let links = [""] + view.updateHint(withText: text, links: links, isSelectable: false, shouldUseIndexes: false) + } + + func timerEnd() { + let text = "code_send_isReceived".localized + let links = ["have_u_receive".localized] + view.updateHint(withText: text, links: links, isSelectable: true, shouldUseIndexes: true) + } + + + // MARK: - VerifyNumberInteractorOutputProtocol + + func logined() { + wireFrame.showMainScreen() + } + + func registered(contact: Contact) { + wireFrame.showProfileScreen(contact: contact) + } + + func showError(with message: String) { + view.isNextEnabled = true + AlertManager.sharedInstance.showAlertOk(message: message) + } + + func smsCodeFetched(_ code: String) { + view.show(code: code) + } + +} + + +// MARK: - SetInjectable + +extension VerifyNumberPresenter { + + struct Dependencies { + let view: VerifyNumberViewProtocol + let interactor: VerifyNumberInteractorInputProtocol + let wireFrame: VerifyNumberWireFrameProtocol + } + + func inject(dependencies: VerifyNumberPresenter.Dependencies) { + view = dependencies.view + interactor = dependencies.interactor + wireFrame = dependencies.wireFrame + } + +} diff --git a/Nynja/Modules/Auth/VerifyNumber/VerifyNumberProtocols.swift b/Nynja/Modules/Auth/VerifyNumber/VerifyNumberProtocols.swift new file mode 100644 index 000000000..e5940615c --- /dev/null +++ b/Nynja/Modules/Auth/VerifyNumber/VerifyNumberProtocols.swift @@ -0,0 +1,91 @@ +// +// VerifyNumberProtocols.swift +// Nynja +// +// Created by Volodymyr Hryhoriev on 9/17/18. +// Copyright © 2018 TecSynt Solutions. All rights reserved. +// + +import UIKit + +protocol VerifyNumberWireFrameProtocol: class { + + func presentVerifyNumber(navigation: UINavigationController, phone: String) + + /** + * Add here your methods for communication PRESENTER -> WIREFRAME + */ + + func showMainScreen() + func showProfileScreen(contact: Contact) +} + +protocol VerifyNumberViewProtocol: class { + + var presenter: VerifyNumberPresenterProtocol! { get set } + + /** + * Add here your methods for communication PRESENTER -> VIEW + */ + + var isNextEnabled: Bool { get set } + + func setup(with phone: String) + func show(code: String) + func updateHint(withText text: String, links: [String], isSelectable: Bool, shouldUseIndexes: Bool) +} + +protocol VerifyNumberPresenterProtocol: BasePresenterProtocol { + + var view: VerifyNumberViewProtocol! { get set } + var interactor: VerifyNumberInteractorInputProtocol! { get set } + var wireFrame: VerifyNumberWireFrameProtocol! { get set } + + /** + * Add here your methods for communication VIEW -> PRESENTER + */ + + func showed() + + func resendSMS() + func voiceCall() + func resetCode() + func codeEntered(_ code: String?) + func nextTapped() +} + +protocol VerifyNumberInteractorOutputProtocol: class { + + /** + * Add here your methods for communication INTERACTOR -> PRESENTER + */ + + func logined() + func registered(contact: Contact) + func showError(with message: String) + func smsCodeFetched(_ code: String) + + func timerTick(with count: Int) + func timerEnd() +} + +protocol VerifyNumberInteractorInputProtocol: BaseInteractorProtocol { + + var presenter: VerifyNumberInteractorOutputProtocol! { get set } + + /** + * Add here your methods for communication PRESENTER -> INTERACTOR + */ + + var phone: String { get } + var enteredCode: String? { get set } + + func configure() + + func startCodeTimer() + + func checkPassword() + func resendSMS() + func voiceCall() + func fetchSMSCode() +} diff --git a/Nynja/Modules/Auth/VerifyNumber/View/VerifyNumberViewController.swift b/Nynja/Modules/Auth/VerifyNumber/View/VerifyNumberViewController.swift new file mode 100644 index 000000000..96ee9f549 --- /dev/null +++ b/Nynja/Modules/Auth/VerifyNumber/View/VerifyNumberViewController.swift @@ -0,0 +1,207 @@ +// +// VerifyNumberViewController.swift +// Nynja +// +// Created by Volodymyr Hryhoriev on 9/17/18. +// Copyright © 2018 TecSynt Solutions. All rights reserved. +// + +import UIKit +import SnapKit + +final class VerifyNumberViewController: BaseVC, VerifyNumberViewProtocol, CodeFieldDelegate, UITextViewDelegate, KeyboardInteractive, SetInjectable { + + weak var presenter: VerifyNumberPresenterProtocol! { + didSet { + _presenter = presenter + } + } + + + // MARK: - Views + + lazy var loginView: LoginView = { + let lv = LoginView() + + lv.textViewDelegate = self + lv.codeFieldDelegate = self + + self.view.addSubview(lv) + lv.snp.makeConstraints({ (make) in + make.left.right.bottom.equalToSuperview() + adjustVerticalInset(.top, make: make) + }) + + return lv + }() + + lazy var hint: UILabel = { + let lbl = UILabel() + lbl.textColor = Constants.colors.red.getColor() + lbl.isHidden = true + + self.loginView.addSubview(lbl) + lbl.snp.makeConstraints({ (make) in + make.bottom.equalTo(loginView.hint).offset(20) + make.centerX.equalTo(view) + }) + + return lbl + }() + + + // MARK: - BaseVC + + override func initialize() { + self.loginView.iconImage.image = UIImage(named:"auth_light_logo") + + loginView.phoneField.isHidden = true + loginView.nextButton.isHidden = true + loginView.checkButton.isHidden = true + + loginView.showHint = { [weak self] in + self?.hint.isHidden = false + } + + setupCountryField() + setupHint() + setupCodeField() + + self.view.isUserInteractionEnabled = true + loginView.isUserInteractionEnabled = true + + loginView.nextButton.addTarget(self, action: #selector(nextTapped), for: .touchUpInside) + + presenter.showed() + } + + private func setupCountryField() { + loginView.countryField.input.textColor = Constants.colors.red.getColor() + loginView.countryField.input.isUserInteractionEnabled = false + loginView.countryField.imageView.image = UIImage(named:"phone")! + loginView.countryField.input.textAlignment = .center + loginView.countryField.content.snp.updateConstraints { (make) in + make.left.equalTo(loginView.countryField.imageView.snp.right) + } + } + + private func setupHint() { + let text = "code_send".localized + let links = [String]() + let scWidth = UIScreen.main.bounds.width + let width = scWidth * 0.855 + let height = width * 0.048 + + let size = text.getFontSize(fontName: Constants.fonts.regular, width: width, height: height) + let font = UIFont(name: Constants.fonts.regular, size: size)! + loginView.hint.setup(text: text, links: links, font: font, useIndexes: true) + } + + private func setupCodeField() { + loginView.codeField.isHidden = false + loginView.codeField.tintColor = Constants.colors.red.getColor() + + loginView.codeField.input.didResetHandler = { [weak self] in + self?.presenter.resetCode() + } + + loginView.codeField.content.snp.updateConstraints { (make) in + make.left.equalTo(loginView.codeField.imageView.snp.right) + } + } + + func keyboardNotified(endFrame: CGRect) { + var bottomInset = LoginView.Constraints.nextButton.bottomInset + if endFrame.origin.y < UIScreen.main.bounds.size.height { + bottomInset += endFrame.height + } + loginView.nextButton.snp.updateConstraints({ (make) in + make.bottom.equalTo(loginView).inset(bottomInset) + }) + } + + + // MARK: - View lifecycle + + override func viewDidAppear(_ animated: Bool) { + super.viewDidAppear(animated) + loginView.nextButton.isHidden = false + loginView.nextButton.isEnabled = false + loginView.codeField.input.becomeFirstResponder() + } + + + // MARK: - UITextViewDelegate + + func textView(_ textView: UITextView, shouldInteractWith URL: URL, in characterRange: NSRange) -> Bool { + let text = "code_send".localized + loginView.hint.setup(text: text, links: [String](), font: loginView.hint.font!) + + AlertManager.sharedInstance.showAlertWithTwoActions( + title: "", + message: "alert_get_code".localized, + firstActionTitle: "alert_sms".localized, + secondActionTitle: "alert_voice".localized, + firstAction: { [weak self] in + self?.presenter.resendSMS() + }, secondAction: { [weak self] in + self?.presenter.voiceCall() + }) + return false + } + + + // MARK: - VerifyViewNumberProtocol + + var isNextEnabled: Bool { + get { return loginView.nextButton.isEnabled } + set { loginView.nextButton.isEnabled = newValue } + } + + func setup(with phone: String) { + loginView.countryField.input.text = phone + } + + func show(code: String) { + DispatchQueue.main.async { [weak self] in + self?.hint.text = code + } + } + + func updateHint(withText text: String, links: [String], isSelectable: Bool, shouldUseIndexes: Bool) { + if let font = loginView.hint.font { + loginView.hint.setup( + text: text, + links: links, + font: font, + isSelectable: isSelectable, + useIndexes: shouldUseIndexes) + } + } + + + // MARK: - CodeFieldDelegate + + func codeEntered(code: String?) { + presenter.codeEntered(code) + } + + @objc private func nextTapped() { + loginView.codeField.input.resignFirstResponder() + presenter.nextTapped() + } + +} + + +// MARK: - SetInjectable + +extension VerifyNumberViewController { + struct Dependencies { + let presenter: VerifyNumberPresenterProtocol + } + + func inject(dependencies: VerifyNumberViewController.Dependencies) { + presenter = dependencies.presenter + } +} diff --git a/Nynja/Modules/Auth/VerifyNumber/Wireframe/VerifyNumberWireFrame.swift b/Nynja/Modules/Auth/VerifyNumber/Wireframe/VerifyNumberWireFrame.swift new file mode 100644 index 000000000..991db0dd2 --- /dev/null +++ b/Nynja/Modules/Auth/VerifyNumber/Wireframe/VerifyNumberWireFrame.swift @@ -0,0 +1,57 @@ +// +// VerifyNumberWireFrame.swift +// Nynja +// +// Created by Volodymyr Hryhoriev on 9/17/18. +// Copyright © 2018 TecSynt Solutions. All rights reserved. +// + +import UIKit + +final class VerifyNumberWireFrame: VerifyNumberWireFrameProtocol, NavigableWireframeProtocol { + + weak var navigation: UINavigationController? + + func presentVerifyNumber(navigation: UINavigationController, phone: String) { + self.navigation = navigation + + // Dependencies + let serviceFactory = ServiceFactory() + let mqttService = serviceFactory.makeMQTTService() + + // Module components + let view = VerifyNumberViewController() + let presenter = VerifyNumberPresenter() + let interactor = VerifyNumberInteractor() + + // Connecting + view.inject( + dependencies: .init(presenter: presenter)) + presenter.inject( + dependencies: .init( + view: view, + interactor: interactor, + wireFrame: self)) + interactor.inject( + dependencies: .init( + presenter: presenter, + mqttService: mqttService, + phone: phone)) + + + navigation.pushViewController(view, animated: true) + navigation.viewControllers = [view] + } + + func showMainScreen() { + tryToPresentModule { navigation in + MainWireFrame().presentMain(navigation: navigation, isRegistered: false) + } + } + + func showProfileScreen(contact: Contact) { + tryToPresentModule { navigation in + EditProfileWireFrame().presentEditProfile(navigation: navigation, isRegistered: true, main: nil) + } + } +} diff --git a/Nynja/Modules/Auth/View/ViewController/VerifyNumberVC.swift b/Nynja/Modules/Auth/View/ViewController/VerifyNumberVC.swift deleted file mode 100644 index f32cacf7f..000000000 --- a/Nynja/Modules/Auth/View/ViewController/VerifyNumberVC.swift +++ /dev/null @@ -1,187 +0,0 @@ -// -// VerifyNumberVC.swift -// Nynja -// -// Created by Anton Makarov on 26.06.2017. -// Copyright © 2017 TecSynt Solutions. All rights reserved. -// - -import Foundation -import UIKit - -class VerifyNumberVC: BaseVC, VerifyViewProtocol, CodeFieldDelegate, UITextViewDelegate { - - weak var presenter: VerifyPresenterProtocol! { - didSet { - _presenter = presenter - } - } - - var phone: String! - - // MARK: Timer - var timer = Timer() - var count = 60 - - // MARK: Views - lazy var loginView: LoginView = { - let lv = LoginView() - - lv.textViewDelegate = self - lv.codeFieldDelegate = self - - self.view.addSubview(lv) - lv.snp.makeConstraints({ (make) in - make.left.right.bottom.equalTo(self.view) - - adjustVerticalInset(.top, make: make) - }) - - return lv - }() - - lazy var hint: UILabel = { - let lbl = UILabel() - lbl.textColor = Constants.colors.red.getColor() - lbl.isHidden = true - self.loginView.addSubview(lbl) - - lbl.snp.makeConstraints({ (make) in - make.bottom.equalTo(self.loginView.hint).offset(20) - make.centerX.equalTo(self.view) - }) - return lbl - }() - - func showCode(_ code: String) { - DispatchQueue.main.async { - self.hint.text = code - } - } - - // MARK: BaseVC - override func initialize() { - // Initialize timer for SMS receiving - timer = Timer.scheduledTimer(timeInterval: 1, target: self, selector: #selector(timerAction), userInfo: nil, repeats: true) - - self.loginView.iconImage.image = UIImage(named:"auth_light_logo") - - loginView.phoneField.isHidden = true - loginView.nextButton.isHidden = true - loginView.checkButton.isHidden = true - loginView.showHint = { - self.hint.isHidden = false - } - // Configure "Phone number" field - loginView.countryField.input.text = phone - loginView.countryField.input.textColor = Constants.colors.red.getColor() - loginView.countryField.input.isUserInteractionEnabled = false - loginView.countryField.imageView.image = UIImage(named:"phone")! - loginView.countryField.input.textAlignment = .center - loginView.countryField.content.snp.updateConstraints { (make) in - make.left.equalTo(self.loginView.countryField.imageView.snp.right) - } - - // Configure "Hint" label - let text = "code_send".localized - let links = [String]() - let scWidth = UIScreen.main.bounds.width - let width = scWidth * 0.855 - let height = width * 0.048 - - let size = text.getFontSize(fontName: Constants.fonts.regular, width: width, height: height) - let font = UIFont(name: Constants.fonts.regular, size: size)! - loginView.hint.setup(text: text, links: links,font:font, useIndexes:true) - - // Configure "Code" field - loginView.codeField.isHidden = false - loginView.codeField.tintColor = Constants.colors.red.getColor() - loginView.codeField.content.snp.updateConstraints { (make) in - make.left.equalTo(self.loginView.codeField.imageView.snp.right) - } - - self.view.isUserInteractionEnabled = true - loginView.isUserInteractionEnabled = true - - loginView.nextButton.addTarget(self, action: #selector(nextTapped), for: .touchUpInside) - } - - override func keyboardNotified(endFrame: CGRect) { - var bottomInset = LoginView.Constraints.nextButton.bottomInset - if endFrame.origin.y < UIScreen.main.bounds.size.height { - bottomInset += endFrame.height - } - self.loginView.nextButton.snp.updateConstraints({ (make) in - make.bottom.equalTo(self.loginView).inset(bottomInset) - }) - } - - // MARK: View lifecycle - - override func viewDidAppear(_ animated: Bool) { - super.viewDidAppear(animated) - loginView.nextButton.isHidden = false - loginView.nextButton.isEnabled = false - loginView.codeField.input.becomeFirstResponder() - } - - // MARK: Verification timer - @objc func timerAction() { - if count > 1 { - count -= 1 - let text = "code_send".localized + "\n \("You_should_receive_it_within".localized) \(count) \("seconds".localized)." - let links = [""] - if let font = loginView.hint.font { - loginView.hint.setup(text: text, links: links,font:font, isSelectable: false) - } - } else { - timer.invalidate() - count = 60 - let text = "code_send_isReceived".localized - let links = ["have_u_receive".localized] - if let font = loginView.hint.font { - loginView.hint.setup(text: text, links: links,font:font, isSelectable: true, useIndexes: true) - } - } - } - - // MARK: UITextViewDelegate - func textView(_ textView: UITextView, shouldInteractWith URL: URL, in characterRange: NSRange) -> Bool { - let text = "code_send".localized - loginView.hint.setup(text: text, links: [String](),font:loginView.hint.font!) - - AlertManager.sharedInstance.showAlertWithTwoActions(title: "", - message: "alert_get_code".localized, firstActionTitle: "alert_sms".localized, secondActionTitle: "alert_voice".localized, firstAction: { [unowned self] in - self.presenter.resendSMS() - self.timer = Timer.scheduledTimer(timeInterval: 1, target: self, - selector: #selector(self.timerAction), - userInfo: nil, - repeats: true) - }, secondAction: { [unowned self] in - self.presenter.voiceCall() - self.timer = Timer.scheduledTimer(timeInterval: 1, target: self, - selector: #selector(self.timerAction), - userInfo: nil, - repeats: true) - }) - return false - } - - // MARK: Utils - func showWrongCodeAlert() { - AlertManager.sharedInstance.showAlertOk(message: "wrong_sms".localized) - } - - // MARK: CodeFieldDelegate - func codeEntered(code: String?) { - if let fullCode = code { - self.presenter.handleEnteredPassword(code: fullCode) - } - self.loginView.nextButton.isEnabled = code != nil - } - - @objc func nextTapped() { - self.loginView.codeField.input.resignFirstResponder() - self.presenter.handleNextTapped() - } -} diff --git a/Nynja/Modules/Auth/WireFrame/AuthWireframe.swift b/Nynja/Modules/Auth/WireFrame/AuthWireframe.swift deleted file mode 100644 index 09bf759f7..000000000 --- a/Nynja/Modules/Auth/WireFrame/AuthWireframe.swift +++ /dev/null @@ -1,53 +0,0 @@ -// -// AuthAuthWireframe.swift -// Nynja -// -// Created by Anton Makarov on 31/05/2017. -// Copyright © 2017 TecSynt Solutions. All rights reserved. -// - -import UIKit - -class AuthWireFrame: AuthWireFrameProtocol { - - weak var navigation : UINavigationController? - - func presentAuth(navigation: UINavigationController) { - let view = LoginViewController() - let verifyView = VerifyNumberVC() - let presenter = AuthPresenter() - let interactor = AuthInteractor() - - self.navigation = navigation - - // Connecting - verifyView.presenter = presenter - view.presenter = presenter - presenter.view = view - presenter.verifyView = verifyView - presenter.wireFrame = self - presenter.interactor = interactor - interactor.presenter = presenter - - self.navigation?.pushViewController(view, animated: true) - self.navigation?.viewControllers = [view] - } - - func showTermOfUse() { - if navigation != nil { - WebViewWireFrame().presentWebView(navigation: navigation!, link: "http://\(Host().url)/terms.htm") - } - } - - func showMainScreen() { - if let navigation = navigation { - MainWireFrame().presentMain(navigation: navigation, isRegistered: false) - } - } - - func showProfileScreen(contact: Contact) { - if let navigation = navigation { - EditProfileWireFrame().presentEditProfile(navigation: navigation, isRegistered: true, main: nil) - } - } -} diff --git a/Nynja/Modules/Call/CallInProgressProtocols.swift b/Nynja/Modules/Call/CallInProgressProtocols.swift index 9abde5aa4..4219919f1 100644 --- a/Nynja/Modules/Call/CallInProgressProtocols.swift +++ b/Nynja/Modules/Call/CallInProgressProtocols.swift @@ -10,8 +10,6 @@ import UIKit protocol CallInProgressWireFrameProtocol: class { - func presentCall(navigation: UINavigationController, callInProgressMode: CallInProgressMode, contact: Contact, main: MainWireFrame?) - /** * Add here your methods for communication PRESENTER -> WIREFRAME */ @@ -57,7 +55,7 @@ protocol CallInProgressPresenterProtocol: class { func willShow() - func acceptCall(withVideo: Bool) + func acceptCall() func declineCall() func speakerAction() func messageAcion(with roomId:String, isVideo: Bool) @@ -114,7 +112,7 @@ protocol CallInProgressInteractorInputProtocol: class { * Add here your methods for communication PRESENTER -> INTERACTOR */ - func acceptCall(withVideo: Bool) + func acceptCall() func declineCall() func speakerAction() func microphoneAction() diff --git a/Nynja/Modules/Call/CallScreens/CallInProgress/Interactor/CallInProgressInteractor.swift b/Nynja/Modules/Call/CallScreens/CallInProgress/Interactor/CallInProgressInteractor.swift index e30b75554..429806af9 100644 --- a/Nynja/Modules/Call/CallScreens/CallInProgress/Interactor/CallInProgressInteractor.swift +++ b/Nynja/Modules/Call/CallScreens/CallInProgress/Interactor/CallInProgressInteractor.swift @@ -13,7 +13,8 @@ class CallInProgressInteractor: CallInProgressInteractorInputProtocol, NynjaCall var timer: Timer? private let callService = NynjaCommunicatorService.sharedInstance - + private let audioSessionManager = AudioSessionManager.shared + init() { callService.callDelegate = self @@ -26,14 +27,14 @@ class CallInProgressInteractor: CallInProgressInteractorInputProtocol, NynjaCall selector: #selector(applicationWillResignActive), name: NSNotification.Name.UIApplicationWillResignActive, object: nil) - + } deinit { NotificationCenter.default.removeObserver(self, name: NSNotification.Name.UIApplicationWillResignActive, object: nil) - + NotificationCenter.default.removeObserver(self, name: NSNotification.Name.UIApplicationDidBecomeActive, object: nil) @@ -43,7 +44,7 @@ class CallInProgressInteractor: CallInProgressInteractorInputProtocol, NynjaCall guard let roomId = nynCall?.externalInfo else { return nil } return RoomDAO.findRoom(by: roomId) } - + weak var nynCall: NYNCall? var duration = 0 @@ -70,17 +71,20 @@ class CallInProgressInteractor: CallInProgressInteractorInputProtocol, NynjaCall return MessageSendingService(dependencies: dependencies) }() - func acceptCall(withVideo: Bool) { + private func callClosed() { + try? audioSessionManager.resetAudioSession() + presenter.callClosed() + } + + func acceptCall() { guard let ncall = self.nynCall else { return } callService.acceptConference(call: ncall) } func rejectCall() { - if let nc = self.nynCall { callService.rejectConference(call: nc) - - self.presenter.callClosed() + callClosed() } } @@ -91,30 +95,30 @@ class CallInProgressInteractor: CallInProgressInteractorInputProtocol, NynjaCall call.hangup() } - self.presenter.callClosed() + callClosed() } private func declineTwoPartycall(call: NYNCall) { if call.isOutgoing() { call.hangup() + } else if call.callState == .new { + call.reject() } else { - if call.callState == .new { - call.reject() - } else { - call.hangup() - } + call.hangup() } - self.presenter.callClosed() + callClosed() } func declineCall() { - if let nc = self.nynCall { - if nc.isConference() { - declineConferenceCall(call: nc) - } else { - declineTwoPartycall(call: nc) - } + guard let nc = self.nynCall else { + return + } + + if nc.isConference() { + declineConferenceCall(call: nc) + } else { + declineTwoPartycall(call: nc) } } @@ -123,7 +127,7 @@ class CallInProgressInteractor: CallInProgressInteractorInputProtocol, NynjaCall nc.hangup() } } - + func endCall() { if let nc = self.nynCall { nc.end() @@ -131,10 +135,10 @@ class CallInProgressInteractor: CallInProgressInteractorInputProtocol, NynjaCall } func speakerAction() { - if AudioManager.sharedInstance.speaker == .loud { - AudioManager.sharedInstance.speaker = .soft + if audioSessionManager.speaker == .loud { + audioSessionManager.speaker = .soft } else { - AudioManager.sharedInstance.speaker = .loud + audioSessionManager.speaker = .loud } } @@ -196,11 +200,11 @@ class CallInProgressInteractor: CallInProgressInteractorInputProtocol, NynjaCall func didAddVideoStreamForCall(call: NYNCall) { self.presenter.didAddRemoteVideoStream() } - + func didRemoveVideoStreamForCall(call: NYNCall) { self.presenter.didRemoveRemoteVideoStream() } - + func didStartLocalCapturerForCall(call: NYNCall) { self.presenter.didStartLocalCapturer() } @@ -208,11 +212,11 @@ class CallInProgressInteractor: CallInProgressInteractorInputProtocol, NynjaCall func didStopLocalCapturerForCall(call: NYNCall) { self.presenter.didStopLocalCapturer() } - + func addRemoteVideoRenderer(inView view: UIView) { callService.attachRemoteVideoRenderer(inView: view) } - + func removeRemoteVideoRenderer(inView view: UIView) { callService.detachRemoteVideoRenderer(inView: view) } @@ -220,14 +224,14 @@ class CallInProgressInteractor: CallInProgressInteractorInputProtocol, NynjaCall func attachLocalVideoPreview(inView view: UIView) { callService.attachLocalVideoPreview(inView: view) } - + func detachLocalVideoPreview(inView view: UIView) { callService.detachLocalVideoPreview(inView: view) } - + func cameraAction() { LogService.log(topic: .callSystem) { return "call interactor cameraAction" } - + if let call = self.nynCall { if call.isCameraRunning() { LogService.log(topic: .callSystem) { return "call interactor cameraAction, call will stop camera"} @@ -245,7 +249,7 @@ class CallInProgressInteractor: CallInProgressInteractorInputProtocol, NynjaCall if let call = self.nynCall { return call.isCameraRunning() } - + return false } @@ -260,10 +264,10 @@ class CallInProgressInteractor: CallInProgressInteractorInputProtocol, NynjaCall func unmuteCamera() { self.nynCall?.unmuteCamera() } - + func updateCallStatus() { if let c = self.nynCall { - + switch c.callState { case NYNCallState.new: @@ -298,8 +302,7 @@ class CallInProgressInteractor: CallInProgressInteractorInputProtocol, NynjaCall } } - func stopTimer () { - + func stopTimer() { if timer != nil { timer?.invalidate() timer = nil @@ -308,13 +311,11 @@ class CallInProgressInteractor: CallInProgressInteractorInputProtocol, NynjaCall } func callEnded(call: NYNCall, isError: Bool) { - self.presenter.callClosed() - stopTimer() + callClosed() } - func readyToStart(call:NYNCall) { - + func readyToStart(call: NYNCall) { call.start() } @@ -357,7 +358,7 @@ class CallInProgressInteractor: CallInProgressInteractorInputProtocol, NynjaCall self.presenter.update(participants: ncall.participants) } } - + func conferenceCreated(call: NYNCall) { sendCall(ncall: call) } @@ -376,9 +377,7 @@ class CallInProgressInteractor: CallInProgressInteractorInputProtocol, NynjaCall if let ncall = self.nynCall { for ctc in contacts { - let name = "\(ctc.names ?? "") \(ctc.surnames ?? "")" - - callService.addConferenceMember(conferenceId: ncall.callId, phoneId: ctc.phone_id!, name: name) + callService.addConferenceMember(conferenceId: ncall.callId, phoneId: ctc.phone_id!, name: ctc.name ?? "") } } } @@ -396,6 +395,6 @@ class CallInProgressInteractor: CallInProgressInteractorInputProtocol, NynjaCall @objc func applicationWillResignActive() { self.nynCall?.muteCamera() } - + } diff --git a/Nynja/Modules/Call/CallScreens/CallInProgress/Presenter/CallInProgressPresenter.swift b/Nynja/Modules/Call/CallScreens/CallInProgress/Presenter/CallInProgressPresenter.swift index 48eb5e0e0..a9a8be60c 100644 --- a/Nynja/Modules/Call/CallScreens/CallInProgress/Presenter/CallInProgressPresenter.swift +++ b/Nynja/Modules/Call/CallScreens/CallInProgress/Presenter/CallInProgressPresenter.swift @@ -17,8 +17,8 @@ class CallInProgressPresenter: CallInProgressPresenterProtocol, CallInProgressIn var contact: Contact? var room: Room? - func acceptCall(withVideo: Bool) { - interactor.acceptCall(withVideo: withVideo) + func acceptCall() { + interactor.acceptCall() } diff --git a/Nynja/Modules/Call/CallScreens/CallInProgress/View/CallInProgressViewController.swift b/Nynja/Modules/Call/CallScreens/CallInProgress/View/CallInProgressViewController.swift index 2137d2d60..d2c34bee6 100644 --- a/Nynja/Modules/Call/CallScreens/CallInProgress/View/CallInProgressViewController.swift +++ b/Nynja/Modules/Call/CallScreens/CallInProgress/View/CallInProgressViewController.swift @@ -36,7 +36,7 @@ enum CallInProgressMode { import UIKit -class CallInProgressViewController: BaseVC, CallInProgressViewProtocol, BottomCallViewProtocol, UICollectionViewDelegate, UICollectionViewDataSource, UICollectionViewDelegateFlowLayout, GroupCollectionViewCellDelegate, GroupAddParticipantsCollectionViewCellDelegate, SpeakerDelegate { +class CallInProgressViewController: BaseVC, CallInProgressViewProtocol, BottomCallViewProtocol, UICollectionViewDelegate, UICollectionViewDataSource, UICollectionViewDelegateFlowLayout, GroupCollectionViewCellDelegate, GroupAddParticipantsCollectionViewCellDelegate, AudioSessionManagerDelegate { var presenter: CallInProgressPresenterProtocol! var contact: Contact? @@ -53,11 +53,14 @@ class CallInProgressViewController: BaseVC, CallInProgressViewProtocol, BottomCa let middleGVHeight: Float = 100.0 let statusBarHeight: Float = Float(UIApplication.shared.statusBarFrame.size.height) + var isIncomingCallRinging = false var collapsed: Bool = false var initialized: Bool = false var needLocalRenderer: Bool = false var needRemoteRenderer: Bool = false var willAppearAtLeastOnce: Bool = false + + private let audioSessionManager = AudioSessionManager.shared private struct ConstraintConstants { @@ -119,7 +122,7 @@ class CallInProgressViewController: BaseVC, CallInProgressViewProtocol, BottomCa view.snp.makeConstraints({ (make) in make.top.left.right.equalTo(self.view) - make.height.equalTo(ConstraintConstants.gradientHeight.adjustedByHeight) + make.height.equalTo(ConstraintConstants.gradientHeight.adjustedByWidth) }) return view @@ -198,7 +201,7 @@ class CallInProgressViewController: BaseVC, CallInProgressViewProtocol, BottomCa self.view.addSubview(cv) cv.delegate = self cv.dataSource = self - cv.backgroundColor = Constants.colors.grayForCallScreenBottom.getColor() + cv.backgroundColor = Constants.colors.callBackground.getColor() cv.register(GroupCollectionViewCell.self, forCellWithReuseIdentifier: String(describing: GroupCollectionViewCell.self)) cv.register(GroupAddParticipantsCollectionViewCell.self, forCellWithReuseIdentifier: String(describing: GroupAddParticipantsCollectionViewCell.self)) @@ -268,6 +271,57 @@ class CallInProgressViewController: BaseVC, CallInProgressViewProtocol, BottomCa return view }() + + lazy var bottomViewAcceptReject : UIView = { + + let view = UIView() + view.isUserInteractionEnabled = true + view.backgroundColor = .clear + self.view.addSubview(view) + + view.snp.makeConstraints({ (make) in + make.bottom.left.right.equalTo(self.view) + make.height.equalTo(self.bottomViewHeight) + }) + + return view + }() + + lazy var rejectButton : UIButton = { + let btn = UIButton() + let img = UIImage.init(named:"ic_decline_call") + btn.backgroundColor = .clear + btn.setImage(img, for: .normal) + self.bottomViewAcceptReject.addSubview(btn) + btn.addTarget(self, action: #selector(rejectButtonPressed), for: .touchUpInside) + let center = UIScreen.main.bounds.width / 3.0 + + btn.snp.makeConstraints({ (make) in + make.width.height.equalTo(ConstraintConstants.declineButtonSize) + make.width.height.equalTo(ConstraintConstants.declineButtonSize) + make.centerY.equalTo(self.bottomViewAcceptReject.snp.centerY) + make.centerX.equalTo(self.view).offset(-center + ConstraintConstants.declineButtonSize/2) + }) + return btn + }() + + lazy var acceptButton : UIButton = { + let btn = UIButton() + let img = UIImage.init(named:"ic_accept_call_big") + btn.backgroundColor = .clear + btn.setImage(img, for: .normal) + self.bottomViewAcceptReject.addSubview(btn) + btn.addTarget(self, action: #selector(acceptButtonPressed), for: .touchUpInside) + let center = UIScreen.main.bounds.width / 3.0 + + btn.snp.makeConstraints({ (make) in + make.width.height.equalTo(ConstraintConstants.declineButtonSize) + make.width.height.equalTo(ConstraintConstants.declineButtonSize) + make.centerY.equalTo(self.bottomViewAcceptReject.snp.centerY) + make.centerX.equalTo(self.view).offset(center - ConstraintConstants.declineButtonSize/2) + }) + return btn + }() lazy var declineButton : UIButton = { let btn = UIButton() @@ -287,19 +341,16 @@ class CallInProgressViewController: BaseVC, CallInProgressViewProtocol, BottomCa }() lazy var speakerButton : UIButton = { - let btn = UIButton() - - AudioManager.sharedInstance.speaker = .soft - + btn.setImage(#imageLiteral(resourceName: "ic_speaker_off"), for: .normal) btn.setImage(#imageLiteral(resourceName: "ic_speaker_on"), for: .selected) - btn.isSelected = (AudioManager.sharedInstance.speaker == .loud) - AudioManager.sharedInstance.speakerDelegate = self + btn.isSelected = (audioSessionManager.speaker == .loud) + audioSessionManager.delegate = self btn.layer.masksToBounds = true + btn.isEnabled = false self.bottomView.addSubview(btn) - let offsetX = UIScreen.main.bounds.width / 3.0 btn.addTarget(self, action: #selector(speakerButtonPressed), for: .touchUpInside) @@ -351,7 +402,7 @@ class CallInProgressViewController: BaseVC, CallInProgressViewProtocol, BottomCa let image = self.isCameraRunning() ? #imageLiteral(resourceName: "ic_video_on_voice_call") : #imageLiteral(resourceName: "ic_video_off_voice_call") button.setImage(image, for: .normal) button.addTarget(self, action: #selector(onCameraButtonPressed), for: .touchUpInside) - button.isEnabled = (.oneToOneVideo == self.callInProgressMode) + button.isEnabled = false self.bottomView.addSubview(button) let offsetX = UIScreen.main.bounds.width / 3.0 @@ -369,6 +420,7 @@ class CallInProgressViewController: BaseVC, CallInProgressViewProtocol, BottomCa let btn = UIButton() let muteImage = (self.isMuted()) ? #imageLiteral(resourceName: "ic_mute_voice_call"): #imageLiteral(resourceName: "ic_unmute_voice_call") btn.setImage(muteImage, for: .normal) + btn.isEnabled = false btn.layer.masksToBounds = true self.bottomView.addSubview(btn) btn.addTarget(self, action: #selector(microphoneButtonPressed), for: .touchUpInside) @@ -405,7 +457,8 @@ class CallInProgressViewController: BaseVC, CallInProgressViewProtocol, BottomCa self.bottomView.addSubview(btn) let offsetX = UIScreen.main.bounds.width / 3.0 btn.addTarget(self, action: #selector(messageButtonPressed), for: .touchUpInside) - + btn.isEnabled = false + btn.snp.makeConstraints({ (make) in make.width.equalTo(ConstraintConstants.buttonSize) make.height.equalTo(ConstraintConstants.buttonSize) @@ -454,7 +507,8 @@ class CallInProgressViewController: BaseVC, CallInProgressViewProtocol, BottomCa let button = UIButton() button.setImage(#imageLiteral(resourceName: "ic_more_voice_call"), for: .normal) button.addTarget(self, action: #selector(onMoreButtonPressed), for: .touchUpInside) - + button.isEnabled = false + self.bottomView.addSubview(button) let offsetX = UIScreen.main.bounds.width / 3.0 @@ -564,7 +618,10 @@ class CallInProgressViewController: BaseVC, CallInProgressViewProtocol, BottomCa return button }() - func setupVisibility() { + func setupInitialVisibility() { + + self.bottomView.isHidden = self.isIncomingCallRinging + self.bottomViewAcceptReject.isHidden = !self.isIncomingCallRinging // for all modes in view self.nameLabel.isHidden = false @@ -578,6 +635,8 @@ class CallInProgressViewController: BaseVC, CallInProgressViewProtocol, BottomCa self.openCloseCallsButton.isEnabled = false // for all modes in bottom view + self.acceptButton.isHidden = false + self.rejectButton.isHidden = false self.declineButton.isHidden = false self.speakerButton.isHidden = false self.speakerLabel.isHidden = false @@ -587,10 +646,8 @@ class CallInProgressViewController: BaseVC, CallInProgressViewProtocol, BottomCa self.muteLabel.isHidden = false self.messageButton.isHidden = false self.chatLabel.isHidden = false - self.messageButton.isEnabled = (nil != self.contact) || (self.roomId.count > 0) self.moreButton.isHidden = false self.moreLabel.isHidden = false - self.moreButton.isEnabled = false switch self.callInProgressMode! { @@ -603,6 +660,9 @@ class CallInProgressViewController: BaseVC, CallInProgressViewProtocol, BottomCa self.backgroundImage.setImage(url: self.contact?.avatarUrl, placeHolder: UIImage(named: "ava_placeholder")) self.view.bringSubview(toFront: self.backgroundImage) self.view.insertSubview(self.backgroundGradientView, aboveSubview: self.backgroundImage) + self.view.backgroundColor = Constants.colors.grayForCallScreenTop.getColor() + statusLabel.text = "call_incoming".localized + case .oneToOneVideo: self.switchCameraButton.isHidden = true self.myVideoView.isHidden = true @@ -615,6 +675,9 @@ class CallInProgressViewController: BaseVC, CallInProgressViewProtocol, BottomCa self.view.bringSubview(toFront: self.otherVideoView) self.view.bringSubview(toFront: self.myVideoView) self.view.bringSubview(toFront: self.switchCameraButton) + self.view.backgroundColor = Constants.colors.grayForCallScreenTop.getColor() + statusLabel.text = "call_incoming_video".localized + case .groupAudio: self.collectionView.isHidden = false self.myVideoView.isHidden = true @@ -623,9 +686,12 @@ class CallInProgressViewController: BaseVC, CallInProgressViewProtocol, BottomCa self.backgroundImage.isHidden = true self.backgroundGradientView.isHidden = true self.view.bringSubview(toFront: self.collectionView) + self.view.backgroundColor = Constants.colors.callBackground.getColor() + statusLabel.text = "call_incoming_audio_conference".localized } self.view.bringSubview(toFront: self.bottomView) + self.view.bringSubview(toFront: self.bottomViewAcceptReject) self.view.bringSubview(toFront: self.nameLabel) self.view.bringSubview(toFront: self.statusLabel) } @@ -704,10 +770,8 @@ class CallInProgressViewController: BaseVC, CallInProgressViewProtocol, BottomCa override func initialize() { super.initialize() - self.view.backgroundColor = Constants.colors.grayForCallScreenTop.getColor() - self.presenter.willShow() - setupVisibility() + setupInitialVisibility() // mark view as initialized self.initialized = true @@ -725,7 +789,7 @@ class CallInProgressViewController: BaseVC, CallInProgressViewProtocol, BottomCa var rows:Int = dataSource.count - if rows > 0 { + if rows > 0 , self.moderator { rows = dataSource.count + 1 } @@ -737,7 +801,7 @@ class CallInProgressViewController: BaseVC, CallInProgressViewProtocol, BottomCa let cell:UICollectionViewCell? - if 0 == indexPath.row { + if 0 == indexPath.row, self.moderator { let cellPlus:GroupAddParticipantsCollectionViewCell = collectionView.dequeueReusableCell(withReuseIdentifier: String(describing: GroupAddParticipantsCollectionViewCell.self), for: indexPath) as! GroupAddParticipantsCollectionViewCell @@ -750,7 +814,7 @@ class CallInProgressViewController: BaseVC, CallInProgressViewProtocol, BottomCa let cellPart:GroupCollectionViewCell = collectionView.dequeueReusableCell(withReuseIdentifier: String(describing: GroupCollectionViewCell.self), for: indexPath) as! GroupCollectionViewCell - let callPart:NYNCallParticipant? = dataSource[indexPath.row - 1] as? NYNCallParticipant + let callPart:NYNCallParticipant? = dataSource[indexPath.row - (self.moderator ? 1 : 0)] as? NYNCallParticipant if let cp = callPart { @@ -794,21 +858,35 @@ class CallInProgressViewController: BaseVC, CallInProgressViewProtocol, BottomCa //MARK: bottom view delegate - func acceptButtonPressed() { - //presenter.acceptCall(withVideo: (self.callMode == .incamingVideoCall) || (self.callMode == .incamingVideoGroupCall)) - } - @objc func declineButtonPressed() { presenter.declineCall() } + + @objc func rejectButtonPressed() { + + self.bottomView.isHidden = false + self.bottomViewAcceptReject.isHidden = true + self.isIncomingCallRinging = false + + presenter.rejectCall() + } + + @objc func acceptButtonPressed() { + + self.bottomView.isHidden = false + self.bottomViewAcceptReject.isHidden = true + self.isIncomingCallRinging = false + + presenter.acceptCall() + } @objc func speakerButtonPressed() { presenter.speakerAction() } - func speakerUpdated(state: Speaker) { + func speakerUpdated(to state: AudioSessionManager.Speaker) { speakerButton.isSelected = (state == .loud) } @@ -1004,14 +1082,35 @@ class CallInProgressViewController: BaseVC, CallInProgressViewProtocol, BottomCa func callConnecting() { statusLabel.text = "call_connecting".localized + disableButtons() } func callRinging() { statusLabel.text = "call_ringing".localized + disableButtons() } func callConnected() { updateTitle() + enableButtons() + } + + private func enableButtons() { + + self.messageButton.isEnabled = (nil != self.contact) || (self.roomId.count > 0) + self.cameraButton.isEnabled = (.oneToOneVideo == self.callInProgressMode) + self.moreButton.isEnabled = true + self.speakerButton.isEnabled = true + self.microphoneButton.isEnabled = true + } + + private func disableButtons() { + + self.moreButton.isEnabled = false + self.cameraButton.isEnabled = false + self.messageButton.isEnabled = false + self.speakerButton.isEnabled = false + self.microphoneButton.isEnabled = false } private func handleDidAddRemoteVideoStream() { diff --git a/Nynja/Modules/Call/CallScreens/CallInProgress/WireFrame/CallInProgressWireframe.swift b/Nynja/Modules/Call/CallScreens/CallInProgress/WireFrame/CallInProgressWireframe.swift index 157a8d11d..af7c58e2c 100644 --- a/Nynja/Modules/Call/CallScreens/CallInProgress/WireFrame/CallInProgressWireframe.swift +++ b/Nynja/Modules/Call/CallScreens/CallInProgress/WireFrame/CallInProgressWireframe.swift @@ -16,36 +16,12 @@ class CallInProgressWireframe: CallInProgressWireFrameProtocol { weak var nynCall: NYNCall? weak var external: EditParticipantsDelegate? - func presentCall(navigation: UINavigationController, callInProgressMode: CallInProgressMode, contact: Contact, main: MainWireFrame?) { - - self.navigation = navigation - self.mainWF = main + func presentDialInCall(navigation: UINavigationController, callInProgressMode: CallInProgressMode, contact: Contact?, call: NYNCall? = nil, main: MainWireFrame?, isIncomingRingig:Bool) { let view = CallInProgressViewController() let presenter = CallInProgressPresenter() let interactor = CallInProgressInteractor() - view.callInProgressMode = callInProgressMode - self.view = view - - presenter.callInProgressType = callInProgressMode - presenter.contact = contact - - // Connecting - view.presenter = presenter - presenter.view = view - presenter.wireFrame = self - presenter.interactor = interactor - interactor.presenter = presenter - - navigation.pushViewController(view as UIViewController, animated: true) - } - - func presentDialInCall(navigation: UINavigationController, callInProgressMode: CallInProgressMode, contact: Contact?, call: NYNCall? = nil, main: MainWireFrame?) { - - let view = CallInProgressViewController() - let presenter = CallInProgressPresenter() - let interactor = CallInProgressInteractor() interactor.nynCall = call self.navigation = navigation view.callInProgressMode = callInProgressMode @@ -63,10 +39,12 @@ class CallInProgressWireframe: CallInProgressWireFrameProtocol { presenter.interactor = interactor interactor.presenter = presenter + view.isIncomingCallRinging = isIncomingRingig + navigation.pushViewController(view as UIViewController, animated: true) } - func presentCreateGroupCall(navigation: UINavigationController, callInProgressMode: CallInProgressMode, main: MainWireFrame?, call: NYNCall) { + func presentCreateGroupCall(navigation: UINavigationController, callInProgressMode: CallInProgressMode, main: MainWireFrame?, call: NYNCall, isIncomingRingig:Bool) { let view = CallInProgressViewController() let presenter = CallInProgressPresenter() @@ -91,6 +69,8 @@ class CallInProgressWireframe: CallInProgressWireFrameProtocol { presenter.interactor = interactor interactor.presenter = presenter + view.isIncomingCallRinging = isIncomingRingig + navigation.pushViewController(view as UIViewController, animated: true) } diff --git a/Nynja/Modules/Call/View/BottomCallView.swift b/Nynja/Modules/Call/View/BottomCallView.swift index 2cd20f74d..ba1f61c5e 100644 --- a/Nynja/Modules/Call/View/BottomCallView.swift +++ b/Nynja/Modules/Call/View/BottomCallView.swift @@ -24,10 +24,12 @@ protocol BottomCallViewProtocol: class { func onMoreButtonPressed() } -class BottomCallView: UIView, SpeakerDelegate { +class BottomCallView: UIView, AudioSessionManagerDelegate { weak var delegate: BottomCallViewProtocol? + private let audioSessionManager = AudioSessionManager.shared + private struct ConstraintConstants { static let cameraButtonCenterXOutGoingCall: CGFloat = -40.0 static let cameraButtonHeight: CGFloat = 45.0 @@ -165,11 +167,11 @@ class BottomCallView: UIView, SpeakerDelegate { lazy var speakerButton : UIButton = { let btn = UIButton() - AudioManager.sharedInstance.speaker = .soft + audioSessionManager.speaker = .soft btn.setImage(#imageLiteral(resourceName: "ic_speaker_off"), for: .normal) btn.setImage(#imageLiteral(resourceName: "ic_speaker_on"), for: .selected) - btn.isSelected = (AudioManager.sharedInstance.speaker == .loud) - AudioManager.sharedInstance.speakerDelegate = self + btn.isSelected = (audioSessionManager.speaker == .loud) + audioSessionManager.delegate = self btn.layer.masksToBounds = true self.addSubview(btn) let bottomPadding = UIScreen.main.bounds.width * 0.13 @@ -614,19 +616,17 @@ class BottomCallView: UIView, SpeakerDelegate { } @objc func speakerButtonPressed() { - let state = AudioManager.sharedInstance.speaker + let state = audioSessionManager.speaker switch state { - case .loud: - AudioManager.sharedInstance.speaker = .soft + case .loud, .unknown: + audioSessionManager.speaker = .soft case .soft: - AudioManager.sharedInstance.speaker = .loud - default:() - AudioManager.sharedInstance.speaker = .soft + audioSessionManager.speaker = .loud } } - func speakerUpdated(state: Speaker) { + func speakerUpdated(to state: AudioSessionManager.Speaker) { speakerButton.isSelected = (state == .loud) } diff --git a/Nynja/Modules/Call/View/GroupAddParticipantsCollectionViewCell.swift b/Nynja/Modules/Call/View/GroupAddParticipantsCollectionViewCell.swift index 8ffaf814b..e5dcc93a3 100644 --- a/Nynja/Modules/Call/View/GroupAddParticipantsCollectionViewCell.swift +++ b/Nynja/Modules/Call/View/GroupAddParticipantsCollectionViewCell.swift @@ -55,8 +55,8 @@ class GroupAddParticipantsCollectionViewCell: UICollectionViewCell { func setup() { - self.backgroundColor = Constants.colors.grayForCallScreenBottom.getColor() - self.contentView.backgroundColor = Constants.colors.grayForCallScreenBottom.getColor() + self.backgroundColor = Constants.colors.callBackground.getColor() + self.contentView.backgroundColor = Constants.colors.callBackground.getColor() let avatar = MyImageViewAdd() let label = UILabel() @@ -81,7 +81,7 @@ class GroupAddParticipantsCollectionViewCell: UICollectionViewCell { self.avatarImageView.addGestureRecognizer(tr) } - labelName.backgroundColor = Constants.colors.grayForCallScreenBottom.getColor() + labelName.backgroundColor = Constants.colors.callBackground.getColor() labelName.textColor = .white labelName.minimumScaleFactor = 0.1 //or whatever suits your need labelName.adjustsFontSizeToFitWidth = true diff --git a/Nynja/Modules/Call/View/GroupCollectionViewCell.swift b/Nynja/Modules/Call/View/GroupCollectionViewCell.swift index 47ad9557c..286797c47 100644 --- a/Nynja/Modules/Call/View/GroupCollectionViewCell.swift +++ b/Nynja/Modules/Call/View/GroupCollectionViewCell.swift @@ -39,8 +39,8 @@ class GroupCollectionViewCell: UICollectionViewCell { func setup() { - self.backgroundColor = Constants.colors.grayForCallScreenBottom.getColor() - self.contentView.backgroundColor = Constants.colors.grayForCallScreenBottom.getColor() + self.backgroundColor = Constants.colors.callBackground.getColor() + self.contentView.backgroundColor = Constants.colors.callBackground.getColor() let avatar = MyImageView() let label = UILabel() @@ -63,7 +63,7 @@ class GroupCollectionViewCell: UICollectionViewCell { self.avatarImageView.addGestureRecognizer(lp) } - labelName.backgroundColor = Constants.colors.grayForCallScreenBottom.getColor() + labelName.backgroundColor = Constants.colors.callBackground.getColor() labelName.textColor = .white labelName.minimumScaleFactor = 0.1 //or whatever suits your need labelName.adjustsFontSizeToFitWidth = true diff --git a/Nynja/Modules/Channel/NewChannel/Presenter/NewChannelPresenter.swift b/Nynja/Modules/Channel/NewChannel/Presenter/NewChannelPresenter.swift index 04dc2f264..8a5648a48 100644 --- a/Nynja/Modules/Channel/NewChannel/Presenter/NewChannelPresenter.swift +++ b/Nynja/Modules/Channel/NewChannel/Presenter/NewChannelPresenter.swift @@ -263,7 +263,7 @@ final class NewChannelPresenter: BasePresenter, NewChannelPresenterProtocol, New } -// MARK: - SetInjectible +// MARK: - SetInjectable extension NewChannelPresenter { struct Dependencies { diff --git a/Nynja/Modules/ChannelsList/ChannelsListProtocols.swift b/Nynja/Modules/ChannelsList/ChannelsListProtocols.swift index d3aaa2b82..adfe44f1a 100644 --- a/Nynja/Modules/ChannelsList/ChannelsListProtocols.swift +++ b/Nynja/Modules/ChannelsList/ChannelsListProtocols.swift @@ -48,7 +48,7 @@ protocol ChannelsListPresenterProtocol: BasePresenterProtocol { /** * Add here your methods for communication VIEW -> PRESENTER */ - func channelSelected(at index: Int) + func tapped(_ channel: Room) func filter(with text: String?) } @@ -58,6 +58,7 @@ protocol ChannelsListInteractorOutputProtocol: class { * Add here your methods for communication INTERACTOR -> PRESENTER */ func channelsFetched(_ channels: [Room]) + func channelsFiltered(_ channels: [Room]) } protocol ChannelsListInteractorInputProtocol: BaseInteractorProtocol { @@ -67,4 +68,5 @@ protocol ChannelsListInteractorInputProtocol: BaseInteractorProtocol { /** * Add here your methods for communication PRESENTER -> INTERACTOR */ + func filter(with text: String) } diff --git a/Nynja/Modules/ChannelsList/Interactor/ChannelsListInteractor.swift b/Nynja/Modules/ChannelsList/Interactor/ChannelsListInteractor.swift index 1b6c8c061..69b2c0e4f 100644 --- a/Nynja/Modules/ChannelsList/Interactor/ChannelsListInteractor.swift +++ b/Nynja/Modules/ChannelsList/Interactor/ChannelsListInteractor.swift @@ -8,64 +8,116 @@ import Foundation -final class ChannelsListInteractor: BaseInteractor, ChannelsListInteractorInputProtocol, SetInjectable { - - override var subscribes: [SubscribeType]? { - return [.room(nil)] - } +final class ChannelsListInteractor: BaseInteractor, ChannelsListInteractorInputProtocol, InitializeInjectable { weak var presenter: ChannelsListInteractorOutputProtocol! - private var conversationsProvider: ConversationsProviding! - private var mode: ChannelsListMode! + private var conversationsProvider: ConversationsProviding + private var mode: ChannelsListMode + private var channels: [Room] = [] + private var searchText: String = "" - // MARK: - ChannelsListInteractorInputProtocol + + // MARK: - InitializeInjectable + + struct Dependencies { + let presenter: ChannelsListInteractorOutputProtocol + let conversationsProvider: ConversationsProviding + + let mode: ChannelsListMode + } + + required init(dependencies: Dependencies) { + presenter = dependencies.presenter + conversationsProvider = dependencies.conversationsProvider + mode = dependencies.mode + } + + override var subscribes: [SubscribeType]? { + return [.room(nil)] + } override func loadData() { super.loadData() fetchChannels() } - private func fetchChannels() { - var channels: [Room] = [] + + // MARK: - ChannelsListInteractorInputProtocol + + func filter(with text: String) { + searchText = text.trimmed() + applyFilter(with: searchText) + } + + + // MARK: - StorageSubscriber + + override func update(with changes: [StorageChange], type: SubscribeType) { + guard case .room = type else { + return + } + if changes.count == 1, let dbRoom = changes.first?.entity as? DBRoom { + let room = Room(room: dbRoom) + if room.kind == .channel { + if mode == .mine && ![.owner, .admin].contains(room.role) { + return + } + + channels = updateChatsList(with: room) + applyFilter(with: searchText) + } + } else { + loadChannels() + applyFilter(with: searchText) + } + } + + + //MARK: - Private + + private func loadChannels() { if mode == .all { channels = conversationsProvider.fetchChannels() } else { channels = conversationsProvider.fetchMyChannels() } - - presenter.channelsFetched(channels) } - // MARK: - StorageSubscriber + private func fetchChannels() { + loadChannels() + presenter.channelsFetched(channels) + } - override func update(with changes: [StorageChange], type: SubscribeType) { - guard let _ = changes.first?.entity as? DBRoom else { - return + private func applyFilter(with text: String) { + if !text.isEmpty { + let filteredChats = channels.filter { group in + guard let fullname = group.name else { + return false + } + return text.isIn(string: fullname, options: .caseInsensitive) + } + + presenter.channelsFiltered(filteredChats) + } else { + presenter.channelsFetched(channels) } - - fetchChannels() } -} - - -// MARK: - SetInjectable - -extension ChannelsListInteractor { - struct Dependencies { - let presenter: ChannelsListInteractorOutputProtocol - let conversationsProvider: ConversationsProviding + private func updateChatsList(with room: Room) -> [Room] { + guard let roomId = room.id else { + return channels + } - let mode: ChannelsListMode - } - - func inject(dependencies: ChannelsListInteractor.Dependencies) { - presenter = dependencies.presenter - conversationsProvider = dependencies.conversationsProvider - mode = dependencies.mode + var newChats = channels + if let index = channels.index(where: { $0.id == roomId }) { + newChats[index] = room + } else { + newChats.append(room) + } + return newChats.sorted(by: conversationsProvider.comparator) } } diff --git a/Nynja/Modules/ChannelsList/Presenter/ChannelsListPresenter.swift b/Nynja/Modules/ChannelsList/Presenter/ChannelsListPresenter.swift index dfd686a2d..5c0ddf4d8 100644 --- a/Nynja/Modules/ChannelsList/Presenter/ChannelsListPresenter.swift +++ b/Nynja/Modules/ChannelsList/Presenter/ChannelsListPresenter.swift @@ -28,10 +28,6 @@ final class ChannelsListPresenter: BasePresenter, ChannelsListPresenterProtocol, private var mode: ChannelsListMode! - private var channels: [Room] = [] - private var currentChannels: [Room] = [] - - override func screenLoaded() { super.screenLoaded() @@ -45,6 +41,7 @@ final class ChannelsListPresenter: BasePresenter, ChannelsListPresenterProtocol, view.screenTitle = title.localized.uppercased() } + // MARK: - ChannelsListPresenterProtocol func filter(with text: String?) { @@ -58,16 +55,10 @@ final class ChannelsListPresenter: BasePresenter, ChannelsListPresenterProtocol, } private func filterChannels(with text: String) { - currentChannels = channels.filter { room in - let name = room.name ?? "" - return text.isIn(string: name, options: .caseInsensitive) - } - - updateCollectionState(for: .search, channels: currentChannels) + interactor.filter(with: text) } - func channelSelected(at index: Int) { - let channel = currentChannels[index] + func tapped(_ channel: Room) { wireFrame.showChannel(channel) } @@ -75,10 +66,11 @@ final class ChannelsListPresenter: BasePresenter, ChannelsListPresenterProtocol, // MARK: - ChannelsListInteractorOutputProtocol func channelsFetched(_ channels: [Room]) { - self.channels = channels - currentChannels = channels - - updateCollectionState(for: .default, channels: currentChannels) + updateCollectionState(for: .default, channels: channels) + } + + func channelsFiltered(_ channels: [Room]) { + updateCollectionState(for: .search, channels: channels) } diff --git a/Nynja/Modules/ChannelsList/View/ChannelsListViewController.swift b/Nynja/Modules/ChannelsList/View/ChannelsListViewController.swift index 84497e8b1..648867bbb 100644 --- a/Nynja/Modules/ChannelsList/View/ChannelsListViewController.swift +++ b/Nynja/Modules/ChannelsList/View/ChannelsListViewController.swift @@ -128,13 +128,15 @@ final class ChannelsListViewController: BaseVC, ChannelsListViewProtocol, SetInj extension ChannelsListViewController: UITableViewDelegate { func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { - presenter.channelSelected(at: indexPath.row) + let index = indexPath.row + let room = dataSource.rooms[index] + presenter.tapped(room) } } -// MARK: - SetInjectible +// MARK: - SetInjectable extension ChannelsListViewController { struct Dependencies { diff --git a/Nynja/Modules/ChannelsList/WireFrame/ChannelsListWireFrame.swift b/Nynja/Modules/ChannelsList/WireFrame/ChannelsListWireFrame.swift index e41b1a519..19077e274 100644 --- a/Nynja/Modules/ChannelsList/WireFrame/ChannelsListWireFrame.swift +++ b/Nynja/Modules/ChannelsList/WireFrame/ChannelsListWireFrame.swift @@ -29,7 +29,10 @@ final class ChannelsListWireFrame: ChannelsListWireFrameProtocol { // Module components let view = ChannelsListViewController() let presenter = ChannelsListPresenter() - let interactor = ChannelsListInteractor() + let interactor = ChannelsListInteractor( + dependencies: .init(presenter: presenter, + conversationsProvider: conversationProvider, + mode: mode)) // Connecting view.inject(dependencies: ChannelsListViewController.Dependencies(presenter: presenter, @@ -38,9 +41,6 @@ final class ChannelsListWireFrame: ChannelsListWireFrameProtocol { interactor: interactor, wireFrame: self, mode: mode)) - interactor.inject(dependencies: ChannelsListInteractor.Dependencies(presenter: presenter, - conversationsProvider: conversationProvider, - mode: mode)) navigation.pushViewController(view, animated: animated) } diff --git a/Nynja/Modules/ChatsList/Interactor/ChatsListInteractor.swift b/Nynja/Modules/ChatsList/Interactor/ChatsListInteractor.swift index c40b768de..02aa17311 100644 --- a/Nynja/Modules/ChatsList/Interactor/ChatsListInteractor.swift +++ b/Nynja/Modules/ChatsList/Interactor/ChatsListInteractor.swift @@ -53,13 +53,18 @@ class ChatsListInteractor: BaseInteractor, ChatsListInteractorInputProtocol, Ini // MARK: - StorageSubscriber override func update(with changes: [StorageChange], type: SubscribeType) { - if case .contact = type { - if let dbContact = changes.first?.entity as? DBContact { - let contact = Contact(contact: dbContact) - chats = updateChatsList(with: contact) - applyFilter(with: searchText) - } + guard case .contact = type else { + return + } + + if changes.count == 1, let dbContact = changes.first?.entity as? DBContact { + let contact = Contact(contact: dbContact) + chats = updateChatsList(with: contact) + } else { + chats = conversationsProvider.fetchChats() } + + applyFilter(with: searchText) } diff --git a/Nynja/Modules/ChatsList/View/ChatsListViewController.swift b/Nynja/Modules/ChatsList/View/ChatsListViewController.swift index 29506c794..c422eca04 100644 --- a/Nynja/Modules/ChatsList/View/ChatsListViewController.swift +++ b/Nynja/Modules/ChatsList/View/ChatsListViewController.swift @@ -8,7 +8,7 @@ import UIKit -class ChatsListViewController: BaseVC, ChatsListViewProtocol, TextInputProtocol, BackSwipable { +final class ChatsListViewController: BaseVC, ChatsListViewProtocol, BackSwipable { var presenter: ChatsListPresenterProtocol! { didSet { @@ -20,14 +20,15 @@ class ChatsListViewController: BaseVC, ChatsListViewProtocol, TextInputProtocol, return [controlContainerView] } - lazy var swipeBackHelper: SwipeBackHelper = { + private(set) lazy var swipeBackHelper: SwipeBackHelper = { return SwipeBackHelper(with: self) }() private var emptyStateDS: EmptyStateTableViewDS! private var dataSource: ChatListTableDS! - // MARK: - Subviews + + // MARK: - Views lazy var tableView: UITableView = { let tv = UITableView.default @@ -70,13 +71,20 @@ class ChatsListViewController: BaseVC, ChatsListViewProtocol, TextInputProtocol, }() - // MARK: - BaseVC + // MARK: - Life Cycle override func initialize() { super.initialize() setupUI() } + override func prepareForDissappear() { + super.prepareForDissappear() + if !swipeBackHelper.isSwipeActive { + endEditing() + } + } + // MARK: - ChatsListViewProtocol @@ -94,16 +102,17 @@ class ChatsListViewController: BaseVC, ChatsListViewProtocol, TextInputProtocol, tableView.reloadData() } + // MARK: - UI Setup private func setupUI() { screenTitle = "chats_list_title".localized.uppercased() - self.navigationView.isSeparatorVisible = true + navigationView.isSeparatorVisible = true swipeBackHelper.addGesture() - self.controlContainerView.isHidden = false - setupTableView() + + view.bringSubview(toFront: controlContainerView) } private func setupTableView() { @@ -124,20 +133,26 @@ class ChatsListViewController: BaseVC, ChatsListViewProtocol, TextInputProtocol, extension ChatsListViewController: UITableViewDelegate { func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { + tableView.deselectRow(at: indexPath, animated: false) + prepareForDissappear() + let index = indexPath.row let contact = dataSource.chatList[index] presenter.tappedContact(contact: contact) } - } // MARK: - ChatListMessageCellModelDelegate extension ChatsListViewController: ChatListMessageCellModelDelegate { } -extension ChatsListViewController { - struct Constraints { - struct controlContainerView { +// MARK: - Layout + +private extension ChatsListViewController { + + enum Constraints { + + enum controlContainerView { static let bottomOffset = 28.adjustedByWidth } } diff --git a/Nynja/Modules/ChatsList/WireFrame/ChatsListWireframe.swift b/Nynja/Modules/ChatsList/WireFrame/ChatsListWireframe.swift index b55a56e88..608c9433c 100644 --- a/Nynja/Modules/ChatsList/WireFrame/ChatsListWireframe.swift +++ b/Nynja/Modules/ChatsList/WireFrame/ChatsListWireframe.swift @@ -38,7 +38,10 @@ class ChatsListWireFrame: ChatsListWireFrameProtocol { guard let navigation = main?.contentNavigation else { return } - MessageWireFrame().presentMessages(navigation: navigation, chat: contact, main: main, initialMessage: nil) + MessageWireFrame().presentMessages(navigation: navigation, + chat: contact, + main: main, + initialMessage: nil, + animated: true) } - } diff --git a/Nynja/Modules/Contacts/Interactor/ContactsInteractor.swift b/Nynja/Modules/Contacts/Interactor/ContactsInteractor.swift index ced32e0d3..5d089698f 100644 --- a/Nynja/Modules/Contacts/Interactor/ContactsInteractor.swift +++ b/Nynja/Modules/Contacts/Interactor/ContactsInteractor.swift @@ -65,7 +65,7 @@ class ContactsInteractor: BaseInteractor, ContactsInteractorInputProtocol, IoHan } func add(contact: Contact) { - if contact.originalStatus == .authorization { + if contact.hasPendingIncomingRequest() { mqttService.confirmFriend(friendPhoneId: contact.phoneId) } else { mqttService.friendRequest(friendPhoneId: contact.phoneId) diff --git a/Nynja/Modules/Contacts/View/ViewController/ContactsViewController.swift b/Nynja/Modules/Contacts/View/ViewController/ContactsViewController.swift index b43c0e49a..ee8a624d0 100644 --- a/Nynja/Modules/Contacts/View/ViewController/ContactsViewController.swift +++ b/Nynja/Modules/Contacts/View/ViewController/ContactsViewController.swift @@ -8,7 +8,7 @@ import UIKit -class ContactsViewController: BaseVC, ContactsViewProtocol, ContactCellDelegate, FastScrollable { +class ContactsViewController: BaseVC, ContactsViewProtocol, ContactCellDelegate, FastScrollable, KeyboardInteractive { var presenter: ContactsPresenterProtocol! { didSet { @@ -110,7 +110,7 @@ class ContactsViewController: BaseVC, ContactsViewProtocol, ContactCellDelegate, //MARK: - Keyboard - override func keyboardNotified(endFrame: CGRect) { + func keyboardNotified(endFrame: CGRect) { if endFrame.origin.y >= UIScreen.main.bounds.size.height { updateToHide(view: controlContainerView, offset: -bottomInset) } else { diff --git a/Nynja/Modules/CreateGroup/CreateGroupProtocols.swift b/Nynja/Modules/CreateGroup/CreateGroupProtocols.swift index 0ce0d54b6..17b5b3db2 100644 --- a/Nynja/Modules/CreateGroup/CreateGroupProtocols.swift +++ b/Nynja/Modules/CreateGroup/CreateGroupProtocols.swift @@ -20,7 +20,7 @@ protocol CreateGroupWireFrameProtocol: class { * Add here your methods for communication PRESENTER -> WIREFRAME */ func changeGroupName(name: String) - func changeAlias(alias: String, nicks: [String]) + func changeAlias(alias: String) func updateParticipants(contacts: [Contact]) func updateAvatar() func showGroupChat(room: Room) diff --git a/Nynja/Modules/CreateGroup/Presenter/CreateGroupPresenter.swift b/Nynja/Modules/CreateGroup/Presenter/CreateGroupPresenter.swift index 2105d1f1c..4176a20c1 100644 --- a/Nynja/Modules/CreateGroup/Presenter/CreateGroupPresenter.swift +++ b/Nynja/Modules/CreateGroup/Presenter/CreateGroupPresenter.swift @@ -38,8 +38,7 @@ class CreateGroupPresenter: BasePresenter, CreateGroupPresenterProtocol, CreateG if let nick = mySelf.nick { myAlias = nick } else { - let fullName = mySelf.fullName ?? "" - myAlias = fullName.replacingOccurrences(of: " ", with: "_") + myAlias = mySelf.fullName ?? "" } } @@ -69,11 +68,7 @@ class CreateGroupPresenter: BasePresenter, CreateGroupPresenterProtocol, CreateG } func changeAlias() { - if let nicks = room.members?.map({ member -> String in - return member.alias ?? "" - }) { - self.wireFrame.changeAlias(alias: myAlias, nicks: nicks) - } + self.wireFrame.changeAlias(alias: myAlias) } func updateParticipants() { diff --git a/Nynja/Modules/CreateGroup/WireFrame/CreateGroupWireframe.swift b/Nynja/Modules/CreateGroup/WireFrame/CreateGroupWireframe.swift index ed4d92797..8fcddd641 100644 --- a/Nynja/Modules/CreateGroup/WireFrame/CreateGroupWireframe.swift +++ b/Nynja/Modules/CreateGroup/WireFrame/CreateGroupWireframe.swift @@ -38,8 +38,8 @@ class CreateGroupWireFrame: CreateGroupWireFrameProtocol { self.navigation?.view.layoutIfNeeded() } - func changeAlias(alias: String, nicks: [String]) { - MyGroupAliasWireFrame().presentMyGroupAlias(navigation: navigation!, currentAlias: alias, delegate: external,nicks: nicks, mode: .create) + func changeAlias(alias: String) { + MyGroupAliasWireFrame().presentMyGroupAlias(navigation: navigation!, currentAlias: alias, delegate: external, mode: .create) self.navigation?.view.layoutIfNeeded() } diff --git a/Nynja/Modules/EditProfile/View/EditName/EditProfileVC.swift b/Nynja/Modules/EditProfile/View/EditName/EditProfileVC.swift index 383dbb7bf..7c2547232 100644 --- a/Nynja/Modules/EditProfile/View/EditName/EditProfileVC.swift +++ b/Nynja/Modules/EditProfile/View/EditName/EditProfileVC.swift @@ -8,7 +8,7 @@ import UIKit -class EditProfileVC: BaseVC, EditProfileViewProtocol { +class EditProfileVC: BaseVC, EditProfileViewProtocol, KeyboardInteractive { var presenter: EditProfilePresenterProtocol! { didSet { @@ -147,7 +147,7 @@ class EditProfileVC: BaseVC, EditProfileViewProtocol { } // MARK: - BaseVC - override func keyboardNotified(endFrame: CGRect) { + func keyboardNotified(endFrame: CGRect) { if endFrame.origin.y >= UIScreen.main.bounds.size.height { self.doneButton.snp.updateConstraints({ (make) in make.bottom.equalTo(-bottomInset).priority(bottomPriority) diff --git a/Nynja/Modules/EditProfile/View/EditProfileViewController.swift b/Nynja/Modules/EditProfile/View/EditProfileViewController.swift index d267c71e7..d9ed7054b 100644 --- a/Nynja/Modules/EditProfile/View/EditProfileViewController.swift +++ b/Nynja/Modules/EditProfile/View/EditProfileViewController.swift @@ -8,7 +8,7 @@ import UIKit -class EditProfileViewController: BaseVC, EditProfileViewProtocol { +class EditProfileViewController: BaseVC, EditProfileViewProtocol, KeyboardInteractive { var presenter: EditProfilePresenterProtocol! { didSet { @@ -154,7 +154,7 @@ class EditProfileViewController: BaseVC, EditProfileViewProtocol { } // MARK: - BaseVC - override func keyboardNotified(endFrame: CGRect) { + func keyboardNotified(endFrame: CGRect) { let bottomInset = Constraints.doneButton.bottomInset.adjustedByHeight if endFrame.origin.y >= UIScreen.main.bounds.size.height { self.doneButton.snp.updateConstraints({ (make) in diff --git a/Nynja/Modules/EditUsername/View/EditUsernameViewController.swift b/Nynja/Modules/EditUsername/View/EditUsernameViewController.swift index 4bc461b32..50ea414d2 100644 --- a/Nynja/Modules/EditUsername/View/EditUsernameViewController.swift +++ b/Nynja/Modules/EditUsername/View/EditUsernameViewController.swift @@ -8,7 +8,7 @@ import UIKit -class EditUsernameViewController: BaseVC, EditUsernameViewProtocol { +class EditUsernameViewController: BaseVC, EditUsernameViewProtocol, KeyboardInteractive { var presenter: EditUsernamePresenterProtocol! { didSet { @@ -167,7 +167,7 @@ class EditUsernameViewController: BaseVC, EditUsernameViewProtocol { } // MARK: - BaseVC - override func keyboardNotified(endFrame: CGRect) { + func keyboardNotified(endFrame: CGRect) { if endFrame.origin.y >= UIScreen.main.bounds.size.height { self.doneButton.snp.updateConstraints({ (make) in make.bottom.equalTo(-bottomInset).priority(bottomPriority) diff --git a/Nynja/Modules/Favorites/View/FavoritesViewController.swift b/Nynja/Modules/Favorites/View/FavoritesViewController.swift index 040204246..d28d98b67 100644 --- a/Nynja/Modules/Favorites/View/FavoritesViewController.swift +++ b/Nynja/Modules/Favorites/View/FavoritesViewController.swift @@ -25,7 +25,7 @@ class FavoritesViewController: BaseVC, FavoritesViewProtocol, ItemSelectorDelega private var allStars = [Star]() private var filtered = [Star]() - lazy var swipeBackHelper: SwipeBackHelper = { + private(set) lazy var swipeBackHelper: SwipeBackHelper = { return SwipeBackHelper(with: self) }() diff --git a/Nynja/Modules/Favorites/WireFrame/FavoritesWireframe.swift b/Nynja/Modules/Favorites/WireFrame/FavoritesWireframe.swift index 93835fd8b..a296e7d17 100644 --- a/Nynja/Modules/Favorites/WireFrame/FavoritesWireframe.swift +++ b/Nynja/Modules/Favorites/WireFrame/FavoritesWireframe.swift @@ -34,10 +34,11 @@ class FavoritesWireFrame: FavoritesWireFrameProtocol { } func showChat(with contact: Contact, message: Message? = nil) { - if (contact.status as? StringAtom)?.string != "authorization" { - let initialMessage = ChatInitialMessage(localId: message?.msg_id, serverId: message?.id) - main?.showChat(contact, initialMessage: initialMessage) + guard !contact.hasPendingIncomingRequest() else { + return } + let initialMessage = ChatInitialMessage(localId: message?.msg_id, serverId: message?.id) + main?.showChat(contact, initialMessage: initialMessage) } func showGroupChat(with room: Room, message: Message? = nil) { diff --git a/Nynja/Modules/Flows/CameraFlow/Camera/View/CameraViewController.swift b/Nynja/Modules/Flows/CameraFlow/Camera/View/CameraViewController.swift index b9f9b1eaf..602ebb6c8 100644 --- a/Nynja/Modules/Flows/CameraFlow/Camera/View/CameraViewController.swift +++ b/Nynja/Modules/Flows/CameraFlow/Camera/View/CameraViewController.swift @@ -154,7 +154,12 @@ final class CameraViewController: UIViewController, CameraViewProtocol, SetInjec super.viewDidDisappear(animated) captureSession?.stopRunning() + + DispatchQueue.global.async { + try? AudioSessionManager.shared.resetAudioSession() + } } + } //MARK: - Actions diff --git a/Nynja/Modules/Flows/CameraFlow/CameraCoordinator.swift b/Nynja/Modules/Flows/CameraFlow/CameraCoordinator.swift index 1f712e574..beff13979 100644 --- a/Nynja/Modules/Flows/CameraFlow/CameraCoordinator.swift +++ b/Nynja/Modules/Flows/CameraFlow/CameraCoordinator.swift @@ -99,6 +99,7 @@ extension CameraFlowCoordinator { parameters: CameraVideoPreviewWireframe.Parameters(videoURL: videoURL, contact: contact, room: room, isCanSave: true), dependencies: CameraVideoPreviewWireframe.Dependencies( resourceManager: serviceFactory.makeResourceManager(), + audioSessionManager: serviceFactory.makeAudioSessionManager(), messageFactory: serviceFactory.makeMessageFactory(), messageSendingService: serviceFactory.makeMessageSendingService(), cameraSettingsService: serviceFactory.makeCameraSettingsService(with: .cameraFlow))) diff --git a/Nynja/Modules/Flows/CameraFlow/VideoPreview/CameraVideoPreviewProtocols.swift b/Nynja/Modules/Flows/CameraFlow/VideoPreview/CameraVideoPreviewProtocols.swift index 63bf537b8..a9dfe437b 100644 --- a/Nynja/Modules/Flows/CameraFlow/VideoPreview/CameraVideoPreviewProtocols.swift +++ b/Nynja/Modules/Flows/CameraFlow/VideoPreview/CameraVideoPreviewProtocols.swift @@ -48,6 +48,8 @@ protocol CameraVideoPreviewInputInteractorProtocol { func getVideoUrl() -> URL func getVideoDuration() -> Int + func prepareToSend() + func save() func send() func sendAsFile() diff --git a/Nynja/Modules/Flows/CameraFlow/VideoPreview/Interactor/CameraVideoPreviewInteractor.swift b/Nynja/Modules/Flows/CameraFlow/VideoPreview/Interactor/CameraVideoPreviewInteractor.swift index fa2a3732e..a16aca9ec 100644 --- a/Nynja/Modules/Flows/CameraFlow/VideoPreview/Interactor/CameraVideoPreviewInteractor.swift +++ b/Nynja/Modules/Flows/CameraFlow/VideoPreview/Interactor/CameraVideoPreviewInteractor.swift @@ -19,6 +19,7 @@ final class CameraVideoPreviewInteractor: CameraVideoPreviewInputInteractorProto private(set) var isCanSave: Bool = true private var resourceManager: ResourceManagerProtocol! + private var audioSessionManager: AudioSessionManager! private var messageFactory: MessageFactoryProtocol! private var messageSendingService: MessageSendingServiceProtocol! private var camersSettingsService: CameraSettingsServiceProtocol! @@ -44,6 +45,7 @@ extension CameraVideoPreviewInteractor { let isCanSave: Bool let resourceManager: ResourceManagerProtocol + let audioSessionManager: AudioSessionManager let messageFactory: MessageFactoryProtocol let messageSendingService: MessageSendingServiceProtocol @@ -63,6 +65,9 @@ extension CameraVideoPreviewInteractor { messageSendingService = dependencies.messageSendingService camersSettingsService = dependencies.camersSettingsService presenter = dependencies.presenter + + audioSessionManager = dependencies.audioSessionManager + try? audioSessionManager.addCategoryOptions(.defaultToSpeaker) } } @@ -96,6 +101,14 @@ extension CameraVideoPreviewInteractor { } } } + + func prepareToSend() { + stopReproducing() + + DispatchQueue.global.async { [weak audioSessionManager = audioSessionManager] in + try? audioSessionManager?.resetAudioSession() + } + } func send() { convert(url: sourceURL) { [weak self] (url) in @@ -120,6 +133,8 @@ extension CameraVideoPreviewInteractor { } func startReproducing() { + try? audioSessionManager.setActive(true) + timer = Timer.scheduledTimer(timeInterval: 0.25, target: self, selector: #selector(tick), userInfo: nil, repeats: true) timer?.fire() diff --git a/Nynja/Modules/Flows/CameraFlow/VideoPreview/Presenter/CameraVideoPreviewPresenter.swift b/Nynja/Modules/Flows/CameraFlow/VideoPreview/Presenter/CameraVideoPreviewPresenter.swift index 4b5dc2480..82377034a 100644 --- a/Nynja/Modules/Flows/CameraFlow/VideoPreview/Presenter/CameraVideoPreviewPresenter.swift +++ b/Nynja/Modules/Flows/CameraFlow/VideoPreview/Presenter/CameraVideoPreviewPresenter.swift @@ -46,13 +46,13 @@ extension CameraVideoPreviewPresenter { } func send() { - interactor.stopReproducing() + interactor.prepareToSend() interactor.send() wireframe.end(from: view) } func sendAsFile() { - interactor.stopReproducing() + interactor.prepareToSend() interactor.sendAsFile() wireframe.end(from: view) } diff --git a/Nynja/Modules/Flows/CameraFlow/VideoPreview/View/CameraVideoPreviewViewController.swift b/Nynja/Modules/Flows/CameraFlow/VideoPreview/View/CameraVideoPreviewViewController.swift index d04963c76..5c4be09f7 100644 --- a/Nynja/Modules/Flows/CameraFlow/VideoPreview/View/CameraVideoPreviewViewController.swift +++ b/Nynja/Modules/Flows/CameraFlow/VideoPreview/View/CameraVideoPreviewViewController.swift @@ -94,8 +94,6 @@ final class CameraVideoPreviewViewController: UIViewController, CameraVideoPrevi setupNotifications() timerView.setTimeValue(seconds: presenter.getVideoLenghtInSeconds()) - - try? AudioSessionManager.shared.request(category: .playback) } open override func viewWillAppear(_ animated: Bool) { diff --git a/Nynja/Modules/Flows/CameraFlow/VideoPreview/Wireframe/CameraVideoPreviewWireframe.swift b/Nynja/Modules/Flows/CameraFlow/VideoPreview/Wireframe/CameraVideoPreviewWireframe.swift index be2d0b743..f82dbb9b0 100644 --- a/Nynja/Modules/Flows/CameraFlow/VideoPreview/Wireframe/CameraVideoPreviewWireframe.swift +++ b/Nynja/Modules/Flows/CameraFlow/VideoPreview/Wireframe/CameraVideoPreviewWireframe.swift @@ -31,6 +31,7 @@ extension CameraVideoPreviewWireframe { struct Dependencies { let resourceManager: ResourceManagerProtocol + let audioSessionManager: AudioSessionManager let messageFactory: MessageFactoryProtocol let messageSendingService: MessageSendingServiceProtocol let cameraSettingsService: CameraSettingsServiceProtocol @@ -57,6 +58,7 @@ extension CameraVideoPreviewWireframe { room: parameters.room, isCanSave: parameters.isCanSave, resourceManager: dependencies.resourceManager, + audioSessionManager: dependencies.audioSessionManager, messageFactory: dependencies.messageFactory, messageSendingService: dependencies.messageSendingService, presenter: presenter, diff --git a/Nynja/Modules/Flows/GalleryFlow/GalleryCoordinator.swift b/Nynja/Modules/Flows/GalleryFlow/GalleryCoordinator.swift index da7fbb5d4..104dc33ad 100644 --- a/Nynja/Modules/Flows/GalleryFlow/GalleryCoordinator.swift +++ b/Nynja/Modules/Flows/GalleryFlow/GalleryCoordinator.swift @@ -170,13 +170,14 @@ extension GalleryFlowCoordinator { mainView.present(view, animated: false, completion: nil) } - private func galleryWireframe(_ galleryWireframe: GalleryWireframe, with mainView: UIViewController, didEndWith viewoURL: URL) { + private func galleryWireframe(_ galleryWireframe: GalleryWireframe, with mainView: UIViewController, didEndWith videoURL: URL) { let wireframe = CameraVideoPreviewWireframe(coordinator: self) let view = wireframe.prepareModule( - parameters: CameraVideoPreviewWireframe.Parameters(videoURL: viewoURL, contact: contact, room: room, isCanSave: false), + parameters: CameraVideoPreviewWireframe.Parameters(videoURL: videoURL, contact: contact, room: room, isCanSave: false), dependencies: CameraVideoPreviewWireframe.Dependencies( resourceManager: serviceFactory.makeResourceManager(), + audioSessionManager: serviceFactory.makeAudioSessionManager(), messageFactory: serviceFactory.makeMessageFactory(), messageSendingService: serviceFactory.makeMessageSendingService(), cameraSettingsService: serviceFactory.makeCameraSettingsService(with: sourceFlow))) @@ -190,6 +191,7 @@ extension GalleryFlowCoordinator { parameters: MultiplePreviewWireframe.Parameters(activeItem: activeItem), dependencies: MultiplePreviewWireframe.Dependencies( resourceManager: serviceFactory.makeResourceManager(), + audioSessionManager: serviceFactory.makeAudioSessionManager(), galleryDataSource: galleryDataSource, cameraSettingsService: serviceFactory.makeCameraSettingsService(with: sourceFlow))) diff --git a/Nynja/Modules/Flows/GalleryFlow/MultiplePreview/Interactor/MultiplePreviewInteractor.swift b/Nynja/Modules/Flows/GalleryFlow/MultiplePreview/Interactor/MultiplePreviewInteractor.swift index 4de7f6b35..b50806547 100644 --- a/Nynja/Modules/Flows/GalleryFlow/MultiplePreview/Interactor/MultiplePreviewInteractor.swift +++ b/Nynja/Modules/Flows/GalleryFlow/MultiplePreview/Interactor/MultiplePreviewInteractor.swift @@ -13,6 +13,7 @@ final class MultiplePreviewInteractor: MultiplePreviewInputInteractorProtocol, S private weak var presenter: MultiplePreviewOutputInteractorProtocol! private var activeItem: GalleryItem! private var resourceManager: ResourceManagerProtocol! + private var audioSessionManager: AudioSessionManager! private var galleryDataSource: GalleryDataSourceInterface! private var cameraSettingsService: CameraSettingsServiceProtocol! @@ -41,10 +42,20 @@ extension MultiplePreviewInteractor { return Int(durationTime) } + + func prepareToBack() { + stopVideoPlaying() + + DispatchQueue.global.async { [weak audioSessionManager = audioSessionManager] in + try? audioSessionManager?.resetAudioSession() + } + } func startVideoPlaying() { isVideoPlaying = true + try? audioSessionManager.setActive(true) + timer = Timer.scheduledTimer(timeInterval: 0.25, target: self, selector: #selector(tick), userInfo: nil, repeats: true) timer?.fire() @@ -132,6 +143,7 @@ extension MultiplePreviewInteractor { let presenter: MultiplePreviewOutputInteractorProtocol let activeItem: GalleryItem let resourceManager: ResourceManagerProtocol + let audioSessionManager: AudioSessionManager let galleryDataSource: GalleryDataSourceInterface let cameraSettingsService: CameraSettingsServiceProtocol } @@ -143,5 +155,8 @@ extension MultiplePreviewInteractor { galleryDataSource = dependencies.galleryDataSource cameraSettingsService = dependencies.cameraSettingsService galleryDataSource.addlistener(self) + + audioSessionManager = dependencies.audioSessionManager + try? audioSessionManager.addCategoryOptions(.defaultToSpeaker) } } diff --git a/Nynja/Modules/Flows/GalleryFlow/MultiplePreview/MultiplePreviewProtocols.swift b/Nynja/Modules/Flows/GalleryFlow/MultiplePreview/MultiplePreviewProtocols.swift index dcca690b5..30561ea9b 100644 --- a/Nynja/Modules/Flows/GalleryFlow/MultiplePreview/MultiplePreviewProtocols.swift +++ b/Nynja/Modules/Flows/GalleryFlow/MultiplePreview/MultiplePreviewProtocols.swift @@ -61,6 +61,8 @@ protocol MultiplePreviewInputInteractorProtocol { func getVideoDuration() -> Int + func prepareToBack() + func startVideoPlaying() func stopVideoPlaying() func endVideoPlaying() diff --git a/Nynja/Modules/Flows/GalleryFlow/MultiplePreview/Presenter/MultiplePreviewPresenter.swift b/Nynja/Modules/Flows/GalleryFlow/MultiplePreview/Presenter/MultiplePreviewPresenter.swift index 8b9c220a0..0caef57ef 100644 --- a/Nynja/Modules/Flows/GalleryFlow/MultiplePreview/Presenter/MultiplePreviewPresenter.swift +++ b/Nynja/Modules/Flows/GalleryFlow/MultiplePreview/Presenter/MultiplePreviewPresenter.swift @@ -28,7 +28,7 @@ extension MultiplePreviewPresenter { } func back() { - interactor.stopVideoPlaying() + interactor.prepareToBack() wireframe.back(from: view as! UIViewController) } @@ -94,7 +94,8 @@ extension MultiplePreviewPresenter { func prepareContentForEnding(completion: @escaping ([(ResourceManagerMediaType, URL)]) -> Void) { view.showHUD() - interactor.stopVideoPlaying() + view.videoDidStopPlaying() + interactor.prepareToBack() interactor.fetchSelected { [weak self] (arr) in DispatchQueue.main.async { self?.view.hideHUD() diff --git a/Nynja/Modules/Flows/GalleryFlow/MultiplePreview/View/MultiplePreviewViewController.swift b/Nynja/Modules/Flows/GalleryFlow/MultiplePreview/View/MultiplePreviewViewController.swift index d1ff64a11..de5f57642 100644 --- a/Nynja/Modules/Flows/GalleryFlow/MultiplePreview/View/MultiplePreviewViewController.swift +++ b/Nynja/Modules/Flows/GalleryFlow/MultiplePreview/View/MultiplePreviewViewController.swift @@ -260,8 +260,6 @@ extension MultiplePreviewViewController { setupNotifications() view.layoutIfNeeded() - - try? AudioSessionManager.shared.request(category: .playback) } } diff --git a/Nynja/Modules/Flows/GalleryFlow/MultiplePreview/Wireframe/MultiplePreviewWireframe.swift b/Nynja/Modules/Flows/GalleryFlow/MultiplePreview/Wireframe/MultiplePreviewWireframe.swift index 85ba271d7..c81b267c8 100644 --- a/Nynja/Modules/Flows/GalleryFlow/MultiplePreview/Wireframe/MultiplePreviewWireframe.swift +++ b/Nynja/Modules/Flows/GalleryFlow/MultiplePreview/Wireframe/MultiplePreviewWireframe.swift @@ -29,6 +29,7 @@ extension MultiplePreviewWireframe { struct Dependencies { let resourceManager: ResourceManagerProtocol + let audioSessionManager: AudioSessionManager let galleryDataSource: GalleryDataSourceInterface let cameraSettingsService: CameraSettingsServiceProtocol } @@ -47,7 +48,13 @@ extension MultiplePreviewWireframe { let viewDependencies = MultiplePreviewViewController.Dependencies(presenter: presenter) let presenterDependencies = MultiplePreviewPresenter.Dependencies(wireframe: self, view: view, interactor: interactor) - let interactorDependencies = MultiplePreviewInteractor.Dependencies(presenter: presenter, activeItem: parameters.activeItem, resourceManager: dependencies.resourceManager, galleryDataSource: dependencies.galleryDataSource, cameraSettingsService: dependencies.cameraSettingsService) + let interactorDependencies = MultiplePreviewInteractor.Dependencies( + presenter: presenter, + activeItem: parameters.activeItem, + resourceManager: dependencies.resourceManager, + audioSessionManager: dependencies.audioSessionManager, + galleryDataSource: dependencies.galleryDataSource, + cameraSettingsService: dependencies.cameraSettingsService) presenter.inject(dependencies: presenterDependencies) interactor.inject(dependencies: interactorDependencies) diff --git a/Nynja/Modules/ForwardSelector/Models/ForwardTargets.swift b/Nynja/Modules/ForwardSelector/Models/ForwardTargets.swift index 746031e07..5a3c35c8e 100644 --- a/Nynja/Modules/ForwardSelector/Models/ForwardTargets.swift +++ b/Nynja/Modules/ForwardSelector/Models/ForwardTargets.swift @@ -12,6 +12,10 @@ struct ForwardTargets { var contacts: [Contact] var groups: [Room] + var isEmpty: Bool { + return contacts.isEmpty && groups.isEmpty + } + init(contacts: [Contact], groups: [Room]) { self.contacts = contacts self.groups = groups diff --git a/Nynja/Modules/ForwardSelector/View/ViewController/ForwardSelectorViewController.swift b/Nynja/Modules/ForwardSelector/View/ViewController/ForwardSelectorViewController.swift index ce7420dc3..9b3cd8d97 100644 --- a/Nynja/Modules/ForwardSelector/View/ViewController/ForwardSelectorViewController.swift +++ b/Nynja/Modules/ForwardSelector/View/ViewController/ForwardSelectorViewController.swift @@ -8,7 +8,7 @@ import UIKit -final class ForwardSelectorViewController: BaseVC, ForwardSelectorViewProtocol { +final class ForwardSelectorViewController: BaseVC, ForwardSelectorViewProtocol, KeyboardInteractive { var presenter: ForwardSelectorPresenterProtocol! { didSet { @@ -263,7 +263,7 @@ final class ForwardSelectorViewController: BaseVC, ForwardSelectorViewProtocol { // MARK: - Keyboard - override func keyboardNotified(endFrame: CGRect) { + func keyboardNotified(endFrame: CGRect) { if endFrame.origin.y >= UIScreen.main.bounds.size.height { updateToHide(view: bottomActionsView, offset: -bottomInset) } else { diff --git a/Nynja/Modules/GroupRules/View/GroupRulesViewController.swift b/Nynja/Modules/GroupRules/View/GroupRulesViewController.swift index f9a3f60eb..6f20791a0 100644 --- a/Nynja/Modules/GroupRules/View/GroupRulesViewController.swift +++ b/Nynja/Modules/GroupRules/View/GroupRulesViewController.swift @@ -9,7 +9,7 @@ import UIKit import SnapKit -class GroupRulesViewController: BaseVC, GroupRulesViewProtocol { +class GroupRulesViewController: BaseVC, GroupRulesViewProtocol, KeyboardInteractive { var presenter: GroupRulesPresenterProtocol! { didSet { @@ -155,7 +155,7 @@ class GroupRulesViewController: BaseVC, GroupRulesViewProtocol { // MARK: - Keyboard - override func keyboardNotified(endFrame: CGRect) { + func keyboardNotified(endFrame: CGRect) { if isKeyboardGoingToHide(endFrame) { updateToHide(view: controlsContainer, offset: 0) } else { diff --git a/Nynja/Modules/GroupStorage/View/GroupStorageListVC.swift b/Nynja/Modules/GroupStorage/View/GroupStorageListVC.swift index afbdace84..78d8c5776 100644 --- a/Nynja/Modules/GroupStorage/View/GroupStorageListVC.swift +++ b/Nynja/Modules/GroupStorage/View/GroupStorageListVC.swift @@ -213,7 +213,6 @@ class GroupStorageListVC : UIViewController, GroupStorageCellDelegate, ContextMe delegate?.pathForUrl(url, completion: { (path) in if path != nil { - MQTTService.sharedInstance.disconnect() let localUrl = URL(fileURLWithPath: path!) AlertManager.sharedInstance.showNativeShare(with: [localUrl]) } diff --git a/Nynja/Modules/GroupsList/GroupsListProtocols.swift b/Nynja/Modules/GroupsList/GroupsListProtocols.swift index febeb35ca..6f973d065 100644 --- a/Nynja/Modules/GroupsList/GroupsListProtocols.swift +++ b/Nynja/Modules/GroupsList/GroupsListProtocols.swift @@ -65,6 +65,5 @@ protocol GroupsListInteractorInputProtocol: BaseInteractorProtocol { var currentUserAccountId: String? { get } - init(conversationsProvider: ConversationsProviding) func filter(with text: String) } diff --git a/Nynja/Modules/GroupsList/Interactor/GroupsListInteractor.swift b/Nynja/Modules/GroupsList/Interactor/GroupsListInteractor.swift index e52d77196..c16ee18b5 100644 --- a/Nynja/Modules/GroupsList/Interactor/GroupsListInteractor.swift +++ b/Nynja/Modules/GroupsList/Interactor/GroupsListInteractor.swift @@ -6,13 +6,28 @@ // Copyright © 2017 TecSynt Solutions. All rights reserved. // -class GroupsListInteractor: BaseInteractor, GroupsListInteractorInputProtocol { +class GroupsListInteractor: BaseInteractor, GroupsListInteractorInputProtocol, InitializeInjectable { weak var presenter: GroupsListInteractorOutputProtocol! private let storageService: StorageService private let conversationsProvider: ConversationsProviding + private var chats: [Room] = [] + private var searchText: String = "" + + + // MARK: - InitializeInjectable + + struct Dependencies { + let storageService: StorageService + let conversationsProvider: ConversationsProviding + } + + required init(dependencies: Dependencies) { + storageService = dependencies.storageService + conversationsProvider = dependencies.conversationsProvider + } // MARK: - BaseInteractor @@ -32,42 +47,65 @@ class GroupsListInteractor: BaseInteractor, GroupsListInteractorInputProtocol { // MARK: - GroupsListInteractorInputProtocol - required init(conversationsProvider: ConversationsProviding) { - storageService = StorageService.sharedInstance - self.conversationsProvider = conversationsProvider + func filter(with text: String) { + searchText = text.trimmed() + applyFilter(with: searchText) } + + // MARK: - StorageSubscriber + + override func update(with changes: [StorageChange], type: SubscribeType) { + guard case .room = type else { + return + } + + if changes.count == 1, let dbRoom = changes.first?.entity as? DBRoom { + let room = Room(room: dbRoom) + if room.kind == .group { + chats = updateChatsList(with: room) + applyFilter(with: searchText) + } + } else { + chats = conversationsProvider.fetchGroups() + applyFilter(with: searchText) + } + } + + + //MARK: - Private + private func fetchGroups(_ filter: String? = nil) { - let groups = conversationsProvider.fetchGroups() - presenter.didFetch(groups: groups) + chats = conversationsProvider.fetchGroups() + presenter.didFetch(groups: chats) } - func filter(with text: String) { - let text = text.trimmed() - + private func applyFilter(with text: String) { if !text.isEmpty { - var groups = conversationsProvider.fetchGroups() - - groups = groups.filter { group in - guard let name = group.name else { + let filteredChats = chats.filter { group in + guard let fullname = group.name else { return false } - return text.isIn(string: name, options: .caseInsensitive) + return text.isIn(string: fullname, options: .caseInsensitive) } - presenter.didFilter(groups: groups) + presenter.didFilter(groups: filteredChats) } else { - fetchGroups() + presenter.didFetch(groups: chats) } } - - // MARK: - StorageSubscriber - - override func update(with changes: [StorageChange], type: SubscribeType) { - if case .room = type { - fetchGroups() + private func updateChatsList(with room: Room) -> [Room] { + guard let roomId = room.id else { + return chats } + + var newChats = chats + if let index = chats.index(where: { $0.id == roomId }) { + newChats[index] = room + } else { + newChats.append(room) + } + return newChats.sorted(by: conversationsProvider.comparator) } - } diff --git a/Nynja/Modules/GroupsList/View/GroupsListViewController.swift b/Nynja/Modules/GroupsList/View/GroupsListViewController.swift index 0a64523a3..780533112 100644 --- a/Nynja/Modules/GroupsList/View/GroupsListViewController.swift +++ b/Nynja/Modules/GroupsList/View/GroupsListViewController.swift @@ -8,7 +8,7 @@ import UIKit -class GroupsListViewController: BaseVC, GroupsListViewProtocol, BackSwipable { +final class GroupsListViewController: BaseVC, GroupsListViewProtocol, BackSwipable { var presenter: GroupsListPresenterProtocol! { didSet { @@ -20,7 +20,7 @@ class GroupsListViewController: BaseVC, GroupsListViewProtocol, BackSwipable { return [controlContainerView] } - lazy var swipeBackHelper: SwipeBackHelper = { + private(set) lazy var swipeBackHelper: SwipeBackHelper = { return SwipeBackHelper(with: self) }() @@ -70,32 +70,37 @@ class GroupsListViewController: BaseVC, GroupsListViewProtocol, BackSwipable { return searchField }() - // MARK: - View lifecycle - - override func viewWillAppear(_ animated: Bool) { - super.viewWillAppear(animated) - presenter.viewWillAppear() - } - - // MARK: - BaseVC + // MARK: - View lifecycle override func initialize() { super.initialize() - setupUI() } + override func prepareForDissappear() { + super.prepareForDissappear() + if !swipeBackHelper.isSwipeActive { + endEditing() + } + } + + override func viewWillAppear(_ animated: Bool) { + super.viewWillAppear(animated) + presenter.viewWillAppear() + } + // MARK: - UI Setup private func setupUI() { - self.swipeBackHelper.addGesture() - - self.navigationView.isSeparatorVisible = true + navigationView.isSeparatorVisible = true screenTitle = Strings.groupsListTitle.localized.uppercased() + swipeBackHelper.addGesture() setupTableView() + + view.bringSubview(toFront: controlContainerView) } private func setupTableView() { @@ -133,7 +138,6 @@ class GroupsListViewController: BaseVC, GroupsListViewProtocol, BackSwipable { return self.rawValue.localized } } - } @@ -142,10 +146,12 @@ class GroupsListViewController: BaseVC, GroupsListViewProtocol, BackSwipable { extension GroupsListViewController: UITableViewDelegate { func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { + tableView.deselectRow(at: indexPath, animated: false) + prepareForDissappear() + let group = dataSource.rooms[indexPath.row] presenter.tappedGroup(group) } - } @@ -159,15 +165,16 @@ extension GroupsListViewController: ChatListMessageCellModelDelegate { } return presenter.senderInfo(for: room) } - } // MARK: - Layout -extension GroupsListViewController { - struct Constraints { - struct controlContainerView { +private extension GroupsListViewController { + + enum Constraints { + + enum controlContainerView { static let bottomOffset = 28.adjustedByWidth } } diff --git a/Nynja/Modules/GroupsList/WireFrame/GroupsListWireframe.swift b/Nynja/Modules/GroupsList/WireFrame/GroupsListWireframe.swift index fc3034cd1..3db6d4df5 100644 --- a/Nynja/Modules/GroupsList/WireFrame/GroupsListWireframe.swift +++ b/Nynja/Modules/GroupsList/WireFrame/GroupsListWireframe.swift @@ -24,7 +24,9 @@ class GroupsListWireFrame: GroupsListWireFrameProtocol { // Compomentes let view = GroupsListViewController() let presenter = GroupsListPresenter() - let interactor = GroupsListInteractor(conversationsProvider: conversationsProvider) + let interactor = GroupsListInteractor( + dependencies: .init(storageService: StorageService.sharedInstance, + conversationsProvider: ConversationsProvider())) // Connecting view.presenter = presenter @@ -41,7 +43,10 @@ class GroupsListWireFrame: GroupsListWireFrameProtocol { guard let navigation = navigation, let main = main else { return } - MessageWireFrame().presentMessages(navigation: navigation, chat: group, main: main, initialMessage: nil) + MessageWireFrame().presentMessages(navigation: navigation, + chat: group, + main: main, + initialMessage: nil, + animated: true) } - } diff --git a/Nynja/Modules/History/View/HistoryViewController.swift b/Nynja/Modules/History/View/HistoryViewController.swift index 8c2b2e611..d2f51e93d 100644 --- a/Nynja/Modules/History/View/HistoryViewController.swift +++ b/Nynja/Modules/History/View/HistoryViewController.swift @@ -8,7 +8,7 @@ import UIKit -class HistoryViewController: BaseVC, HistoryViewProtocol, HistoryTableDSDelegate { +class HistoryViewController: BaseVC, HistoryViewProtocol, HistoryTableDSDelegate, KeyboardInteractive { var presenter: HistoryPresenterProtocol! { didSet { @@ -91,7 +91,7 @@ class HistoryViewController: BaseVC, HistoryViewProtocol, HistoryTableDSDelegate //MARK: - Keyboard - override func keyboardNotified(endFrame: CGRect) { + func keyboardNotified(endFrame: CGRect) { if endFrame.origin.y >= UIScreen.main.bounds.size.height { updateToHide(view: controlContainerView, offset: -bottomInset) } else { diff --git a/Nynja/Modules/InviteFriends/WireFrame/InviteFriendsWireframe.swift b/Nynja/Modules/InviteFriends/WireFrame/InviteFriendsWireframe.swift index 9c05f4b5a..0908aad2c 100644 --- a/Nynja/Modules/InviteFriends/WireFrame/InviteFriendsWireframe.swift +++ b/Nynja/Modules/InviteFriends/WireFrame/InviteFriendsWireframe.swift @@ -58,8 +58,7 @@ class InviteFriendsWireFrame: InviteFriendsWireFrameProtocol { } func shareNynja() { - let activityVC = UIActivityViewController(activityItems: ["nynja_share_string".localized], applicationActivities: nil) - viewController?.present(activityVC, animated: true, completion: nil) + AlertManager.sharedInstance.showNativeShare(with: ["nynja_share_string".localized]) } func closeSMSscreen(smsScreen: MFMessageComposeViewController) { diff --git a/Nynja/Modules/LanguageSettings/LanguageSelector/Interactor/LanguageSelectroInteractor.swift b/Nynja/Modules/LanguageSettings/LanguageSelector/Interactor/LanguageSelectroInteractor.swift index ad549e282..610754656 100644 --- a/Nynja/Modules/LanguageSettings/LanguageSelector/Interactor/LanguageSelectroInteractor.swift +++ b/Nynja/Modules/LanguageSettings/LanguageSelector/Interactor/LanguageSelectroInteractor.swift @@ -6,7 +6,7 @@ // Copyright © 2018 TecSynt Solutions. All rights reserved. // -class LanguageSelectorInteractor: BaseInteractor, LanguageSelectorInteractorInputProtocol, ReachabilityServiceObserver { +class LanguageSelectorInteractor: BaseInteractor, LanguageSelectorInteractorInputProtocol, ConnectionServiceDelegate { weak var presenter: LanguageSelectorInteractorOutputProtocol! @@ -20,11 +20,11 @@ class LanguageSelectorInteractor: BaseInteractor, LanguageSelectorInteractorInpu override init() { super.init() - ReachabilityService.sharedInstance.addRechabilityObserver(self) + ConnectionService.shared.addSubscriber(self) } deinit { - ReachabilityService.sharedInstance.removeRechabilityObserver(self) + ConnectionService.shared.removeSubscriber(self) } //MARK: - BaseInteractor @@ -45,15 +45,22 @@ class LanguageSelectorInteractor: BaseInteractor, LanguageSelectorInteractorInpu } func filterLanguages(with text: String) { - let filtered = languages.filter { $0.name.starts(with: text) } - presenter.languagesFiltered(filtered) + let text = text.trimmed() + + if !text.isEmpty { + let filtered = languages.filter { $0.name.contains(substring: text, options: .caseInsensitive) } + presenter.languagesFiltered(filtered) + } else { + presenter.languagesFetched(languages) + } } - //MARK: - ReachabilityServiceObserver - - func reachabilityStatusChanged(isReachable: Bool) { - if isReachable { - fetchLanguages() + func connectionStatusChanged(_ sender: ConnectionService, service: ConnectionService.Service, oldValue: ConnectionService.ConnectionServiceState) { + if service == .networking { + guard let networkStatus = sender.state[.networking] else { return } + if networkStatus == .connected || networkStatus == .switched { + fetchLanguages() + } } } } diff --git a/Nynja/Modules/LanguageSettings/LanguageSelector/View/LanguageSelectorViewController.swift b/Nynja/Modules/LanguageSettings/LanguageSelector/View/LanguageSelectorViewController.swift index f7d0169ea..b176e6648 100644 --- a/Nynja/Modules/LanguageSettings/LanguageSelector/View/LanguageSelectorViewController.swift +++ b/Nynja/Modules/LanguageSettings/LanguageSelector/View/LanguageSelectorViewController.swift @@ -9,7 +9,7 @@ import UIKit import NynjaUIKit -final class LanguageSelectorViewController: BaseVC, LanguageSelectorViewProtocol, LanguageSelectorTableActionDelegate { +final class LanguageSelectorViewController: BaseVC, LanguageSelectorViewProtocol, LanguageSelectorTableActionDelegate, KeyboardInteractive { var presenter: LanguageSelectorPresenterProtocol! { didSet { @@ -125,7 +125,7 @@ final class LanguageSelectorViewController: BaseVC, LanguageSelectorViewProtocol //MARK: - Keyboard - override func keyboardNotified(endFrame: CGRect) { + func keyboardNotified(endFrame: CGRect) { if endFrame.origin.y >= UIScreen.main.bounds.size.height { updateToHide(view: controlContainerView, offset: -bottomInset) } else { diff --git a/Nynja/Modules/Main/Interactor/MainInteractor.swift b/Nynja/Modules/Main/Interactor/MainInteractor.swift index 71868b0be..5cad4ae25 100644 --- a/Nynja/Modules/Main/Interactor/MainInteractor.swift +++ b/Nynja/Modules/Main/Interactor/MainInteractor.swift @@ -7,6 +7,7 @@ // import SDWebImage +import Intercom class MainInteractor: MainInteractorInputProtocol, EditPhotoDelegate, MQTTServiceDelegate { @@ -25,8 +26,9 @@ class MainInteractor: MainInteractorInputProtocol, EditPhotoDelegate, MQTTServic UIApplication.shared.registerUserNotificationSettings(notificationsSettings) UIApplication.shared.registerForRemoteNotifications() let _ = PushService.sharedInstance - + MQTTService.sharedInstance.addSubscriber(self) + setupIntercom() } func checkSession() { @@ -134,5 +136,20 @@ class MainInteractor: MainInteractorInputProtocol, EditPhotoDelegate, MQTTServic func saveLogoutState() { MQTTService.sharedInstance.state = .notAuthenticated(isLoggedOutFromServer: false) } + + private func setupIntercom() { + Intercom.logout() + guard let roster = RosterDAO.currentRoster else { + Intercom.registerUnidentifiedUser() + return + } + let rand = "\(UIDevice.current.persistentIdentifier)" + let userAttributes = ICMUserAttributes() + let id = roster.id != nil ? "\(roster.phoneId!)" : rand + Intercom.registerUser(withUserId: id) + userAttributes.name = roster.myContact?.fullName + userAttributes.phone = roster.phone + Intercom.updateUser(userAttributes) + } } diff --git a/Nynja/Modules/Main/View/ComingSoonExtension.swift b/Nynja/Modules/Main/View/ComingSoonExtension.swift new file mode 100644 index 000000000..b7282b6d0 --- /dev/null +++ b/Nynja/Modules/Main/View/ComingSoonExtension.swift @@ -0,0 +1,15 @@ +// +// ComingSoonExtension.swift +// Nynja +// +// Created by Roman Chopovenko on 9/27/18. +// Copyright © 2018 TecSynt Solutions. All rights reserved. +// + +import Foundation + +extension ComingSoonProtocol { + func unavailableFunctionality() { + AlertManager.sharedInstance.showAlert(title: "coming_soon".localized, dismissInterval: 3) + } +} diff --git a/Nynja/Modules/Main/View/ComingSoonProtocol.swift b/Nynja/Modules/Main/View/ComingSoonProtocol.swift new file mode 100644 index 000000000..1e6d32707 --- /dev/null +++ b/Nynja/Modules/Main/View/ComingSoonProtocol.swift @@ -0,0 +1,13 @@ +// +// ComingSoonProtocol.swift +// Nynja +// +// Created by Roman Chopovenko on 9/27/18. +// Copyright © 2018 TecSynt Solutions. All rights reserved. +// + +import Foundation + +protocol ComingSoonProtocol { + func unavailableFunctionality() +} diff --git a/Nynja/Modules/Main/View/MainNavigationItem.swift b/Nynja/Modules/Main/View/MainNavigationItem.swift index 48cc9a8c4..4f29d1c0a 100644 --- a/Nynja/Modules/Main/View/MainNavigationItem.swift +++ b/Nynja/Modules/Main/View/MainNavigationItem.swift @@ -45,7 +45,7 @@ enum MainNavigationItem: String { case username = "wheel_item_username" case phoneNumber = "wheel_item_changeNumber" case payment = "wheel_item_transfer" - + case help = "wheel_item_help" // Chats section case starred = "wheel_item_starred" diff --git a/Nynja/Modules/Main/View/MainViewController+NavigateProtocol.swift b/Nynja/Modules/Main/View/MainViewController+NavigateProtocol.swift index 80311b819..da5d7374f 100644 --- a/Nynja/Modules/Main/View/MainViewController+NavigateProtocol.swift +++ b/Nynja/Modules/Main/View/MainViewController+NavigateProtocol.swift @@ -8,12 +8,10 @@ import Foundation import Photos +import Intercom extension MainViewController: NavigateProtocol { - func unavailableFunctionality() { - AlertManager.sharedInstance.showAlertOk(message: "coming_soon".localized) - } - + //MARK: - First lvl func showSearch(indexPath: IndexPath?) { @@ -208,6 +206,11 @@ extension MainViewController: NavigateProtocol { closeWheel(indexPath: indexPath) } + func helpFeedBack(indexPath: IndexPath?) { + Intercom.presentMessenger() + closeWheel(indexPath: indexPath) + } + // MARK: - Contact Actions func showListContacts(indexPath: IndexPath?) { presenter.showMyContacts() @@ -328,4 +331,6 @@ extension MainViewController: NavigateProtocol { closeWheel(indexPath: indexPath) } + + } diff --git a/Nynja/Modules/Main/View/MainViewController.swift b/Nynja/Modules/Main/View/MainViewController.swift index 794c28002..0e52c3ee5 100644 --- a/Nynja/Modules/Main/View/MainViewController.swift +++ b/Nynja/Modules/Main/View/MainViewController.swift @@ -473,6 +473,7 @@ class MainViewController: BaseVC, MainViewProtocol, HitTestDelegate, UINavigatio } func hideReturnToCall() { + returnToCallView.content.tearDown() returnToCallView.isHidden = true } diff --git a/Nynja/Modules/Main/View/NavigateProtocol.swift b/Nynja/Modules/Main/View/NavigateProtocol.swift index 66ccf2481..b00a29af4 100644 --- a/Nynja/Modules/Main/View/NavigateProtocol.swift +++ b/Nynja/Modules/Main/View/NavigateProtocol.swift @@ -8,9 +8,7 @@ import Photos -protocol NavigateProtocol: FirstLevelNavigateProtocol, SecondLevelNavigateProtocol, ThirdLevelNavigateProtocol { - func unavailableFunctionality() -} +typealias NavigateProtocol = FirstLevelNavigateProtocol & SecondLevelNavigateProtocol & ThirdLevelNavigateProtocol & ComingSoonProtocol protocol FirstLevelNavigateProtocol: class { func showSearch(indexPath: IndexPath?) @@ -57,6 +55,7 @@ protocol SecondLevelNavigateProtocol: class { func showMedia(indexPath: IndexPath?) func showGallery(indexPath: IndexPath?) func showRecentsMedia(indexPath: IndexPath?) + func helpFeedBack(indexPath: IndexPath?) // MARK: - Profile Actions diff --git a/Nynja/Modules/Main/WireFrame/MainWireframe.swift b/Nynja/Modules/Main/WireFrame/MainWireframe.swift index d1b5b8540..7774e1b29 100644 --- a/Nynja/Modules/Main/WireFrame/MainWireframe.swift +++ b/Nynja/Modules/Main/WireFrame/MainWireframe.swift @@ -289,7 +289,7 @@ class MainWireFrame: MainWireFrameProtocol, NynjaCommunicatorServiceDelegate { func returnToCall(call: NYNCall?) { if let c = call { - presentCallInProgressViewForCall(call: c) + presentCallInProgressViewForCall(call: c, isIncomingRingig: false) } } @@ -389,7 +389,7 @@ class MainWireFrame: MainWireFrameProtocol, NynjaCommunicatorServiceDelegate { func logout() { if let nav = self.navigation { - AuthWireFrame().presentAuth(navigation: nav) + LoginWireFrame().presentLogin(navigation: nav) } } @@ -488,18 +488,14 @@ class MainWireFrame: MainWireFrameProtocol, NynjaCommunicatorServiceDelegate { AlertManager.sharedInstance.showAlertOk(message: "voice_call_the_feature_currently_unavailable".localized) } - func presentCallInProgressViewForCall(call:NYNCall) { + func presentCallInProgressViewForCall(call:NYNCall, isIncomingRingig:Bool) { self.isVideo = call.recvVideo let callMode: CallInProgressMode = self.isVideo ? .oneToOneVideo : call.isConference() ? .groupAudio : .oneToOneAudio if callMode == .groupAudio { -// TODO: Crashes after invoking this function. Please take a look - CallInProgressWireframe().presentCreateGroupCall(navigation: navigation!, callInProgressMode: callMode, main: self, call: call) - -// let callMode: CallMode = self.isVideo ? .outGoingVideoGroupCall : .outGoingGroupCall -// CallWireFrame().presentCreateGroupCall(navigation: navigation!, callMode: callMode, main: self, call: call) + CallInProgressWireframe().presentCreateGroupCall(navigation: navigation!, callInProgressMode: callMode, main: self, call: call, isIncomingRingig:isIncomingRingig) } else { @@ -510,42 +506,35 @@ class MainWireFrame: MainWireFrameProtocol, NynjaCommunicatorServiceDelegate { if let messageVC = contentNavigation.viewControllers.last as? MessageVC { contact = messageVC.presenter.interactor.contact } - //contact = self.view?.presenter.interactor.findContactBy(phoneId: call.callee) } else { contact = self.view?.presenter.interactor.findContactBy(phoneId: call.caller) } - if let ctc = contact { - CallInProgressWireframe().presentDialInCall(navigation: navigation!, callInProgressMode: callMode, contact:ctc, call: call, main: self) - } else { - - CallInProgressWireframe().presentDialInCall(navigation: navigation!, callInProgressMode: callMode, contact:nil, call: call, main: self) - } + CallInProgressWireframe().presentDialInCall(navigation: navigation!, callInProgressMode: callMode, contact:contact, call: call, main: self, isIncomingRingig:isIncomingRingig) } } func dialing(call: NYNCall) { self.view?.view.endEditing(true) -// let callMode: CallMode = self.isVideo ? .outGoingVideoGroupCall : .outGoingGroupCall -// CallWireFrame().presentDialInCall(navigation: navigation!, callMode: callMode, call: call, main: self) - presentCallInProgressViewForCall(call: call) + presentCallInProgressViewForCall(call: call, isIncomingRingig:false) } func incomingCallRinging(call: NYNCall) { -// let callMode: CallMode = .incamingGroupCall -// CallWireFrame().presentCreateGroupCall(navigation: navigation!, callMode: callMode, main: self, call: call) - presentCallInProgressViewForCall(call: call) + presentCallInProgressViewForCall(call: call, isIncomingRingig:true) + } + + func incomingCallAccepted(call: NYNCall) { + + presentCallInProgressViewForCall(call: call, isIncomingRingig:false) } func creatingGroupCall(name: String, call: NYNCall) { -// let callMode: CallMode = self.isVideo ? .outGoingVideoGroupCall : .outGoingGroupCall -// CallWireFrame().presentCreateGroupCall(navigation: navigation!, callMode: callMode, main: self, call: call) - presentCallInProgressViewForCall(call: call) + presentCallInProgressViewForCall(call: call, isIncomingRingig:false) } // MARK: Chats group @@ -572,12 +561,16 @@ class MainWireFrame: MainWireFrameProtocol, NynjaCommunicatorServiceDelegate { func showMySelfChat(contact: Contact) { ChatsListWireFrame().presentChatsList(navigation: contentNavigation, main: self, animated: false) - MessageWireFrame().presentMessages(navigation: contentNavigation, chat: contact, main: self) + MessageWireFrame().presentMessages(navigation: contentNavigation, chat: contact, main: self, animated: false) } func showChat(_ chat: ChatModel, initialMessage: ChatInitialMessage?) { self.setupPrevScreenAsList(chat: chat) - MessageWireFrame().presentMessages(navigation: contentNavigation, chat: chat, main: self, initialMessage: initialMessage) + MessageWireFrame().presentMessages(navigation: contentNavigation, + chat: chat, + main: self, + initialMessage: initialMessage, + animated: false) } func setupPrevScreenAsList(chat: ChatModel) { diff --git a/Nynja/Modules/MapSearch/View/ViewController/MapSearchViewController.swift b/Nynja/Modules/MapSearch/View/ViewController/MapSearchViewController.swift index 87a354765..9819da04d 100644 --- a/Nynja/Modules/MapSearch/View/ViewController/MapSearchViewController.swift +++ b/Nynja/Modules/MapSearch/View/ViewController/MapSearchViewController.swift @@ -9,7 +9,7 @@ import UIKit import GooglePlaces -class MapSearchViewController: BaseVC, MapSearchViewProtocol, MapSearchDSDelegate { +class MapSearchViewController: BaseVC, MapSearchViewProtocol, MapSearchDSDelegate, KeyboardInteractive { var presenter: MapSearchPresenterProtocol! { didSet { @@ -104,7 +104,7 @@ class MapSearchViewController: BaseVC, MapSearchViewProtocol, MapSearchDSDelegat // MARK: - Keyboard - override func keyboardNotified(endFrame: CGRect) { + func keyboardNotified(endFrame: CGRect) { if endFrame.origin.y >= UIScreen.main.bounds.size.height { updateToHide(view: controlContainerView, offset: -bottomInset) } else { diff --git a/Nynja/Modules/Message/Interactor/MessageInteractor+Fetch.swift b/Nynja/Modules/Message/Interactor/MessageInteractor+Fetch.swift index 8b0bcbfda..30fb03171 100644 --- a/Nynja/Modules/Message/Interactor/MessageInteractor+Fetch.swift +++ b/Nynja/Modules/Message/Interactor/MessageInteractor+Fetch.swift @@ -58,8 +58,8 @@ extension MessageInteractor { } let messages = fetchMessagesFromStorage(for: type) - self.configuration.messages = messages - self.prepareMessagesToUsing(type) + configuration.messages = messages + prepareMessages(messages, type: type) } func fetchNewFromStorage() { @@ -70,26 +70,26 @@ extension MessageInteractor { } let oldMessages = configuration.messages - var newMessages = fetchMessagesFromStorage(for: type) + let newMessages = fetchMessagesFromStorage(for: type) if let startID = newMessages.first?.msg_id, oldMessages.index(where: { $0.msg_id == startID }) != nil { // If all history fetched and the first message already exists in local data source. configuration.messages = newMessages - prepareMessagesToUsing(type, isNew: false) + prepareMessages(newMessages, type: type, isNew: false) } else { if let startID = oldMessages.first?.msg_id, let index = newMessages.index(where: { $0.msg_id == startID }) { - newMessages = Array(newMessages[0.. 0 ? initialSelfReader : nil + presentationConfiguration.unreadCount = unreadCount + presentationConfiguration.shouldShowUnread = !isAfterConnectionAppeared + presentationConfiguration.links = fetchLinks() + presentationConfiguration.mentions = fetchMentions(for: messages) + presentationConfiguration.translations = fetchTranslations(for: messages) + presentationConfiguration.transcriptions = fetchTranscriptions(for: messages) + presentationConfiguration.transcribingModels = fetchTranscriptionsProgress(for: messages) isAfterConnectionAppeared = false - if let rosterId = StorageService.sharedInstance.rosterId { - configuration.stars = StarDAO.fetchStars(rosterId: rosterId) + if let rosterId = storageService.rosterId { + presentationConfiguration.stars = StarDAO.fetchStars(rosterId: rosterId) } + configuration.join(presentationConfiguration) + if isNew { - presenter?.setupNew(with: configuration) + presenter?.setupNew(with: presentationConfiguration) } else { - presenter?.setup(with: configuration) + presenter?.setup(with: presentationConfiguration) } } - private func fetchRepliedModels(_ type: FetchType) -> ChatConfiguration.RepliedModels { - let serverIds = configuration.messages.compactMap { $0.isReply ? $0.link : nil } + private func fetchRepliedModels(for messages: [Message], type: FetchType) -> ChatConfiguration.RepliedModels { + let serverIds = messages.compactMap { $0.isReply ? $0.linkedId : nil } let repliedMessages = MessageDAO.fetchMessages(type, serverIds: Set(serverIds)) var repliedModels = ChatConfiguration.RepliedModels() - repliedMessages.forEach { message in - if let serverId = message.id, let sender = sender(for: message) { - let mentions = payloadParser.parse(message) - repliedModels[serverId] = RepliedMessageModel(message: message, author: sender.nick ?? sender.fullname, mentions: mentions) + + for message in repliedMessages { + guard let serverId = message.id else { + continue } + let sender = self.sender(for: message) + let author = sender.nick ?? sender.fullname + let mentions = payloadParser.parse(message) + let isDeleted = message.isDeleted + + repliedModels[serverId] = RepliedMessageModel(message: message, + author: author, + mentions: mentions, + isDeleted: isDeleted) } return repliedModels @@ -205,10 +219,10 @@ extension MessageInteractor { return result } - private func fetchTranslations() -> ChatConfiguration.Translations { + private func fetchTranslations(for messages: [Message]) -> ChatConfiguration.Translations { var result = ChatConfiguration.Translations() - configuration.messages.forEach { + messages.forEach { if let localId = $0.msg_id, let translation = fetchTranslationInfo($0, links: configuration.links[localId]) { result[localId] = translation } @@ -216,10 +230,10 @@ extension MessageInteractor { return result } - private func fetchTranscriptions() -> ChatConfiguration.Transcriptions { + private func fetchTranscriptions(for messages: [Message]) -> ChatConfiguration.Transcriptions { var result = ChatConfiguration.Transcriptions() - configuration.messages.forEach { + messages.forEach { if let localId = $0.msg_id, let info = fetchTranscriptionInfo($0, links: configuration.links[localId]) { result[localId] = info } @@ -227,10 +241,10 @@ extension MessageInteractor { return result } - private func fetchTranscriptionsProgress() -> ChatConfiguration.ConversionsProgress { + private func fetchTranscriptionsProgress(for messages: [Message]) -> ChatConfiguration.ConversionsProgress { var result = ChatConfiguration.ConversionsProgress() - configuration.messages.forEach { + messages.forEach { if let localId = $0.msg_id, let progress = fetchConversionProgress(with: localId) { result[localId] = progress } @@ -255,5 +269,4 @@ extension MessageInteractor { return .lastMessage } } - } diff --git a/Nynja/Modules/Message/Interactor/MessageInteractor+History.swift b/Nynja/Modules/Message/Interactor/MessageInteractor+History.swift index 7ce3c9129..55d9f6cb5 100644 --- a/Nynja/Modules/Message/Interactor/MessageInteractor+History.swift +++ b/Nynja/Modules/Message/Interactor/MessageInteractor+History.swift @@ -10,12 +10,18 @@ import Foundation extension MessageInteractor { + enum HistoryCheckResult { + case valid + case updateRequired + case broken(MessageHistoryGaps) + } + struct MessageHistoryGaps { - typealias Range = CountableClosedRange + fileprivate typealias Range = CountableClosedRange private let ranges: [Range]? - init(ranges: [Range]) { + fileprivate init(ranges: [Range]) { self.ranges = ranges } @@ -32,57 +38,70 @@ extension MessageInteractor { } } - enum HistoryCheckResult { - case validSequence - case updateRequired - case gaps(MessageHistoryGaps) - } - func checkLocalHistory() -> HistoryCheckResult { - guard chat.last_msg?.statusString != "update" else { + guard chat.last_msg?.messageStatus != .update else { return .updateRequired } guard let gaps = fetchAllHistoryGaps(), !gaps.isEmpty else { - return .validSequence + return .valid } - return .gaps(gaps) + return .broken(gaps) } private func fetchAllHistoryGaps() -> MessageHistoryGaps? { - guard let fetchType = fetchType else { return nil } - + guard let fetchType = fetchType, var lastMessage = chat.last_msg else { + return nil + } + // Check if lastMessage doesn't have serverId + if !lastMessage.isDelivered { + if let lastDeliveredMessage = MessageDAO.fetchLastDeliveredMessage(ofType: fetchType, orderedBy: .serverId) { + lastMessage = lastDeliveredMessage + } + } + return fetchAllHistoryGaps(in: fetchType, lastMessage: lastMessage) + } + + private func fetchAllHistoryGaps(in fetchType: FetchType, lastMessage: Message) -> MessageHistoryGaps { var gaps = [MessageHistoryGaps.Range]() + var previousMessage = DBMessage(message: lastMessage) + + func isVisible(_ message: DBMessage) -> Bool { + guard let localStatus = message.localStatus else { + return true + } + return !localStatus.contains(.replied) + } - var endMessageId: MessageServerId? // Latest message id - var repliedMessageId: MessageServerId? // Oldest replied message id + func canMoveToNext(with serverId: MessageServerId) -> Bool { + guard let previousServerId = previousMessage?.serverId else { + return false + } + return serverId < previousServerId + } + + func isValidChain(_ message: DBMessage) -> Bool { + return previousMessage?.next == message.serverId || previousMessage?.isTrusted == true + } + + // Latest message id + var endMessageId: MessageServerId? - let appendGapIfExists = { (message: DBMessage) in + let appendGapIfExists: (DBMessage) -> Void = { message in if let end = endMessageId, let start = message.serverId, start <= end { gaps.append(start...end) } endMessageId = nil } - var previousMessage = chat.last_msg.flatMap { DBMessage(message: $0) } - - let isValidChain = { (message: DBMessage) -> Bool in - return previousMessage?.next == message.serverId || previousMessage?.isTrusted == true - } - // Iterate over all messages that isn't filtered by status. // Ignore first message, because it's equal to chat's last message. - try? MessageDAO.dropFirst(for: fetchType) { message in - guard let id = message.serverId else { return } - - defer { previousMessage = message } - - if id == repliedMessageId { - repliedMessageId = nil - - } else if message.isReply, let link = message.link { - repliedMessageId = repliedMessageId.flatMap { min($0, link) } ?? link + try? MessageDAO.dropFirst(for: fetchType, orderedBy: .desc, orderColumn: .serverId) { message in + guard let serverId = message.serverId, isVisible(message), canMoveToNext(with: serverId) else { + return } + defer { previousMessage = message } + if isValidChain(message) { appendGapIfExists(message) @@ -93,14 +112,6 @@ extension MessageInteractor { } previousMessage.map { appendGapIfExists($0) } - /* - if let repliedMessageId = repliedMessageId, - let start = gaps.last?.lowerBound, - let end = gaps.first?.upperBound, - !(start...end).contains(repliedMessageId) { - - gaps.append(repliedMessageId...start) - }*/ return MessageHistoryGaps(ranges: gaps) } diff --git a/Nynja/Modules/Message/Interactor/MessageInteractor+MessageHandlerSubscriber.swift b/Nynja/Modules/Message/Interactor/MessageInteractor+MessageHandlerSubscriber.swift index 20de1b0f4..6809294bb 100644 --- a/Nynja/Modules/Message/Interactor/MessageInteractor+MessageHandlerSubscriber.swift +++ b/Nynja/Modules/Message/Interactor/MessageInteractor+MessageHandlerSubscriber.swift @@ -24,6 +24,6 @@ extension MessageInteractor { if let mime = message.files?.first?.mime, mime == SendMessageType.audioCall.rawValue { return } - soundService.playOutcomingMessageSound() + systemSoundManager.playOutcomingMessageSound() } } diff --git a/Nynja/Modules/Message/Interactor/MessageInteractor+Schedule.swift b/Nynja/Modules/Message/Interactor/MessageInteractor+Schedule.swift index 3fef93b33..5cd777e93 100644 --- a/Nynja/Modules/Message/Interactor/MessageInteractor+Schedule.swift +++ b/Nynja/Modules/Message/Interactor/MessageInteractor+Schedule.swift @@ -11,7 +11,7 @@ import Foundation extension MessageInteractor { func scheduleInfo(for inputMessage: InputScheduleMessage) -> ScheduleInfo? { - guard messageSendingService.isCanSendMessageTo(contact: contact) else { + if let contact = contact, !messageSendingService.canSendMessage(to: contact) { presenter?.blockMessageSending() return nil } diff --git a/Nynja/Modules/Message/Interactor/MessageInteractor+StorageSubscriber.swift b/Nynja/Modules/Message/Interactor/MessageInteractor+StorageSubscriber.swift index 1f19440a9..39f53736e 100644 --- a/Nynja/Modules/Message/Interactor/MessageInteractor+StorageSubscriber.swift +++ b/Nynja/Modules/Message/Interactor/MessageInteractor+StorageSubscriber.swift @@ -7,16 +7,18 @@ // extension MessageInteractor { - + + // MARK: - Check SubscribeType + func handleUpdate(with info: StorageChange, type: SubscribeType) { - if isMemberUpdated(with: info, type: type) { - handleUpdateMember() + if let dbMember = updatedDBMember(with: info, type: type) { + handleUpdate(member: dbMember) } else if let contact = updatedContact(with: info, type: type) { handleUpdate(contact: contact) } else if let room = updatedRoom(with: info, type: type) { handleUpdate(room: room) } else if let message = changedMessage(with: info, type: type) { - if message.statusString == "deleted" { + if let localStatus = message.localStatus, localStatus.contains(.deleted) { handleMessageDelete(message) } else { guard info.kind != .delete else { @@ -31,15 +33,12 @@ extension MessageInteractor { handleChanged(star: updatedStar, kind: info.kind) } } - - - // MARK: - Check SubscribeType - - private func isMemberUpdated(with info: StorageChange, type: SubscribeType) -> Bool { - if case .member(let id) = type, let roomId = id, room?.id == roomId, let _ = info.entity as? DBMember { - return true + + private func updatedDBMember(with info: StorageChange, type: SubscribeType) -> DBMember? { + if case .member(let id) = type, let roomId = id, room?.id == roomId, let dbMember = info.entity as? DBMember { + return dbMember } - return false + return nil } private func updatedContact(with info: StorageChange, type: SubscribeType) -> DBContact? { @@ -81,18 +80,39 @@ extension MessageInteractor { // MARK: - Handle member - - private func handleUpdateMember() { - guard let room = self.room, - room.kind == .channel, - let roomId = room.id, - let newRoom = RoomDAO.findRoom(by: roomId) else { - return + private func handleUpdate(member: DBMember) { + guard let room = room, let kind = room.kind else { + return } - + + switch kind { + case .channel: + updateChannel() + case .group: + updateMember(with: member) + } + } + + private func updateChannel() { + guard let roomId = room?.id, let newRoom = RoomDAO.findRoom(by: roomId) else { + return + } + chat = newRoom } - + + private func updateMember(with dbMember: DBMember) { + guard let oldMember = member(for: dbMember.phoneId) else { + return + } + + let updatedMember = Member(member: dbMember) + guard updatedMember.alias != oldMember.alias else { + return + } + + notifyAboutUpdated(member: updatedMember) + } // MARK: - Update Contact & Room @@ -135,7 +155,7 @@ extension MessageInteractor { } private func handleMessageInsertOrUpdate(_ message: Message) { - guard !["delete", "edit"].contains(message.statusString ?? "") else { + if let status = message.messageStatus, status.isIn([.delete, .edit]) { return } @@ -155,7 +175,10 @@ extension MessageInteractor { } private func messageReceived(_ message: Message) { - if message.isStatusClear { + if case .clear? = message.messageStatus { + guard !message.isDeleted else { + return + } processingQueue.async { [weak self] in guard let `self` = self else { return } self.fetchFromStorage() @@ -188,7 +211,7 @@ extension MessageInteractor { let oldMessage = configuration.messages[index] configuration.messages[index] = message - if let _ = oldMessage.id { + if oldMessage.isDelivered { if message.isEdited { autoTranslateReceiptMessageIfNeeded(message) } @@ -196,7 +219,7 @@ extension MessageInteractor { let config = createMessageConfiguration(message) presenter?.updateMessage(with: config) - if message.statusString == "update" { + if case .update? = message.messageStatus { presenter?.scrollToBottomIfNeeded() } } else if message.files?.count != oldMessage.files?.count { @@ -238,10 +261,19 @@ extension MessageInteractor { func createMessageConfiguration(_ message: Message) -> MessageConfiguration { var repliedModel: RepliedMessageModel? - if message.isReply { - if let serverId = message.link, let repliedMessage = messageBy(serverId: serverId), let sender = sender(for: repliedMessage) { + if message.isReply, let serverId = message.linkedId { + let repliedMessage = MessageDAO.fetchMessage(serverId: serverId) + + if let repliedMessage = repliedMessage { + let sender = self.sender(for: repliedMessage) + let author = sender.nick ?? sender.fullname let mentions = payloadParser.parse(repliedMessage) - repliedModel = RepliedMessageModel(message: repliedMessage, author: sender.nick ?? sender.fullname, mentions: mentions) + + repliedModel = RepliedMessageModel(message: repliedMessage, + author: author, + mentions: mentions, + isDeleted: repliedMessage.isDeleted) + configuration.repliedModels[serverId] = repliedModel } } @@ -284,10 +316,7 @@ extension MessageInteractor { } func startSendingMessage() { - if let _ = repliedMessage { - declineReply() - self.presenter?.startSendingMessage() - } + self.presenter?.startSendingMessage() } func fetchTranslationInfo(_ message: Message, links: [String]?) -> TranslationInfo? { diff --git a/Nynja/Modules/Message/Interactor/MessageInteractor+Translation.swift b/Nynja/Modules/Message/Interactor/MessageInteractor+Translation.swift index b8b4d45c8..717a27c01 100644 --- a/Nynja/Modules/Message/Interactor/MessageInteractor+Translation.swift +++ b/Nynja/Modules/Message/Interactor/MessageInteractor+Translation.swift @@ -71,7 +71,7 @@ extension MessageInteractor { var attribute: [String: MentionInfo] = [:] let replacement: (MentionInfo) -> String = { mentionInfo in - return "\(MentionInfo.tag)_\(mentionInfo.accountId)_\(mentionInfo.range.lowerBound.encodedOffset)" + return "\(MentionInfo.translationAlias)_\(mentionInfo.accountId)_\(mentionInfo.range.lowerBound.encodedOffset)" } let result = MessagePayloadRenderer.processPlainTextPayload(into: &attribute, @@ -183,15 +183,20 @@ extension MessageInteractor { //MARK: - Autotranslate on receipt - func messagesForUpdateAutotranslateOnReceipt() -> [Message] { + private func messagesForUpdateAutotranslateOnReceipt() -> [Message] { guard let count = translationCount else { return [] } + let messages = configuration.messages let languageToTranslation = conversationLanguageSettingService.chatLanguage.lang - let index = configuration.messages.count - Int(count) - 1 + let index = messages.count - Int(count) - 1 - return Array(configuration.messages[...index] + guard messages.indices.contains(index) else { + return [] + } + + return messages[...index] .filter { guard $0.canBeTranslated, let translationPayload = messageParser.parse($0, to: .translation(.language(languageToTranslation))).first?.payload, @@ -199,21 +204,27 @@ extension MessageInteractor { return false } return true - }) + } } - func messagesForAutotranslateOnReceipt() -> [Message] { - guard let count = translationCount else { + private func messagesForAutotranslateOnReceipt() -> [Message] { + guard let count = translationCount, count != 0 else { return [] } + let messages = configuration.messages let languageToTranslation = conversationLanguageSettingService.chatLanguage.lang - let index = configuration.messages.count - Int(count) + let index = messages.count - Int(count) - return Array(configuration.messages[index...] + guard messages.indices.contains(index) else { + return [] + } + + return messages[index...] .filter { $0.canBeTranslated && - messageParser.parse($0, to: .translation(.language(languageToTranslation))).isEmpty }) + messageParser.parse($0, to: .translation(.language(languageToTranslation))).isEmpty + } } func autoTranslateReceiptMessageIfNeeded(_ message: Message) { @@ -258,7 +269,7 @@ extension MessageInteractor { let mentions = payloadParser.parse(message) var attribute: [String: String] = [:] let replacement: (MentionInfo) -> String = { mentionInfo in - return "\(MentionInfo.tag)_\(mentionInfo.accountId)" + return "\(MentionInfo.translationAlias)_\(mentionInfo.accountId)" } let result = MessagePayloadRenderer.processPlainTextPayload(into: &attribute, diff --git a/Nynja/Modules/Message/Interactor/MessageInteractor+Utils.swift b/Nynja/Modules/Message/Interactor/MessageInteractor+Utils.swift index 7350d048b..13a5cb855 100644 --- a/Nynja/Modules/Message/Interactor/MessageInteractor+Utils.swift +++ b/Nynja/Modules/Message/Interactor/MessageInteractor+Utils.swift @@ -20,12 +20,13 @@ extension MessageInteractor { return configuration.messages[index] } + // MARK: - Message By - func messageBy(localId: String) -> Message? { + + func messageBy(localId: MessageLocalId) -> Message? { if let index = indexOfMessage(with: localId) { return configuration.messages[index] } - return nil } @@ -33,17 +34,17 @@ extension MessageInteractor { if let index = indexOfMessage(with: serverId) { return configuration.messages[index] } - return nil } + // MARK: - Index - func indexOfMessage(with localId: String) -> Int? { + + func indexOfMessage(with localId: MessageLocalId) -> Int? { return configuration.messages.index(where: { $0.msg_id == localId }) } func indexOfMessage(with serverId: MessageServerId) -> Int? { return configuration.messages.index(where: { $0.id == serverId }) } - } diff --git a/Nynja/Modules/Message/Interactor/MessageInteractor.swift b/Nynja/Modules/Message/Interactor/MessageInteractor.swift index 34321344a..9a95e6bbe 100644 --- a/Nynja/Modules/Message/Interactor/MessageInteractor.swift +++ b/Nynja/Modules/Message/Interactor/MessageInteractor.swift @@ -10,7 +10,7 @@ import UIKit import CoreLocation -final class MessageInteractor: BaseInteractor, MessageInteractorInputProtocol, HistoryHandlerDelegate, TypingHandlerDelegate, IoHandlerDelegate, ReachabilityServiceObserver, MQTTServiceDelegate, MessageProcessingDelegate, MessageHandlerSubscriber, MessageInteractorCallProtocol { +final class MessageInteractor: BaseInteractor, MessageInteractorInputProtocol, HistoryHandlerDelegate, TypingHandlerDelegate, ConnectionServiceDelegate, MQTTServiceDelegate, MessageProcessingDelegate, MessageHandlerSubscriber, MessageInteractorCallProtocol { private var callService = NynjaCommunicatorService.sharedInstance @@ -69,6 +69,7 @@ final class MessageInteractor: BaseInteractor, MessageInteractorInputProtocol, H var translationCount: Int64? var initialUnreadCount = 0 + var initialSelfReader: Int64? private(set) var editingMessage: Message? @@ -89,7 +90,7 @@ final class MessageInteractor: BaseInteractor, MessageInteractorInputProtocol, H let storageService: StorageService let processingManager = DefaultMessagesProcessingManager.shared - let soundService = SoundService.sharedInstance + let systemSoundManager = SystemSoundManager.sharedInstance let payloadParser: MessagePayloadParserInput let payloadBuilder: MessagePayloadBuilderInput @@ -144,6 +145,10 @@ final class MessageInteractor: BaseInteractor, MessageInteractorInputProtocol, H return MessageSendingService(dependencies: dependencies) }() + private(set) lazy var senderService: SenderService = { + return SenderService(dependencies: .init(mqttService: mqttService)) + }() + var initialMessage: ChatInitialMessage? // MARK: -- Private @@ -177,8 +182,9 @@ final class MessageInteractor: BaseInteractor, MessageInteractorInputProtocol, H } mqttService.addSubscriber(self) - ReachabilityService.sharedInstance.addRechabilityObserver(self) + ConnectionService.shared.addSubscriber(self) MessageHandler.addSubscriber(self) + HistoryHandler.addSubscriber(self) NynjaCommunicatorService.sharedInstance.messageInteractorCallProtocol = self subscribeToTranscribeProcessing() @@ -189,8 +195,8 @@ final class MessageInteractor: BaseInteractor, MessageInteractorInputProtocol, H callService.messageInteractorCallProtocol = nil mqttService.removeSubscriber(self) MessageHandler.removeSubscriber(self) - ReachabilityService.sharedInstance.removeRechabilityObserver(self) - + HistoryHandler.removeSubscriber(self) + ConnectionService.shared.removeSubscriber(self) unsubscribeFromTranscribeProcessing() } @@ -204,20 +210,21 @@ final class MessageInteractor: BaseInteractor, MessageInteractorInputProtocol, H // MARK: - BaseInteractor override func loadData() { super.loadData() + prepareInitialValues() fetchData() } // MARK: - MessageInteractorInputProtocol func configure() { + senderService.updateSubscribes() + processingManager.delegate = self - IoHandler.delegate = self - HistoryHandler.delegate = self TypingHandler.delegate = self isAfterConnectionAppeared = false - initialUnreadCount = Int(chat.unreadCount) + prepareInitialValues() translationCount = chat.unreadCount setupChatLanguageIfNeeded() @@ -230,6 +237,11 @@ final class MessageInteractor: BaseInteractor, MessageInteractorInputProtocol, H fetchRoomFromStorage() sendForwardMessage() } + + private func prepareInitialValues() { + initialUnreadCount = Int(chat.unreadCount) + initialSelfReader = chat.selfReader + } func goAway() { initialMessage = nil @@ -249,7 +261,7 @@ final class MessageInteractor: BaseInteractor, MessageInteractorInputProtocol, H } let shouldReadInMySelf = isMyselfChat && message.isCursor - let shouldReadInOther = !message.isOwn + let shouldReadInOther = !isMyselfChat && (!message.isOwn || message.isSystem) if shouldReadInMySelf || shouldReadInOther { readMessage(serverId) @@ -291,7 +303,7 @@ final class MessageInteractor: BaseInteractor, MessageInteractorInputProtocol, H guard let msgId = messageID ?? chat.last_msg?.id, let rosterId = storageService.phoneId else { return nil } - let needsUpdate = messageID == nil && chat.last_msg?.statusString == "update" + let needsUpdate = messageID == nil && chat.last_msg?.messageStatus == .update do { let requestModel = needsUpdate @@ -332,15 +344,20 @@ final class MessageInteractor: BaseInteractor, MessageInteractorInputProtocol, H } } + func currentMembersCount() -> UInt { + guard let id = self.room?.id else { return 0 } + return callService.currentMembersCountForCallWithRoom(id) + } + // MARK: - Fetch Data private func fetchData() { fetchChatModel() switch checkLocalHistory() { - case .updateRequired, .validSequence: + case .updateRequired, .valid: fetchMessages() - case let .gaps(gaps): + case let .broken(gaps): processingQueue.async { [weak self] in self?.fetchFromStorage() } @@ -372,21 +389,30 @@ final class MessageInteractor: BaseInteractor, MessageInteractorInputProtocol, H // MARK: - Sender - func sender(for message: Message) -> MessageSender? { + func sender(for message: Message) -> MessageSender { let isOwner = message.from == myContact?.phone_id - if let _ = (message.feed_id as? muc), let from = message.from, let member = member(for: from) { - return MessageSender(phoneId: member.phone_id, fullname: member.fullName ?? "", nick: member.alias, avatar: member.avatar) + + if message.feed_id is muc, let from = message.from, let member = member(for: from) { + return messageSender(from: member) } else if let contact = self.contact { if isOwner, let myContact = myContact { - return MessageSender(phoneId: myContact.phone_id, fullname: myContact.fullName ?? "", nick: myContact.nick, avatar: myContact.avatar) + return messageSender(from: myContact) } else { - return MessageSender(phoneId: contact.phone_id, fullname: contact.fullName ?? "", nick: contact.nick, avatar: contact.avatar) + return messageSender(from: contact) } } return .deleted } + private func messageSender(from member: Member) -> MessageSender { + return MessageSender(phoneId: member.phone_id, fullname: member.fullName ?? "", nick: member.alias, avatar: member.avatar) + } + + private func messageSender(from contact: Contact) -> MessageSender { + return MessageSender(phoneId: contact.phone_id, fullname: contact.fullName ?? "", nick: contact.nick, avatar: contact.avatar) + } + func member(for phoneId: String) -> Member? { return self.room?.allMembersWithRemoved?.first { (member) -> Bool in if let id = member.phone_id { @@ -403,14 +429,6 @@ final class MessageInteractor: BaseInteractor, MessageInteractorInputProtocol, H return Contact(contact: contact) } - func findContact(phoneNumber: String) { - if let contact = ContactDAO.findContactBy(phone: phoneNumber) { - presenter?.didReceiveContactByPhoneNumber(contact) - } else { - mqttService.tryFindContact(number: phoneNumber) - } - } - func askForInternetStatus() { if !ReachabilityService.sharedInstance.isReachable { presenter?.internetStatusChanged(.waiting) @@ -525,7 +543,7 @@ final class MessageInteractor: BaseInteractor, MessageInteractorInputProtocol, H } private func compare(_ left: Message?, with right: Message?) -> Bool { - return Date(timestamp: (left?.created as? Int64) ?? 0) > Date(timestamp: (right?.created as? Int64) ?? 0) + return Date(timestamp: left?.created ?? 0) > Date(timestamp: right?.created ?? 0) } private func flatMap(desc: Desc?) -> LocationType? { @@ -553,7 +571,7 @@ final class MessageInteractor: BaseInteractor, MessageInteractorInputProtocol, H urls[mainURL] = message } return urls.values - .sorted { Date(timestamp: ($0.created as? Int64) ?? 0) < Date(timestamp: ($1.created as? Int64) ?? 0) } + .sorted { Date(timestamp: $0.created ?? 0) < Date(timestamp: $1.created ?? 0) } .compactMap { guard let mainFile = $0.mainFile, let thumbnailFile = $0.thumb, let mainURL = mainFile.url, let thumbnailURL = thumbnailFile.url else { @@ -621,9 +639,6 @@ final class MessageInteractor: BaseInteractor, MessageInteractorInputProtocol, H if let url = message.mainUrl { configuration.progressModels.removeValue(forKey: url) } - if let serverId = message.link { - configuration.repliedModels.removeValue(forKey: serverId) - } presenter?.removeMessage(id) } } @@ -705,7 +720,7 @@ final class MessageInteractor: BaseInteractor, MessageInteractorInputProtocol, H } let message = Message(message: msg) - message.created = Date.currentTimestamp as AnyObject + message.created = Date.currentTimestamp let starLocalId = IdBuilder(format: .starClientId) .addValueForComponent(messageServerId, .key) @@ -773,9 +788,10 @@ final class MessageInteractor: BaseInteractor, MessageInteractorInputProtocol, H messageEditService.saveEditMessageAction(messageId: id, editInfo: editInfo) } - sendMessage(editedMessage) + //FIXME: - hot fix for autotranslate + sendMessage(Message(message: editedMessage)) - editedMessage.status = nil + editedMessage.messageStatus = nil try? storageService.perform(action: .save, with: editedMessage) updateEditedMessageUI(editedMessage) @@ -830,7 +846,11 @@ final class MessageInteractor: BaseInteractor, MessageInteractorInputProtocol, H let messageForDelete = messageFactory.makeMessageForDelete(message: message, seenBy: [seenBy]) mqttService.sendMessage(message: messageForDelete) - messageForDelete.status = StringAtom(string: "deleted") + // Don't update message status to 'delete' in local database, because new message with 'delete' status + // will be received from the server in response. + messageForDelete.messageStatus = message.messageStatus + messageForDelete.localStatus = .deleted + try? storageService.perform(action: .save, with: messageForDelete) if let fetchType = fetchType, let newLastMessage = MessageDAO.fetchLastMessage(of: fetchType) { @@ -853,7 +873,7 @@ final class MessageInteractor: BaseInteractor, MessageInteractorInputProtocol, H } func processForwardMessageTap(serverId: MessageServerId) { - guard let link = MessageDAO.fetchMessage(serverId: serverId)?.link, + guard let link = MessageDAO.fetchMessage(serverId: serverId)?.linkedId, let linkedMessage = MessageDAO.fetchMessage(serverId: link), let localId = linkedMessage.msg_id else { return @@ -897,30 +917,29 @@ final class MessageInteractor: BaseInteractor, MessageInteractorInputProtocol, H } // MARK: - HistoryHandlerDelegate - var isNew = false + private var isNew = false private var isHistoryUpdating: Bool = false func getHistorySuccess() { guard !isHistoryUpdating else { isHistoryUpdating = false - isNew = false + processingQueue.async { + self.isNew = false + } fetchData() return } - if isNew { - processingQueue.async { [weak self] in - guard let `self` = self else { return } + processingQueue.async { [weak self] in + guard let `self` = self else { return } + + if self.isNew { self.fetchNewFromStorage() - self.autoTranslateReceiptMessagesIfNeeded() - } - } else { - isNew = true - processingQueue.async { [weak self] in - guard let `self` = self else { return } + } else { + self.isNew = true self.fetchFromStorage() - self.autoTranslateReceiptMessagesIfNeeded() } + self.autoTranslateReceiptMessagesIfNeeded() } } @@ -938,20 +957,16 @@ final class MessageInteractor: BaseInteractor, MessageInteractorInputProtocol, H } } } - - // MARK: - IoHandlerDelegate - - func getContactSuccess(contact: Contact) { - presenter?.didReceiveContactByPhoneNumber(contact) - } - - // MARK: - ReachabilityServiceObserver - func reachabilityStatusChanged(isReachable: Bool) { - if !isReachable { - presenter?.internetStatusChanged(.waiting) - } else { - presenter?.internetStatusChanged(.connecting) - isAfterConnectionAppeared = true + + func connectionStatusChanged(_ sender: ConnectionService, service: ConnectionService.Service, oldValue: ConnectionService.ConnectionServiceState) { + if service == .networking { + guard let networkStatus = sender.state[.networking] else { return } + if networkStatus == .disconnected { + presenter?.internetStatusChanged(.waiting) + } else if networkStatus == .connected || networkStatus == .switched { + presenter?.internetStatusChanged(.connecting) + isAfterConnectionAppeared = true + } } } @@ -1039,11 +1054,11 @@ final class MessageInteractor: BaseInteractor, MessageInteractorInputProtocol, H extension MessageInteractor { func sendMessage(_ message: Message) { - guard messageSendingService.isCanSendMessageTo(contact: contact) else { + if let contact = contact, !messageSendingService.canSendMessage(to: contact) { presenter?.blockMessageSending() return } - + let types: [SendMessageType] = [.location, .image, .video, .contact, .place, .audioCall] if let mime = message.mainFile?.mime, let type = SendMessageType(rawValue: mime), types.contains(type) { @@ -1060,12 +1075,13 @@ extension MessageInteractor { translate(message, lang: SelectedLang(sendingLang)) { [weak self] output in guard let `self` = self else { return } let translationDesc = self.messageFactory.autotranslationDesc(text: output.inputText, translatedText: output.translatedText, lang: output.translatedLang) - var files = message.files ?? [] + var files = message.files?.filter { $0.type != .autotranslate } ?? [] files.append(translationDesc) message.files = files if let repliedMessage = self.repliedMessage { self.messageSendingService.sendReplayedMessage(repliedMessage, message: message) + self.declineReply() } else { self.messageSendingService.sendMessage(message) } @@ -1073,6 +1089,7 @@ extension MessageInteractor { } else { if let repliedMessage = repliedMessage { messageSendingService.sendReplayedMessage(repliedMessage, message: message) + declineReply() } else { messageSendingService.sendMessage(message) } @@ -1116,9 +1133,29 @@ extension MessageInteractor { } } - func getMessageSenderContact(for sender: MessageSender) -> Contact? { - guard let phoneId = sender.phoneId else { return nil } - guard let contact = ContactDAO.findContactBy(phoneId: phoneId) else { return nil } - return contact + func findContact(with phoneId: String, completion: ((Contact?) -> Void)?) { + senderService.searchContact(with: phoneId) { result in + switch result { + case .success(let contact): + completion?(contact) + case .error: + completion?(nil) + } + } + } + +} + +extension MessageInteractor { + func notifyAboutUpdated(member: Member) { + guard let memberId = member.phone_id else { + return + } + + let messages = configuration.messages.filter { $0.from == memberId } + let localIds = messages.compactMap { $0.msg_id } + let sender = messageSender(from: member) + + presenter?.update(sender: sender, inMessagesWithIds: localIds) } } diff --git a/Nynja/Modules/Message/Models/Configurations/ChatConfiguration.swift b/Nynja/Modules/Message/Models/Configurations/ChatConfiguration.swift index 17b4162f5..e7da50d2c 100644 --- a/Nynja/Modules/Message/Models/Configurations/ChatConfiguration.swift +++ b/Nynja/Modules/Message/Models/Configurations/ChatConfiguration.swift @@ -7,7 +7,9 @@ // struct ChatConfiguration { + typealias ProgressModels = [URL: ProgressModel] typealias RepliedModels = [Int64: RepliedMessageModel] + typealias Stars = [String: Star] typealias Links = [String: [String]] typealias Mentions = [String: [MentionInfo]] typealias Translations = [String: TranslationInfo] @@ -15,17 +17,42 @@ struct ChatConfiguration { typealias ConversionsProgress = [String: ConvertionProgressModel] var messages: [Message] = [] - var progressModels: [URL : ProgressModel] = [:] - var translatingModels: ConversionsProgress = ConversionsProgress() - var transcribingModels: ConversionsProgress = ConversionsProgress() + + var progressModels: ProgressModels = [:] var repliedModels: RepliedModels = [:] - var stars: [String: Star] = [:] + var stars: Stars = [:] + var links: Links = [:] + var mentions: Mentions = [:] + + var translatingModels: ConversionsProgress = [:] + var transcribingModels: ConversionsProgress = [:] + var translations: Translations = [:] + var transcriptions: Transcriptions = [:] + var position: PositionType = .none var reader: Int64? + var selfReader: Int64? var unreadCount: Int = 0 var shouldShowUnread: Bool = false - var links: Links = Links() - var mentions: Mentions = Mentions() - var translations: Translations = Translations() - var transcriptions: Transcriptions = Transcriptions() + + mutating func join(_ configuration: ChatConfiguration) { + // left messages as it is, but join other data + + progressModels.mergeUniquingOther(with: configuration.progressModels) + repliedModels.mergeUniquingOther(with: configuration.repliedModels) + stars.mergeUniquingOther(with: configuration.stars) + links.mergeUniquingOther(with: configuration.links) + mentions.mergeUniquingOther(with: configuration.mentions) + + translatingModels.mergeUniquingOther(with: configuration.translatingModels) + transcribingModels.mergeUniquingOther(with: configuration.transcribingModels) + translations.mergeUniquingOther(with: configuration.translations) + transcriptions.mergeUniquingOther(with: configuration.transcriptions) + + position = configuration.position + reader = configuration.reader + selfReader = configuration.selfReader + unreadCount = configuration.unreadCount + shouldShowUnread = configuration.shouldShowUnread + } } diff --git a/Nynja/Modules/Message/Models/Mention/Entity/MentionInfo.swift b/Nynja/Modules/Message/Models/Mention/Entity/MentionInfo.swift index d47dc84cf..7734b0dde 100644 --- a/Nynja/Modules/Message/Models/Mention/Entity/MentionInfo.swift +++ b/Nynja/Modules/Message/Models/Mention/Entity/MentionInfo.swift @@ -11,6 +11,7 @@ import Foundation /// Model for payload tags link [mention memberId=".." accountId=".."]@alias(optional)[\mention] final class MentionInfo: BBCodeEntity { + static let translationAlias = "mntzqrpd" static let tag = "mention" static let attributes = [Attributes.memberId, Attributes.accountId] diff --git a/Nynja/Modules/Message/Models/Mention/InputController/MentionController.swift b/Nynja/Modules/Message/Models/Mention/InputController/MentionController.swift index 9cc0c44d1..a7dc95f7b 100644 --- a/Nynja/Modules/Message/Models/Mention/InputController/MentionController.swift +++ b/Nynja/Modules/Message/Models/Mention/InputController/MentionController.swift @@ -17,6 +17,12 @@ protocol MentionControllerInput: class { var textUpdateHandler: ((String, [Mention]) -> Void)? { get set } var cursorUpdateHandler: ((Int) -> Void)? { get set } + func setup(_ initialMentions: [Mention]) + + func reset() + + func hasMentions(in range: NSRange) -> Bool + func addMention(for member: Member) /// Handler for textViewDidChange(_:) and textViewDidChangeSelection(_:) @@ -24,9 +30,6 @@ protocol MentionControllerInput: class { /// Handler for textView(_:shouldChangeTextIn:replacementText:) func handleReplacementText(_ replacementText: String, in range: NSRange, currentText: String) -> Bool - - func setup(_ initialMentions: [Mention]) - func reset() } // MARK: - Controller @@ -53,6 +56,11 @@ final class MentionController: MentionControllerInput { mentions.removeAll() } + func hasMentions(in range: NSRange) -> Bool { + let range = range.nativeRange + return mentions.contains { $0.indices.intersects(range) } + } + // MARK: - Utils @@ -124,7 +132,7 @@ extension MentionController { func handleReplacementText(_ replacementText: String, in replacementRange: NSRange, currentText: String) -> Bool { var mentionsToRemove: [Mention] = [] - let range: Range = replacementRange.lowerBound.. 0 ? text.index(before: replacementIndex) : replacementIndex - if previousIndex != text.startIndex, text[previousIndex] != " " { + if replacementIndex != text.startIndex, text[previousIndex] != " " { return true } } else if !replacementText.ends(with: " ") { diff --git a/Nynja/Modules/Message/Models/Mention/Payload/BBCodeTags/MessagePayloadParser.swift b/Nynja/Modules/Message/Models/Mention/Payload/BBCodeTags/MessagePayloadParser.swift index 6cf706547..c9702756b 100644 --- a/Nynja/Modules/Message/Models/Mention/Payload/BBCodeTags/MessagePayloadParser.swift +++ b/Nynja/Modules/Message/Models/Mention/Payload/BBCodeTags/MessagePayloadParser.swift @@ -11,13 +11,17 @@ import Foundation typealias PayloadParseResult = [MentionInfo] protocol MessagePayloadParserInput: class { + func parse(_ text: String) -> PayloadParseResult? func parse(_ message: Message) -> PayloadParseResult? func parseTranslation(_ message: Message, info: Desc.TranslatedInfo) -> PayloadParseResult? - func parse(_ text: String) -> PayloadParseResult? } final class MessagePayloadParser: MessagePayloadParserInput { + func parse(_ text: String) -> PayloadParseResult? { + return parse(payload: text, shouldFetchAliasFromDatabase: false) + } + func parse(_ message: Message) -> PayloadParseResult? { guard message.hasMentions, let payload = message.mainFile?.payload else { return nil @@ -32,10 +36,6 @@ final class MessagePayloadParser: MessagePayloadParserInput { return parse(payload: info.translate, shouldFetchAliasFromDatabase: !message.isForward) } - func parse(_ text: String) -> PayloadParseResult? { - return parse(payload: text, shouldFetchAliasFromDatabase: false) - } - private func parse(payload: String, shouldFetchAliasFromDatabase: Bool) -> PayloadParseResult? { return BBCodeParser.findOccurences(of: MentionInfo.self, in: payload) { tag in guard let attributes = tag.attributes, diff --git a/Nynja/Modules/Message/Models/Mention/Text/InputTextStorage.swift b/Nynja/Modules/Message/Models/Mention/Text/InputTextStorage.swift new file mode 100644 index 000000000..e8245d4fb --- /dev/null +++ b/Nynja/Modules/Message/Models/Mention/Text/InputTextStorage.swift @@ -0,0 +1,56 @@ +// +// InputTextStorage.swift +// Nynja +// +// Created by Anton Poltoratskyi on 01.10.2018. +// Copyright © 2018 TecSynt Solutions. All rights reserved. +// + +import UIKit + +/* Note for subclassing NSTextStorage: NSTextStorage is a semi-abstract subclass of NSMutableAttributedString. It implements change management (beginEditing/endEditing), verification of attributes, delegate handling, and layout management notification. The one aspect it does not implement is the actual attributed string storage --- this is left up to the subclassers, which need to override the two NSMutableAttributedString primitives in addition to two NSAttributedString primitives: + + - (NSString *)string; + - (NSDictionary *)attributesAtIndex:(NSUInteger)location effectiveRange:(NSRangePointer)range; + + - (void)replaceCharactersInRange:(NSRange)range withString:(NSString *)str; + - (void)setAttributes:(NSDictionary *)attrs range:(NSRange)range; + + These primitives should perform the change then call edited:range:changeInLength: to get everything else to happen. + */ +public final class InputTextStorage: NSTextStorage { + + private let backingStore = NSMutableAttributedString() + + public weak var inputDelegate: InputTextStorageDelegate? + + public override var string: String { + return backingStore.string + } + + public override func attributes(at location: Int, effectiveRange range: NSRangePointer?) -> [NSAttributedStringKey : Any] { + return backingStore.attributes(at: location, effectiveRange: range) + } + + public override func replaceCharacters(in range: NSRange, with str: String) { + performEdit { + backingStore.replaceCharacters(in: range, with: str) + let diff = str.utf16.count - range.length + edited(.editedCharacters, range: range, changeInLength: diff) + } + } + + public override func setAttributes(_ attrs: [NSAttributedStringKey : Any]?, range: NSRange) { + performEdit { + let attributes = inputDelegate?.inputTextStorage(self, modifiedAttributesFor: attrs, range: range) ?? attrs + backingStore.setAttributes(attributes, range: range) + edited(.editedAttributes, range: range, changeInLength: 0) + } + } + + private func performEdit(block: () -> Void) { + beginEditing() + block() + endEditing() + } +} diff --git a/Nynja/Modules/Message/Models/Mention/Text/InputTextStorageDelegate.swift b/Nynja/Modules/Message/Models/Mention/Text/InputTextStorageDelegate.swift new file mode 100644 index 000000000..8b4de9d38 --- /dev/null +++ b/Nynja/Modules/Message/Models/Mention/Text/InputTextStorageDelegate.swift @@ -0,0 +1,25 @@ +// +// InputTextStorageDelegate.swift +// Nynja +// +// Created by Anton Poltoratskyi on 01.10.2018. +// Copyright © 2018 TecSynt Solutions. All rights reserved. +// + +import UIKit + +public typealias TextAttributes = [NSAttributedStringKey: Any] + +public protocol InputTextStorageDelegate: class { + func inputTextStorage(_ textStorage: InputTextStorage, + modifiedAttributesFor proposedAttributes: TextAttributes?, + range: NSRange) -> TextAttributes? +} + +extension InputTextStorageDelegate { + public func inputTextStorage(_ textStorage: InputTextStorage, + modifiedAttributesFor proposedAttributes: TextAttributes?, + range: NSRange) -> TextAttributes? { + return proposedAttributes + } +} diff --git a/Nynja/Modules/Message/Models/Mention/NSAttributedStringKey+Mention.swift b/Nynja/Modules/Message/Models/Mention/Text/NSAttributedStringKey+Mention.swift similarity index 100% rename from Nynja/Modules/Message/Models/Mention/NSAttributedStringKey+Mention.swift rename to Nynja/Modules/Message/Models/Mention/Text/NSAttributedStringKey+Mention.swift diff --git a/Nynja/Modules/Message/Presenter/MessagePresenter+MentionUnreadCounter.swift b/Nynja/Modules/Message/Presenter/MessagePresenter+MentionUnreadCounter.swift index 2e1b5d1d1..37fd3d4b3 100644 --- a/Nynja/Modules/Message/Presenter/MessagePresenter+MentionUnreadCounter.swift +++ b/Nynja/Modules/Message/Presenter/MessagePresenter+MentionUnreadCounter.swift @@ -12,7 +12,7 @@ extension MessagePresenter { // MARK: - Unread Counter - func prepareMentionedMessages(in room: Room) { + func fetchMentionedMessages(in room: Room) { guard let mentions = room.mentions?.compactMap({ MessageServerId($0) }) else { return } @@ -27,11 +27,13 @@ extension MessagePresenter { defer { lastVisibleMessageId = lastUnreadId } - guard !unreadMentionIds.isEmpty, uniqueUnreadMentionIds.contains(serverMessageId) else { + guard !unreadMentionIds.isEmpty else { return lastUnreadMentionCount } - let index = unreadMentionIds.index { $0 > lastUnreadId } ?? unreadMentionIds.count - lastUnreadMentionCount = unreadMentionIds.count - index + let totalCount = unreadMentionIds.count + let index = unreadMentionIds.index { $0 > lastUnreadId } ?? totalCount + + lastUnreadMentionCount = totalCount - index return lastUnreadMentionCount } @@ -39,13 +41,18 @@ extension MessagePresenter { func showNextMentionedMessage() { guard let nextUnreadMentionedMessageServerId = nextUnreadMentionIdentifier(after: lastVisibleMessageId) else { // Should never happen but handle this case. - resetUnreadMentionCounterData() + resetMentionsCounterView() view.scrollToBottom() return } view.scrollToMessage(serverId: nextUnreadMentionedMessageServerId, at: .center, animated: true) } + func resetMentionsCounterView() { + resetUnreadMentionCounterData() + view.resetUnreadMentionCounter() + } + func updateMentionsCounter(in room: Room) { guard room.hasMentions else { return @@ -58,14 +65,6 @@ extension MessagePresenter { // MARK: - Utils - func resetMentionsCounterView() { - guard lastUnreadMentionCount > 0 else { - return - } - resetUnreadMentionCounterData() - view.resetUnreadMentionCounter() - } - private func resetUnreadMentionCounterData() { uniqueUnreadMentionIds.removeAll() unreadMentionIds.removeAll() @@ -88,7 +87,7 @@ extension MessagePresenter { return } var unique: Set = [] - for case let mention in mentions { + for mention in mentions { guard let id = MessageServerId(mention) else { continue } diff --git a/Nynja/Modules/Message/Presenter/MessagePresenter.swift b/Nynja/Modules/Message/Presenter/MessagePresenter.swift index 35505f2ee..a567e58e0 100644 --- a/Nynja/Modules/Message/Presenter/MessagePresenter.swift +++ b/Nynja/Modules/Message/Presenter/MessagePresenter.swift @@ -71,7 +71,7 @@ class MessagePresenter: BasePresenter, MessagePresenterProtocol, MessageInteract super.screenLoaded() interactor.askForInternetStatus() - interactor.room.map { prepareMentionedMessages(in: $0) } + interactor.room.map { fetchMentionedMessages(in: $0) } } @@ -106,7 +106,7 @@ class MessagePresenter: BasePresenter, MessagePresenterProtocol, MessageInteract if wasViewDisappeared { let unreadCount = Int(interactor.chat.unreadCount) if unreadCount != 0 { - view.updateUnreadTitle(unreadCount) + view.updateUnreadTitle(interactor.chat.selfReader, unreadCount: unreadCount) } } } @@ -144,17 +144,6 @@ class MessagePresenter: BasePresenter, MessagePresenterProtocol, MessageInteract }) } - func openSharedContact(contact: Contact) { - if let phoneNumber = interactor.myContact?.phoneNumber, contact.phoneNumber == phoneNumber { - wireFrame.showMyProfileScreen() - } else if let contact = ContactDAO.findContactBy(phoneId: contact.phoneId) { - wireFrame.showProfileScreen(contact: contact) - } else { - contact.status = nil - wireFrame.showProfileScreen(contact: contact) - } - } - func sendMessage(text: String, translation: TranslationManualView.TranlationResult?, mentions: [Mention]) { let trimmedText = text.trimmingCharacters(in: .whitespacesAndNewlines) let message = InputTextMessage(text: trimmedText, mentions: mentions, translation: translation) @@ -294,7 +283,6 @@ class MessagePresenter: BasePresenter, MessagePresenterProtocol, MessageInteract func forwardMessage(localId: String) { interactor.prepareToForward(localId: localId) wireFrame.showForwardSelector(with: localId, delegate: self) - view.endInputBarInteraction() } func translateMessage(localId: String) { @@ -446,7 +434,7 @@ class MessagePresenter: BasePresenter, MessagePresenterProtocol, MessageInteract func blockWheelActions(_ isBlock: Bool) { isActionsEnabled = !isBlock - WCDataManager.shared.updateActionsState(isBlock) + WCDataManager.shared.setActionsState(disable: isBlock) WCDataManager.shared.wheelContainer?.reloadData() } @@ -563,9 +551,9 @@ class MessagePresenter: BasePresenter, MessagePresenterProtocol, MessageInteract func deleteAndLeave() { wireFrame.deleteAndLeave() } - + func setup(with configuration: ChatConfiguration) { - let filtered = configuration.messages.filter { ($0.status as? StringAtom)?.string != "delete" } + let filtered = configuration.messages.filter { $0.messageStatus != .delete } var cells = [BaseChatCellModel]() var unreadIndex: Int? @@ -574,17 +562,17 @@ class MessagePresenter: BasePresenter, MessagePresenterProtocol, MessageInteract for (index, message) in filtered.enumerated() { if index == 0 { time = Date(timestamp: message.createdInt) - cells.append(dateModel(for: message, time: time)) + cells.append(.makeTimeModel(with: time)) } else { let tmp = Date(timestamp: message.createdInt) if !tmp.isDayEqual(to: time) { time = tmp - cells.append(dateModel(for: message, time: time)) + cells.append(.makeTimeModel(with: time)) } } var repliedModel: RepliedMessageModel? - if message.isReply, let serverId = message.link { + if message.isReply, let serverId = message.linkedId { repliedModel = configuration.repliedModels[serverId] } @@ -612,16 +600,13 @@ class MessagePresenter: BasePresenter, MessagePresenterProtocol, MessageInteract } } - if configuration.shouldShowUnread, configuration.unreadCount > 0 { - let model = BaseChatCellModel.unreadModel() - - var index = cells.count - configuration.unreadCount - if index < 0 { - index = 0 - } - - cells.insert(model, at: index) - unreadIndex = index + 1 + if configuration.shouldShowUnread, + let index = cells.unreadIndex( + selfReader: configuration.selfReader, + unreadCount: configuration.unreadCount) { + + cells.insert(.makeUnreadModel(), at: index) + unreadIndex = index } else { unreadIndex = nil } @@ -636,33 +621,35 @@ class MessagePresenter: BasePresenter, MessagePresenterProtocol, MessageInteract } func setupNew(with configuration: ChatConfiguration) { - let filtered = configuration.messages.filter { ($0.status as? StringAtom)?.string != "delete" } + let filtered = configuration.messages.filter { $0.messageStatus != .delete } var cells = [BaseChatCellModel]() var time: Date! for (index, message) in filtered.enumerated() { if index == 0 { time = Date(timestamp: message.createdInt) - cells.append(dateModel(for: message, time: time)) + cells.append(.makeTimeModel(with: time)) } else { let tmp = Date(timestamp: message.createdInt) if !tmp.isDayEqual(to: time) { time = tmp - cells.append(dateModel(for: message, time: time)) + cells.append(.makeTimeModel(with: time)) } } var repliedModel: RepliedMessageModel? - if message.isReply, let serverId = message.link { + if message.isReply, let serverId = message.linkedId { repliedModel = configuration.repliedModels[serverId] } if let systemMessage = message.systemMessage(for: interactor.chat) { cells.append(systemModel(message: systemMessage, mod: message)) + } else if let localId = message.msg_id, var model = self.getCellModel(message: message, repliedModel: repliedModel, readerID: configuration.reader, + links: configuration.links[localId], mentions: configuration.mentions[localId], translation: configuration.translations[localId], transcription: configuration.transcriptions[localId], @@ -698,7 +685,7 @@ class MessagePresenter: BasePresenter, MessagePresenterProtocol, MessageInteract } func didChangeCallInvitationState(_ call: NYNCall) { - self.view.displayRejoinBanner(display: call.isRunning, count: call.participantsCount) + self.view.displayRejoinBanner(display: call.isRunning, count: call.membersCount) } private func createDisplayChatConfiguration(with cells: [BaseChatCellModel], @@ -710,8 +697,8 @@ class MessagePresenter: BasePresenter, MessagePresenterProtocol, MessageInteract position = .unread(index) } - let isUnreadShown: Bool = configuration.shouldShowUnread && unreadIndex != nil - + let isUnreadShown = configuration.shouldShowUnread && unreadIndex != nil + return DisplayChatConfiguration(cells: cells, position: position, isUnreadShown: isUnreadShown) } @@ -726,12 +713,6 @@ class MessagePresenter: BasePresenter, MessagePresenterProtocol, MessageInteract return model } - private func dateModel(for message: Message, time: Date) -> BaseChatCellModel { - let model = BaseChatCellModel() - model.time = time - return model - } - private func isOutgoingMessage(_ message: Message) -> Bool { let isSender = message.from == interactor.myContact?.phone_id @@ -951,6 +932,10 @@ class MessagePresenter: BasePresenter, MessagePresenterProtocol, MessageInteract view.updateDeliveryStatus(.sent, messageId: localId) } + func update(sender: MessageSender, inMessagesWithIds messageIds: [String]) { + view.update(sender: sender, inMessagesWithIds: messageIds) + } + func messageRead(_ localId: String) { view.updateDeliveryStatus(.read, messageId: localId) } @@ -972,8 +957,15 @@ class MessagePresenter: BasePresenter, MessagePresenterProtocol, MessageInteract } func handleOpponentAvatarTap(for sender: MessageSender) { - guard let contact = self.interactor.getMessageSenderContact(for: sender) else { return } - self.openSharedContact(contact: contact) + guard let phoneId = sender.phoneId else { + return + } + interactor.findContact(with: phoneId) { [weak self] contact in + guard let contact = contact else { + return + } + self?.openProfileScreen(contact: contact) + } } // MARK: - Utils private func updateStatus(_ status: String) { @@ -998,6 +990,10 @@ class MessagePresenter: BasePresenter, MessagePresenterProtocol, MessageInteract interactor.rejoinRunningCall() } + func currentMembersCount() -> UInt { + return interactor.currentMembersCount() + } + func openMarketplaceScreen() { self.wireFrame.openMarketplaceScreen() } diff --git a/Nynja/Modules/Message/Protocols/MentionTransitionProtocol.swift b/Nynja/Modules/Message/Protocols/MentionTransitionProtocol.swift index 0fc1187b5..fafd6ce2d 100644 --- a/Nynja/Modules/Message/Protocols/MentionTransitionProtocol.swift +++ b/Nynja/Modules/Message/Protocols/MentionTransitionProtocol.swift @@ -17,11 +17,11 @@ extension MentionTransitionProtocol where Self: MessagePresenter { func showMention(_ mentionInfo: MentionInfo) { let phoneId = mentionInfo.accountId - if let contact = interactor.fetchContact(phoneId: phoneId) { - openProfileScreen(contact: contact) - - } else if let phoneNumber = Contact.phoneNumber(from: phoneId) { - interactor.findContact(phoneNumber: phoneNumber) + interactor.findContact(with: phoneId) { [weak self] contact in + guard let contact = contact else { + return + } + self?.openProfileScreen(contact: contact) } } } diff --git a/Nynja/Modules/Message/Protocols/MessageProtocols.swift b/Nynja/Modules/Message/Protocols/MessageProtocols.swift index ed7dd7ad8..9a5dd81bf 100644 --- a/Nynja/Modules/Message/Protocols/MessageProtocols.swift +++ b/Nynja/Modules/Message/Protocols/MessageProtocols.swift @@ -26,7 +26,8 @@ protocol MessageWireframeProtocol: DocumentInteractionInput { func presentMessages(navigation: UINavigationController, chat: ChatModel, main: MainWireFrame?, - initialMessage: ChatInitialMessage?) + initialMessage: ChatInitialMessage?, + animated: Bool) func openLocation(with type: LocationType) func presentImageModally(imageURL: URL, on view: UIViewController, with transitionInfo: ImagePreviewTransitionInfo) @@ -61,7 +62,7 @@ protocol MessagePresenterProtocol: BasePresenterProtocol, func openSettingsGroupScreen(room: Room) func openReplies(messageId: String) - func openSharedContact(contact: Contact) + func openProfileScreen(contact: Contact) func openLocation(with type: LocationType) func openImage(imageURL: URL, with transitionInfo: ImagePreviewTransitionInfo) func openFile(at url: URL) @@ -126,6 +127,7 @@ protocol MessagePresenterProtocol: BasePresenterProtocol, func hasCallInProgress() -> Bool func rejoinRunningCall() func openMarketplaceScreen() + func currentMembersCount() -> UInt } //MARK: Interactor - @@ -151,6 +153,8 @@ protocol MessageInteractorOutputProtocol: class, MentionFetchOutputProtocol, Mes func messageSent(_ localId: MessageLocalId) func messageRead(_ localId: MessageLocalId) + func update(sender: MessageSender, inMessagesWithIds messageIds: [String]) + func startSendingMessage() func blockWheelActions(_ isBlock: Bool) @@ -174,7 +178,7 @@ protocol MessageInteractorOutputProtocol: class, MentionFetchOutputProtocol, Mes } -protocol MessageInteractorInputProtocol: BaseInteractorProtocol, MentionFetchInputProtocol, MessageSearchInputProtocol, MessageChannelActionsProtocol, Translation, Transcription { +protocol MessageInteractorInputProtocol: BaseInteractorProtocol, MentionFetchInputProtocol, MessageChannelActionsProtocol, Translation, Transcription { var presenter: MessageInteractorOutputProtocol? { get set } @@ -235,7 +239,7 @@ protocol MessageInteractorInputProtocol: BaseInteractorProtocol, MentionFetchInp func member(for phoneId: String) -> Member? - func sender(for message: Message) -> MessageSender? + func sender(for message: Message) -> MessageSender func askForInternetStatus() @@ -243,13 +247,15 @@ protocol MessageInteractorInputProtocol: BaseInteractorProtocol, MentionFetchInp func clearEditMessageObject() func scheduleInfo(for message: InputScheduleMessage) -> ScheduleInfo? - func getMessageSenderContact(for sender: MessageSender) -> Contact? + func findContact(with phoneId: String, completion: ((Contact?) -> Void)?) func hasRunningCall() -> Bool func hasCallInProgress() -> Bool func rejoinRunningCall() func prepareToForward(localId: MessageLocalId) func didSelectForwardTargets(_ targets: ForwardTargets) + + func currentMembersCount() -> UInt } //MARK: View - @@ -269,7 +275,7 @@ protocol MessageViewProtocol: class { func updateMessage(_ model: BaseChatCellModel) func updateStar(starID: String?, messageId: String) - func updateUnreadTitle(_ count: Int) + func updateUnreadTitle(_ selfRead: Int64?, unreadCount: Int) func updateUnreadMentions() func resetUnreadMentionCounter() @@ -284,6 +290,8 @@ protocol MessageViewProtocol: class { func updateHeaderStatus(_ status: String) func updateDeliveryStatus(_ status: DeliveryStatus, messageId: String) func removeMessage(_ messageId: String) + + func update(sender: MessageSender, inMessagesWithIds messageIds: [String]) func updateInputBar(_ displayMode: InputBar.DisplayMode) diff --git a/Nynja/Modules/Message/Protocols/VoiceAudioInteractive.swift b/Nynja/Modules/Message/Protocols/VoiceAudioInteractive.swift index 5a51440ce..01807f97e 100644 --- a/Nynja/Modules/Message/Protocols/VoiceAudioInteractive.swift +++ b/Nynja/Modules/Message/Protocols/VoiceAudioInteractive.swift @@ -10,38 +10,46 @@ import Foundation protocol VoiceAudioInteractive: class { var audioManager: AudioManager { get } + var audioSessionManager: AudioSessionManager { get } var proximityManager: ProximitySensorManager { get } - var currentPlayingModel: BaseChatCellModel? { get set } + var currentPlayingModel: AudioPlayable? { get set } } // MARK: - Audio Actions extension VoiceAudioInteractive { func resume() { - currentPlayingModel?.playStatus = .play - currentPlayingModel?.notifyAudioHandler() - audioManager.resume() + if let _ = try? audioManager.resume() { + updatePlayStatus(.play) + } } func pause() { - currentPlayingModel?.playStatus = .pause - currentPlayingModel?.notifyAudioHandler() audioManager.pause() + updatePlayStatus(.pause) } - func stop() { + private func updateUIForStopState() { + proximityManager.monitoringState = .disabled currentPlayingModel?.audioCurrentTime = nil - currentPlayingModel?.playStatus = .stop - currentPlayingModel?.notifyAudioHandler() + updatePlayStatus(.stop) currentPlayingModel = nil } func stopPlaying() { - audioManager.stop() - proximityManager.monitoringState = .disabled - // audioManager.speaker = .loud - stop() + guard let currentUrl = currentPlayingModel?.fileUrl else { + return + } + + audioManager.stop(with: currentUrl) + updateUIForStopState() + } + + private func updatePlayStatus(_ status: PlayStatus) { + currentPlayingModel?.playStatus = status + currentPlayingModel?.notifyAudioHandler() } + } // MARK: - Proximity Sensor @@ -50,7 +58,7 @@ extension VoiceAudioInteractive where Self: ProximitySensorManagerDelegate { func proximityStateChanged(_ manager: ProximitySensorManager, state: ProximityState) { switch state { case .closeToUser: - audioManager.speaker = .soft + audioSessionManager.speaker = .soft if !audioManager.isPlaying { resume() @@ -67,7 +75,7 @@ extension VoiceAudioInteractive where Self: ProximitySensorManagerDelegate { extension VoiceAudioInteractive where Self: AudioManagerDelegate { func didFinishPlayingAudio(_ audioManager: AudioManager, with url: URL) { - stopPlaying() + updateUIForStopState() } func didChangedCurrentTime(_ audioManager: AudioManager, currentTime: TimeInterval) { @@ -81,21 +89,13 @@ extension VoiceAudioInteractive where Self: AudioManagerDelegate { extension VoiceAudioInteractive where Self: BaseChatCellDelegate { func didPlayTapped(_ cell: BaseChatCell, url: URL) { - proximityManager.monitoringState = .enabled - audioManager.speaker = .loud - - if audioManager.currentUrl != url { - stop() - - if let _ = try? audioManager.play(with: url) { - currentPlayingModel = cell.model - currentPlayingModel?.playStatus = .play - currentPlayingModel?.notifyAudioHandler() - } - } else { - currentPlayingModel = cell.model - resume() + guard let _ = try? audioManager.play(with: url, at: cell.model?.audioCurrentTime ?? 0) else { + return } + + proximityManager.monitoringState = .enabled + currentPlayingModel = cell.model + updatePlayStatus(.play) } func didPauseTapped(_ cell: BaseChatCell, url: URL) { @@ -103,16 +103,68 @@ extension VoiceAudioInteractive where Self: BaseChatCellDelegate { } func didChangeProgress(_ cell: BaseChatCell, url: URL, progress: Double) { - if audioManager.currentUrl != url { - stopPlaying() + if audioManager.currentUrl == url { + pause() + } else { + try? audioManager.prepareToPlay(with: url) currentPlayingModel = cell.model - currentPlayingModel?.playStatus = .stop - currentPlayingModel?.notifyAudioHandler() } - audioManager.changedProgress(progress, for: url) + + guard let model = cell.model, + let audioDuration = model.audioDuration else { + return + } + + model.audioCurrentTime = audioDuration * progress / 100.0 } func isCanChangeProgress(_ cell: BaseChatCell, url: URL) -> Bool { return true } } + +extension VoiceAudioInteractive where Self: InputBarDelegate { + + func didPlayTapped(_ model: AudioPlayable) { + guard let url = model.fileUrl, let _ = try? audioManager.play(with: url, at: model.audioCurrentTime ?? 0) else { + return + } + + currentPlayingModel = model + updatePlayStatus(.play) + proximityManager.monitoringState = .enabled + } + + func didPauseTapped(_ model: AudioPlayable) { + pause() + } + + func didChangeProgress(_ model: AudioPlayable, progress: Double) { + guard let url = model.fileUrl else { + return + } + if audioManager.currentUrl == url { + pause() + } else { + try? audioManager.prepareToPlay(with: url) + currentPlayingModel = model + } + + guard let audioDuration = model.audioDuration else { + return + } + + currentPlayingModel?.audioCurrentTime = audioDuration * progress / 100.0 + } + + func isCanChangeProgress(_ model: AudioPlayable) -> Bool { + return true + } + + func tryToStartRecording() { + stopPlaying() + } +} + + + diff --git a/Nynja/Modules/Message/View/MessageVC+CellDelegate.swift b/Nynja/Modules/Message/View/MessageVC+CellDelegate.swift index 64ecea94f..28d5d0ab1 100644 --- a/Nynja/Modules/Message/View/MessageVC+CellDelegate.swift +++ b/Nynja/Modules/Message/View/MessageVC+CellDelegate.swift @@ -8,9 +8,28 @@ import Foundation -extension MessageVC: VoiceAudioInteractive, BaseChatCellDelegate, AudioManagerDelegate, ProximitySensorManagerDelegate { +extension MessageVC: VoiceAudioInteractive, AudioManagerDelegate, ProximitySensorManagerDelegate { } + +extension MessageVC: InputBarDelegate { } + +extension MessageVC: InputTextStorageDelegate { + + func inputTextStorage(_ textStorage: InputTextStorage, + modifiedAttributesFor proposedAttributes: TextAttributes?, + range: NSRange) -> TextAttributes? { + var result = proposedAttributes + if mentionController.hasMentions(in: range) { + result?[.foregroundColor] = mentionAttributes[.foregroundColor] + } else { + result?[.foregroundColor] = defaultInputTextAttributes[.foregroundColor] + } + return result + } +} + +// MARK: - BaseChatCellDelegate +extension MessageVC: BaseChatCellDelegate { - // MARK: - BaseChatCellDelegate func didCellTapped(_ cell: BaseChatCell) { guard let model = cell.model, let type = model.type else { return @@ -77,7 +96,6 @@ extension MessageVC: VoiceAudioInteractive, BaseChatCellDelegate, AudioManagerDe func didReplyCounterTapped(_ cell: BaseChatCell) { if let id = cell.model?.id { presenter.openReplies(messageId: id) - endInputBarInteraction() } } @@ -95,7 +113,6 @@ extension MessageVC: VoiceAudioInteractive, BaseChatCellDelegate, AudioManagerDe guard let sender = cell.model?.sender else { return } - endInputBarInteraction() - self.presenter.handleOpponentAvatarTap(for: sender) + presenter.handleOpponentAvatarTap(for: sender) } } diff --git a/Nynja/Modules/Message/View/MessageVC.swift b/Nynja/Modules/Message/View/MessageVC.swift index 6825a8dde..f83305f6b 100644 --- a/Nynja/Modules/Message/View/MessageVC.swift +++ b/Nynja/Modules/Message/View/MessageVC.swift @@ -26,8 +26,6 @@ final class MessageVC: BaseVC, MessageViewProtocol, ReplyPreviewDelegate, BackSw } } - var progressDictionary = [ProgressIdentifier: [(ProgressModel)->Void]]() - var loadingStatus = false var messageDS: MessageCollectionViewDataSource! @@ -36,9 +34,18 @@ final class MessageVC: BaseVC, MessageViewProtocol, ReplyPreviewDelegate, BackSw var contextMenuPresented = false var audioManager = AudioManager.sharedInstance + var audioSessionManager = AudioSessionManager.shared var proximityManager = ProximitySensorManager.shared - var currentPlayingModel: BaseChatCellModel? + var currentPlayingModel: AudioPlayable? let mentionController: MentionControllerInput = MentionController() + + var defaultInputTextAttributes: TextAttributes { + return TextInputContent.defaultTextAttributes + } + + var mentionAttributes: TextAttributes { + return TextInputContent.mentionAttributes + } private var buttonState: ButtonState = .none { didSet { @@ -134,6 +141,7 @@ final class MessageVC: BaseVC, MessageViewProtocol, ReplyPreviewDelegate, BackSw private(set) lazy var inputBar: InputBar = { let inputBar = InputBar() + inputBar.delegate = self inputBar.displayMode = .new inputBar.newSendHandler = { [weak self] content in @@ -216,7 +224,7 @@ final class MessageVC: BaseVC, MessageViewProtocol, ReplyPreviewDelegate, BackSw self.view.addSubview(preview) preview.snp.makeConstraints({ (make) in make.left.right.equalToSuperview() - make.bottom.equalTo(inputBar.snp.top) + make.bottom.equalTo(inputBar.snp.top).offset(-Constraints.replyPreview.bottomInset) }) return preview @@ -256,10 +264,9 @@ final class MessageVC: BaseVC, MessageViewProtocol, ReplyPreviewDelegate, BackSw }() private(set) lazy var swipeBackHelper: SwipeBackHelper = { - let helper = SwipeBackHelper(with: self, gestureCompletion: { [weak self] in - self?.endInputBarInteraction() + return SwipeBackHelper(with: self, gestureCompletion: { [weak self] in + self?.prepareForDissappear() }) - return helper }() @@ -425,7 +432,12 @@ final class MessageVC: BaseVC, MessageViewProtocol, ReplyPreviewDelegate, BackSw mentionCounterView.isHidden = true stickerSearchResultView.isHidden = true - displayRejoinBanner(display: presenter.hasRunningCall(), count: 0) + displayRejoinBanner(display: presenter.hasRunningCall(), count: presenter.currentMembersCount()) + } + + override func prepareForDissappear() { + super.prepareForDissappear() + endInputBarInteraction() } deinit { @@ -574,15 +586,14 @@ final class MessageVC: BaseVC, MessageViewProtocol, ReplyPreviewDelegate, BackSw } } - let defaultAttributes = TextInputContent.defaultTextAttributes - let mentionAttributes = TextInputContent.mentionAttributes - mentionController.textUpdateHandler = { [weak self] inputText, mentions in - let attributedString = NSMutableAttributedString(string: inputText, attributes: defaultAttributes) + guard let `self` = self else { return } + + let attributedString = NSMutableAttributedString(string: inputText, attributes: self.defaultInputTextAttributes) for mention in mentions { - attributedString.setAttributes(mentionAttributes, range: mention.indices.nsRange) + attributedString.setAttributes(self.mentionAttributes, range: mention.indices.nsRange) } - self?.inputBar.updateInputText(attributedString.copy() as! NSAttributedString) + self.inputBar.updateInputText(attributedString.copy() as! NSAttributedString) } mentionController.cursorUpdateHandler = { [weak self] cursorPosition in @@ -663,6 +674,8 @@ final class MessageVC: BaseVC, MessageViewProtocol, ReplyPreviewDelegate, BackSw // MARK: - Mentions + + private(set) var isMentionsVisible = false func setupUnreadMentions(after index: Int) { var serverId = messageDS.cellModel(at: index).serverID @@ -674,14 +687,17 @@ final class MessageVC: BaseVC, MessageViewProtocol, ReplyPreviewDelegate, BackSw if let id = serverId { let count = presenter.mentionsCount(after: id) mentionCounterView.count = count - toggleMentionsCounter(count > 0) + + isMentionsVisible = count > 0 + toggleMentionsCounter(isMentionsVisible) } else { + isMentionsVisible = false presenter.resetMentionsCounterView() } } - func showNexMentionedMessage() { + private func showNexMentionedMessage() { presenter.showNextMentionedMessage() } @@ -786,7 +802,6 @@ final class MessageVC: BaseVC, MessageViewProtocol, ReplyPreviewDelegate, BackSw } @objc func avatarPressed() { - endInputBarInteraction() presenter.avatarTapped() } @@ -825,18 +840,16 @@ final class MessageVC: BaseVC, MessageViewProtocol, ReplyPreviewDelegate, BackSw adjustPosition(with: configuration, isLastMessageVisible: isLastMessageVisible) adjustButtonState(configuration) - - updateUnreadMentions() } func updateNewData(_ history: [BaseChatCellModel]) { - let lastTimeShowed = history.last { $0.time != nil }.flatMap { $0.time } + let lastTimeShowed = history.last { $0.modelType.time != nil } - if let time = lastTimeShowed { + if let time = lastTimeShowed?.modelType.time { for i in messageDS.indices { let i = messageDS.presentationIndex(from: i) - guard let timeCell = messageDS.cellModel(at: i).time, - timeFormatter.string(from: timeCell) == timeFormatter.string(from: time) else { + guard let timeFromCell = messageDS.cellModel(at: i).modelType.time, + timeFormatter.string(from: timeFromCell) == timeFormatter.string(from: time) else { continue } messageDS.remove(at: i) @@ -848,8 +861,6 @@ final class MessageVC: BaseVC, MessageViewProtocol, ReplyPreviewDelegate, BackSw collectionView.reloadData() loadingStatus = false - - updateUnreadMentions() } private func adjustPosition(with configuration: DisplayChatConfiguration, isLastMessageVisible: Bool) { @@ -870,8 +881,9 @@ final class MessageVC: BaseVC, MessageViewProtocol, ReplyPreviewDelegate, BackSw case .message(let localId): scrollToMessage(with: localId) case .unread(let index): - let index = messageDS.presentationIndex(from: index) + 1 + let index = messageDS.presentationIndex(from: index) scrollToUnreadMessage(with: index) + updateUnreadMentions() case .checkpoint(let checkpoint): guard case .none = lastPosition else { return @@ -903,13 +915,11 @@ final class MessageVC: BaseVC, MessageViewProtocol, ReplyPreviewDelegate, BackSw } func showNativeShare(for fileUrl: URL) { - MQTTService.sharedInstance.disconnect() AlertManager.sharedInstance.showNativeShare(with: [fileUrl]) } func openAddContactScreen(contact: Contact) { - endInputBarInteraction() - presenter.openSharedContact(contact: contact) + presenter.openProfileScreen(contact: contact) } func getPreviousMessages(id: MessageServerId) { @@ -940,14 +950,11 @@ final class MessageVC: BaseVC, MessageViewProtocol, ReplyPreviewDelegate, BackSw } func updateProgress(progressModel: ProgressModel?) { - DispatchQueue.main.async { - guard let url = progressModel?.url.absoluteString, let model = progressModel else { + DispatchQueue.main.async { [weak self] in + guard let messageDS = self?.messageDS, let progressModel = progressModel else { return } - self.progressDictionary.keys.forEach { - guard $0.url == url else { return } - self.progressDictionary[$0]?.forEach { block in block(model) } - } + messageDS.update(progress: progressModel) } } @@ -969,21 +976,30 @@ final class MessageVC: BaseVC, MessageViewProtocol, ReplyPreviewDelegate, BackSw model.deliveryStatus = messageDS.cellModel(at: index).deliveryStatus // TODO: castil messageDS.set(model, at: index) + updateReplies(of: messageId) { replyModel in + if replyModel.type == .text { + // TODO: in this case we don'a actually need to reload cell, because height won't be changed. + // It would be better to update only cells layout. + replyModel.text = model.text ?? "" + } + } + + collectionView.reloadData() + } + + private func updateReplies(of messageId: MessageLocalId, update: (RepliedMessageModel) -> Void) { for idx in messageDS.indices { let cellModel = messageDS.cellModel(at: idx) switch cellModel.messageType { case let .reply(replyModel, _): - if replyModel.id == messageId && replyModel.type == .text { - // TODO: in this case we don'a actually need to reload cell, because height won't be changed. - // It would be better to update only cells layout. - replyModel.text = model.text ?? "" + if replyModel.id == messageId { + update(replyModel) } default: break } } - collectionView.reloadData() } func showContextMenu(fromCell cell: BaseChatCell, convertingModel: ConvertionMessageModel? = nil, targetView: UIView? = nil) { @@ -1080,6 +1096,23 @@ final class MessageVC: BaseVC, MessageViewProtocol, ReplyPreviewDelegate, BackSw collectionView.reloadData() } + func update(sender: MessageSender, inMessagesWithIds messageIds: [String]) { + dispatchAsyncMain { [weak self] in + guard let `self` = self else { + return + } + + let indices = messageIds.compactMap({ (messageId) -> Int? in + return self.messageDS.index(where: { $0.id == messageId }) + }) + + let models = indices.map { self.messageDS.cellModel(at: $0) } + models.forEach { $0.sender = sender } + + self.collectionView.reloadData() + } + } + func updateHeader(with viewModel: AvatarViewModel) { avatarView.setup(with: viewModel) } @@ -1096,14 +1129,17 @@ final class MessageVC: BaseVC, MessageViewProtocol, ReplyPreviewDelegate, BackSw collectionView.reloadData() } - func updateUnreadTitle(_ count: Int) { - let index = messageDS.unreadIndex(count: count) + func updateUnreadTitle(_ selfReader: Int64?, unreadCount: Int) { + guard let index = messageDS.unreadIndex(selfReader: selfReader, unreadCount: unreadCount) else { + return + } + let unreadIndex = messageDS.unreadCellIndex if let unreadIndex = unreadIndex { messageDS.move(at: unreadIndex, to: index) } else { - messageDS.insert(.unreadModel(), at: index) + messageDS.insert(.makeUnreadModel(), at: index) } collectionView.reloadData() collectionView.layoutIfNeeded() @@ -1288,7 +1324,7 @@ final class MessageVC: BaseVC, MessageViewProtocol, ReplyPreviewDelegate, BackSw continue } guard model.deliveryStatus != .read else { - if model.time != nil || model.unread != nil { + if !model.modelType.isMessage { continue } break @@ -1347,15 +1383,23 @@ final class MessageVC: BaseVC, MessageViewProtocol, ReplyPreviewDelegate, BackSw guard let messageDS = messageDS else { return } + + updateReplies(of: messageId) { replyModel in + replyModel.isDeleted = true + } + let indexes = messageDS.indexesForDelete(where: { $0.id == messageId }) - if !indexes.isEmpty { - let offset = collectionViewLayout.additionalOffsetForDeletingItems(at: indexes.map { IndexPath(item: $0, section: 0) }) - - messageDS.remove(at: indexes) + + guard !indexes.isEmpty else { collectionView.reloadData() - - collectionViewLayout.applyOffsetIfNeeded(offset: offset) + return } + let offset = collectionViewLayout.additionalOffsetForDeletingItems(at: indexes.map { IndexPath(item: $0, section: 0) }) + + messageDS.remove(at: indexes) + collectionView.reloadData() + + collectionViewLayout.applyOffsetIfNeeded(offset: offset) } func getNextPage() { @@ -1384,7 +1428,7 @@ final class MessageVC: BaseVC, MessageViewProtocol, ReplyPreviewDelegate, BackSw } private func removeUnreadTitle() { - guard let prevIndex = messageDS.index(where: { $0.unread == true }) else { + guard let prevIndex = messageDS.index(where: { $0.modelType.isUnread }) else { return } let indexPath = IndexPath(item: prevIndex, section: 0) @@ -1504,7 +1548,7 @@ extension MessageVC { return } let cellModel = messageDS.cellModel(at: index) - guard cellModel.unread == true else { + guard cellModel.modelType.isUnread else { return } scrollToMessage(with: index) diff --git a/Nynja/Modules/Message/View/MessageVCLayout.swift b/Nynja/Modules/Message/View/MessageVCLayout.swift index a14a149fa..0f7309484 100644 --- a/Nynja/Modules/Message/View/MessageVCLayout.swift +++ b/Nynja/Modules/Message/View/MessageVCLayout.swift @@ -22,7 +22,7 @@ extension MessageVC { enum replyPreview { static let height = ReplyPreview.Constraints.height - static let bottomInset = MainViewController.Constraints.inputBar.height.adjustedByWidth + static let bottomInset = 16.adjustedByWidth } enum gradientView { diff --git a/Nynja/Modules/Message/View/Views/CollectionView/MessageCollectionViewDataSource.swift b/Nynja/Modules/Message/View/Views/CollectionView/MessageCollectionViewDataSource.swift index e07023d47..e89ddd6d7 100644 --- a/Nynja/Modules/Message/View/Views/CollectionView/MessageCollectionViewDataSource.swift +++ b/Nynja/Modules/Message/View/Views/CollectionView/MessageCollectionViewDataSource.swift @@ -49,7 +49,7 @@ final class MessageCollectionViewDataSource: NSObject { } var unreadCellIndex: Int? { - guard let index = cells.index(where: { $0.unread == true }) else { + guard let index = cells.index(where: { $0.modelType.isUnread }) else { return nil } return presentationIndex(from: index) @@ -59,14 +59,26 @@ final class MessageCollectionViewDataSource: NSObject { self.cells = cells } + func update(progress: ProgressModel) { + cells.forEach { + guard $0.progressModel?.url == progress.url else { + return + } + $0.progressModel = progress + } + } + func forEach(block: (BaseChatCellModel) -> Void) { for cell in cells { block(cell) } } - func unreadIndex(count: Int) -> Int { - return isReversed ? min(self.count, count) : cells.count - count + func unreadIndex(selfReader: MessageServerId?, unreadCount: Int) -> Int? { + guard let index = cells.unreadIndex(selfReader: selfReader, unreadCount: unreadCount) else { + return nil + } + return presentationIndex(from: index) } func index(where predicate: (BaseChatCellModel) -> Bool) -> Int? { @@ -83,7 +95,7 @@ final class MessageCollectionViewDataSource: NSObject { //Check if previous cell is date cell then remove it let dataIndexPreviousCell = dataIndexRemovingCell - 1 - guard cells.count == (dataIndexRemovingCell + 1), dataIndexPreviousCell >= 0, cells[dataIndexPreviousCell].time != nil else { + guard cells.count == (dataIndexRemovingCell + 1), dataIndexPreviousCell >= 0, cells[dataIndexPreviousCell].modelType.isTime else { return [presentationIndexRemovingCell] } let presentationIndexPreviousCell = presentationIndex(from: dataIndexPreviousCell) @@ -129,18 +141,15 @@ final class MessageCollectionViewDataSource: NSObject { var newCells = [cell] - if lastCell.time == nil && !lastTime.isDayEqual(to: time) { - let timestamp = BaseChatCellModel() - timestamp.time = time - newCells.insert(timestamp, at: 0) + if let time = lastCell.modelType.time, !lastTime.isDayEqual(to: time) { + newCells.insert(.makeTimeModel(with: time), at: 0) } cells.append(contentsOf: newCells) } func insert(_ cell: BaseChatCellModel, at index: Int) { - // + 1 for reverse - cells.insert(cell, at: dataIndex(from: index) + 1) + cells.insert(cell, at: dataIndex(from: index)) } func remove(at index: Int) { @@ -183,7 +192,7 @@ extension MessageCollectionViewDataSource: UICollectionViewDataSource { func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell { let model = cellModel(at: indexPath) - if model.unread != nil { + if model.modelType.isUnread { if let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "unread", for: indexPath) as? UnreadCell { cell.setup() cell.accessibilityIdentifier = "chat_cell_unread_\(indexPath.section)" @@ -191,13 +200,13 @@ extension MessageCollectionViewDataSource: UICollectionViewDataSource { } } - if model.messageType == .system { + if case .system = model.messageType { if let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "system", for: indexPath) as? SystemCell { cell.setup(message: model.text!) cell.accessibilityIdentifier = "system_cell_unread_\(indexPath.section)" return cell } - } else if model.time == nil { + } else if model.modelType.isMessage { var cell: BaseChatCell! if let identifier = MessageCellFactory.identifier(for: model) { @@ -211,8 +220,8 @@ extension MessageCollectionViewDataSource: UICollectionViewDataSource { cell.setup(model: model) return cell } else { - if let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "time", for: indexPath) as? TimeCell { - cell.setup(date: model.time!) + if let time = model.modelType.time, let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "time", for: indexPath) as? TimeCell { + cell.setup(date: time) cell.accessibilityIdentifier = "time_cell_unread_\(indexPath.section)" return cell } diff --git a/Nynja/Modules/Message/View/Views/CollectionView/MessageCollectionViewDelegate.swift b/Nynja/Modules/Message/View/Views/CollectionView/MessageCollectionViewDelegate.swift index 422da5f56..79369dc3a 100644 --- a/Nynja/Modules/Message/View/Views/CollectionView/MessageCollectionViewDelegate.swift +++ b/Nynja/Modules/Message/View/Views/CollectionView/MessageCollectionViewDelegate.swift @@ -8,13 +8,6 @@ import UIKit -enum ScrollDirection { - case top - case bottom -} - - - final class MessageCollectionViewDelegate: NSObject, UICollectionViewDelegateFlowLayout { unowned var view: MessageVC @@ -32,13 +25,6 @@ final class MessageCollectionViewDelegate: NSObject, UICollectionViewDelegateFlo setupMentions(cell: cell, indexPath: indexPath) } - func collectionView(_ collectionView: UICollectionView, didEndDisplaying cell: UICollectionViewCell, forItemAt indexPath: IndexPath) { - guard let cell = cell as? BaseChatCell, let url = cell.model?.progressModel?.url.absoluteString else { - return - } - view.progressDictionary.removeValue(forKey: .init(id: cell.id, url: url)) - } - func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize { return CGSize(width: view.view.bounds.width, height: heightForItem(at: indexPath)) } @@ -46,19 +32,9 @@ final class MessageCollectionViewDelegate: NSObject, UICollectionViewDelegateFlo private func heightForItem(at indexPath: IndexPath) -> CGFloat { let model = view.messageDS.cellModel(at: indexPath) - if model.messageType == .system { + if case .system = model.messageType { return 40 - } - - if let _ = model.type { - return height(for: model) - } - - if model.unread != nil { - return 30 - } - - if model.time == nil { + } else if model.type != nil || model.modelType.isMessage { return height(for: model) } else { return 30 @@ -114,6 +90,10 @@ final class MessageCollectionViewDelegate: NSObject, UICollectionViewDelegateFlo // MARK: - Private private func setupMentions(cell: UICollectionViewCell, indexPath: IndexPath) { + guard view.isMentionsVisible else { + lastVisibleIndex = nil + return + } func setup() { lastVisibleIndex = indexPath.item view.setupUnreadMentions(after: indexPath.item) @@ -129,19 +109,10 @@ final class MessageCollectionViewDelegate: NSObject, UICollectionViewDelegateFlo } private func setupProgress(cell: UICollectionViewCell) { - guard let cell = cell as? BaseChatCell, let url = cell.model?.progressModel?.url.absoluteString else { + guard let cell = cell as? BaseChatCell else { return } - let block: (ProgressModel) -> Void = { model in - cell.updateProgressClosure(model: model) - } - - let identifier = ProgressIdentifier(id: cell.id, url: url) - if view.progressDictionary[identifier] == nil { - view.progressDictionary[identifier] = [block] - } else { - view.progressDictionary[identifier]?.append(block) - } + cell.model?.subscribeToProgressChanges(with: cell) } private func loadData() { diff --git a/Nynja/Modules/Message/View/Views/CollectionView/ScrollDirection.swift b/Nynja/Modules/Message/View/Views/CollectionView/ScrollDirection.swift new file mode 100644 index 000000000..f077dc5c2 --- /dev/null +++ b/Nynja/Modules/Message/View/Views/CollectionView/ScrollDirection.swift @@ -0,0 +1,12 @@ +// +// ScrollDirection.swift +// Nynja +// +// Created by Anton Poltoratskyi on 10.09.2018. +// Copyright © 2018 TecSynt Solutions. All rights reserved. +// + +enum ScrollDirection { + case top + case bottom +} diff --git a/Nynja/Modules/Message/View/Views/TableView/Cells/ChatCells/BaseChatCell/BaseChatCell.swift b/Nynja/Modules/Message/View/Views/TableView/Cells/ChatCells/BaseChatCell/BaseChatCell.swift index 54b6ed8be..f2f7c07ea 100644 --- a/Nynja/Modules/Message/View/Views/TableView/Cells/ChatCells/BaseChatCell/BaseChatCell.swift +++ b/Nynja/Modules/Message/View/Views/TableView/Cells/ChatCells/BaseChatCell/BaseChatCell.swift @@ -239,6 +239,7 @@ class BaseChatCell: UICollectionViewCell, MessageBaseImageViewDataSource, Messag override func prepareForReuse() { super.prepareForReuse() model?.resetHandlers() + model?.unsubscribeFormProgressChanges() } @@ -502,7 +503,9 @@ class BaseChatCell: UICollectionViewCell, MessageBaseImageViewDataSource, Messag } static func shouldShowSender(_ model: BaseChatCellModel) -> Bool { - guard model.messageType != .system else { return false } + if case .system = model.messageType { + return false + } switch model.infoType { case .date: @@ -548,11 +551,6 @@ class BaseChatCell: UICollectionViewCell, MessageBaseImageViewDataSource, Messag return textSize } - - func updateProgressClosure(model: ProgressModel) { - downloadProgressModel = model - setupProgress() - } } protocol BaseChatCellImagePreviewTransitionAnimatable { @@ -566,6 +564,12 @@ extension BaseChatCell: BaseChatCellImagePreviewTransitionAnimatable { } //MARK: - Processing handler + extension BaseChatCell: ProgressDisplayable { + func updateProgress() { + setupProgress() + } + } + private extension BaseChatCell { var shouldShowNetworkProcessing: Bool { @@ -580,12 +584,7 @@ private extension BaseChatCell { } var downloadProgressModel: ProgressModel? { - set { - model?.progressModel = newValue - } - get { - return model?.progressModel - } + return model?.progressModel } var conversionProgressModel: ConvertionProgressModel? { @@ -650,7 +649,7 @@ private extension BaseChatCell { hideNetworkProcessingUI() case .atProgress: updateProcessingLabel(isHidden: false) - showNetworkProcessingUI() + processingButton.isHidden = false setupProcessingCancelButton() case .initial: if downloadProgressModel?.status != .notStarted { diff --git a/Nynja/Modules/Message/View/Views/TableView/Cells/Models/BaseChatCellModel.swift b/Nynja/Modules/Message/View/Views/TableView/Cells/Models/BaseChatCellModel.swift index 542007821..ac3e00542 100644 --- a/Nynja/Modules/Message/View/Views/TableView/Cells/Models/BaseChatCellModel.swift +++ b/Nynja/Modules/Message/View/Views/TableView/Cells/Models/BaseChatCellModel.swift @@ -28,38 +28,13 @@ enum PlayStatus { case stop } -enum MessageType: Hashable { +enum MessageType { case regular case forward indirect case reply(RepliedMessageModel, MessageType) indirect case convertion(ConvertionMessageModel, MessageType) case system - var hashValue: Int { - get { - return self.getHash() - } - } - - static func ==(lhs: MessageType, rhs: MessageType) -> Bool { - return lhs.hashValue == rhs.hashValue - } - - private func getHash() -> Int { - switch self { - case .regular: - return "regular".hashValue - case .reply(let model, _): - return ("reply" + model.id).hashValue - case .system: - return "system".hashValue - case .forward: - return "forward".hashValue - case .convertion: - return "convertion".hashValue - } - } - var ejectReply: MessageType? { switch self { case .reply: @@ -127,7 +102,7 @@ enum InfoType { case channel } -final class BaseChatCellModel { +final class BaseChatCellModel: AudioPlayable, ProgressObservable { var messageType: MessageType = .regular @@ -145,12 +120,11 @@ final class BaseChatCellModel { var serverID: Int64? - var id: String? + var id: MessageLocalId? var type: SendMessageType! + var modelType: ModelType = .message var isOwner: Bool = true var timeStamp: Date? - var time: Date? - var unread: Bool? var replied: Int = 0 var currentSize: Int64 = 0 @@ -185,6 +159,7 @@ final class BaseChatCellModel { let str: String = self.getTransferProgressString(with: model.fileSize, progress: model.progress) self.fileTransferProgressText = str } + self.notifyAboutProgressChanges() } } var convertProgressModel: ConvertionProgressModel? @@ -211,12 +186,6 @@ final class BaseChatCellModel { return fileUrl != nil } - static func unreadModel() -> BaseChatCellModel { - let model = BaseChatCellModel() - model.unread = true - return model - } - func getTransferProgressString(with fileSize: Int64, progress: Float) -> String { let loaded: Float = Float(fileSize) * progress return Int64(loaded).readableSize(from: fileSize) @@ -237,11 +206,104 @@ final class BaseChatCellModel { var audioStateHandler: (() -> Void)? - func notifyAudioHandler() { - audioStateHandler?() + var progressHandler: ((ProgressModel) -> Void)? +} + + +extension BaseChatCellModel { + + enum ModelType { + case message + case time(Date) + case unread + + var isMessage: Bool { + if case .message = self { + return true + } + + return false + } + + var isTime: Bool { + if case .time = self { + return true + } + + return false + } + + var isUnread: Bool { + if case .unread = self { + return true + } + + return false + } + + var time: Date? { + if case .time(let time) = self { + return time + } + + return nil + } + } + + static func makeTimeModel(with date: Date) -> BaseChatCellModel { + let model = BaseChatCellModel() + model.modelType = .time(date) + return model + } + + static func makeUnreadModel() -> BaseChatCellModel { + let model = BaseChatCellModel() + model.modelType = .unread + return model + } + +} + + +extension Array where Element == BaseChatCellModel { + + func unreadIndex(selfReader: MessageServerId?, unreadCount: Int) -> Int? { + guard let lastSeenIndex = self.lastSeenIndex(selfReader: selfReader, unreadCount: unreadCount) else { + return nil + } + + let from = lastSeenIndex + 1 + let to = self.count + for index in from.. Int? { + guard let selfReader = selfReader else { + return nil + } + + let possibleIndex = self.lastIndex { (cell) -> Bool in + guard let serverId = cell.serverID else { + return false + } + return serverId <= selfReader + } + + if let index = possibleIndex { + return index + } else if unreadCount <= self.count(where: { $0.modelType.isMessage }) { + return 0 + } + + return nil } + } diff --git a/Nynja/Modules/Message/View/Views/TableView/Cells/Models/ChangableProgress.swift b/Nynja/Modules/Message/View/Views/TableView/Cells/Models/ChangableProgress.swift new file mode 100644 index 000000000..f748b7ebc --- /dev/null +++ b/Nynja/Modules/Message/View/Views/TableView/Cells/Models/ChangableProgress.swift @@ -0,0 +1,40 @@ +// +// ChangableProgress.swift +// Nynja +// +// Created by Andrey Reznik on 01.10.2018. +// Copyright © 2018 TecSynt Solutions. All rights reserved. +// + +import Foundation + +protocol ProgressObservable: class { + + var progressModel: ProgressModel? { get set } + var progressHandler: ((ProgressModel) -> Void)? { get set } + + func notifyAboutProgressChanges() + func subscribeToProgressChanges(with object: ProgressDisplayable) + func unsubscribeFormProgressChanges() +} + + +extension ProgressObservable { + + func notifyAboutProgressChanges() { + guard let progressModel = progressModel else { + return + } + progressHandler?(progressModel) + } + + func subscribeToProgressChanges(with object: ProgressDisplayable) { + progressHandler = { [weak object] _ in + object?.updateProgress() + } + } + + func unsubscribeFormProgressChanges() { + progressHandler = nil + } +} diff --git a/Nynja/Modules/Message/View/Views/TableView/Cells/Models/ProgressDisplayable.swift b/Nynja/Modules/Message/View/Views/TableView/Cells/Models/ProgressDisplayable.swift new file mode 100644 index 000000000..b03242bca --- /dev/null +++ b/Nynja/Modules/Message/View/Views/TableView/Cells/Models/ProgressDisplayable.swift @@ -0,0 +1,13 @@ +// +// ProgressDisplayable.swift +// Nynja +// +// Created by Andrey Reznik on 01.10.2018. +// Copyright © 2018 TecSynt Solutions. All rights reserved. +// + +import Foundation + +protocol ProgressDisplayable: class { + func updateProgress() +} diff --git a/Nynja/Modules/Message/View/Views/TableView/Cells/Models/RepliedMessageModel.swift b/Nynja/Modules/Message/View/Views/TableView/Cells/Models/RepliedMessageModel.swift index 081e65bf8..a6a82ecfc 100644 --- a/Nynja/Modules/Message/View/Views/TableView/Cells/Models/RepliedMessageModel.swift +++ b/Nynja/Modules/Message/View/Views/TableView/Cells/Models/RepliedMessageModel.swift @@ -11,15 +11,16 @@ import Foundation final class RepliedMessageModel { /// local id of replied message - var id: String + var id: MessageLocalId var type: SendMessageType var author: String var text: String var duration: Double? var date: Date? var mentions: [MentionInfo]? + var isDeleted: Bool = false - init?(message: Message, author: String, mentions: [MentionInfo]?) { + init?(message: Message, author: String, mentions: [MentionInfo]?, isDeleted: Bool) { guard let id = message.msg_id, let desc = message.mainFile, let type = SendMessageType(rawValue: desc.mime ?? "") else { return nil } self.id = id @@ -42,8 +43,10 @@ final class RepliedMessageModel { self.text = fileName } - self.date = Date(timestamp: (message.created as? Int64) ?? 0) + self.date = Date(timestamp: message.created ?? 0) self.mentions = mentions + + self.isDeleted = isDeleted } init?(cellModel: BaseChatCellModel) { @@ -73,7 +76,7 @@ final class RepliedMessageModel { self.text = fileName } - self.date = cellModel.time + self.date = cellModel.timeStamp self.mentions = cellModel.mentions } } diff --git a/Nynja/Modules/Message/View/Views/TableView/Cells/Views/Message/MessageImageView.swift b/Nynja/Modules/Message/View/Views/TableView/Cells/Views/Message/MessageImageView.swift index 659ea7ecd..c18413e0c 100644 --- a/Nynja/Modules/Message/View/Views/TableView/Cells/Views/Message/MessageImageView.swift +++ b/Nynja/Modules/Message/View/Views/TableView/Cells/Views/Message/MessageImageView.swift @@ -35,7 +35,7 @@ class MessageImageView: MessageBaseImageView { url = localUrl } - imageView.setImage(url: url, placeHolder: #imageLiteral(resourceName: "camera")) { [weak self] downloadedUrl, image in + imageView.setImage(url: url, placeHolder: #imageLiteral(resourceName: "camera")) { [weak self] downloadedUrl, image, _ in self?.imageView.image = image?.resizeImage(targetSize: size, scale: UIScreen.main.scale) } } diff --git a/Nynja/Modules/Message/View/Views/TableView/Cells/Views/Message/MessageLocationView.swift b/Nynja/Modules/Message/View/Views/TableView/Cells/Views/Message/MessageLocationView.swift index 0b333f2df..25d94057a 100644 --- a/Nynja/Modules/Message/View/Views/TableView/Cells/Views/Message/MessageLocationView.swift +++ b/Nynja/Modules/Message/View/Views/TableView/Cells/Views/Message/MessageLocationView.swift @@ -16,9 +16,7 @@ class MessageLocationView: MessageBaseImageView { adjustConstraints(with: Constraints.ImageView.size) - if let url = model.text?.locationUrl { - imageView.setImage(url: url, placeHolder: #imageLiteral(resourceName: "camera")) - } + imageView.setImage(url: model.text?.locationUrl, placeHolder: #imageLiteral(resourceName: "camera")) } static func size(for model: BaseChatCellModel) -> CGSize? { @@ -26,7 +24,8 @@ class MessageLocationView: MessageBaseImageView { } } -extension MessageLocationView { +private extension MessageLocationView { + enum Constraints { enum ImageView { @@ -34,6 +33,5 @@ extension MessageLocationView { static let width = CGFloat(236.adjustedByWidth) static let height = CGFloat(116.adjustedByWidth) } - } } diff --git a/Nynja/Modules/Message/View/Views/TableView/Cells/Views/Message/MessageStickerView.swift b/Nynja/Modules/Message/View/Views/TableView/Cells/Views/Message/MessageStickerView.swift index 52ad74a19..a25780bdb 100644 --- a/Nynja/Modules/Message/View/Views/TableView/Cells/Views/Message/MessageStickerView.swift +++ b/Nynja/Modules/Message/View/Views/TableView/Cells/Views/Message/MessageStickerView.swift @@ -64,7 +64,7 @@ final class MessageStickerView: MessageContentView, BubbleImageSizeCalculatable adjustConstraints(with: size) let url = URL(string: model.text ?? "") - imageView.setImage(url: url) { [weak self] downloadedUrl, image in + imageView.setImage(url: url) { [weak self] downloadedUrl, image, _ in guard downloadedUrl == url else { return } diff --git a/Nynja/Modules/Message/View/Views/TableView/Cells/Views/Message/MessageVideoView.swift b/Nynja/Modules/Message/View/Views/TableView/Cells/Views/Message/MessageVideoView.swift index d8ae21dd9..b6875cc1d 100644 --- a/Nynja/Modules/Message/View/Views/TableView/Cells/Views/Message/MessageVideoView.swift +++ b/Nynja/Modules/Message/View/Views/TableView/Cells/Views/Message/MessageVideoView.swift @@ -53,7 +53,7 @@ class MessageVideoView: MessageBaseImageView { url = localUrl } - imageView.setImage(url: url, placeHolder: #imageLiteral(resourceName: "camera")) { [weak self] downloadedUrl, image in + imageView.setImage(url: url, placeHolder: #imageLiteral(resourceName: "camera")) { [weak self] downloadedUrl, image, _ in self?.imageView.image = image?.resizeImage(targetSize: size, scale: UIScreen.main.scale) } } diff --git a/Nynja/Modules/Message/View/Views/TableView/Cells/Views/Message/MessageVoiceView.swift b/Nynja/Modules/Message/View/Views/TableView/Cells/Views/Message/MessageVoiceView.swift index f72aaec9f..622305480 100644 --- a/Nynja/Modules/Message/View/Views/TableView/Cells/Views/Message/MessageVoiceView.swift +++ b/Nynja/Modules/Message/View/Views/TableView/Cells/Views/Message/MessageVoiceView.swift @@ -170,7 +170,7 @@ class MessageVoiceView: MessageContentView { guard model.type == .audio else { return } model.audioStateHandler = { [weak self, unowned model] in - self?.setup(model) + self?.updateUI(for: model) } setup(model) } @@ -204,25 +204,29 @@ class MessageVoiceView: MessageContentView { } } - private func setupUI(for model: BaseChatCellModel) { + private func updateUI(for model: BaseChatCellModel) { switch model.playStatus { case .play: playButton.isSelected = true case .pause, .stop: playButton.isSelected = false } - let color = model.isOwner ? Constants.colors.forwardVoiceMessageSeparator : Constants.colors.selfBubleColor - separatorView.backgroundColor = color.getColor() if let total = model.audioDuration, let current = model.audioCurrentTime { - let percent = current / total * 100 + let percent = current / total * 100.0 waveformView.updateProgress(percent) } else { - waveformView.updateProgress(0) + waveformView.updateProgress(0.0) } setupDurationLabel(with: model) } + private func setupUI(for model: BaseChatCellModel) { + let color = model.isOwner ? Constants.colors.forwardVoiceMessageSeparator : Constants.colors.selfBubleColor + separatorView.backgroundColor = color.getColor() + updateUI(for: model) + } + private func setupDurationLabel(with model: BaseChatCellModel) { guard let duration = model.audioDuration else { return } durationLabel.text = AudioDurationFormatter.string(duration: duration, currentTime: model.audioCurrentTime) diff --git a/Nynja/Modules/Message/View/Views/TableView/Cells/Views/Reply/ReplyInfoView.swift b/Nynja/Modules/Message/View/Views/TableView/Cells/Views/Reply/ReplyInfoView.swift index f6991fc16..2f45a6afe 100644 --- a/Nynja/Modules/Message/View/Views/TableView/Cells/Views/Reply/ReplyInfoView.swift +++ b/Nynja/Modules/Message/View/Views/TableView/Cells/Views/Reply/ReplyInfoView.swift @@ -118,8 +118,13 @@ final class ReplyInfoView: BaseView { } func configure(with model: RepliedMessageModel) { - let contentText = MessagePayloadRenderer.processPlainTextPayload(with: model.mentions, + let contentText: String + if model.isDeleted { + contentText = "deleted_message_replied_preview".localized + } else { + contentText = MessagePayloadRenderer.processPlainTextPayload(with: model.mentions, in: model.text).text + } authorLabel.text = model.author contentLabel.text = contentText @@ -138,7 +143,7 @@ final class ReplyInfoView: BaseView { } } - let voiceHeight = model.type == .audio ? Constraints.voiceView.height : 0 + let voiceHeight = model.type == .audio && !model.isDeleted ? Constraints.voiceView.height : 0 voiceView.snp.updateConstraints { $0.height.equalTo(voiceHeight) } } } diff --git a/Nynja/Modules/Message/WireFrame/MessageWireframe.swift b/Nynja/Modules/Message/WireFrame/MessageWireframe.swift index 175dfdb58..bb78f4530 100644 --- a/Nynja/Modules/Message/WireFrame/MessageWireframe.swift +++ b/Nynja/Modules/Message/WireFrame/MessageWireframe.swift @@ -28,7 +28,8 @@ class MessageWireFrame: MessageWireframeProtocol, DocumentInteractionWireFrame { func presentMessages(navigation: UINavigationController, chat: ChatModel, main: MainWireFrame?, - initialMessage: ChatInitialMessage? = nil) { + initialMessage: ChatInitialMessage? = nil, + animated: Bool) { let view = MessageVC() let presenter = MessagePresenter() @@ -52,7 +53,7 @@ class MessageWireFrame: MessageWireframeProtocol, DocumentInteractionWireFrame { self.main = main self.main?.messageinteractor = interactor - navigation.pushViewController(view, animated: false) + navigation.pushViewController(view, animated: animated) } diff --git a/Nynja/Modules/MyGroupAlias/Interactor/MyGroupAliasInteractor.swift b/Nynja/Modules/MyGroupAlias/Interactor/MyGroupAliasInteractor.swift deleted file mode 100644 index c4fcf9495..000000000 --- a/Nynja/Modules/MyGroupAlias/Interactor/MyGroupAliasInteractor.swift +++ /dev/null @@ -1,40 +0,0 @@ -// -// MyGroupAliasMyGroupAliasInteractor.swift -// Nynja -// -// Created by Sergey Lomov on 08/11/2017. -// Copyright © 2017 TecSynt Solutions. All rights reserved. -// - -class MyGroupAliasInteractor: BaseInteractor, MyGroupAliasInteractorInputProtocol { - - weak var presenter: MyGroupAliasInteractorOutputProtocol! - - var currentAlias: String! - var nicks: [String]! - - - //MARK: - BaseInteractor - - override func loadData() { - super.loadData() - presenter?.update(currentAlias) - } - - - //MARK: - MyGroupAliasInteractorInputProtocol - - func checkAlias(_ alias: String) { - var myNick = "" - if let phoneId = StorageService.sharedInstance.phoneId, let myContact = ContactDAO.findContactBy(phoneId: phoneId) { - myNick = Member(contact: myContact).alias ?? "" - } - nicks.forEach { - if $0 == alias && alias != myNick { - presenter.aliasBusy(alias) - return - } - } - presenter.aliasAvailable(alias) - } -} diff --git a/Nynja/Modules/MyGroupAlias/MyGroupAliasProtocols.swift b/Nynja/Modules/MyGroupAlias/MyGroupAliasProtocols.swift index 35b515ded..a0499a7a5 100644 --- a/Nynja/Modules/MyGroupAlias/MyGroupAliasProtocols.swift +++ b/Nynja/Modules/MyGroupAlias/MyGroupAliasProtocols.swift @@ -14,7 +14,7 @@ protocol MyGroupAliasEditorDelegate: class { protocol MyGroupAliasWireFrameProtocol: class { - func presentMyGroupAlias(navigation: UINavigationController, currentAlias: String, delegate: MyGroupAliasEditorDelegate?, nicks: [String], mode: GroupMode) + func presentMyGroupAlias(navigation: UINavigationController, currentAlias: String, delegate: MyGroupAliasEditorDelegate?, mode: GroupMode) /** * Add here your methods for communication PRESENTER -> WIREFRAME @@ -32,46 +32,16 @@ protocol MyGroupAliasViewProtocol: class { */ func setup(alias: String) - func showBusyMessage() } -protocol MyGroupAliasPresenterProtocol: AnyObject, BasePresenterProtocol { +protocol MyGroupAliasPresenterProtocol: BasePresenterProtocol { var view: MyGroupAliasViewProtocol! { get set } - var interactor: MyGroupAliasInteractorInputProtocol! { get set } var wireFrame: MyGroupAliasWireFrameProtocol! { get set } - var mode: GroupMode { get set } - /** * Add here your methods for communication VIEW -> PRESENTER */ - func saveAlias(_ alias:String) -} - -protocol MyGroupAliasInteractorOutputProtocol: class { - - /** - * Add here your methods for communication INTERACTOR -> PRESENTER - */ - - func update(_ alias: String) - - func aliasAvailable(_ alias: String) - func aliasBusy(_ alias: String) -} - -protocol MyGroupAliasInteractorInputProtocol: BaseInteractorProtocol { - - var presenter: MyGroupAliasInteractorOutputProtocol! { get set } - - var currentAlias: String! { get set } - var nicks: [String]! { get set } - - /** - * Add here your methods for communication PRESENTER -> INTERACTOR - */ - - func checkAlias(_ alias: String) + func save(alias: String) } diff --git a/Nynja/Modules/MyGroupAlias/Presenter/MyGroupAliasPresenter.swift b/Nynja/Modules/MyGroupAlias/Presenter/MyGroupAliasPresenter.swift index 38cdd950b..b07fd6283 100644 --- a/Nynja/Modules/MyGroupAlias/Presenter/MyGroupAliasPresenter.swift +++ b/Nynja/Modules/MyGroupAlias/Presenter/MyGroupAliasPresenter.swift @@ -6,18 +6,41 @@ // Copyright © 2017 TecSynt Solutions. All rights reserved. // -class MyGroupAliasPresenter: BasePresenter, MyGroupAliasPresenterProtocol, MyGroupAliasInteractorOutputProtocol { +class MyGroupAliasPresenter: BasePresenter, MyGroupAliasPresenterProtocol, InitializeInjectable { + + weak var view: MyGroupAliasViewProtocol! + var wireFrame: MyGroupAliasWireFrameProtocol! weak var delegate: MyGroupAliasEditorDelegate? - var currentAlias: String! { - get { - return interactor.currentAlias - } + let currentAlias: String + let mode: GroupMode + + + // MARK: - InitializeInjectable + + required init(dependencies: Dependencies) { + view = dependencies.view + wireFrame = dependencies.wireFrame + + delegate = dependencies.delegate + + currentAlias = dependencies.currentAlias + mode = dependencies.mode + } + + struct Dependencies { + let view: MyGroupAliasViewProtocol + let wireFrame: MyGroupAliasWireFrameProtocol + + let delegate: MyGroupAliasEditorDelegate? + + let currentAlias: String + let mode: GroupMode } - //MARK: - BasePresenter + // MARK: - BasePresenter override var itemsFactory: WCItemsFactory? { if mode == .create { @@ -27,40 +50,18 @@ class MyGroupAliasPresenter: BasePresenter, MyGroupAliasPresenterProtocol, MyGro } } - - //MARK: - MyGroupAliasPresenterProtocol - - weak var view: MyGroupAliasViewProtocol! - var interactor: MyGroupAliasInteractorInputProtocol! { - didSet { - _interactor = interactor - } - } - var wireFrame: MyGroupAliasWireFrameProtocol! - - var mode: GroupMode = .create - - func saveAlias(_ alias: String) { - if currentAlias == alias { - aliasAvailable(alias) - } else { - interactor.checkAlias(alias) - } + override func loadData() { + super.loadData() + view.setup(alias: currentAlias) } - //MARK: - MyGroupAliasInteractorOutputProtocol - - func update(_ alias: String) { - view.setup(alias: alias) - } - - func aliasBusy(_ alias: String) { - view.showBusyMessage() - } + // MARK: - MyGroupAliasPresenterProtocol - func aliasAvailable(_ alias: String) { - delegate?.myGroupAliasWasChanged(newValue: alias) + func save(alias: String) { + if currentAlias != alias { + delegate?.myGroupAliasWasChanged(newValue: alias) + } wireFrame.hide() } } diff --git a/Nynja/Modules/MyGroupAlias/View/MyGroupAliasViewController.swift b/Nynja/Modules/MyGroupAlias/View/MyGroupAliasViewController.swift index f48bf72cd..d9a695031 100644 --- a/Nynja/Modules/MyGroupAlias/View/MyGroupAliasViewController.swift +++ b/Nynja/Modules/MyGroupAlias/View/MyGroupAliasViewController.swift @@ -16,76 +16,15 @@ class MyGroupAliasViewController: BaseVC, MyGroupAliasViewProtocol { } } - // MARK: Views - private lazy var aliasField: EditField = { - let height = Constraints.aliasField.height.adjustedByHeight - let inputHeight = Constraints.aliasField.inputHeight.adjustedByHeight - - let width = Constraints.aliasField.width.adjustedByWidth - - let labelHeight = Constraints.aliasField.labelHeight.adjustedByHeight - - let cf = createEditField(with: CGRect(x: 0, y: 0, width: width, height: height), labelHeight: labelHeight, inputHeight: inputHeight) - let horizontalInset = Constraints.aliasField.horizontalInset.adjustedByWidth - self.view.addSubview(cf) - cf.snp.makeConstraints({ (make) in - make.height.equalTo(height) - make.left.equalTo(horizontalInset) - make.right.equalTo(-horizontalInset) - make.top.equalTo(navigationView.snp.bottom).offset(Constraints.aliasField.topInset.adjustedByHeight) - }) - return cf - }() - private lazy var cancelButton: UIButton = { - let button = UIButton(type: .system) - - let width = Constraints.cancelButton.width.adjustedByWidth - let labelHeight = width * Constraints.cancelButton.labelHeightProportion - - // Set font with adjusted size - let fontName = Constants.fonts.medium - let fontSize = "A".getFontSize(fontName: fontName, width: width, height: labelHeight) - button.titleLabel?.font = UIFont(name: fontName, size: fontSize) - button.setTitle(Strings.cancelGroupAlias.localized, for: .normal) - - // Set title and its color - button.setTitleColor(Constants.colors.lightBlue.getColor(), for: .normal) - - self.view.addSubview(button) - button.snp.makeConstraints({ (make) in - make.top.greaterThanOrEqualTo(aliasField.snp.bottom).offset(Constraints.cancelButton.topInset.adjustedByHeight) - make.left.right.equalTo(aliasField) - make.height.equalTo(Constraints.cancelButton.height.adjustedByHeight) - }) - - return button - }() + // MARK: - Subviews - private lazy var saveButton: NynjaButton = { - let height = Constraints.saveButton.height.adjustedByHeight - let width = Constraints.saveButton.width.adjustedByWidth - let labelHeight = width * Constraints.saveButton.labelHeightProportion - - let frame = CGRect(x: 0, y: 0, width: width, height: height) - - let button = NynjaButton(frame: frame, fontName: Constants.fonts.medium, labelHeight: labelHeight) - - // Set title and its color - button.setTitle(Strings.saveAlias.localized.uppercased(), for: .normal) - - self.view.addSubview(button) - button.snp.makeConstraints({ (make) in - make.height.equalTo(height) - make.top.equalTo(cancelButton.snp.bottom).offset(Constraints.saveButton.topInset.adjustedByHeight) - make.left.right.equalTo(cancelButton) - }) - - return button - }() + private lazy var aliasField: EditField = makeAliasField() + private lazy var cancelButton: UIButton = makeCancelButton() + private lazy var saveButton: NynjaButton = makeSaveButton() - //MARK: - View Lifecycle + // MARK: - View Lifecycle override func viewDidAppear(_ animated: Bool) { super.viewDidAppear(animated) @@ -93,7 +32,8 @@ class MyGroupAliasViewController: BaseVC, MyGroupAliasViewProtocol { } - //MARK: - BaseVC + // MARK: - BaseVC + override func initialize() { super.initialize() @@ -107,14 +47,20 @@ class MyGroupAliasViewController: BaseVC, MyGroupAliasViewProtocol { setupTestingKeysInSubviews() } - //MARK: - Setup + + // MARK: - Setup func setup(alias: String) { aliasField.input.text = alias } +} + +// MARK: - Private + +private extension MyGroupAliasViewController { - //MARK: - Actions + // MARK: - Actions @objc func cancelButtonTapper() { self.navigationController?.popViewController(animated: false) @@ -124,18 +70,14 @@ class MyGroupAliasViewController: BaseVC, MyGroupAliasViewProtocol { aliasField.input.text = aliasField.input.text?.trimmingCharacters(in: .whitespacesAndNewlines) if valid() { self.view.endEditing(true) - self.presenter.saveAlias(aliasField.input.text ?? "") + self.presenter.save(alias: aliasField.input.text ?? "") } } - func showBusyMessage() { - AlertManager.sharedInstance.showAlertOk(message: Strings.aliasBusyMessage.localized) - } - - //MARK: - Private + // MARK: - Helper - private func createEditField(with frame: CGRect, labelHeight: CGFloat, inputHeight: CGFloat) -> EditField { + func createEditField(with frame: CGRect, labelHeight: CGFloat, inputHeight: CGFloat) -> EditField { let cf = EditField(frame: frame) cf.leftLabel.textColor = Constants.colors.darkGray.getColor() @@ -150,7 +92,7 @@ class MyGroupAliasViewController: BaseVC, MyGroupAliasViewProtocol { return cf } - private func valid() -> Bool { + func valid() -> Bool { if let alias = aliasField.input.text { if alias == "" { AlertManager.sharedInstance.showAlertOk(message: Strings.aliasEmptyMessage.localized) @@ -169,10 +111,86 @@ class MyGroupAliasViewController: BaseVC, MyGroupAliasViewProtocol { } } + +// MARK: - Subviews maker + +private extension MyGroupAliasViewController { + + func makeAliasField() -> EditField { + let height = Constraints.aliasField.height.adjustedByHeight + let inputHeight = Constraints.aliasField.inputHeight.adjustedByHeight + + let width = Constraints.aliasField.width.adjustedByWidth + + let labelHeight = Constraints.aliasField.labelHeight.adjustedByHeight + + let cf = createEditField(with: CGRect(x: 0, y: 0, width: width, height: height), labelHeight: labelHeight, inputHeight: inputHeight) + let horizontalInset = Constraints.aliasField.horizontalInset.adjustedByWidth + self.view.addSubview(cf) + cf.snp.makeConstraints({ (make) in + make.height.equalTo(height) + make.left.equalTo(horizontalInset) + make.right.equalTo(-horizontalInset) + make.top.equalTo(navigationView.snp.bottom).offset(Constraints.aliasField.topInset.adjustedByHeight) + }) + return cf + } + + func makeCancelButton() -> UIButton { + let button = UIButton(type: .system) + + let width = Constraints.cancelButton.width.adjustedByWidth + let labelHeight = width * Constraints.cancelButton.labelHeightProportion + + // Set font with adjusted size + let fontName = Constants.fonts.medium + let fontSize = "A".getFontSize(fontName: fontName, width: width, height: labelHeight) + button.titleLabel?.font = UIFont(name: fontName, size: fontSize) + button.setTitle(Strings.cancelGroupAlias.localized, for: .normal) + + // Set title and its color + button.setTitleColor(Constants.colors.lightBlue.getColor(), for: .normal) + + self.view.addSubview(button) + button.snp.makeConstraints({ (make) in + make.top.greaterThanOrEqualTo(aliasField.snp.bottom).offset(Constraints.cancelButton.topInset.adjustedByHeight) + make.left.right.equalTo(aliasField) + make.height.equalTo(Constraints.cancelButton.height.adjustedByHeight) + }) + + return button + } + + func makeSaveButton() -> NynjaButton { + let height = Constraints.saveButton.height.adjustedByHeight + let width = Constraints.saveButton.width.adjustedByWidth + let labelHeight = width * Constraints.saveButton.labelHeightProportion + + let frame = CGRect(x: 0, y: 0, width: width, height: height) + + let button = NynjaButton(frame: frame, fontName: Constants.fonts.medium, labelHeight: labelHeight) + + // Set title and its color + button.setTitle(Strings.saveAlias.localized.uppercased(), for: .normal) + + self.view.addSubview(button) + button.snp.makeConstraints({ (make) in + make.height.equalTo(height) + make.top.equalTo(cancelButton.snp.bottom).offset(Constraints.saveButton.topInset.adjustedByHeight) + make.left.right.equalTo(cancelButton) + }) + + return button + } +} + + +// MARK: - Layout + extension MyGroupAliasViewController { - struct Constraints { + enum Constraints { - struct aliasField { + enum aliasField { static let height: CGFloat = 48.0 static let width: CGFloat = 382.0 @@ -183,7 +201,7 @@ extension MyGroupAliasViewController { static let horizontalInset = 16.0 } - struct cancelButton { + enum cancelButton { static let height = 44.0 static let width = aliasField.width @@ -193,7 +211,7 @@ extension MyGroupAliasViewController { static let labelHeightProportion = labelHeight / width } - struct saveButton { + enum saveButton { static let height: CGFloat = 48.0 static let width = CGFloat(aliasField.width) @@ -212,7 +230,6 @@ extension MyGroupAliasViewController { case aliasEmptyMessage = "Alias_Empty_Message" case aliasMaxSizeMessage = "Alias_Max_Size_Message" - case aliasBusyMessage = "Alias_Busy_Message" case invalidAlias = "Invalid_alias" var localized: String { @@ -221,7 +238,9 @@ extension MyGroupAliasViewController { } } + // MARK: - Testable + extension MyGroupAliasViewController: TestableViewControllerProtocol { private enum Keys: String { diff --git a/Nynja/Modules/MyGroupAlias/WireFrame/MyGroupAliasWireframe.swift b/Nynja/Modules/MyGroupAlias/WireFrame/MyGroupAliasWireframe.swift index 57431ad09..74b070cc7 100644 --- a/Nynja/Modules/MyGroupAlias/WireFrame/MyGroupAliasWireframe.swift +++ b/Nynja/Modules/MyGroupAlias/WireFrame/MyGroupAliasWireframe.swift @@ -11,27 +11,25 @@ import UIKit class MyGroupAliasWireFrame: MyGroupAliasWireFrameProtocol { weak var navigation : UINavigationController? - func presentMyGroupAlias(navigation: UINavigationController, currentAlias: String, delegate: MyGroupAliasEditorDelegate?, nicks: [String], mode: GroupMode) { - + func presentMyGroupAlias(navigation: UINavigationController, + currentAlias: String, + delegate: MyGroupAliasEditorDelegate?, + mode: GroupMode) { + let view = MyGroupAliasViewController() - let presenter = MyGroupAliasPresenter() - presenter.delegate = delegate - let interactor = MyGroupAliasInteractor() - interactor.nicks = nicks - interactor.currentAlias = currentAlias - + let presenter = MyGroupAliasPresenter( + dependencies: .init(view: view, + wireFrame: self, + delegate: delegate, + currentAlias: currentAlias, + mode: mode)) + self.navigation = navigation // Connecting view.presenter = presenter - presenter.mode = mode - presenter.view = view - presenter.wireFrame = self - presenter.interactor = interactor - interactor.presenter = presenter navigation.pushViewController(view as UIViewController, animated: true) - } func hide() { diff --git a/Nynja/Modules/OtherUser/Interactor/OtherUserInteractor.swift b/Nynja/Modules/OtherUser/Interactor/OtherUserInteractor.swift index 5b6c21439..1f7d78e71 100644 --- a/Nynja/Modules/OtherUser/Interactor/OtherUserInteractor.swift +++ b/Nynja/Modules/OtherUser/Interactor/OtherUserInteractor.swift @@ -9,13 +9,13 @@ class OtherUserInteractor: BaseInteractor, OtherUserInteractorInputProtocol, SetInjectable { weak var presenter: OtherUserInteractorOutputProtocol? - - private var muteChatService: MuteChatServiceProtocol! + + private var mqttService: MQTTService! private var storageService: StorageService! + private var muteChatService: MuteChatServiceProtocol! + private var historyRequestModelFactory: HistoryRequestModelFactoryProtocol! - private let historyRequestModelFactory: HistoryRequestModelFactoryProtocol = HistoryRequestModelFactory() - - var contact: Contact! + private(set) var contact: Contact enum InputContact { case contact(Contact) @@ -40,10 +40,10 @@ class OtherUserInteractor: BaseInteractor, OtherUserInteractorInputProtocol, Set } - //MARK: - BaseInteractor + // MARK: - BaseInteractor override var subscribes: [SubscribeType]? { - guard let contactId = contact?.phone_id else { + guard let contactId = contact.phone_id else { return nil } return [.contact(contactId)] @@ -55,14 +55,14 @@ class OtherUserInteractor: BaseInteractor, OtherUserInteractorInputProtocol, Set } - //MARK: - OtherUserInteractorInputProtocol + // MARK: - OtherUserInteractorInputProtocol func sendRequestToFriends(_ to: String) { - MQTTService.sharedInstance.friendRequest(friendPhoneId: to) + mqttService.friendRequest(friendPhoneId: to) } func acceptRequestToFriends(_ from: String) { - MQTTService.sharedInstance.confirmFriend(friendPhoneId: from) + mqttService.confirmFriend(friendPhoneId: from) } func toggleMute(_ isMuted: Bool) { @@ -74,19 +74,17 @@ class OtherUserInteractor: BaseInteractor, OtherUserInteractorInputProtocol, Set } func block(_ isBlock: Bool, to: String) { - MQTTService.sharedInstance.block(isBlock, to: to) + mqttService.block(isBlock, to: to) } func clearHistory(_ contact: Contact) { guard let phoneId = storageService.phoneId else { return } - do { let historyModel = try historyRequestModelFactory.makeDeleteRequest(rosterId: phoneId, chat: contact) - MQTTService.sharedInstance.sendHistoryRequest(with: historyModel) - } - catch { + mqttService.sendHistoryRequest(with: historyModel) + } catch { assertionFailure(error.localizedDescription) } } @@ -97,13 +95,12 @@ class OtherUserInteractor: BaseInteractor, OtherUserInteractorInputProtocol, Set override func update(with changes: [StorageChange], type: SubscribeType) { switch type { case .contact(let id): - if let _ = id { - if let dbContact = changes.first?.entity as? DBContact { - let updatedContact = Contact(contact: dbContact) - contact = updatedContact - presenter?.update(contact) - } + guard id != nil, let dbContact = changes.first?.entity as? DBContact else { + break } + let updatedContact = Contact(contact: dbContact) + contact = updatedContact + presenter?.update(contact) default: break } @@ -116,13 +113,17 @@ class OtherUserInteractor: BaseInteractor, OtherUserInteractorInputProtocol, Set extension OtherUserInteractor { struct Dependencies { let presenter: OtherUserInteractorOutputProtocol - let muteChatService: MuteChatServiceProtocol + let mqttService: MQTTService let storageService: StorageService + let muteChatService: MuteChatServiceProtocol + let historyRequestFactory: HistoryRequestModelFactoryProtocol } func inject(dependencies: Dependencies) { presenter = dependencies.presenter - muteChatService = dependencies.muteChatService + mqttService = dependencies.mqttService storageService = dependencies.storageService + muteChatService = dependencies.muteChatService + historyRequestModelFactory = dependencies.historyRequestFactory } } diff --git a/Nynja/Modules/OtherUser/OtherUserProtocols.swift b/Nynja/Modules/OtherUser/OtherUserProtocols.swift index e92b7ae3f..df4b69b22 100644 --- a/Nynja/Modules/OtherUser/OtherUserProtocols.swift +++ b/Nynja/Modules/OtherUser/OtherUserProtocols.swift @@ -10,7 +10,7 @@ import Foundation import CoreLocation import MaterialComponents.MaterialFlexibleHeader -//MARK: - Delegate +// MARK: - Delegate protocol OtherUserProfileDelegate: class { func otherUserProfile(_ userProfileWireFrame: OtherUserWireframeProtocol, didBlockContact contact: Contact) @@ -21,9 +21,9 @@ extension OtherUserProfileDelegate { func otherUserProfile(_ userProfileWireFrame: OtherUserWireframeProtocol, didUnblockContact contact: Contact) { } } -//MARK: - Wireframe +// MARK: - Wireframe protocol OtherUserWireframeProtocol: class { - weak var main: MainWireFrame? {get set} + var main: MainWireFrame? { get set } func present(navigation: UINavigationController, main: MainWireFrame?, contact: Contact?, delegate: OtherUserProfileDelegate?) func dismissSelf() @@ -35,7 +35,7 @@ protocol OtherUserWireframeProtocol: class { } -//MARK: - Presenter +// MARK: - Presenter protocol OtherUserPresenterProtocol: BasePresenterProtocol { var delegate: OtherUserProfileDelegate? { get set } @@ -58,14 +58,14 @@ protocol OtherUserPresenterProtocol: BasePresenterProtocol { func clearHistory() } -//MARK: - Interactor +// MARK: - Interactor protocol OtherUserInteractorOutputProtocol: class { func update(_ contact: Contact) } protocol OtherUserInteractorInputProtocol: BaseInteractorProtocol { var presenter: OtherUserInteractorOutputProtocol? { get set } - var contact: Contact! { get } + var contact: Contact { get } func sendRequestToFriends(_ to: String) func acceptRequestToFriends(_ from: String) @@ -74,12 +74,11 @@ protocol OtherUserInteractorInputProtocol: BaseInteractorProtocol { func clearHistory(_ contact: Contact) } -//MARK: - View +// MARK: - View protocol OtherUserViewProtocol: class { - var header: MDCFlexibleHeaderViewController! { get set } var presenter: OtherUserPresenterProtocol! { get set } + func setup(_ contact: Contact?) - } diff --git a/Nynja/Modules/OtherUser/Presenter/OtherUserPresenter.swift b/Nynja/Modules/OtherUser/Presenter/OtherUserPresenter.swift index afe89e507..c5d4586e6 100644 --- a/Nynja/Modules/OtherUser/Presenter/OtherUserPresenter.swift +++ b/Nynja/Modules/OtherUser/Presenter/OtherUserPresenter.swift @@ -52,7 +52,7 @@ class OtherUserPresenter: BasePresenter, OtherUserPresenterProtocol, OtherUserIn } func addToFriend() { - if contact.originalStatus == .authorization { + if contact.hasPendingIncomingRequest() { interactor.acceptRequestToFriends(contact.phoneId) } else { interactor.sendRequestToFriends(contact.phoneId) diff --git a/Nynja/Modules/OtherUser/View/OtherUserContainerViewController.swift b/Nynja/Modules/OtherUser/View/OtherUserContainerViewController.swift index 3afea3b9a..45977373a 100644 --- a/Nynja/Modules/OtherUser/View/OtherUserContainerViewController.swift +++ b/Nynja/Modules/OtherUser/View/OtherUserContainerViewController.swift @@ -11,7 +11,7 @@ import MaterialComponents.MaterialFlexibleHeader class OtherUserContainerViewController: MDCFlexibleHeaderContainerViewController, BackSwipable { - lazy var swipeBackHelper: SwipeBackHelper = { + private(set) lazy var swipeBackHelper: SwipeBackHelper = { let helper = SwipeBackHelper(with: self, gestureCompletion: nil) return helper }() diff --git a/Nynja/Modules/OtherUser/WireFrame/OtherUserWireFrame.swift b/Nynja/Modules/OtherUser/WireFrame/OtherUserWireFrame.swift index 22eac6dd8..246112d57 100644 --- a/Nynja/Modules/OtherUser/WireFrame/OtherUserWireFrame.swift +++ b/Nynja/Modules/OtherUser/WireFrame/OtherUserWireFrame.swift @@ -47,12 +47,16 @@ class OtherUserWireFrame: OtherUserWireframeProtocol { // Interactor Dependencies let serviceFactory = ServiceFactory() - let muteChatService = serviceFactory.makeMuteChatService() + let mqttService = serviceFactory.makeMQTTService() let storageSerice = serviceFactory.makeStorageService() + let muteChatService = serviceFactory.makeMuteChatService() + let historyRequestFactory = serviceFactory.makeHistoryRequestFactory() interactor.inject(dependencies: OtherUserInteractor.Dependencies(presenter: presenter, + mqttService: mqttService, + storageService: storageSerice, muteChatService: muteChatService, - storageService: storageSerice)) + historyRequestFactory: historyRequestFactory)) self.navigation = navigation diff --git a/Nynja/Modules/Participants/View/ParticipantsViewController.swift b/Nynja/Modules/Participants/View/ParticipantsViewController.swift index 40ef3ce68..03b9d1808 100644 --- a/Nynja/Modules/Participants/View/ParticipantsViewController.swift +++ b/Nynja/Modules/Participants/View/ParticipantsViewController.swift @@ -8,7 +8,7 @@ import UIKit -class ParticipantsViewController: BaseVC, ParticipantsViewProtocol { +class ParticipantsViewController: BaseVC, ParticipantsViewProtocol, KeyboardInteractive { var presenter: ParticipantsPresenterProtocol! { didSet { @@ -170,7 +170,7 @@ class ParticipantsViewController: BaseVC, ParticipantsViewProtocol { // MARK: - Keyboard - override func keyboardNotified(endFrame: CGRect) { + func keyboardNotified(endFrame: CGRect) { if endFrame.origin.y >= UIScreen.main.bounds.size.height { updateToHide(view: controlContainerView, offset: -bottomInset) } else { diff --git a/Nynja/Modules/Profile/View/DetailsView/ProfileDetailsView.swift b/Nynja/Modules/Profile/View/DetailsView/ProfileDetailsView.swift index c88adbf9c..b16bb6c6a 100644 --- a/Nynja/Modules/Profile/View/DetailsView/ProfileDetailsView.swift +++ b/Nynja/Modules/Profile/View/DetailsView/ProfileDetailsView.swift @@ -195,8 +195,6 @@ class ProfileDetailsView: UIView { phoneLabel.isHidden = false walletButton.isHidden = false infoView.isHidden = false - - walletButton.isEnabled = false } // MARK: Recognizer diff --git a/Nynja/Modules/Profile/View/ProfileViewController.swift b/Nynja/Modules/Profile/View/ProfileViewController.swift index fb9ee1048..64e8f075a 100644 --- a/Nynja/Modules/Profile/View/ProfileViewController.swift +++ b/Nynja/Modules/Profile/View/ProfileViewController.swift @@ -17,15 +17,15 @@ class ProfileViewController: BaseVC, ProfileViewProtocol { } } - var tableViewDS: ProfileTablewViewDS! - var tableViewDelegate: ProfileTableViewDelegate! + private var tableViewDS: ProfileTablewViewDS! + private var tableViewDelegate: ProfileTableViewDelegate! private let payloadParser: MessagePayloadParserInput = MessagePayloadParser() - //MARK: - Subviews + // MARK: - Views - lazy var detailsView: ProfileDetailsView = { + private lazy var detailsView: ProfileDetailsView = { let height = Constraints.detailsView.height.adjustedByWidth let width = self.view.bounds.width @@ -36,7 +36,7 @@ class ProfileViewController: BaseVC, ProfileViewProtocol { return detailsView }() - lazy var tableView: UITableView = { + private lazy var tableView: UITableView = { let tbView = UITableView.init(frame: CGRect.zero, style: .grouped) tbView.backgroundColor = .clear @@ -52,16 +52,13 @@ class ProfileViewController: BaseVC, ProfileViewProtocol { }() - //MARK: - BaseVC + // MARK: - Life Cycle override func initialize() { super.initialize() setupUI() } - - //MARK: - Layout - override func viewDidLayoutSubviews() { super.viewDidLayoutSubviews() @@ -72,7 +69,7 @@ class ProfileViewController: BaseVC, ProfileViewProtocol { } - //MARK: - Setup UI + // MARK: - Setup UI private func setupUI() { setupTableView() @@ -104,9 +101,11 @@ class ProfileViewController: BaseVC, ProfileViewProtocol { detailsView.avatarImageView.isUserInteractionEnabled = true let tapRecognizer = UITapGestureRecognizer(target: self, action: #selector(avatarTapped)) detailsView.avatarImageView.addGestureRecognizer(tapRecognizer) + detailsView.walletButton.addTarget(self, action: #selector(walletTapped), for: .touchUpInside) } - //MARK: - ProfileViewProtocol + + // MARK: - ProfileViewProtocol func setup(contact: Contact) { detailsView.avatarImageView.setImage(url: contact.avatarUrl, @@ -143,7 +142,8 @@ class ProfileViewController: BaseVC, ProfileViewProtocol { } } - //MARK: - Actions + + // MARK: - Actions @objc private func qrTaped() { presenter.showQRGenerator() @@ -153,11 +153,18 @@ class ProfileViewController: BaseVC, ProfileViewProtocol { presenter.showImagePreview(from: detailsView.avatarImageView) } - @objc private func walletButtonAction() { - presenter.walletTriggered() + @objc private func walletTapped() { + unavailableFunctionality() } + } + +// MARK: - ComingSoonProtocol + +extension ProfileViewController: ComingSoonProtocol {} + + // MARK: - ChatListMessageCellModelDelegate extension ProfileViewController: ChatListMessageCellModelDelegate { @@ -167,7 +174,8 @@ extension ProfileViewController: ChatListMessageCellModelDelegate { } } -//MARK: - ProfileContactCellDelegate + +// MARK: - ProfileContactCellDelegate extension ProfileViewController: ProfileContactCellDelegate { @@ -185,7 +193,7 @@ extension ProfileViewController: ProfileContactCellDelegate { } -//MARK: - ProfileViewSectionDelegate +// MARK: - ProfileViewSectionDelegate extension ProfileViewController: ProfileViewSectionDelegate { @@ -232,6 +240,9 @@ extension ProfileViewController: ProfileViewSectionDelegate { } } + +// MARK: - SetInjectable + extension ProfileViewController: SetInjectable { func inject(dependencies: ProfileViewController.Dependencies) { presenter = dependencies.presenter diff --git a/Nynja/Modules/Profile/View/TableView/Cells/ScheduledMesssageCell/ProfileScheduledMesssageCell.swift b/Nynja/Modules/Profile/View/TableView/Cells/ScheduledMesssageCell/ProfileScheduledMesssageCell.swift index a96923f10..a5ba1152a 100644 --- a/Nynja/Modules/Profile/View/TableView/Cells/ScheduledMesssageCell/ProfileScheduledMesssageCell.swift +++ b/Nynja/Modules/Profile/View/TableView/Cells/ScheduledMesssageCell/ProfileScheduledMesssageCell.swift @@ -183,8 +183,6 @@ class ProfileScheduledMesssageCell: UITableViewCell, ConfigurableCell { private func setupMessageLabel(_ message: ScheduledMessage) { guard let sendType = message.job?.data?.first?.sendType else { return } switch sendType { - case .audio: - messageLabel.text = "voice_message".localized case .text: guard let message = message.job?.data?.first, let payload = message.mainFile?.payload else { messageLabel.text = nil @@ -193,8 +191,10 @@ class ProfileScheduledMesssageCell: UITableViewCell, ConfigurableCell { let parser = type(of: self).payloadParser let mentions = parser.parse(message) messageLabel.text = MessagePayloadRenderer.processPlainTextPayload(with: mentions, in: payload).text + case .sticker: + messageLabel.text = message.job?.data?.first?.mainFile?.messageRepresentation default: - messageLabel.text = message.job?.data?.first?.mainFile?.payload + messageLabel.text = message.job?.data?.first?.mainFile?.type?.messageDescription } } diff --git a/Nynja/Modules/Profile/View/TableView/ProfileTableViewDelegate.swift b/Nynja/Modules/Profile/View/TableView/ProfileTableViewDelegate.swift index a78976467..3df25d70d 100644 --- a/Nynja/Modules/Profile/View/TableView/ProfileTableViewDelegate.swift +++ b/Nynja/Modules/Profile/View/TableView/ProfileTableViewDelegate.swift @@ -150,6 +150,7 @@ fileprivate extension ProfileTableViewDelegate { if let actionTitle = section.profileSection.actionTitle { button.setTitle(actionTitle, for: .normal) + button.accessibilityIdentifier = "\(section.profileSection.accessibilityId)_footer_action_button" } button.addTarget(self, action: #selector(footerTapped(_:)), for: .touchUpInside) diff --git a/Nynja/Modules/Profile/WireFrame/ProfileWireframe.swift b/Nynja/Modules/Profile/WireFrame/ProfileWireframe.swift index a86f99e87..119210b33 100644 --- a/Nynja/Modules/Profile/WireFrame/ProfileWireframe.swift +++ b/Nynja/Modules/Profile/WireFrame/ProfileWireframe.swift @@ -61,11 +61,11 @@ class ProfileWireFrame: ProfileWireFrameProtocol { } func showChat(with contact: Contact, message: Message? = nil) { - if contact.originalStatus != .authorization { + if contact.hasPendingIncomingRequest() { + showProfile(contact) + } else { let initialMessage = ChatInitialMessage(localId: message?.msg_id, serverId: message?.id) main?.showChat(contact, initialMessage: initialMessage) - } else { - showProfile(contact) } } diff --git a/Nynja/Modules/QRCodeReader/View/QRCodeReaderViewController.swift b/Nynja/Modules/QRCodeReader/View/QRCodeReaderViewController.swift index 203cfc1a4..6d7013edf 100644 --- a/Nynja/Modules/QRCodeReader/View/QRCodeReaderViewController.swift +++ b/Nynja/Modules/QRCodeReader/View/QRCodeReaderViewController.swift @@ -136,14 +136,14 @@ class QRCodeReaderViewController: BaseVC, QRCodeReaderViewProtocol { // MARK: - View lifecycle override func viewDidLoad() { super.viewDidLoad() - ReachabilityService.sharedInstance.addRechabilityObserver(self) + ConnectionService.shared.addSubscriber(self) screenTitle = Constants.LocalizableKeys.byQRCode.localized.uppercased() self.setupAppStateNotifications() self.setupUI() } deinit { - ReachabilityService.sharedInstance.removeRechabilityObserver(self) + ConnectionService.shared.removeSubscriber(self) self.removeNotificationObservers() } @@ -341,10 +341,15 @@ extension QRCodeReaderViewController: AVCaptureMetadataOutputObjectsDelegate { } } -extension QRCodeReaderViewController: ReachabilityServiceObserver { - func reachabilityStatusChanged(isReachable: Bool) { - if isReachable == false { - AlertManager.sharedInstance.showNoInternetConnection() +extension QRCodeReaderViewController: ConnectionServiceDelegate { + + func connectionStatusChanged(_ sender: ConnectionService, service: ConnectionService.Service, oldValue: ConnectionService.ConnectionServiceState) { + if service == .networking { + if sender.state[.networking] == .disconnected { + AlertManager.sharedInstance.showNoInternetConnection() + } } } } + + diff --git a/Nynja/Modules/Replies/Presenter/RepliesPresenter.swift b/Nynja/Modules/Replies/Presenter/RepliesPresenter.swift index 5d04e312b..d57886a32 100644 --- a/Nynja/Modules/Replies/Presenter/RepliesPresenter.swift +++ b/Nynja/Modules/Replies/Presenter/RepliesPresenter.swift @@ -52,7 +52,7 @@ class RepliesPresenter: BasePresenter, RepliesPresenterProtocol, RepliesInteract func setup(messages: [Message], mentions: [String: [MentionInfo]], progressModels: [URL : ProgressModel], position: PositionType, reader: Int64? = nil) { - let filtered = messages.filter { $0.statusString != "delete" } + let filtered = messages.filter { $0.messageStatus != .delete } var cells = [BaseChatCellModel]() for i in 0.. CGFloat { let model = dataSource.cells[indexPath.item] - if model.type != nil { - return height(for: model) - } else if model.unread != nil { - return 30 - } else if model.time == nil { + if model.type != nil || model.modelType.isMessage { return height(for: model) } else { return 30 diff --git a/Nynja/Modules/Replies/View/RepliesDS.swift b/Nynja/Modules/Replies/View/RepliesDS.swift index cd65ab844..7cdc1c1f7 100644 --- a/Nynja/Modules/Replies/View/RepliesDS.swift +++ b/Nynja/Modules/Replies/View/RepliesDS.swift @@ -29,7 +29,7 @@ final class RepliesDS: NSObject, UICollectionViewDataSource { func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell { let model = cells[indexPath.row] - if model.time == nil { + if model.modelType.isMessage { var cell: BaseChatCell! if let identifier = MessageCellFactory.identifier(for: model) { @@ -42,8 +42,6 @@ final class RepliesDS: NSObject, UICollectionViewDataSource { cell.setup(model: model) - // view.cellDisplayed(at: indexPath) - return cell } diff --git a/Nynja/Modules/Replies/View/RepliesVC.swift b/Nynja/Modules/Replies/View/RepliesVC.swift index 135b520b1..78115403e 100644 --- a/Nynja/Modules/Replies/View/RepliesVC.swift +++ b/Nynja/Modules/Replies/View/RepliesVC.swift @@ -23,8 +23,9 @@ final class RepliesVC: BaseVC, RepliesViewProtocol { var collectionViewDelegate: RepliesCollectionViewDelegate? var audioManager = AudioManager.sharedInstance + var audioSessionManager = AudioSessionManager.shared var proximityManager = ProximitySensorManager.shared - var currentPlayingModel: BaseChatCellModel? + var currentPlayingModel: AudioPlayable? //MARK: - Subviews private lazy var repliesNumberLabel: UILabel = { diff --git a/Nynja/Modules/ScheduleMessage/Interactor/ScheduleMessageInteractor.swift b/Nynja/Modules/ScheduleMessage/Interactor/ScheduleMessageInteractor.swift index 4b682e480..90a4ebc89 100644 --- a/Nynja/Modules/ScheduleMessage/Interactor/ScheduleMessageInteractor.swift +++ b/Nynja/Modules/ScheduleMessage/Interactor/ScheduleMessageInteractor.swift @@ -8,36 +8,52 @@ import Foundation -class ScheduleMessageInteractor: BaseInteractor, ScheduleMessageInteractorInputProtocol { +final class ScheduleMessageInteractor: BaseInteractor, ScheduleMessageInteractorInputProtocol, IoHandlerDelegate { - fileprivate typealias DateTimeInfo = ([Feature], Int64) + private typealias DateTimeInfo = ([Feature], Int64) + + weak var presenter: ScheduleMessageInteractorOutputProtocol! override var subscribes: [SubscribeType]? { return [.job(nil)] } - weak var presenter: ScheduleMessageInteractorOutputProtocol! - - var mode: ScheduledMessageMode + let mode: ScheduledMessageMode var targets: ForwardTargets? { return info?.targets } - // MARK: - Private Properties private var info: ScheduleInfo? private var job: Job? // FIXME: remove it - add localId to Job. private var sentMessagesLocalIds: [MessageLocalId]? + private let minScheduleIntervalInMinutes = 2 + + + // MARK: - Dependencies + private let timeZoneManager = TimeZoneManager.shared + private let mqttService = MQTTService.sharedInstance + private let storageService = StorageService.sharedInstance + + private let messageSendingService: MessageSendingServiceProtocol = { + return MessageSendingService(dependencies: .init(mqttService: .sharedInstance, + storageService: .sharedInstance, + processingManager: DefaultMessagesProcessingManager.shared)) + }() + + // MARK: - ScheduleMessageInteractorInputProtocol + required init(mode: ScheduledMessageMode) { self.mode = mode super.init() + IoHandler.delegate = self } func fetchInfo() { @@ -55,10 +71,19 @@ class ScheduleMessageInteractor: BaseInteractor, ScheduleMessageInteractorInputP } func sendScheduledMessage(with timeZone: TimeZoneLocal, date: Date) { - guard let phoneId = StorageService.sharedInstance.phoneId, let info = self.info, let (features, timestamp) = prepareDateTimeInfo(with: timeZone, date: date) else { return } - + guard let info = self.info, let phoneId = storageService.phoneId else { + return + } + guard let (features, timestamp) = prepareDateTimeInfo(with: timeZone, date: date) else { + presentInvalidTimeError() + return + } + guard canSendScheduledMessage() else { + presenter.presentError(errorMessage: "u_are_blocked".localized) + return + } let message = info.message - message.created = timestamp as AnyObject + message.created = timestamp let messages = info.targets.messages(from: message, phoneId: phoneId) sentMessagesLocalIds = messages.compactMap { $0.msg_id } @@ -67,8 +92,13 @@ class ScheduleMessageInteractor: BaseInteractor, ScheduleMessageInteractorInputP } func editScheduledMessage(with timeZone: TimeZoneLocal, date: Date) { - guard let job = self.job, let (features, timestamp) = prepareDateTimeInfo(with: timeZone, date: date) else { return } - + guard let job = self.job else { + return + } + guard let (features, timestamp) = prepareDateTimeInfo(with: timeZone, date: date) else { + presentInvalidTimeError() + return + } job.time = timestamp job.settings?.first?.value = features.first?.value @@ -81,7 +111,7 @@ class ScheduleMessageInteractor: BaseInteractor, ScheduleMessageInteractorInputP } func updateTargets(_ targets: ForwardTargets) { - guard let phoneId = StorageService.sharedInstance.phoneId, let message = job?.messages.first else { return } + guard let phoneId = storageService.phoneId, let message = job?.messages.first else { return } let messages = targets.messages(from: message, phoneId: phoneId) job?.data = messages @@ -89,29 +119,54 @@ class ScheduleMessageInteractor: BaseInteractor, ScheduleMessageInteractorInputP prepareInfo() } + + // MARK: - Validation + + private func presentInvalidTimeError() { + presenter.presentError(errorMessage: "schedule_error".localized) + } + + // MARK: - StorageSubscriber + override func update(with changes: [StorageChange], type: SubscribeType) { - if case .job = type { - for info in changes { - guard let job = info.entity as? Job, let id = job.id else { continue } - - if id == self.job?.id { - if info.kind == .delete { - presenter.scheduledMessageDeleted() - } else if info.kind == .update { - presenter.scheduledMessageUpdated() - } - } else if info.kind == .insert, StringAtom.string(job.status) == "pending" { - let ids = job.messages.compactMap { $0.msg_id } - if ids == sentMessagesLocalIds { - presenter.processPending(job: job) - } + switch type { + case .job: + handleUpdatedJob(changes: changes) + default: + break + } + } + + private func handleUpdatedJob(changes: [StorageChange]) { + for info in changes { + guard let job = info.entity as? Job, let id = job.id else { continue } + + if id == self.job?.id { + if info.kind == .delete { + presenter.scheduledMessageDeleted() + } else if info.kind == .update { + presenter.scheduledMessageUpdated() + } + } else if info.kind == .insert, StringAtom.string(job.status) == "pending" { + let ids = job.messages.compactMap { $0.msg_id } + if ids == sentMessagesLocalIds { + presenter.processPending(job: job) } } } } - // MARK: - Private + + // MARK: - IoHandlerDelegate + + func invalidData() { + presentInvalidTimeError() + } + + + // MARK: - Private Utils + private var initialDate: Date? { if let timestamp = job?.time, let timeZone = initialTimeZone { return dateFromUtc(with: timestamp, timeZone: timeZone) @@ -122,33 +177,45 @@ class ScheduleMessageInteractor: BaseInteractor, ScheduleMessageInteractorInputP } private var initialTimeZone: TimeZoneLocal? { - let id = TimeZone.current.identifier - let cityName = String(describing: id.split(separator: "/").last!) - - var timeZone = timeZoneManager.timeZoneBy(cityName: cityName) - if let utc = job?.settings?.first?.value { - timeZone = timeZoneManager.timeZoneBy(utc: utc) + return timeZoneManager.timeZoneBy(utc: utc) + } else { + return timeZoneManager.timeZoneBy(cityName: cityName) } - - return timeZone } private var cityName: String { let id = TimeZone.current.identifier - return String(describing: id.split(separator: "/").last!) + return String(id.split(separator: "/").last!) + } + + private func canSendScheduledMessage() -> Bool { + guard let targets = targets, !targets.isEmpty else { + return false + } + guard case let contacts = targets.contacts, !contacts.isEmpty else { + return true + } + // Need at least one contact to whom user can send a message. + return targets.contacts.contains { contact in + guard let phoneId = contact.phone_id, let contact = ContactDAO.findContactBy(phoneId: phoneId) else { + return false + } + return messageSendingService.canSendMessage(to: contact) + } } private func prepareDateTimeInfo(with timeZone: TimeZoneLocal, date: Date) -> DateTimeInfo? { - let timestamp = self.timestamp(from: date, timeZone: timeZone) + let minDate = Date.currentDate.zeroSeconds!.adding(.minute, value: minScheduleIntervalInMinutes)! + let minFutureTime = minDate.timestamp + + let scheduledTime = timestamp(from: date, timeZone: timeZone) - if timestamp < Date.currentTimestamp { - presenter.presentError(errorMessage: "schedule_error".localized) + guard scheduledTime >= minFutureTime else { return nil } - let features = [Feature(timeZone: timeZone)] - return (features, timestamp) + return (features, scheduledTime) } private func dateFromUtc(with timestamp: Int64, timeZone: TimeZoneLocal) -> Date { @@ -165,7 +232,7 @@ class ScheduleMessageInteractor: BaseInteractor, ScheduleMessageInteractorInputP } private func timestamp(from date: Date, timeZone: TimeZoneLocal) -> Int64 { - var timestamp = Int64(date.zeroSeconds!.timeIntervalSince1970 * 1000) // mills + var timestamp = date.zeroSeconds!.timestamp // mills if let currentTimeZone = timeZoneManager.timeZoneBy(cityName: cityName), let zone = timeZone.timeZone, let currentZone = currentTimeZone.timeZone { let diff = Int64(currentZone.secondsFromGMT(for: date) - zone.secondsFromGMT(for: date)) @@ -183,5 +250,4 @@ class ScheduleMessageInteractor: BaseInteractor, ScheduleMessageInteractorInputP self.info = info } - } diff --git a/Nynja/Modules/ScheduleMessage/Models/ScheduleContentType.swift b/Nynja/Modules/ScheduleMessage/Models/ScheduleContentType.swift index 7e291678d..8f66d9ba1 100644 --- a/Nynja/Modules/ScheduleMessage/Models/ScheduleContentType.swift +++ b/Nynja/Modules/ScheduleMessage/Models/ScheduleContentType.swift @@ -23,12 +23,19 @@ enum ScheduleContentType { let model = TextItemModel(text: text, mentions: mentions) self = .text(model: model) case .audio: - if let url = URL(string: desc.payload ?? "") { - let model = AudioItemModel(url: url, amplitudes: desc.amplitudes, duration: desc.audioDuration) + if let url = URL(string: desc.payload ?? ""), let localUrl = url.mediaURL { + let model = AudioItemModel(url: localUrl, + amplitudes: desc.amplitudes, + duration: desc.audioDuration, + currentTime: 0) self = .audio(model: model) } else { return nil } + case .sticker: + let text = desc.messageRepresentation ?? "" + let model = TextItemModel(text: text, mentions: nil) + self = .text(model: model) default: let model = OtherItemModel(image: type.icon, description: type.messageDescription) self = .other(model: model) diff --git a/Nynja/Modules/ScheduleMessage/Presenter/ScheduleMessagePresenter.swift b/Nynja/Modules/ScheduleMessage/Presenter/ScheduleMessagePresenter.swift index 64fc5dcb8..36aa0e1a5 100644 --- a/Nynja/Modules/ScheduleMessage/Presenter/ScheduleMessagePresenter.swift +++ b/Nynja/Modules/ScheduleMessage/Presenter/ScheduleMessagePresenter.swift @@ -6,7 +6,7 @@ // Copyright © 2018 Michael Katkov. All rights reserved. // -class ScheduleMessagePresenter: BasePresenter, ScheduleMessagePresenterProtocol, ScheduleMessageInteractorOutputProtocol, DateTimePickerDelegate, TimeZoneSelectorDelegate, ForwardSelectorDelegate, ReachabilityServiceObserver { +final class ScheduleMessagePresenter: BasePresenter, ScheduleMessagePresenterProtocol, ScheduleMessageInteractorOutputProtocol, DateTimePickerDelegate, TimeZoneSelectorDelegate, ForwardSelectorDelegate, ConnectionServiceDelegate { weak var delegate: ScheduleMessageDelegate? @@ -22,11 +22,11 @@ class ScheduleMessagePresenter: BasePresenter, ScheduleMessagePresenterProtocol, override init() { super.init() - ReachabilityService.sharedInstance.addRechabilityObserver(self) + ConnectionService.shared.addSubscriber(self) } deinit { - ReachabilityService.sharedInstance.removeRechabilityObserver(self) + ConnectionService.shared.removeSubscriber(self) } // MARK: - ScheduleMessagePresenterProtocol @@ -160,10 +160,12 @@ class ScheduleMessagePresenter: BasePresenter, ScheduleMessagePresenterProtocol, AlertManager.sharedInstance.showActionSheet(title: nil, message: ScheduleMessageViewController.Strings.delete.localized, actions: [deleteAction, cancelAction]) } - // MARK: - ReachabilityServiceObserver - func reachabilityStatusChanged(isReachable: Bool) { - if !isReachable { - AlertManager.sharedInstance.showNoInternetConnection() + func connectionStatusChanged(_ sender: ConnectionService, service: ConnectionService.Service, oldValue: ConnectionService.ConnectionServiceState) { + if service == .networking { + guard let state = sender.state[.networking] else { return } + if state == .disconnected { + AlertManager.sharedInstance.showNoInternetConnection() + } } } diff --git a/Nynja/Modules/ScheduleMessage/ScheduleMessageProtocols.swift b/Nynja/Modules/ScheduleMessage/ScheduleMessageProtocols.swift index fc75beac1..fb6c9a4ab 100644 --- a/Nynja/Modules/ScheduleMessage/ScheduleMessageProtocols.swift +++ b/Nynja/Modules/ScheduleMessage/ScheduleMessageProtocols.swift @@ -85,7 +85,7 @@ protocol ScheduleMessageInteractorOutputProtocol: class { protocol ScheduleMessageInteractorInputProtocol: BaseInteractorProtocol { - var mode: ScheduledMessageMode { get set } + var mode: ScheduledMessageMode { get } var targets: ForwardTargets? { get } diff --git a/Nynja/Modules/ScheduleMessage/View/ScheduleMessageViewController.swift b/Nynja/Modules/ScheduleMessage/View/ScheduleMessageViewController.swift index 87d7de31e..c7c4c030d 100644 --- a/Nynja/Modules/ScheduleMessage/View/ScheduleMessageViewController.swift +++ b/Nynja/Modules/ScheduleMessage/View/ScheduleMessageViewController.swift @@ -9,7 +9,7 @@ import UIKit import SnapKit -class ScheduleMessageViewController: BaseVC, ScheduleMessageViewProtocol, SaveDeleteViewDelegate { +final class ScheduleMessageViewController: BaseVC, ScheduleMessageViewProtocol, SaveDeleteViewDelegate { var presenter: ScheduleMessagePresenterProtocol! { didSet { @@ -28,8 +28,12 @@ class ScheduleMessageViewController: BaseVC, ScheduleMessageViewProtocol, SaveDe font: TextItemView.font, textColor: TextItemView.textColor ) + private lazy var audioManager = AudioManager.sharedInstance + private lazy var audioSessionManager = AudioSessionManager.shared + private lazy var proximityManager = ProximitySensorManager.shared + private var audioModel: AudioItemModel? - // MARK: - Views + //MARK: - Views private lazy var messageToLabel: UILabel = { let label = genTitleLabel() @@ -165,7 +169,8 @@ class ScheduleMessageViewController: BaseVC, ScheduleMessageViewProtocol, SaveDe }() - // MARK: - View (Additional) + //MARK: - View (Additional) + func genTitleLabel() -> UILabel { let label = UILabel(height: CGFloat(Constraints.messageToLabel.height), color: Text.Title.textColor, @@ -173,9 +178,15 @@ class ScheduleMessageViewController: BaseVC, ScheduleMessageViewProtocol, SaveDe return label } - // MARK: - Initialize + + //MARK: - Initialize + override func initialize() { super.initialize() + + audioManager.delegate = self + proximityManager.delegate = self + screenTitle = Strings.title.localized.uppercased() shouldShowSeparator = true @@ -188,7 +199,14 @@ class ScheduleMessageViewController: BaseVC, ScheduleMessageViewProtocol, SaveDe presenter.viewDidLoad() } + override func viewDidDisappear(_ animated: Bool) { + super.viewDidDisappear(animated) + stopPlaying() + } + + // MARK: - ScheduleMessageViewProtocol + func setup(with info: ScheduleDisplayInfo) { setupForwardLabel(info.isForward) setupDeleteButton(info) @@ -242,8 +260,31 @@ class ScheduleMessageViewController: BaseVC, ScheduleMessageViewProtocol, SaveDe private func setupMessage(with model: AudioItemModel) { messageTypeLabel.text = Strings.message_voice.localized - let audioItemView = AudioItemView() - audioItemView.setup(with: model) + audioModel = model + let audioItemView = AudioItemView( + dependencies: .init( + canInteractionHandler: { [weak self] in + return true + }, + changeStateHandler: { [weak self] state in + guard let `self` = self, let audioModel = self.audioModel else { + return + } + switch state { + case .play: + if let _ = try? self.audioManager.play(with: audioModel.url, at: audioModel.currentTime ?? 0) { + self.updatePlayStatus(.play) + self.proximityManager.monitoringState = .enabled + } + case .pause, .stop: + self.pause() + } + }, + changeProgressHandler: { [weak self] progress in + self?.pause() + self?.audioModel?.currentTime = (model.duration ?? 0) * progress / 100.0 + }, + audioModel: model)) contentView.addSubview(audioItemView) audioItemView.snp.makeConstraints({ (make) in @@ -361,3 +402,72 @@ extension ScheduleMessageViewController: TestableViewControllerProtocol { } } + +//MARK: - Audio Manager +extension ScheduleMessageViewController: AudioManagerDelegate { + + var audioItemView: AudioItemView? { + return scheduleContentItem as? AudioItemView + } + + func didFinishPlayingAudio(_ audioManager: AudioManager, with url: URL) { + updateUIForStopState() + } + + func didChangedCurrentTime(_ audioManager: AudioManager, currentTime: TimeInterval) { + audioModel?.currentTime = currentTime + + let percent = currentTime / audioManager.currentDuration * 100 + audioItemView?.setPlayProgress(percent: percent) + audioItemView?.setPlayTime(currentTime) + } + + func updateUIForStopState() { + proximityManager.monitoringState = .disabled + audioModel?.currentTime = 0 + audioItemView?.audioPlayingEnd() + } + + func stopPlaying() { + guard let url = audioModel?.url else { + return + } + audioManager.stop(with: url) + updateUIForStopState() + } + + func resume() { + if let _ = try? audioManager.resume() { + updatePlayStatus(.play) + } + } + + func pause() { + audioManager.pause() + updatePlayStatus(.pause) + } + + func updatePlayStatus(_ status: AudioItemView.State) { + audioItemView?.updateAudio(state: status) + } +} + + +//MARK: - Proximity Sensor + +extension ScheduleMessageViewController: ProximitySensorManagerDelegate { + func proximityStateChanged(_ manager: ProximitySensorManager, state: ProximityState) { + switch state { + case .closeToUser: + audioSessionManager.speaker = .soft + + if !audioManager.isPlaying { + resume() + } + case .farFromUser: + if audioManager.isPlaying { + pause() + } + } + } +} diff --git a/Nynja/Modules/ScheduleMessage/View/ScheduleMessageViewControllerConstants.swift b/Nynja/Modules/ScheduleMessage/View/ScheduleMessageViewControllerConstants.swift index 530dd1c58..d484af034 100644 --- a/Nynja/Modules/ScheduleMessage/View/ScheduleMessageViewControllerConstants.swift +++ b/Nynja/Modules/ScheduleMessage/View/ScheduleMessageViewControllerConstants.swift @@ -9,88 +9,87 @@ extension ScheduleMessageViewController { // MARK: - Text behavior - struct Text { - struct Title { + enum Text { + enum Title { static let fontName = Constants.fonts.notoSansRegular static let textColor = Constants.colors.darkGray.getColor() } } // MARK: - Constraints - struct Constraints { + enum Constraints { - struct messageToLabel { + enum messageToLabel { static let left = CGFloat(16.adjustedByWidth) static let top = CGFloat(20.adjustedByHeight) static let height = CGFloat(20.adjustedByHeight) } - struct messageToView { + enum messageToView { static let offset = CGFloat(0.adjustedByWidth) static let top = CGFloat(9.adjustedByHeight) static let height = CGFloat(48.adjustedByHeight) } - struct messageTypeLabel { + enum messageTypeLabel { static let horizontalInset = CGFloat(16.adjustedByWidth) static let top = CGFloat(23.adjustedByHeight) static let height = CGFloat(19.adjustedByHeight) } - struct forwardLabel { + enum forwardLabel { static let height = CGFloat(20.adjustedByWidth) static let topInset = 8.0.adjustedByWidth } - struct contentView { + enum contentView { static let offset = CGFloat(0.adjustedByWidth) } - struct saveDeleteView { + enum saveDeleteView { static let height = CGFloat(44.adjustedByWidth) static let horizontalInset: CGFloat = 0 static let verticalInset = CGFloat(28.adjustedByWidth) } - struct dateTimeItemView { + enum dateTimeItemView { static let height = CGFloat(72.adjustedByHeight) static let offset = CGFloat(0.adjustedByHeight) } - struct dateTimeTitleLabel { + enum dateTimeTitleLabel { static let bottom = 8.0 static let left = CGFloat(16.adjustedByWidth) static let height = CGFloat(24.adjustedByHeight) } - struct timeZoneItemView { + enum timeZoneItemView { static let height = CGFloat(27.0.adjustedByHeight) static let offset = CGFloat(0.adjustedByWidth) static let bottom = CGFloat(24.adjustedByWidth) } - struct timeZoneTitleLabel { + enum timeZoneTitleLabel { static let left = CGFloat(16.adjustedByWidth) static let bottom = CGFloat(8.adjustedByWidth) static let height = CGFloat(24.adjustedByHeight) } - struct audioItemView { + enum audioItemView { static let topOffset = CGFloat(8.adjustedByWidth) static let height = CGFloat(32.adjustedByHeight) } - struct otherItemView { + enum otherItemView { static let topOffset = CGFloat(8.adjustedByWidth) static let horizontalInset = CGFloat(16.adjustedByWidth) static let height = CGFloat(20.adjustedByHeight) } - struct textItemView { + enum textItemView { static let verticalInset = CGFloat(8.adjustedByWidth) static let horizontalInset = CGFloat(16.adjustedByWidth) } - } // MARK: - String @@ -114,5 +113,4 @@ extension ScheduleMessageViewController { return self.rawValue.localized } } - } diff --git a/Nynja/Modules/ScheduleMessage/View/Views/MessageContent/AudioItemView.swift b/Nynja/Modules/ScheduleMessage/View/Views/MessageContent/AudioItemView.swift index cbfdf9404..5f0d420f0 100644 --- a/Nynja/Modules/ScheduleMessage/View/Views/MessageContent/AudioItemView.swift +++ b/Nynja/Modules/ScheduleMessage/View/Views/MessageContent/AudioItemView.swift @@ -12,75 +12,56 @@ struct AudioItemModel { let url: URL let amplitudes: [CGFloat]? let duration: TimeInterval? + var currentTime: TimeInterval? } -class AudioItemView: BaseView, ScheduleItemProtocol { - - var audioUrl: URL! { - get { - return audioPlayer?.url - } - } - - private var audioPlayer: AudioPlayer? +class AudioItemView: BaseView, InitializeInjectable, ScheduleItemProtocol { - private var status: RecordStatus = .stop { + private var status: State = .stop { didSet { - switch status { - case .playing: - playButtonIv.setImage(#imageLiteral(resourceName: "pause").withRenderingMode(.alwaysTemplate), for: .normal) - default: - playButtonIv.setImage(#imageLiteral(resourceName: "play_btn").withRenderingMode(.alwaysTemplate), for: .normal) - } + playButtonIv.setImage(status.stateImage, for: .normal) } } + private var canInteractionHandler: CanInteractionHandler? + private var changeStateHandler: ChangeStateHandler? + private var changeProgressHandler: ChangeProgressHandler? + + private let audioModel: AudioItemModel + //MARK: - Subviews - lazy var playButtonIv: UIButton = { - let view = UIButton() - let image = #imageLiteral(resourceName: "play_btn").withRenderingMode(.alwaysTemplate) - view.setImage(image, for: .normal) - view.tintColor = Constants.colors.red.getColor() - view.addTarget(self, action: #selector(playButtonTapped), for: .touchUpInside) - - self.addSubview(view) - view.snp.makeConstraints({ (make) in - make.centerY.equalTo(self) - make.left.equalTo(self).offset(Constraints.playButtonIv.left) - make.width.height.equalTo(Constraints.playButtonIv.width) - }) - return view - }() + lazy var playButtonIv: UIButton = makePlayButton() + lazy var durationLabel: UILabel = makeDurationLabel() + lazy var waveform: DrawableAudioWaveform = makeWaveform() - lazy var durationLabel: UILabel = { - let label = UILabel(height: CGFloat(Constraints.durationLabel.height), - color: Text.durationLabel.fontColor, - fontName: Text.durationLabel.fontName) - label.font = UIFont.makeFont(with: Text.durationLabel.fontName, height: Text.durationLabel.fontSize) - self.addSubview(label) - label.snp.makeConstraints { make in - make.centerY.equalTo(self) - make.right.equalTo(self).offset(-Constraints.durationLabel.right) - make.height.equalTo(Constraints.durationLabel.height) - } - return label - }() - lazy var waveform: DrawableAudioWaveform = { - var waveform = DrawableAudioWaveform() - waveform.delegate = self - self.addSubview(waveform) - waveform.snp.makeConstraints({ (make) in - make.centerY.equalTo(self) - make.height.equalTo(Constraints.waveform.height) - make.left.equalTo(playButtonIv.snp.right).offset(Constraints.waveform.left) - make.right.equalTo(durationLabel.snp.left).offset(-Constraints.waveform.right) - }) + //MARK: - InitializeInjectable + + struct Dependencies { + let canInteractionHandler: CanInteractionHandler? + let changeStateHandler: ChangeStateHandler? + let changeProgressHandler: ChangeProgressHandler? - return waveform - }() + let audioModel: AudioItemModel + } + + required init(dependencies: Dependencies) { + canInteractionHandler = dependencies.canInteractionHandler + changeStateHandler = dependencies.changeStateHandler + changeProgressHandler = dependencies.changeProgressHandler + + audioModel = dependencies.audioModel + + super.init(frame: .zero) + + setup(with: audioModel) + } + + required init?(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } //MARK: - Base Setup @@ -100,7 +81,7 @@ class AudioItemView: BaseView, ScheduleItemProtocol { } } - audioPlayer = AudioPlayer(url: model.url, delegate: self) + durationLabel.text = model.duration?.string } @@ -108,26 +89,56 @@ class AudioItemView: BaseView, ScheduleItemProtocol { @objc func playButtonTapped() { status.switch() - audioPlayer?.didTapPlay() + changeStateHandler?(status) } //MARK: - ScheduleItemProtocol func stopInteraction() { - guard status == .playing else { + guard status == .play else { return } status = .stop - audioPlayer?.prepareToPause() + changeStateHandler?(.stop) } } -extension AudioItemView: AudioPlayerDelegate { + +//MARK: - Inner types + +extension AudioItemView { - func setDurationTime(_ time: TimeInterval) { - durationLabel.text = time.string + enum State { + case play + case stop + case pause + + var stateImage: UIImage? { + switch self { + case .play: + return #imageLiteral(resourceName: "pause").withRenderingMode(.alwaysTemplate) + case .pause, .stop: + return #imageLiteral(resourceName: "play_btn").withRenderingMode(.alwaysTemplate) + } + } + + mutating func `switch`() { + switch self { + case .play: + self = .pause + default: + self = .play + } + } } + + typealias CanInteractionHandler = () -> Bool + typealias ChangeStateHandler = (State) -> Void + typealias ChangeProgressHandler = (Double) -> Void + +} +extension AudioItemView { func setPlayTime(_ time: TimeInterval) { durationLabel.text = time.string } @@ -136,30 +147,88 @@ extension AudioItemView: AudioPlayerDelegate { waveform.updateProgress(percent) } - func audioPlayerDidReachEnd() { + func audioPlayingEnd() { status = .stop waveform.updateProgress(0) } + + func updateAudio(state: State) { + status = state + } } -// MARK: - DrawableAudioWaveformDelegate + +//MARK: - DrawableAudioWaveformDelegate + extension AudioItemView: DrawableAudioWaveformDelegate { func audioWaveformDidBeginDragging(_ waveform: DrawableAudioWaveform) { status = .stop - audioPlayer?.prepareToPause() + changeStateHandler?(.stop) } func audioWaveform(_ waveform: DrawableAudioWaveform, didChangeProgress progress: Double) { - audioPlayer?.changedProgress(progress) + changeProgressHandler?(progress) } func isAudioWaveformDraggable(_ waveform: DrawableAudioWaveform) -> Bool { - return audioPlayer?.url != nil + return canInteractionHandler?() ?? false } } -// MARK: - Layout + +//MARK: - Subviews maker + +private extension AudioItemView { + + func makePlayButton() -> UIButton { + let view = UIButton() + let image = #imageLiteral(resourceName: "play_btn").withRenderingMode(.alwaysTemplate) + view.setImage(image, for: .normal) + view.tintColor = Constants.colors.red.getColor() + view.addTarget(self, action: #selector(playButtonTapped), for: .touchUpInside) + + self.addSubview(view) + view.snp.makeConstraints({ (make) in + make.centerY.equalTo(self) + make.left.equalTo(self).offset(Constraints.playButtonIv.left) + make.width.height.equalTo(Constraints.playButtonIv.width) + }) + return view + } + + func makeDurationLabel() -> UILabel { + let label = UILabel(height: CGFloat(Constraints.durationLabel.height), + color: Text.durationLabel.fontColor, + fontName: Text.durationLabel.fontName) + label.font = UIFont.makeFont(with: Text.durationLabel.fontName, height: Text.durationLabel.fontSize) + self.addSubview(label) + label.snp.makeConstraints { make in + make.centerY.equalTo(self) + make.right.equalTo(self).offset(-Constraints.durationLabel.right) + make.height.equalTo(Constraints.durationLabel.height) + } + return label + } + + func makeWaveform() -> DrawableAudioWaveform { + let waveform = DrawableAudioWaveform() + waveform.delegate = self + self.addSubview(waveform) + waveform.snp.makeConstraints({ (make) in + make.centerY.equalTo(self) + make.height.equalTo(Constraints.waveform.height) + make.left.equalTo(playButtonIv.snp.right).offset(Constraints.waveform.left) + make.right.equalTo(durationLabel.snp.left).offset(-Constraints.waveform.right) + }) + + return waveform + } +} + + +//MARK: - Layout + extension AudioItemView { struct Text { struct durationLabel { diff --git a/Nynja/Modules/ScheduleMessage/WireFrame/ScheduleMessageWireframe.swift b/Nynja/Modules/ScheduleMessage/WireFrame/ScheduleMessageWireframe.swift index df3abedf0..179084cb6 100644 --- a/Nynja/Modules/ScheduleMessage/WireFrame/ScheduleMessageWireframe.swift +++ b/Nynja/Modules/ScheduleMessage/WireFrame/ScheduleMessageWireframe.swift @@ -8,7 +8,7 @@ import UIKit -class ScheduleMessageWireFrame: ScheduleMessageWireFrameProtocol { +final class ScheduleMessageWireFrame: ScheduleMessageWireFrameProtocol { weak var main: MainWireFrameProtocol? weak var navigation: UINavigationController? diff --git a/Nynja/Modules/SelectCountry/View/SelectCountryViewController.swift b/Nynja/Modules/SelectCountry/View/SelectCountryViewController.swift index 0d2d790a8..7145faf8a 100644 --- a/Nynja/Modules/SelectCountry/View/SelectCountryViewController.swift +++ b/Nynja/Modules/SelectCountry/View/SelectCountryViewController.swift @@ -8,7 +8,7 @@ import UIKit -class SelectCountryViewController: BaseVC, SelectCountryViewProtocol { +class SelectCountryViewController: BaseVC, SelectCountryViewProtocol, KeyboardInteractive { var presenter: SelectCountryPresenterProtocol! @@ -123,7 +123,7 @@ class SelectCountryViewController: BaseVC, SelectCountryViewProtocol { // MARK: - ⌨️ Keyboard - override func keyboardNotified(endFrame: CGRect) { + func keyboardNotified(endFrame: CGRect) { if endFrame.origin.y >= UIScreen.main.bounds.size.height { updateToHide(view: controlContainerView, offset: -bottomInset) } else { diff --git a/Nynja/Modules/Settings/ChangeNumber/ChangeNumberStep2/View/ChangeNumberStep2ViewController.swift b/Nynja/Modules/Settings/ChangeNumber/ChangeNumberStep2/View/ChangeNumberStep2ViewController.swift index 3a0a65bb6..d7682f766 100644 --- a/Nynja/Modules/Settings/ChangeNumber/ChangeNumberStep2/View/ChangeNumberStep2ViewController.swift +++ b/Nynja/Modules/Settings/ChangeNumber/ChangeNumberStep2/View/ChangeNumberStep2ViewController.swift @@ -8,7 +8,7 @@ import UIKit -class ChangeNumberStep2ViewController: BaseVC, LoginWheelContainerViewProtocol, ChangeNumberStep2ViewProtocol { +class ChangeNumberStep2ViewController: BaseVC, LoginWheelContainerViewProtocol, ChangeNumberStep2ViewProtocol, KeyboardInteractive { var presenter: ChangeNumberStep2PresenterProtocol! { didSet { @@ -180,7 +180,7 @@ class ChangeNumberStep2ViewController: BaseVC, LoginWheelContainerViewProtocol, } // MARK: - BaseVC - override func keyboardNotified(endFrame: CGRect) { + func keyboardNotified(endFrame: CGRect) { if endFrame.origin.y >= UIScreen.main.bounds.size.height { nextBtn(hide: true) } else { diff --git a/Nynja/Modules/Settings/NotificationAlertSounds/Interactor/NotificationAlertSoundsInteractor.swift b/Nynja/Modules/Settings/NotificationAlertSounds/Interactor/NotificationAlertSoundsInteractor.swift index fac3187eb..7d2ab36fd 100644 --- a/Nynja/Modules/Settings/NotificationAlertSounds/Interactor/NotificationAlertSoundsInteractor.swift +++ b/Nynja/Modules/Settings/NotificationAlertSounds/Interactor/NotificationAlertSoundsInteractor.swift @@ -16,9 +16,7 @@ final class NotificationAlertSoundsInteractor: NotificationAlertSoundsInteractor private let audioSessionManager = AudioSessionManager.shared - private let soundService = SoundService.sharedInstance - - private var soundPlayer: SoundPlayer? + private let systemSoundManager = SystemSoundManager.sharedInstance private(set) lazy var notificationSettings: NotificationSettings = { return userSettingsService.notifications @@ -28,7 +26,7 @@ final class NotificationAlertSoundsInteractor: NotificationAlertSoundsInteractor // MARK: - NotificationAlertSoundsInteractorInputProtocol func fetchSounds() { - let sounds = soundService.soundBundle.pushSounds + let sounds = SoundBundle.shared.pushSounds presenter.didFetchSounds(sounds) } @@ -47,17 +45,10 @@ final class NotificationAlertSoundsInteractor: NotificationAlertSoundsInteractor } private func playSound(_ sound: Sound) { - guard let soundURL = sound.url else { return } - - do { - guard try audioSessionManager.request(category: .playback) else { - return - } - soundPlayer?.stop() - soundPlayer = try SoundPlayer(soundURL: soundURL) - soundPlayer?.play() - } catch { - LogService.log(topic: .audioSystem) { return error.localizedDescription } + guard let soundUrl = sound.url else { + return } + + try? AudioManager.sharedInstance.play(with: soundUrl, options: .alwaysPlayFromStart) } } diff --git a/Nynja/Modules/Settings/Privacy/View/ViewController/PrivacyListViewController.swift b/Nynja/Modules/Settings/Privacy/View/ViewController/PrivacyListViewController.swift index fa7fe1269..e004857ce 100644 --- a/Nynja/Modules/Settings/Privacy/View/ViewController/PrivacyListViewController.swift +++ b/Nynja/Modules/Settings/Privacy/View/ViewController/PrivacyListViewController.swift @@ -9,7 +9,7 @@ import UIKit import SnapKit -final class PrivacyListViewController: BaseVC, PrivacyListViewProtocol, PrivacyListDataSourceDelegate { +final class PrivacyListViewController: BaseVC, PrivacyListViewProtocol, PrivacyListDataSourceDelegate, KeyboardInteractive { var presenter: PrivacyListPresenterProtocol! { didSet { @@ -114,7 +114,7 @@ final class PrivacyListViewController: BaseVC, PrivacyListViewProtocol, PrivacyL // MARK: - Keyboard - override func keyboardNotified(endFrame: CGRect) { + func keyboardNotified(endFrame: CGRect) { if endFrame.origin.y >= UIScreen.main.bounds.size.height { updateToHide(view: controlContainerView, offset: -bottomInset) } else { diff --git a/Nynja/Modules/Settings/Security/Interactor/SecurityInteractor.swift b/Nynja/Modules/Settings/Security/Interactor/SecurityInteractor.swift index 32a015548..e497dfc85 100644 --- a/Nynja/Modules/Settings/Security/Interactor/SecurityInteractor.swift +++ b/Nynja/Modules/Settings/Security/Interactor/SecurityInteractor.swift @@ -6,7 +6,7 @@ // Copyright © 2018 Michael Katkov. All rights reserved. // -class SecurityInteractor: SecurityInteractorInputProtocol, AuthHandlerDelegate, IoHandlerDelegate, ReachabilityServiceObserver { +class SecurityInteractor: SecurityInteractorInputProtocol, AuthHandlerDelegate, IoHandlerDelegate, ConnectionServiceDelegate { weak var presenter: SecurityInteractorOutputProtocol! @@ -46,12 +46,12 @@ class SecurityInteractor: SecurityInteractorInputProtocol, AuthHandlerDelegate, } func startUpdate() { - ReachabilityService.sharedInstance.addRechabilityObserver(self) + ConnectionService.shared.addSubscriber(self) timer = Timer.scheduledTimer(timeInterval: 10, target: self, selector: #selector(self.update), userInfo: nil, repeats: true) } func stopUpdate() { - ReachabilityService.sharedInstance.removeRechabilityObserver(self) + ConnectionService.shared.removeSubscriber(self) timer?.invalidate() } @@ -96,11 +96,13 @@ class SecurityInteractor: SecurityInteractorInputProtocol, AuthHandlerDelegate, presenter.showItems() } - //MARK: - ReachabilityServiceObserver - func reachabilityStatusChanged(isReachable: Bool) { - presenter.notifyInternetStatus(hasInternet: isReachable) - if isReachable { - getAllSessions() + func connectionStatusChanged(_ sender: ConnectionService, service: ConnectionService.Service, oldValue: ConnectionService.ConnectionServiceState) { + if service == .networking { + guard let networkStatus = sender.state[.networking] else { return } + presenter.notifyInternetStatus(hasInternet: networkStatus == .connected) + if networkStatus == .connected || networkStatus == .switched { + getAllSessions() + } } } diff --git a/Nynja/Modules/Settings/SettingsDataAndStorage/DataAndStorageOption.swift b/Nynja/Modules/Settings/SettingsDataAndStorage/DataAndStorageOption.swift index 6188f4356..bbed1493e 100644 --- a/Nynja/Modules/Settings/SettingsDataAndStorage/DataAndStorageOption.swift +++ b/Nynja/Modules/Settings/SettingsDataAndStorage/DataAndStorageOption.swift @@ -9,6 +9,6 @@ import Foundation enum DataAndStorageOption: Int, PickableEnum { - case photos, voiceMessages, videoMessages, videos, files, music, gifs - var permanentID: String { return ["photos", "voice_messages", "video_messages", "videos", "files", "music", "gifs"][rawValue] }//all parameters are localized keys + case photos, voiceMessages, videos, files + var permanentID: String { return ["photos", "voice_messages", "videos", "files"][rawValue] } } diff --git a/Nynja/Modules/SettingsGroup/Interactor/SettingsGroupInteractor.swift b/Nynja/Modules/SettingsGroup/Interactor/SettingsGroupInteractor.swift index 77e9d8095..e50f93737 100644 --- a/Nynja/Modules/SettingsGroup/Interactor/SettingsGroupInteractor.swift +++ b/Nynja/Modules/SettingsGroup/Interactor/SettingsGroupInteractor.swift @@ -6,7 +6,7 @@ // Copyright © 2017 TecSynt Solutions. All rights reserved. // -final class SettingsGroupInteractor: BaseInteractor, SettingsGroupInteractorInputProtocol, ReachabilityServiceObserver, +final class SettingsGroupInteractor: BaseInteractor, SettingsGroupInteractorInputProtocol, ConnectionServiceDelegate, MQTTServiceDelegate, SetInjectable { weak var presenter: SettingsGroupInteractorOutputProtocol? @@ -21,7 +21,7 @@ MQTTServiceDelegate, SetInjectable { private var mqttService: MQTTService! private var muteChatService: MuteChatServiceProtocol! private var storageService: StorageService! - private var reachabilityService: ReachabilityService! + private var connectionService: ConnectionService! // MARK: - Init @@ -31,7 +31,7 @@ MQTTServiceDelegate, SetInjectable { } deinit { - reachabilityService.removeRechabilityObserver(self) + connectionService.removeSubscriber(self) MQTTService.sharedInstance.removeSubscriber(self) } @@ -143,17 +143,19 @@ MQTTServiceDelegate, SetInjectable { } } - - // MARK: - ReachabilityServiceObserver - - func reachabilityStatusChanged(isReachable: Bool) { - if !isReachable { - presenter?.reachabilityStatusChanged(isReachable) + func connectionStatusChanged(_ sender: ConnectionService, service: ConnectionService.Service, oldValue: ConnectionService.ConnectionServiceState) { + if service == .networking { + let reachabilityState = sender.state[.networking] + if reachabilityState == .disconnected { + presenter?.reachabilityStatusChanged(false) + } } } private func askForInternetStatus() { - presenter?.reachabilityStatusChanged(reachabilityService.isReachable) + let reachabilityState = connectionService.state[.networking] + let isReachable = reachabilityState == .connected + presenter?.reachabilityStatusChanged(isReachable) } @@ -236,7 +238,7 @@ extension SettingsGroupInteractor { let mqttService: MQTTService let muteChatService: MuteChatServiceProtocol let storageService: StorageService - let reachabilityService: ReachabilityService + let connectionService: ConnectionService } func inject(dependencies: SettingsGroupInteractor.Dependencies) { @@ -244,9 +246,9 @@ extension SettingsGroupInteractor { mqttService = dependencies.mqttService muteChatService = dependencies.muteChatService storageService = dependencies.storageService - reachabilityService = dependencies.reachabilityService + connectionService = dependencies.connectionService - reachabilityService.addRechabilityObserver(self) + connectionService.addSubscriber(self) mqttService.addSubscriber(self) } } diff --git a/Nynja/Modules/SettingsGroup/Presenter/SettingsGroupPresenter.swift b/Nynja/Modules/SettingsGroup/Presenter/SettingsGroupPresenter.swift index 76657b6da..f21337090 100644 --- a/Nynja/Modules/SettingsGroup/Presenter/SettingsGroupPresenter.swift +++ b/Nynja/Modules/SettingsGroup/Presenter/SettingsGroupPresenter.swift @@ -47,11 +47,10 @@ class SettingsGroupPresenter: BasePresenter, SettingsGroupPresenterProtocol, Cre // MARK: - Actions func changeAlias() { - if let nicks = room?.members?.map({ member -> String in - return member.alias ?? "" - }) { - wireFrame.changeAlias(alias: (ownMember?.alias)!, nicks: nicks) + guard let alias = ownMember?.alias else { + return } + wireFrame.changeAlias(alias: alias) } func openAvatar(from imageView: UIImageView) { diff --git a/Nynja/Modules/SettingsGroup/SettingsProtocols.swift b/Nynja/Modules/SettingsGroup/SettingsProtocols.swift index bb0ca5f62..de69828ae 100644 --- a/Nynja/Modules/SettingsGroup/SettingsProtocols.swift +++ b/Nynja/Modules/SettingsGroup/SettingsProtocols.swift @@ -19,7 +19,7 @@ protocol SettingsGroupWireframeProtocol: class { func presentAvatarModally(imageURL: URL, on view: UIViewController, with transitionInfo: ImagePreviewTransitionInfo) func changeAvatar() func changeGroupName(name: String) - func changeAlias(alias: String, nicks: [String]) + func changeAlias(alias: String) func showRules(room: Room?) func showStorage(room: Room?) func showLanguageSettings(_ room: Room?) diff --git a/Nynja/Modules/SettingsGroup/WireFrame/SettingsGroupWireFrame.swift b/Nynja/Modules/SettingsGroup/WireFrame/SettingsGroupWireFrame.swift index bc6512639..5a6b06a43 100644 --- a/Nynja/Modules/SettingsGroup/WireFrame/SettingsGroupWireFrame.swift +++ b/Nynja/Modules/SettingsGroup/WireFrame/SettingsGroupWireFrame.swift @@ -33,13 +33,13 @@ class SettingsGroupWireFrame: SettingsGroupWireframeProtocol { let mqttService = serviceFactory.makeMQTTService() let muteChatService = serviceFactory.makeMuteChatService() let storageSerice = serviceFactory.makeStorageService() - let reachabilityService = serviceFactory.makeReachabilityService() + let connectionService = serviceFactory.makeConnectionService() interactor.inject(dependencies: SettingsGroupInteractor.Dependencies(presenter: presenter, mqttService: mqttService, muteChatService: muteChatService, storageService: storageSerice, - reachabilityService: reachabilityService)) + connectionService: connectionService)) // Connecting view.presenter = presenter @@ -73,8 +73,8 @@ class SettingsGroupWireFrame: SettingsGroupWireframeProtocol { self.navigation?.view.layoutIfNeeded() } - func changeAlias(alias: String, nicks: [String]) { - MyGroupAliasWireFrame().presentMyGroupAlias(navigation: navigation!, currentAlias: alias, delegate: external, nicks: nicks, mode: .update) + func changeAlias(alias: String) { + MyGroupAliasWireFrame().presentMyGroupAlias(navigation: navigation!, currentAlias: alias, delegate: external, mode: .update) // self.navigation?.view.layoutIfNeeded() } diff --git a/Nynja/Modules/Splash/Interactor/SplashInteractor.swift b/Nynja/Modules/Splash/Interactor/SplashInteractor.swift index 068502ef3..c791f0e52 100644 --- a/Nynja/Modules/Splash/Interactor/SplashInteractor.swift +++ b/Nynja/Modules/Splash/Interactor/SplashInteractor.swift @@ -44,11 +44,10 @@ class SplashInteractor: SplashInteractorInputProtocol { badgeService.observeBadgeNumber(appDelegate) { (badgeNumber) in application.applicationIconBadgeNumber = Int(badgeNumber) } - + mqttService.initialize() callService.initialize() - - connectionSubscriberService.subscribe() + MediaDownloadManager.setupAppDataUsageSettingsIfNeeded() } diff --git a/Nynja/Modules/Splash/WireFrame/SplashWireframe.swift b/Nynja/Modules/Splash/WireFrame/SplashWireframe.swift index 3ceab7f73..decce3312 100644 --- a/Nynja/Modules/Splash/WireFrame/SplashWireframe.swift +++ b/Nynja/Modules/Splash/WireFrame/SplashWireframe.swift @@ -29,7 +29,7 @@ class SplashWireFrame: SplashWireFrameProtocol { } func showAuth() { - AuthWireFrame().presentAuth(navigation: navigation!) + LoginWireFrame().presentLogin(navigation: navigation!) } func showTutorial() { diff --git a/Nynja/Modules/Stickers/View/CollectionView/Cells/Sticker/StickerCellModel.swift b/Nynja/Modules/Stickers/View/CollectionView/Cells/Sticker/StickerCellModel.swift index fc0bd9728..cdc3ba50e 100644 --- a/Nynja/Modules/Stickers/View/CollectionView/Cells/Sticker/StickerCellModel.swift +++ b/Nynja/Modules/Stickers/View/CollectionView/Cells/Sticker/StickerCellModel.swift @@ -17,8 +17,8 @@ struct StickerCellModel: CellViewModel { let sticker: Sticker func setup(cell: StickerCollectionViewCell) { - cell.imageView.setImage(url: sticker.url, placeHolder: nil, accessibilityPrefix: nil) { (_, _) in - cell.setupTestingKeys() + cell.imageView.setImage(url: sticker.url) { [weak cell] _, _, _ in + cell?.setupTestingKeys() } } } diff --git a/Nynja/Modules/Stickers/View/CollectionView/Cells/Sticker/StickerCollectionViewCell.swift b/Nynja/Modules/Stickers/View/CollectionView/Cells/Sticker/StickerCollectionViewCell.swift index 172ab910c..7771cb68c 100644 --- a/Nynja/Modules/Stickers/View/CollectionView/Cells/Sticker/StickerCollectionViewCell.swift +++ b/Nynja/Modules/Stickers/View/CollectionView/Cells/Sticker/StickerCollectionViewCell.swift @@ -55,6 +55,7 @@ final class StickerCollectionViewCell: UICollectionViewCell, ScalableCell, Testa imageView.transform = .identity } + // MARK: - Testable func setupTestingKeys() { diff --git a/Nynja/Modules/TimeZoneSelector/View/ViewController/TimeZoneSelectorViewController.swift b/Nynja/Modules/TimeZoneSelector/View/ViewController/TimeZoneSelectorViewController.swift index 5945a2e71..e5b6c26c8 100644 --- a/Nynja/Modules/TimeZoneSelector/View/ViewController/TimeZoneSelectorViewController.swift +++ b/Nynja/Modules/TimeZoneSelector/View/ViewController/TimeZoneSelectorViewController.swift @@ -8,7 +8,7 @@ import UIKit -class TimeZoneSelectorViewController: BaseVC, TimeZoneSelectorViewProtocol, TimeZoneSelectorDSDelegate { +class TimeZoneSelectorViewController: BaseVC, TimeZoneSelectorViewProtocol, TimeZoneSelectorDSDelegate, KeyboardInteractive { var presenter: TimeZoneSelectorPresenterProtocol! { didSet { @@ -99,7 +99,7 @@ class TimeZoneSelectorViewController: BaseVC, TimeZoneSelectorViewProtocol, Time // MARK: - Keyboard - override func keyboardNotified(endFrame: CGRect) { + func keyboardNotified(endFrame: CGRect) { if endFrame.origin.y >= UIScreen.main.bounds.size.height { updateToHide(view: controlContainerView, offset: -bottomInset) } else { diff --git a/Nynja/Modules/WebView/Interactor/WebViewInteractor.swift b/Nynja/Modules/WebView/Interactor/WebViewInteractor.swift deleted file mode 100644 index 38b655224..000000000 --- a/Nynja/Modules/WebView/Interactor/WebViewInteractor.swift +++ /dev/null @@ -1,13 +0,0 @@ -// -// WebViewWebViewInteractor.swift -// Nynja -// -// Created by Anton Makarov on 25/06/2017. -// Copyright © 2017 TecSynt Solutions. All rights reserved. -// - -class WebViewInteractor: WebViewInteractorInputProtocol { - - weak var presenter: WebViewInteractorOutputProtocol! - -} diff --git a/Nynja/Modules/WebView/Presenter/WebViewPresenter.swift b/Nynja/Modules/WebView/Presenter/WebViewPresenter.swift deleted file mode 100644 index e4443c31b..000000000 --- a/Nynja/Modules/WebView/Presenter/WebViewPresenter.swift +++ /dev/null @@ -1,20 +0,0 @@ -// -// WebViewWebViewPresenter.swift -// Nynja -// -// Created by Anton Makarov on 25/06/2017. -// Copyright © 2017 TecSynt Solutions. All rights reserved. -// -import UIKit - -class WebViewPresenter: WebViewPresenterProtocol, WebViewInteractorOutputProtocol { - - weak var view: WebViewViewProtocol! - var interactor: WebViewInteractorInputProtocol! - var wireFrame: WebViewWireFrameProtocol! - - - func nextAction() { - (view as? UIViewController)?.dismiss(animated: true, completion: nil) - } -} diff --git a/Nynja/Modules/WebView/View/WebViewViewController.swift b/Nynja/Modules/WebView/View/WebViewViewController.swift deleted file mode 100644 index b1a230878..000000000 --- a/Nynja/Modules/WebView/View/WebViewViewController.swift +++ /dev/null @@ -1,62 +0,0 @@ -// -// WebViewWebViewViewController.swift -// Nynja -// -// Created by Anton Makarov on 25/06/2017. -// Copyright © 2017 TecSynt Solutions. All rights reserved. -// - -import UIKit - -class WebViewViewController: BaseVC, WebViewViewProtocol, UIWebViewDelegate { - - var presenter: WebViewPresenterProtocol! - var link = "http://www.nynja.biz" - - var webView : UIWebView { - let wv = UIWebView() - wv.delegate = self - self.view.addSubview(wv) - let padding = UIScreen.main.bounds.height * 0.18 - - wv.snp.makeConstraints { (make) in - make.leading.trailing.equalTo(self.view) - make.bottom.equalTo(self.view).offset(-padding) - - self.adjustVerticalInset(.top, make: make) - } - return wv - } - - lazy var nextButton : UIButton = { - let btn = UIButton() - let img = UIImage(named:"next_bttn")! - btn.setBackgroundImage(img, for: .normal) - let width = UIScreen.main.bounds.width * 0.17 - btn.layer.cornerRadius = width / 2 - btn.layer.masksToBounds = true - self.view.addSubview(btn) - let bottomPadding = UIScreen.main.bounds.height * 0.035 - - btn.snp.makeConstraints({ (make) in - make.width.height.equalTo(width) - make.right.equalTo(self.view).offset(-width / 4) - make.bottom.equalTo(self.view).offset(-bottomPadding) - }) - return btn - }() - - override func viewDidLoad() { - super.viewDidLoad() - let url = URL(string: link)! - let request = URLRequest(url: url) - webView.loadRequest(request) - nextButton.addTarget(self, action: #selector(nextAction), for: .touchUpInside) - } - - @objc func nextAction() { - self.presenter!.nextAction() - } - -} - diff --git a/Nynja/Modules/WebView/WebViewProtocols.swift b/Nynja/Modules/WebView/WebViewProtocols.swift deleted file mode 100644 index 55ae8c7b1..000000000 --- a/Nynja/Modules/WebView/WebViewProtocols.swift +++ /dev/null @@ -1,55 +0,0 @@ -// -// WebViewWebViewProtocols.swift -// Nynja -// -// Created by Anton Makarov on 25/06/2017. -// Copyright © 2017 TecSynt Solutions. All rights reserved. -// - -import UIKit - -protocol WebViewWireFrameProtocol: class { - - func presentWebView(navigation: UINavigationController,link: String) - - /** - * Add here your methods for communication PRESENTER -> WIREFRAME - */ -} - -protocol WebViewViewProtocol: class { - - var presenter: WebViewPresenterProtocol! { get set } - var link: String {get set} - /** - * Add here your methods for communication PRESENTER -> VIEW - */ -} - -protocol WebViewPresenterProtocol: class { - - var view: WebViewViewProtocol! { get set } - var interactor: WebViewInteractorInputProtocol! { get set } - var wireFrame: WebViewWireFrameProtocol! { get set } - - /** - * Add here your methods for communication VIEW -> PRESENTER - */ - func nextAction() -} - -protocol WebViewInteractorOutputProtocol: class { - - /** - * Add here your methods for communication INTERACTOR -> PRESENTER - */ -} - -protocol WebViewInteractorInputProtocol: class { - - var presenter: WebViewInteractorOutputProtocol! { get set } - - /** - * Add here your methods for communication PRESENTER -> INTERACTOR - */ -} diff --git a/Nynja/Modules/WebView/WireFrame/WebViewWireframe.swift b/Nynja/Modules/WebView/WireFrame/WebViewWireframe.swift deleted file mode 100644 index 8f328f649..000000000 --- a/Nynja/Modules/WebView/WireFrame/WebViewWireframe.swift +++ /dev/null @@ -1,32 +0,0 @@ -// -// WebViewWebViewWireframe.swift -// Nynja -// -// Created by Anton Makarov on 25/06/2017. -// Copyright © 2017 TecSynt Solutions. All rights reserved. -// - -import UIKit - -class WebViewWireFrame: WebViewWireFrameProtocol { - - weak var navigation : UINavigationController? - - func presentWebView(navigation: UINavigationController, link: String) { - let view = WebViewViewController() - let presenter = WebViewPresenter() - let interactor = WebViewInteractor() - - self.navigation = navigation - - // Connecting - view.presenter = presenter - presenter.view = view - presenter.wireFrame = self - presenter.interactor = interactor - interactor.presenter = presenter - view.link = link - navigation.present(view, animated: true, completion: nil) - - } -} diff --git a/Nynja/Modules/tutorial/WireFrame/TutorialWireframe.swift b/Nynja/Modules/tutorial/WireFrame/TutorialWireframe.swift index a814ec356..5ca5deb81 100644 --- a/Nynja/Modules/tutorial/WireFrame/TutorialWireframe.swift +++ b/Nynja/Modules/tutorial/WireFrame/TutorialWireframe.swift @@ -31,6 +31,6 @@ class TutorialWireFrame: TutorialWireFrameProtocol { func getStarted() { guard let navigation = navigation else { return } - AuthWireFrame().presentAuth(navigation: navigation) + LoginWireFrame().presentLogin(navigation: navigation) } } diff --git a/Nynja/NotificationManager.swift b/Nynja/NotificationManager.swift index 339b5c4f6..31042b2f7 100644 --- a/Nynja/NotificationManager.swift +++ b/Nynja/NotificationManager.swift @@ -242,9 +242,7 @@ final class NotificationManager { } private func getContainer(isFromPush: Bool) -> Container? { - guard let message = getMessage() else { return nil } - - if message.statusString == "delete" { + guard let message = getMessage(), message.messageStatus != .delete else { return nil } diff --git a/Nynja/Notifications/AppNotificationsProviding/AppNotificationsProvider.swift b/Nynja/Notifications/AppNotificationsProviding/AppNotificationsProvider.swift new file mode 100644 index 000000000..c1c12c633 --- /dev/null +++ b/Nynja/Notifications/AppNotificationsProviding/AppNotificationsProvider.swift @@ -0,0 +1,47 @@ +// +// AppNotificationsProvider.swift +// Nynja +// +// Created by Volodymyr Hryhoriev on 10/1/18. +// Copyright © 2018 TecSynt Solutions. All rights reserved. +// + +class AppNotificationsProvider: AppNotificationsProviding { + + private let notificationCenter = NotificationCenter.default + + var didEnterBackgroundHandler: AppNotificationsHandler? + var willEnterForegroundHandler: AppNotificationsHandler? + + + // MARK: - Init + + init() { + notificationCenter.addObserver( + self, + selector: #selector(didEnterBackground), + name: .UIApplicationDidEnterBackground, + object: nil) + notificationCenter.addObserver( + self, + selector: #selector(willEnterForeground), + name: .UIApplicationWillEnterForeground, + object: nil) + } + + deinit { + notificationCenter.removeObserver(self) + } + + + // MARK: - Handlers + + @objc private func didEnterBackground() { + didEnterBackgroundHandler?() + } + + @objc private func willEnterForeground() { + willEnterForegroundHandler?() + } + +} diff --git a/Nynja/Notifications/AppNotificationsProviding/AppNotificationsProviding.swift b/Nynja/Notifications/AppNotificationsProviding/AppNotificationsProviding.swift new file mode 100644 index 000000000..4429c09a7 --- /dev/null +++ b/Nynja/Notifications/AppNotificationsProviding/AppNotificationsProviding.swift @@ -0,0 +1,14 @@ +// +// AppNotificationsProviding.swift +// Nynja +// +// Created by Volodymyr Hryhoriev on 10/1/18. +// Copyright © 2018 TecSynt Solutions. All rights reserved. +// + +typealias AppNotificationsHandler = () -> Void + +protocol AppNotificationsProviding { + var didEnterBackgroundHandler: AppNotificationsHandler? { get set } + var willEnterForegroundHandler: AppNotificationsHandler? { get set } +} diff --git a/Nynja/OptionsItemsFactory.swift b/Nynja/OptionsItemsFactory.swift index 4f28559b6..89d3115d1 100644 --- a/Nynja/OptionsItemsFactory.swift +++ b/Nynja/OptionsItemsFactory.swift @@ -36,7 +36,7 @@ class OptionsItemsFactory: WCBaseItemsFactory { } var wheelPosition: ImageActionItemModel { - let item = ImageActionItemModel(navItem: .wheelPosition, action: { [weak navigateDelegate] (item, indexPath) in + let item = ImageActionItemModel(navItem: .wheelPosition, isSelectable: false, action: { [weak navigateDelegate] (item, indexPath) in // navigateDelegate?.showWheelPositionPicker(indexPath: indexPath) navigateDelegate?.unavailableFunctionality() }) @@ -58,7 +58,7 @@ class OptionsItemsFactory: WCBaseItemsFactory { } var theme: ImageActionItemModel { - let item = ImageActionItemModel(navItem: .theme, action: { [weak navigateDelegate] (item, indexPath) in + let item = ImageActionItemModel(navItem: .theme, isSelectable: false, action: { [weak navigateDelegate] (item, indexPath) in // navigateDelegate?.showThemePicker(indexPath: indexPath) navigateDelegate?.unavailableFunctionality() }) @@ -73,7 +73,7 @@ class OptionsItemsFactory: WCBaseItemsFactory { } var about: ImageActionItemModel { - let item = ImageActionItemModel(navItem: .about, action: { [weak navigateDelegate] (item, indexPath) in + let item = ImageActionItemModel(navItem: .about, isSelectable: false, action: { [weak navigateDelegate] (item, indexPath) in // navigateDelegate?.showAbout(indexPath: indexPath) navigateDelegate?.unavailableFunctionality() }) @@ -89,7 +89,7 @@ class OptionsItemsFactory: WCBaseItemsFactory { } var changeNumber: ImageActionItemModel { - let item = ImageActionItemModel(navItem: .phoneNumber, action: { [weak navigateDelegate] (item, indexPath) in + let item = ImageActionItemModel(navItem: .phoneNumber, isSelectable: false, action: { [weak navigateDelegate] (item, indexPath) in // navigateDelegate?.showChangeNumber(indexPath: indexPath) navigateDelegate?.unavailableFunctionality() }) diff --git a/Nynja/Resources/Assets.xcassets/Call items/ic_accept_call_big.imageset/Contents.json b/Nynja/Resources/Assets.xcassets/Call items/ic_accept_call_big.imageset/Contents.json new file mode 100644 index 000000000..08ad96656 --- /dev/null +++ b/Nynja/Resources/Assets.xcassets/Call items/ic_accept_call_big.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "ic_accept_call-68.pdf" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Nynja/Resources/Assets.xcassets/Call items/ic_accept_call_big.imageset/ic_accept_call-68.pdf b/Nynja/Resources/Assets.xcassets/Call items/ic_accept_call_big.imageset/ic_accept_call-68.pdf new file mode 100644 index 000000000..c1c053c46 --- /dev/null +++ b/Nynja/Resources/Assets.xcassets/Call items/ic_accept_call_big.imageset/ic_accept_call-68.pdf @@ -0,0 +1,2398 @@ +%PDF-1.5 % +1 0 obj <>/OCGs[5 0 R]>>/Pages 3 0 R/Type/Catalog>> endobj 2 0 obj <>stream + + + + + application/pdf + + + ic_accept_call-68 + + + Adobe Illustrator CC 2014 (Windows) + 2018-08-31T09:42:59+03:00 + 2018-08-31T09:42:59+03:00 + 2018-08-31T09:42:59+03:00 + + + + 256 + 256 + JPEG + /9j/4AAQSkZJRgABAgEBLAEsAAD/7QAsUGhvdG9zaG9wIDMuMAA4QklNA+0AAAAAABABLAAAAAEA AQEsAAAAAQAB/+4ADkFkb2JlAGTAAAAAAf/bAIQABgQEBAUEBgUFBgkGBQYJCwgGBggLDAoKCwoK DBAMDAwMDAwQDA4PEA8ODBMTFBQTExwbGxscHx8fHx8fHx8fHwEHBwcNDA0YEBAYGhURFRofHx8f Hx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8f/8AAEQgBAAEAAwER AAIRAQMRAf/EAaIAAAAHAQEBAQEAAAAAAAAAAAQFAwIGAQAHCAkKCwEAAgIDAQEBAQEAAAAAAAAA AQACAwQFBgcICQoLEAACAQMDAgQCBgcDBAIGAnMBAgMRBAAFIRIxQVEGE2EicYEUMpGhBxWxQiPB UtHhMxZi8CRygvElQzRTkqKyY3PCNUQnk6OzNhdUZHTD0uIIJoMJChgZhJRFRqS0VtNVKBry4/PE 1OT0ZXWFlaW1xdXl9WZ2hpamtsbW5vY3R1dnd4eXp7fH1+f3OEhYaHiImKi4yNjo+Ck5SVlpeYmZ qbnJ2en5KjpKWmp6ipqqusra6voRAAICAQIDBQUEBQYECAMDbQEAAhEDBCESMUEFURNhIgZxgZEy obHwFMHR4SNCFVJicvEzJDRDghaSUyWiY7LCB3PSNeJEgxdUkwgJChgZJjZFGidkdFU38qOzwygp 0+PzhJSktMTU5PRldYWVpbXF1eX1RlZmdoaWprbG1ub2R1dnd4eXp7fH1+f3OEhYaHiImKi4yNjo +DlJWWl5iZmpucnZ6fkqOkpaanqKmqq6ytrq+v/aAAwDAQACEQMRAD8A9U4q7FXYq7FXYq7FXYq7 FXYq7FUJqGraZp0fqX11HbKenqMAT/qjqfoyE5xjzNONqdZiwC8khH3liWp/mzoNvVbKGW9cdG/u oz9LVb/hcxZ66A5bvOar2v08NsYlM/6Ufbv9jGb782fMUxItYoLROxCmR/vY8f8Ahcxpa6Z5bOhz +1+pl9AjAfM/bt9iR3XnXzXc19TU51r/AL6b0v8Ak2FyiWoyHq6nL25rMnPJL4en7qS2bUdQmNZr qWQ+LyM36zlZmT1cGepyy+qUj7yUPkWh2KoiHUdQhNYbqWM+KSMv6jkhMjq3w1OWP0ykPcSmVr51 8121PT1Odqf79b1f+TgbLI6jIOrnYu3NZj5ZJfH1ffaeWP5s+YoSBdRQXadyVMb/AHqeP/C5fHXT HPd22D2v1MfrEZj5H7NvsZNpn5s6DcUW9hlsnPVv72MfStG/4XMmGugeezvtL7X6ee2QSgf9MPs3 +xlun6tpmox+pY3Udyo6+mwJH+sOo+nMqE4y5G3o9NrMWcXjkJe4ovJuS7FXYq7FXYq7FXYq7FXY q7FXYq7FXYq7FXYq7FXYq7FUm1/zdoehofrk/Kf9m1io0p/2Nfh+bUynLnjDm6vtDtjT6QeuXq/m jc/s+LznXPzT1y9LR6eo0+A/tL8cpHu5FB9A+nNdk1sjy2eI1/tXqMu2L93H5y+f6h8WGz3E9xK0 08jzSvu0kjFmPzJ3zEJJ5vMZMkpnikST3ndZgYOxV2KuxV2KuxV2KuxV2KuxVfBcT28qzQSPDKm6 yRsVYfIjfCCRyZ48koHiiSD3jZmWh/mnrdkVj1BRqFuP2m+CUD2YCh/2Q+nMvHrZDnu9PoPavUYt sv7yPyl8/wBfzejaB5u0PXEH1OfjP+1ay0WUf7GvxfNa5scWeM+T2/Z/bGn1Y9EvV/NOx/b8E5y5 2jsVdirsVdirsVdirsVdirsVdirsVdirsVUL2+s7G2e6vJlgt4xV5HNB/afbIykIiy0588MUDOZE YjqXmHmf80ry6L22ig21v0N0396w/wAkfsD8flmtza0naOzwPavtXPJcNP6Y/wA7+I+7u+/3MCeR 5HZ5GLuxqzMakk9yTmAS8fKRJs7lrFDsVdirsVdirsVdirsVdirsVdirsVdirsVbSR43V42KOpqr KaEEdwRiCmMiDY2LPfLH5pXlqUttaBubfoLpf71R/lD9sfj88z8OtI2lu9h2V7Vzx1DUeqP87+Ie /v8Av970+yvrO+tkurOZZ7eQVSRDUf2H2zZRkJCw99gzwywE4ESieoV8k3OxV2KuxV2KuxV2KuxV 2KuxV2KpP5l806boFp6103OZwfQtlPxuR+pfE5TmzRxiy6ztPtXFo4cU95HlHqf2ebxnzD5n1TXr r1rySkSn9zbrURoPYePuc0+XNKZsvl/aXambVz4pnboOg/HelOVOtdirsVdirsVdirsVdirsVdir sVdirsVdirsVdirsVdiqbeXvM+qaDdetZyViY/vrdqmNx7jx9xluLNKBsOy7N7UzaSfFA7dR0P47 3s3lrzTpuv2nrWrcJkA9e2Y/GhP618Dm4w5o5BYfUOzO1cWshxQ2kOceo/Z5pxlzs3Yq7FXYq7FX Yq7FXYq7FWP+b/N9p5etASBNfTA/V7etOn7b+Cj8f1Y+fOMY83Tds9sw0UP52SXIfpPl97xTUtTv dTvJLy9lMs8hqWPQDsFHYDsM005mRsvleq1WTPkM8huRQ2Rcd2KuxV2KuxV2KuxV2KuxV2KuxV2K uxV2KuxV2KuxV2KuxV2KonTdTvdMvI7yylMU8ZqGHQjuGHcHuMlCZibDkaXVZMGQTxmpB7X5Q832 nmG0JAEN9CB9Yt616/tp4qfw/XucGcZB5vqnY3bMNbD+bkjzH6R5fcyDMh3LsVdirsVdirsVdiqT +afMtpoGmtdTfHO9VtoK7u9P+IjucpzZhjjZdZ2r2nDR4uOW8j9I7z+rveGanqV5qd7Le3khknlN WJ6AdlA7AdhmknMyNl8l1WqyZ8hyTNyKGyLjuxV2KuxV2KuxV2KuxV2KuxV2KuxV2KuxV2KuxV2K uxV2KuxV2KuxVE6ZqV5pl7Fe2chjniNVI6Ed1I7g9xkoTMTYcjS6rJgyDJA1IPc/K3mW01/TVuoa JMlFuYK1KP8A80nsc3eHMMkbD612V2nDWYuOO0h9Q7j+ruTjLnZuxV2KuxV2KqF9e21jZzXl04jt 4FLyOfAfxPbIykIiy0588MUDOZqMRZeEeZ/MN1r2qSXk1ViHw28PZIwdh8+5zR5spnKy+RdqdpT1 eYzly6DuH45pTlTrXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYqm3 ljzDdaDqkd5DVoj8NxD2eMncfPuMtw5TCVh2XZfaU9JmE48uo7x+OT3exvba+s4by1cSW86h43Hg f4jvm8jISFh9dwZ4ZYCcDcZCwr5JudirsVdiryn80vM5urwaLbP/AKPbHldEdGl7L8k/X8s1etzW eEdHzv2r7V8Sf5eB9Mfq85d3w+/3MBzAeOdirsVdirsVdirsVdirsVdirsVdirsVdirsVdirsVdi rsVdirsVdirsVdirsVdirPvyt8zm1vDoty/+j3J5WpPRZe6/J/1/PM/RZqPCer2Psp2r4c/y8z6Z fT5S7vj9/verZtH0R2KuxVJvN2vpoehz3lf37furVfGVgeP/AAP2j8spz5eCNur7Y7QGk08p/wAX KPvP6ubwWR3kdpHYs7kszHcknck5oiXx+UiTZ5lrFDsVdirsVdirsVdiqrbWl1dSiK2heeU9I41L t9ygnCIk8mzFhnkNQBkfIWvvNM1GyoLy1mti32fWjaOvy5AYZQI5imebS5cX1xlH3gj70PkWh2Ku xV2KuxV2KuxV2KuxV2KuxV2KuxV2KuxV2KuxVuN3jdZEYq6EMrDYgjcEYgpjIg2OYe9eUdfTXNDg vK/v1/dXS+Eqgcv+C+0Pnm9wZeONvsHY/aA1enjP+LlL3j9fNOcudo7FXkH5p64b3W10+Nq2+nji 3gZXoWP0Ci/fmo1uS5V3PmntXr/F1HhD6cf+6PP9XzYVmG8q7FXYq7FXYq7FXYqmOg6Ffa3qKWVm vxH4pJD9lEB3ZssxYjM0HO7P0GTVZRjh8T3Dve46DoGnaJYraWaU7yymnORv5mP+dM3eLEICg+s9 n9n4tLjEMY956n3ou9sbS+tZLW7iWaCUUdGG39h98lKIkKLk58EMsDCY4ol4t5z8mXXl+69SOsum yn9xP3U9eD0/a/Xmn1GnOM+T5Z232JPRzsb4jyP6D5/exvMZ0TsVdirsVdirsVdirsVdirsVdirs VdirsVdirsVdirNfys1w2Wttp8jUt9QHFfASpUqfpFV+7MzRZKlXe9V7Ka/wtR4R+nJ/uhy/V8nr +bd9LQmrahHp2mXV9JuttG0lPEgbL9J2yE58MSe5xtZqRgxSyH+EEvnm4nluJ5J5m5yzO0kjHuzG pP35oCbNvi2TIZyMpcybPxWYGDsVdirsVdirsVdir2v8utATS/L8Uzr/AKXfgTzN3Ckfu1+hTX5k 5udJi4YX1L6r7N9njT6YSP15PUfd0Hy+0spzKegdiqje2Vre2slrdRiW3lHGSNuhGRlESFFqz4IZ YGExcTzDxXzn5MuvL916kdZdNlP7ifup68Hp+1+vNPqNOcZ8nyztvsSejnY3xHkf0Hz+9jeYzonY q7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYqvt55beeOeFuEsLrJGw7MpqD9+EGjbPHkMJCUeYNj 4PobSdQj1HTLW+j2W5jWSngSN1+g7Zv4T4og977To9SM+KOQfxAFiX5s6n9X0GGyU0e9l+IeMcXx H/hiuYuunUK73nPa/VcGnGMc5y+yO/308jzUvmzsVdirsVdirsVdiqvp9sLm/trY9J5Uj/4Ngv8A HJQFkBu0+PxMkYfzpAfMvoxVVVCqKKooAOgAzoX24AAUG8UuxV2KqN7ZWt7ayWt1GJbeUcZI26EZ GURIUWrPghlgYTFxPMPDPOHl06DrT2asXt3US27t1KMSKH3BBGaTUYuCVPkvbPZv5POYA3E7x9yS ZS6l2KuxV2KuxV2KuxV2KuxV2KuxV2KuxV2KuxV2KvXPym1P6xoM1kxq9lL8I8I5fiH/AAwbNtoZ 3Cu59J9kNVx6c4zzhL7Jb/fbGfzZvjN5iitQfgtIFBH+XISx/wCF45ja6VzrudD7X5+LUiHSEftO /wB1MJzCeUdirsVdirsVdirsVTLyyQPMmlEmgF5b1P8Az1XLMP1j3hzuy/8AGsX/AAyH+6D6Czfv s7sVdirsVdirx381NRt7rzIsMLBvqcIilYbjmWLEfRyGajWzBnQ6PmXtZqY5NVwx/gjR9/NhuYby 7sVdirsVdirsVdirsVdirsVdirsVdirsVdirsVZt+U18YfMUtqT8F3AwA/y4yGH/AAvLM3Qyqdd7 1fshn4dSYdJx+0b/AHWkfnW6+s+a9TkrXjO0X/Iqkf8AxrlGoleQup7cy+JrMh/pV/pdv0JLlLqn Yq7FXYq7FXYq7FVW0uGtruG4X7UMiyD5qQf4YYmjbZhycExIfwkH5Po2KRJY0kQ8kcBlPiCKjOhB t9vjISAI5FdhZOxV2KsE8/efl05X0vS3BvyOM867iEHsP8v9XzzB1Wq4fTHm8j7Qe0Hg3hwn951P 839v3e95MzMzFmJLE1JO5JOap85JvcuxQ7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FU68lXX 1bzXpklacp1i/wCRtY/+Nsu08qyB2vYeXw9ZjP8ASr/TbfpS3UZjNqF1Mesksjn/AGTE5XM2S4Op nxZZS75E/ah8i0OxV2KuxV2KuxV2KuxV7f8Al5q41HyxbBmrPaD6tKO/wfYP0pTN1pMnFAeT6x7O azx9JH+dD0n4cvspkuZLvXYq7FXg/nLQJdF12e3NWt5SZbaRqksjHuT1KnY5o9Ri4JU+QdtdnnS6 iUf4TvE+R/VySPKHUuxV2KuxV2KuxV2KuxV2KuxV2KuxV2KuxV2KuxV2KojTpjDqFrMOscsbj/Ys DkoGiG/TT4csZd0gftQ+RaHYq7FXYq7FXYq7FXYq7FWW/lv5iXStb+rztxs7+kTk9FkB/dt95p9O Zeky8MqPIvSezPaX5fUcMj6Mm3x6H9Hxez5uH1F2KuxVjvnjywuvaQUiA+vW1ZLVj3P7SfJgPvpm PqcPHHzDpO3ey/zeCh/eR3j+r4/fTw6SN43aORSjoSrqwoQRsQRmkIfJpRMTR2IaxQ7FXYq7FXYq 7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq9m/LzzYNZ036pcvXUrNQJ CTvJH0V/n2b+3NxpM/HGjzD6j7Odr/mcXBM/vYfaO/8AX+1l2Zb0jsVdirzz8xvI7XPPWtMjrcAV vLdergD+8UfzDuO/z66/V6a/VF4r2l7COS8+Ier+Id/mPPv7/e8uzWPn7sVdirsVdirsVdirsVdi rsVdirsVdirsVdirsVdiqI1GEw6hdQnrHLIh/wBixGSmKJb9TDhyyj3SI+1D5FodirsVdirsVdir sVdirsVRWlapeaXfw31o/CeE1HgR3Vh3BGxyUJmJsOTpNVPT5BkgalF7t5c8wWWu6al7bGjfZnhJ +KN6bqf4HN5iyicbD652b2jj1eIZIfEdxTTLXYOxV2KvPPPP5cm5eTVNGQCdviuLMbBz3aP/ACvF e/z66/U6S/VH5PFdvezfGTmwD1dY9/mPPyeXujxuyOpR1JDKwoQR1BBzWEPASiQaOxaxQ4AkgAVJ 6DFWX6Z+V3mW8gE0vpWYYVWOdm5/Sqq1PpzLhopkXyem0vspqsseI8MP63P5AFKfMHlHW9BKm9iD QOaJcxEtGT4VoCD8xlWXBKHN1vaPY+o0n94PSf4huPx70myl1bsVdirsVdirsVdirsVdirsVRGnQ mbULWEdZJY0H+yYDJQFkN+mhxZYx75AfamXnW1+rea9TjpTlO0v/ACNpJ/xtlmojWQud25i8PWZB /Sv/AE2/6Ulyl1TsVdirsVdirsVdirsVdirsVTbyz5kvdA1Fbq3PKJqLcwVosieHsR2OW4cxgbDs uy+08mjy8ceX8Q7x+OT3LSNXstWsIr6yfnDIOh2ZWHVWHYjN3jyCYsPrOj1mPUYxkxm4n8UUZk3K dirsVY55o8i6RrwMrD6tf0+G6jA3/wCMi7cv1++Y+bTRn5F0favYOHV+r6cn84fpHX7/ADeUa/5Q 1zRJKXcBeAmiXMVWjavTfsfY5qsuCUOb532h2NqNKfXG4/zhuP2fFnfkDyALIR6tq0dbw0a2tmH9 14Mw/n8B2+fTO0ulr1S5vXez3s94VZsw9f8ADH+b5nz+738p/me9ko3tla3trJa3UYlt5RxkjboR kZREhRas+CGWBhMXE8w8V85+TLry/depHWXTZT+4n7qevB6ftfrzT6jTnGfJ8s7b7Eno52N8R5H9 B8/vY3mM6J2KuxV2KuxV2KuxV2KuxVOvJVr9Z816ZHSvGdZf+RVZP+Ncu08byB2vYeLxNZjH9K/9 Lv8AoTz82bEw+YoroD4LuBST/lxkqf8AheOX66NTvvdt7X4OHUifScftG33UwnMJ5R2KuxV2KuxV 2KuxV2KuxV2KuxVO/Knmu+8v33qxVktZCBc2xOzDxHgw7HLsGc4z5O27J7WyaPJY3gfqj3/te26R rFhq9il7YyepC+x7MrDqrDsRm6x5BMWH1XR6zHqcYyYzcT9nkUZk3KdirsVcQCKHcYq7FXYq7FVG 9srW+tZbS6jEtvMvGRG6Ef59MjKIkKLVnwQywMJi4l4l5w8n3fl+7qKy6fKT9XuKfTwenRh+OabU ac4z5PlPbPY09HPvxnkf0Hz+9j2Y7pXYq7FXYq7FXYq7FXYqzb8prEzeYpboj4LSBiD/AJchCj/h eWZuhjc77nq/ZDBxakz6Qj9p2+62Tfmzpn1jQYb1RV7KX4j4Ry/Cf+GC5k66Fwvud97X6Xj04yDn CX2S2++nkeal82dirsVdirsVdirsVdirsVdirsVdiqa+XfMmpaDei4s3qjUE0DfYkXwI8fA5bizS gbDseze08ukycUDt1HQ/jvez+W/NOl6/a+raPxnQfvrVj8aH+I9xm4w5o5Bs+o9mdrYdZC4H1DnH qP2eacZc7N2KuxV2KuxV2KuxVD6hp9pqFnLZ3cYlt5hxdD+seBHY5GcRIUWnUaeGaBhMXEvnvUrM 2Wo3VmTyNtNJCW8fTYrX8M0E40SO58X1OHwssofzZEfI0h8i0OxV2KuxV2KuxV2KvXPym0z6voM1 6wo97L8J8Y4vhH/DFs22hhUL730n2Q0vBpzkPOcvsjt99st1bT49R0y6sZNluY2jr4EjZvoO+ZU4 cUSO96PWaYZ8UsZ/iBD55uIJbeeSCZeEsLtHIp7MpoR9+aAijT4tkxmEjGXMGj8FmBg7FXYq7FXY q7FXYq7FXYq7FXYq7FVayvbuyuUurSVobiM1SRDQjDGRibDbgzzxTE4HhkOr0/yv+aVpchLXW6W1 xsBdqP3Tf64/YP4fLNnh1oO0tnvuyvauE6hqPTL+d0Pv7vu9zPY5I5EWSNg6MKq6kEEHuCMzwXsY yEhYNhdil2KuxV2KuxVAa5rdjo2nyXt49EXaNP2nfsi+5yvJkEBZcPX67HpcRyTO33nuD5/vLqS7 u57qX+9uJGlf/Wdix/XmhlKzb43mynJOUzzkSfmpYGp2KuxV2KuxV2Kr7eCW4njghXnLM6xxqO7M aAffhAs0zx4zOQjHmTQ+L6G0nT49O0y1sY91to1jr4kDdvpO+b+EOGIHc+06PTDBijjH8IAReTcl 5B+aehmy1tdQjWlvqA5N4CVKBh9Io335qNbjqV975p7V6DwtR4o+nJ/uhz/X82FZhvKuxV2KuxV2 KuxV2KuxV2KuxV2KuxV2KuxVN9D82a5ojUsbgiGtWtpPjiP+xPT5rQ5bjzyhyLs9B2vqNKf3cvT/ ADTuPx7qegaN+bWlT8Y9Ugezk7ypWSL5kD4x9xzYY9dE/Vs9lova/DPbNEwPeNx+v72Yafrmj6go NleQzk/so4LD5r9ofSMy45Iy5F6XT6/Bm/u5xl8d/lzRuTct2Ksb1/z95f0hWT1hd3Y6W0BDb/5b j4V/X7ZjZdVCHmXRdoe0Om0wq+Of82P6TyH3+TyLzD5i1HXb43V4/wAI2hgWvCNfBR+s981WXKZm y+b9o9pZdXk45n3DoEsyp17sVdirsVdirsVdirNfys0I3uttqMi/6PYDkp7GZxRR9Aq33Zm6LHcr 7nqvZTQeLqPFP04/90eX6/k9fzbPpbsVSbzdoCa5oc9nT9+v721bwlUHj/wX2T88pz4uONOr7Y7P Gr08ofxc4+8fr5PBZEeN2jdSroSrKdiCNiDmiIfH5RINHmGsUOxV2KuxV2KuxV2KuxV2KuxV2Kux V2KuxV2KuxVGwa5rduKQahcxDpRJpFFPoOTGSQ5EuXj1+eH05Jj3SP61t1rGr3a8bq+uLhenGWV3 H/DE4yySPMljl1ubIKnOUvfIlCZBxnYq7FXYq7FXYq7FXYq3GjyOsaKWdyFVRuSTsAMQExiSaHMv evKOgpomhwWdB67D1Lph3lYDl932R8s3uDFwRp9g7H7PGl08Yfxc5e8/ik5y52jsVdiryn80vLBt bwa1bJ/o9yeN0B0WXs3yf9fzzV63DR4h1fO/avsrw5/mID0y+ryl3/H7/ewHMB452KuxV2KuxV2K uxV2KuxV2KuxV2KuxV2KuxV2KuxV2KuxV2KuxV2KuxV2KuxV2Ks+/K3ywbq8OtXKf6PbHjag9Gl7 t8k/X8sz9Fhs8R6PY+ynZXiT/MTHpj9PnLv+H3+56tm0fRHYq7FXYqoX1lbX1nNZ3SCS3nUpIh8D /EdsjKIkKLTnwQywMJi4yFF4R5n8vXWg6pJZzVaI/FbzdnjJ2Pz7HNHmxGEqL5F2p2bPSZjCXLoe 8fjmlOVOtdirsVdirsVdirsVdirsVdirsVdirsVdirsVdirsVdirsVdirsVdirsVdiqbeWPL11r2 qR2cNViHxXE3ZIwdz8+wy3DiM5UHZdl9mz1eYQjy6nuH45Pd7GytrGzhs7ZBHBAoSNR4D+J75vIx ERQfXsGCGKAhAVGIoK+SbXYq7FXYq7FUn80+WrTX9Na1mokyVa2npUo//NJ7jKc2EZI0XWdq9mQ1 mLgltIfSe4/q73hmp6beaZey2V5GY54jRgehHZge4PY5pJwMTRfJdVpcmDIccxUghsi47sVdirsV dirsVdirsVdirsVdirsVdirsVdirsVdirsVdirsVdirsVROmabeanexWVnGZJ5TRQOgHdiewHc5K EDI0HI0ulyZ8gxwFyL3Pyt5atNA01bWGjzPRrmelC7/80jsM3eHCMcaD612V2ZDR4uCO8j9R7z+r uTjLnZuxV2KuxV2KuxV2Ksf83+ULTzDaAEiG+hB+r3FK9f2H8VP4frx8+AZB5um7Z7GhrYfzckeR /QfL7nimpaZe6ZeSWd7EYp4zQqehHYqe4PY5ppwMTRfK9VpcmDIYZBUghsi47sVdirsVdirsVdir sVdirsVdirsVdirsVdirsVdirsVdirsVROm6Ze6neR2dlEZZ5DQKOgHcsewHc5KEDI0HI0ulyZ8g hjFyL2vyh5Qs/L1oQCJr6YD6xcU8P2E8FH45ucGAYx5vqnY3Y0NFD+dklzP6B5feyDMh3LsVdirs VdirsVdirsVdiqT+ZfK2m6/aejdLwmQH0LlR8aE/rXxGU5sMcgous7T7KxayHDPaQ5S6j9nk8Z8w +WNU0G69G8jrEx/c3C1Mbj2Pj7HNPlwygaL5f2l2Xm0k+GY26Hofx3JTlTrXYq7FXYq7FXYq7FXY q7FXYq7FXYq7FXYq7FXYq7FXYqm3l7yxqmvXXo2cdIlP764aojQe58fYZbiwymaDsuzey82rnwwG 3U9B+O57N5a8raboFp6NqvOZwPXuWHxuR+pfAZuMOGOMUH1DszsrFo4cMN5HnLqf2eScZc7N2Kux V2KuxV2KuxV2KuxV2KuxVQvbGzvrZ7W8hWe3kFHjcVH9h98jKIkKLTnwQywMJgSieheYeZ/ytvLU vc6KTc2/U2rf3qj/ACT+2Px+ea3NoiN47vA9q+yk8dz0/qj/ADf4h7u/7/ewJ43jdkkUo6mjKwoQ R2IOYBDx8okGjsWsUOxV2KuxV2KuxV2KuxV2KuxV2KuxV2KuxVtI3kdUjUu7GiqoqST2AGICYxJN Dcs98sflbeXRS51om2t+otV/vWH+Uf2B+PyzPw6IneWz2HZXspPJU9R6Y/zf4j7+77/c9PsrGzsb ZLWzhWC3jFEjQUH9p982UYiIoPfYMEMUBCAEYjoFfJNzsVdirsVdirsVdirsVdirsVdirsVdirsV SbX/ACjoeuIfrkHGf9m6ioso/wBlT4vk1cpy4Iz5ur7Q7H0+rHrj6v5w2P7fi851z8rNbsi0mnsN Qtx+yvwSge6k0P8AsT9Ga7JopDlu8Rr/AGU1GLfF+8j8pfL9XyYbPbz28rQzxvDKmzRyKVYfMHfM Qgjm8xkxygeGQIPcdlmBg7FXYq7FXYq7FXYq7FXYq7FV8FvPcSrDBG80r7LHGpZj8gN8IBPJnjxy meGIJPcN2ZaH+Vmt3pWTUGGn25/Zb45SPZQaD/ZH6My8eikeez0+g9lNRl3y/u4/OXy/X8no2g+U dD0RB9TgDT0o11JRpT/sqbfJaZscWCMOT2/Z/Y+n0o9EfV/OO5/HuTnLnaOxV2KuxV2KuxV2KuxV 2KuxV2KuxV2KuxV2KuxV2KuxVCahpOmajH6d9ax3Kjp6igkf6p6j6MhOEZcxbjanR4s4rJES94Yl qf5TaDcVaymlsnPRf72MfQ1G/wCGzFnoYHls85qvZDTz3xmUD/ph9u/2sZvvym8xQkm1lgu07AMY 3+5hx/4bMaWhmOW7oc/shqY/QYzHyP27fakd15K8121fU0ydqf76X1f+TZbKJafIOjqcvYesx88c vh6vutLZtO1CE0mtZYz4PGy/rGVmBHRwZ6bLH6oyHvBQ+RaHYqiIdO1CY0htZZD4JGzfqGSECejf DTZZfTGR9wKZWvkrzXc09PTJ1r/v1fS/5OFcsjp8h6Odi7D1mTljl8fT99J5Y/lN5imIN1LBaJ3B YyP9yjj/AMNl8dDM89nbYPZDUy+sxgPmfs2+1k2mflNoNvRr2aW9cdV/uoz9C1b/AIbMmGhgOe7v tL7IaeG+Qymf9KPs3+1lun6TpmnR+nY2sdsp6+moBP8ArHqfpzKhCMeQp6PTaPFgFY4iPuCLybku xV2KuxV2KuxV2KuxV2KuxV//2Q== + + + + proof:pdf + uuid:65E6390686CF11DBA6E2D887CEACB407 + xmp.did:164fbb93-217c-5644-88cc-411063b050b8 + uuid:90b3987f-7005-4d8a-aa99-71e554ff0d9d + + uuid:1abccb90-0c26-4942-b156-fd2eb962e3e1 + xmp.did:58fdc1b8-1448-3a44-9e20-282d8ec1cf95 + uuid:65E6390686CF11DBA6E2D887CEACB407 + proof:pdf + + + + + saved + xmp.iid:164fbb93-217c-5644-88cc-411063b050b8 + 2018-08-31T09:42:09+03:00 + Adobe Illustrator CC 2014 (Windows) + / + + + + Web + 1 + False + False + + 68.000000 + 68.000000 + Pixels + + + + Cyan + Yellow + + + + + + Default Swatch Group + 0 + + + + White + RGB + PROCESS + 255 + 255 + 255 + + + Black + RGB + PROCESS + 0 + 0 + 0 + + + RGB Red + RGB + PROCESS + 255 + 0 + 0 + + + RGB Yellow + RGB + PROCESS + 255 + 255 + 0 + + + RGB Green + RGB + PROCESS + 0 + 255 + 0 + + + RGB Cyan + RGB + PROCESS + 0 + 255 + 255 + + + RGB Blue + RGB + PROCESS + 0 + 0 + 255 + + + RGB Magenta + RGB + PROCESS + 255 + 0 + 255 + + + R=193 G=39 B=45 + RGB + PROCESS + 193 + 39 + 45 + + + R=237 G=28 B=36 + RGB + PROCESS + 237 + 28 + 36 + + + R=241 G=90 B=36 + RGB + PROCESS + 241 + 90 + 36 + + + R=247 G=147 B=30 + RGB + PROCESS + 247 + 147 + 30 + + + R=251 G=176 B=59 + RGB + PROCESS + 251 + 176 + 59 + + + R=252 G=238 B=33 + RGB + PROCESS + 252 + 238 + 33 + + + R=217 G=224 B=33 + RGB + PROCESS + 217 + 224 + 33 + + + R=140 G=198 B=63 + RGB + PROCESS + 140 + 198 + 63 + + + R=57 G=181 B=74 + RGB + PROCESS + 57 + 181 + 74 + + + R=0 G=146 B=69 + RGB + PROCESS + 0 + 146 + 69 + + + R=0 G=104 B=55 + RGB + PROCESS + 0 + 104 + 55 + + + R=34 G=181 B=115 + RGB + PROCESS + 34 + 181 + 115 + + + R=0 G=169 B=157 + RGB + PROCESS + 0 + 169 + 157 + + + R=41 G=171 B=226 + RGB + PROCESS + 41 + 171 + 226 + + + R=0 G=113 B=188 + RGB + PROCESS + 0 + 113 + 188 + + + R=46 G=49 B=146 + RGB + PROCESS + 46 + 49 + 146 + + + R=27 G=20 B=100 + RGB + PROCESS + 27 + 20 + 100 + + + R=102 G=45 B=145 + RGB + PROCESS + 102 + 45 + 145 + + + R=147 G=39 B=143 + RGB + PROCESS + 147 + 39 + 143 + + + R=158 G=0 B=93 + RGB + PROCESS + 158 + 0 + 93 + + + R=212 G=20 B=90 + RGB + PROCESS + 212 + 20 + 90 + + + R=237 G=30 B=121 + RGB + PROCESS + 237 + 30 + 121 + + + R=199 G=178 B=153 + RGB + PROCESS + 199 + 178 + 153 + + + R=153 G=134 B=117 + RGB + PROCESS + 153 + 134 + 117 + + + R=115 G=99 B=87 + RGB + PROCESS + 115 + 99 + 87 + + + R=83 G=71 B=65 + RGB + PROCESS + 83 + 71 + 65 + + + R=198 G=156 B=109 + RGB + PROCESS + 198 + 156 + 109 + + + R=166 G=124 B=82 + RGB + PROCESS + 166 + 124 + 82 + + + R=140 G=98 B=57 + RGB + PROCESS + 140 + 98 + 57 + + + R=117 G=76 B=36 + RGB + PROCESS + 117 + 76 + 36 + + + R=96 G=56 B=19 + RGB + PROCESS + 96 + 56 + 19 + + + R=66 G=33 B=11 + RGB + PROCESS + 66 + 33 + 11 + + + + + + Grays + 1 + + + + R=0 G=0 B=0 + RGB + PROCESS + 0 + 0 + 0 + + + R=26 G=26 B=26 + RGB + PROCESS + 26 + 26 + 26 + + + R=51 G=51 B=51 + RGB + PROCESS + 51 + 51 + 51 + + + R=77 G=77 B=77 + RGB + PROCESS + 77 + 77 + 77 + + + R=102 G=102 B=102 + RGB + PROCESS + 102 + 102 + 102 + + + R=128 G=128 B=128 + RGB + PROCESS + 128 + 128 + 128 + + + R=153 G=153 B=153 + RGB + PROCESS + 153 + 153 + 153 + + + R=179 G=179 B=179 + RGB + PROCESS + 179 + 179 + 179 + + + R=204 G=204 B=204 + RGB + PROCESS + 204 + 204 + 204 + + + R=230 G=230 B=230 + RGB + PROCESS + 230 + 230 + 230 + + + R=242 G=242 B=242 + RGB + PROCESS + 242 + 242 + 242 + + + + + + Web Color Group + 1 + + + + R=63 G=169 B=245 + RGB + PROCESS + 63 + 169 + 245 + + + R=122 G=201 B=67 + RGB + PROCESS + 122 + 201 + 67 + + + R=255 G=147 B=30 + RGB + PROCESS + 255 + 147 + 30 + + + R=255 G=29 B=37 + RGB + PROCESS + 255 + 29 + 37 + + + R=255 G=123 B=172 + RGB + PROCESS + 255 + 123 + 172 + + + R=189 G=204 B=212 + RGB + PROCESS + 189 + 204 + 212 + + + + + + + Adobe PDF library 11.00 + + + + + + + + + + + + + + + + + + + + + + + + + +endstream endobj 3 0 obj <> endobj 7 0 obj <>/Resources<>/Properties<>>>/Thumb 11 0 R/TrimBox[0.0 0.0 68.0 68.0]/Type/Page>> endobj 8 0 obj <>stream +HTn1 ynb;XUbTǞܨ~ؙxk[-Kb[}.i22v/|.=¾z7 <3 +Q/dF^Fxz}{)_|L]^֙e~ p'cj$HZ&;p*4hy9 @RUI9=XUiFn# ׍FV*AL$ߝ"QE +HdcLh|眅zqQR<>stream +8;SUL5n*fZ%)R,%C]4dnc?6`a66X]YOnkGrd1fKjkp#6)mlg;\I0'fW>-\fb~> +endstream endobj 12 0 obj [/Indexed/DeviceRGB 255 13 0 R] endobj 13 0 obj <>stream +8;X]O>EqN@%''O_@%e@?J;%+8(9e>X=MR6S?i^YgA3=].HDXF.R$lIL@"pJ+EP(%0 +b]6ajmNZn*!='OQZeQ^Y*,=]?C.B+\Ulg9dhD*"iC[;*=3`oP1[!S^)?1)IZ4dup` +E1r!/,*0[*9.aFIR2&b-C#soRZ7Dl%MLY\.?d>Mn +6%Q2oYfNRF$$+ON<+]RUJmC0InDZ4OTs0S!saG>GGKUlQ*Q?45:CI&4J'_2j$XKrcYp0n+Xl_nU*O( +l[$6Nn+Z_Nq0]s7hs]`XX1nZ8&94a\~> +endstream endobj 5 0 obj <> endobj 14 0 obj [/View/Design] endobj 15 0 obj <>>> endobj 10 0 obj <> endobj 9 0 obj <> endobj 16 0 obj <> endobj 17 0 obj <>stream +%!PS-Adobe-3.0 +%%Creator: Adobe Illustrator(R) 17.0 +%%AI8_CreatorVersion: 18.0.0 +%%For: (Desi Karavelikova) () +%%Title: (Untitled-1) +%%CreationDate: 8/31/2018 9:42 AM +%%Canvassize: 16383 +%%BoundingBox: -1 -68 68 0 +%%HiResBoundingBox: -0.000000005524271 -68 67.9999999944757 0 +%%DocumentProcessColors: Cyan Yellow +%AI5_FileFormat 13.0 +%AI12_BuildNumber: 18 +%AI3_ColorUsage: Color +%AI7_ImageSettings: 0 +%%RGBProcessColor: 0 0 0 ([Registration]) +%AI3_Cropmarks: 0 -68 68 0 +%AI3_TemplateBox: 34.5 -34.5 34.5 -34.5 +%AI3_TileBox: -248 -412 322 338 +%AI3_DocumentPreview: None +%AI5_ArtSize: 14400 14400 +%AI5_RulerUnits: 6 +%AI9_ColorModel: 1 +%AI5_ArtFlags: 0 0 0 1 0 0 1 0 0 +%AI5_TargetResolution: 800 +%AI5_NumLayers: 1 +%AI9_OpenToView: -30 2 12.95 1667 923 18 0 0 5 112 0 0 0 1 1 0 1 1 0 1 +%AI5_OpenViewLayers: 7 +%%PageOrigin:-366 -334 +%AI7_GridSettings: 72 8 72 8 1 0 0.800000011920929 0.800000011920929 0.800000011920929 0.899999976158142 0.899999976158142 0.899999976158142 +%AI9_Flatten: 1 +%AI12_CMSettings: 00.MS +%%EndComments + +endstream endobj 18 0 obj <>stream +%%BoundingBox: -1 -68 68 0 +%%HiResBoundingBox: -0.000000005524271 -68 67.9999999944757 0 +%AI7_Thumbnail: 128 128 8 +%%BeginData: 27619 Hex Bytes +%0000330000660000990000CC0033000033330033660033990033CC0033FF +%0066000066330066660066990066CC0066FF009900009933009966009999 +%0099CC0099FF00CC0000CC3300CC6600CC9900CCCC00CCFF00FF3300FF66 +%00FF9900FFCC3300003300333300663300993300CC3300FF333300333333 +%3333663333993333CC3333FF3366003366333366663366993366CC3366FF +%3399003399333399663399993399CC3399FF33CC0033CC3333CC6633CC99 +%33CCCC33CCFF33FF0033FF3333FF6633FF9933FFCC33FFFF660000660033 +%6600666600996600CC6600FF6633006633336633666633996633CC6633FF +%6666006666336666666666996666CC6666FF669900669933669966669999 +%6699CC6699FF66CC0066CC3366CC6666CC9966CCCC66CCFF66FF0066FF33 +%66FF6666FF9966FFCC66FFFF9900009900339900669900999900CC9900FF +%9933009933339933669933999933CC9933FF996600996633996666996699 +%9966CC9966FF9999009999339999669999999999CC9999FF99CC0099CC33 +%99CC6699CC9999CCCC99CCFF99FF0099FF3399FF6699FF9999FFCC99FFFF +%CC0000CC0033CC0066CC0099CC00CCCC00FFCC3300CC3333CC3366CC3399 +%CC33CCCC33FFCC6600CC6633CC6666CC6699CC66CCCC66FFCC9900CC9933 +%CC9966CC9999CC99CCCC99FFCCCC00CCCC33CCCC66CCCC99CCCCCCCCCCFF +%CCFF00CCFF33CCFF66CCFF99CCFFCCCCFFFFFF0033FF0066FF0099FF00CC +%FF3300FF3333FF3366FF3399FF33CCFF33FFFF6600FF6633FF6666FF6699 +%FF66CCFF66FFFF9900FF9933FF9966FF9999FF99CCFF99FFFFCC00FFCC33 +%FFCC66FFCC99FFCCCCFFCCFFFFFF33FFFF66FFFF99FFFFCC110000001100 +%000011111111220000002200000022222222440000004400000044444444 +%550000005500000055555555770000007700000077777777880000008800 +%000088888888AA000000AA000000AAAAAAAABB000000BB000000BBBBBBBB +%DD000000DD000000DDDDDDDDEE000000EE000000EEEEEEEE0000000000FF +%00FF0000FFFFFF0000FF00FFFFFF00FFFFFF +%524C45FD35FFAE8A65654141181D181D181D181D18413A65658A8AAFAFFD +%64FFA88A3B40171D1717171D171D171D171D171D171D171D1718171D171D +%186583AEAFFD5CFF8A65181D171D171D171D181D171D181D171D181D171D +%181D171D181D171D181D171D18405F8AAFFD56FF83651718171D1718171D +%1718171D1718171D1718171D1718171D1718171D1718171D1718171D1717 +%171D3A8AA8FD50FFAE65181D171D181D181D181D181D181D181D181D181D +%181D181D181D181D181D181D181D181D181D181D181D181D174065AFFD4C +%FF8A3A1D171D171D171D171D171D171D171D171D171D171D171D171D171D +%171D171D171D171D171D171D171D171D171D171D171D174083FD48FF8941 +%171D171D181D171D181D171D181D171D181D171D181D171D181D171D181D +%171D181D171D181D171D181D171D181D171D181D171D171D188AFD44FF8A +%1817171D1718171D1718171D1718171D1718171D1718171D1718171D1718 +%171D1718171D1718171D1718171D1718171D1718171D1718171D17181740 +%83FD40FFAE41171D181D181D181D181D181D181D181D181D181D181D181D +%181D181D181D181D181D181D181D181D181D181D181D181D181D181D181D +%181D181D181D181D3AAFFD3DFF5F1D171D171D171D171D171D171D171D17 +%1D171D171D171D171D171D171D171D171D171D171D171D171D171D171D17 +%1D171D171D171D171D171D171D171D171D17181765AEFD39FF8A181D181D +%171D181D171D181D171D181D171D181D171D181D171D181D171D181D171D +%181D171D181D171D181D171D181D171D181D171D181D171D181D171D181D +%171D181D17408AFD36FFA8651718171D1718171D1718171D1718171D1718 +%171D1718171D1718171D1718171D1718171D1718171D1718171D1718171D +%1718171D1718171D1718171D1718171D1718171D1718171D3AFD34FF8A41 +%171D181D181D181D181D181D181D181D181D181D181D181D181D181D181D +%181D181D181D181D181D181D181D181D181D181D181D181D181D181D181D +%181D181D181D181D181D181D181D18AFFD31FF5F1D171D171D171D171D17 +%1D171D171D171D171D171D171D171D171D171D171D171D171D171D171D17 +%1D171D171D171D171D171D171D171D171D171D171D171D171D171D171D17 +%1D171D171D1718178AFD2FFF3B1D181D171D181D171D181D171D181D171D +%181D171D181D171D181D171D181D171D181D171D181D171D181D171D181D +%171D181D171D181D171D181D171D181D171D181D171D181D171D181D171D +%181D1765FD2DFF3A17171D1718171D1718171D1718171D1718171D171817 +%1D1718171D1718171D1718171D1718171D1718171D1718171D1718171D17 +%18171D1718171D1718171D1718171D1718171D1718171D1718171D171817 +%41AEFD2AFF181D181D181D181D181D181D181D181D181D181D181D181D18 +%1D181D181D181D181D181D181D181D181D181D181D181D181D181D181D18 +%1D181D181D181D181D181D181D181D181D181D181D181D181D181D181D17 +%40AEFD27FFAF181D171D171D171D171D171D171D171D171D171D171D171D +%171D171D171D171D171D171D171D171D171D171D171D171D171D171D171D +%171D171D171D171D171D171D171D171D171D171D171D171D171D171D171D +%171D171DAEFD25FFAF181D181D181D171D181D171D181D171D181D171D18 +%1D171D181D171D181D171D181D171D181D171D181D171D181D171D181D17 +%1D181D171D181D171D181D171D181D171D181D171D181D171D181D171D18 +%1D171D181D171D171DAEFD23FFAF171D1718171D1718171D1718171D1718 +%171D1718171D1718171D1718171D1718171D1718171D1718171D1718171D +%1718171D1718171D1718171D1718171D1718171D1718171D1718171D1718 +%171D1718171D1718171D1718171D1718A8FD22FF181D181D181D181D181D +%181D181D181D181D181D181D181D181D181D181D181D181D181D181D181D +%181D181D181D181D181D181D181D181D181D181D181D181D181D181D181D +%181D181D181D181D181D181D181D181D181D181D181D1740AEFD20FF1818 +%171D171D171D171D171D171D171D171D171D171D171D171D171D171D171D +%171D171D171D171D171D171D171D171D171D171D171D171D171D171D171D +%171D171D171D171D171D171D171D171D171D171D171D171D171D171D171D +%171D1740AEFD1EFF3A1D181D171D181D171D181D171D181D171D181D171D +%181D171D181D171D181D171D181D171D181D171D181D171D181D171D181D +%171D181D171D181D171D181D171D181D171D181D171D181D171D181D171D +%181D171D181D171D181D171D181D1741FD1DFF3A17171D1718171D171817 +%1D1718171D1718171D1718171D1718171D1718171D1718171D1718171D17 +%18171D1718171D1718171D1718171D1718171D1718171D1718171D171817 +%1D1718171D1718171D1718171D1718171D1718171D1718171D17181765FD +%1BFF891D181D181D181D181D181D181D181D181D181D181D181D181D181D +%181D181D181D181D181D181D181D181D181D181D181D181D181D181D181D +%181D181D181D181D181D181D181D181D181D181D181D181D181D181D181D +%181D181D181D181D181D181D178AFD19FF841D171D171D171D171D171D17 +%1D171D171D171D171D171D171D171D171D171D171D171D171D171D171D17 +%1D171D171D171D171D171D171D171D171D171D171D171D171D171D171D17 +%1D171D171D171D171D171D171D171D171D171D171D171D171D171D171D17 +%AEFD18FF40171D181D171D181D171D181D171D181D171D181D171D181D17 +%1D181D171D181D171D181D171D181D171D181D171D181D171D181D171D18 +%1D171D181D171D181D171D181D171D181D171D181D171D181D171D181D17 +%1D181D171D181D171D181D171D181D181D18FD17FF651718171D1718171D +%1718171D1718171D1718171D1718171D1718171D1718171D1718171D1718 +%171D1718171D1718171D1718171D1718171D1718171D1718171D1718171D +%1718171D1718171D1718171D1718171D1718171D1718171D1718171D1718 +%171D1718171D3AFD15FFAE171D181D181D181D181D181D181D181D181D18 +%1D181D181D181D181D181D181D181D181D181D181D181D181D181D181D18 +%1D181D181D181D181D181D181D181D181D181D181D181D181D181D181D18 +%1D181D181D181D181D181D181D181D181D181D181D181D181D181D8AFD14 +%FF181D171D171D171D171D171D171D171D171D171D171D171D171D171D17 +%1D171D171D171D171D171D171D171D171D171D171D171D171D171D171D17 +%1D171D171D171D171D171D171D171D171D171D171D171D171D171D171D17 +%1D171D171D171D171D171D171D171D171D171DAEFD12FF651D171D181D17 +%1D181D171D181D171D181D171D181D171D181D171D181D171D181D171D18 +%1D171D181D171D181D171D181D171D181D171D181D171D181D171D181D17 +%1D181D171D181D171D181D171D181D171D181D171D181D171D181D171D18 +%1D171D181D171D181D171D1765FD11FF841D1718171D1718171D1718171D +%1718171D1718171D1718171D1718171D1718171D1718171D1718171D1718 +%171D1718171D1718171D1718171D1718171D1718171D1718171D1718171D +%1718171D1718171D1718171D1718171D1718171D1718171D1718171D1718 +%171D1718171D17AEFD10FF41171D181D181D181D181D181D181D181D181D +%181D181D181D181D181D181D181D181D181D181D181D181D181D181D181D +%181D181D181D181D181D181D181D181D181D181D181D181D181D181D181D +%181D181D181D181D181D181D181D181D181D181D181D181D181D181D181D +%181D3AFD0FFF8A171D171D171D171D171D171D171D171D171D171D171D17 +%1D171D171D171D171D171D171D171D171D171D171D171D171D171D171D17 +%1D171D171D171D171D171D171D171D171D171D171D171D171D171D171D17 +%1D171D171D171D171D171D171D171D171D171D171D171D171D171D171883 +%FD0EFF3A1D171D181D171D181D171D181D171D181D171D181D171D181D17 +%1D181D171D181D171D171D171D181D171D181D171D181D171D181D171D18 +%1D171D181D171D181D171D181D171D181D171D181D171D181D171D181D17 +%1D181D171D181D171D181D171D181D171D181D171D181D171D1740FD0DFF +%5F1D1718171D1718171D1718171D1718171D1718171D1718171D1718171D +%1718171D171818655F65181D1718171D1718171D1718171D1718171D1718 +%171D1718171D1718171D1718171D1718171D1718171D1718171D1718171D +%1718171D1718171D1718171D1718171D1718171D1718171D178AFD0CFF41 +%181D181D181D181D181D181D181D181D181D181D181D181D181D181D181D +%181D181D65FD05FF651D181D181D181D181D181D181D181D181D181D181D +%181D181D181D181D181D181D181D181D181D181D181D181D181D181D181D +%181D181D181D181D181D181D181D181D181D181D181D3AFD0BFF8A171D17 +%1D171D171D171D171D171D171D171D171D171D171D171D171D171D171D17 +%1D5FFD07FF3B17171D171D171D171D171D171D171D171D171D171D171D17 +%1D171D171D171D171D171D171D171D171D171D171D171D171D171D171D17 +%1D171D171D171D171D171D171D171D171D171883FD0AFF3A1D171D181D17 +%1D181D171D181D171D181D171D181D171D181D171D181D171D181D5FFD09 +%FF651D171D181D171D181D171D181D171D181D171D181D171D181D171D18 +%1D171D181D171D181D171D181D171D181D171D181D171D181D171D181D17 +%1D181D171D181D171D181D171D1741FD09FF841D1718171D1718171D1718 +%171D1718171D1718171D1718171D1718171D1718171D5FFD0BFF3A17171D +%1718171D1718171D1718171D1718171D1718171D1718171D1718171D1718 +%171D1718171D1718171D1718171D1718171D1718171D1718171D1718171D +%1718171D1718171D17AEFD08FF65171D181D181D181D181D181D181D181D +%181D181D181D181D181D181D181D181D40FD0DFF5F1D181D181D181D181D +%181D181D181D181D181D181D181D181D181D181D181D181D181D181D181D +%181D181D181D181D181D181D181D181D181D181D181D181D181D181D181D +%181D65FD08FF181D171D171D171D171D171D171D171D171D171D171D171D +%171D171D171D171D17AEFD0EFF3A1D171D171D171D171D171D171D171D17 +%1D171D171D171D171D171D171D171D171D171D171D171D171D171D171D17 +%1D171D171D171D171D171D171D171D171D171D171D171D171DAEFD06FF8A +%1D171D181D171D181D171D181D171D181D171D181D171D181D171D181D17 +%1D1741FD10FF3B1D181D171D181D171D181D171D181D171D181D171D181D +%171D181D171D181D171D181D171D181D171D181D171D181D171D181D171D +%181D171D181D171D181D171D181D171D178AFD06FF651718171D1718171D +%1718171D1718171D1718171D1718171D1718171D1718171D5FFD11FF3A1D +%1718171D1718171D1718171D1718171D1718171D1718171D1718171D1718 +%171D1718171D1718171D1718171D1718171D1718171D1718171D1718171D +%1718171D1718171D3AFD06FF181D181D181D181D181D181D181D181D181D +%181D181D181D181D181D181D181D18FD13FF651D181D181D181D181D181D +%181D181D181D181D181D181D181D181D181D181D181D181D181D181D181D +%181D181D181D181D181D181D181D181D181D181D181D181D181D1840AEFD +%04FF831D171D171D171D171D171D171D171D171D171D171D171D171D171D +%171D171D1741FD14FF3B18171D171D171D171D171D171D171D171D171D17 +%1D171D171D171D171D171D171D171D171D171D171D171D171D171D171D17 +%1D171D171D171D171D171D171D171D171D178AFD04FF65171D181D171D18 +%1D171D181D171D181D171D181D171D181D171D181D171D181D3BFD15FF3A +%1D171D181D171D181D171D181D171D181D171D181D171D181D171D181D17 +%1D181D171D181D171D181D171D181D171D181D171D181D171D181D171D18 +%1D171D181D171D65FD04FF1818171D1718171D1718171D1718171D171817 +%1D1718171D1718171D1718171D1765FD15FF8A1718171D1718171D171817 +%1D1718171D1718171D1718171D1718171D1718171D1718171D1718171D17 +%18171D1718171D1718171D1718171D1718171D1718171D17181740FFFFFF +%AE1D181D181D181D181D181D181D181D181D181D181D181D181D181D181D +%181D181D65FD15FF8A1D181D181D181D181D181D181D181D181D181D181D +%181D181D181D181D181D181D181D181D181D181D181D181D181D181D181D +%181D181D181D181D181D181D181D181D18AFFFFF8A171D171D171D171D17 +%1D171D171D171D171D171D171D171D171D171D171D171D1789FD15FF8A17 +%1D171D171D171D171D171D171D171D171D171D171D171D171D171D171D17 +%1D171D171D171D171D171D171D171D171D171D171D171D171D171D171D17 +%1D171D171D171D83FFFF5F1D181D171D181D171D181D171D181D171D181D +%171D181D171D181D171D181D171D65FD15FF5F1D181D171D181D171D181D +%171D181D171D181D171D181D171D181D171D181D171D181D171D181D171D +%181D171D181D171D181D171D181D171D181D171D181D171D181D1765FFFF +%40171D1718171D1718171D1718171D1718171D1718171D1718171D171817 +%1D17181765FD14FFAE18171D1718171D1718171D1718171D1718171D1718 +%171D1718171D1718171D1718171D1718171D1718171D1718171D1718171D +%1718171D1718171D1718171D1718171D171818FFFF181D181D181D181D18 +%1D181D181D181D181D181D181D181D181D181D181D181D181D65FD13FFAE +%40171D181D181D181D181D181D181D181D181D181D181D181D181D181D18 +%1D181D181D181D181D181D181D181D181D181D181D181D181D181D181D18 +%1D181D181D181D181D1740FFAE1D171D171D171D171D171D171D171D171D +%171D171D171D171D171D171D171D171D1741FD12FFAE40171D171D171D17 +%1D171D171D171D171D171D171D171D171D171D171D171D171D171D171D17 +%1D171D171D171D171D171D171D171D171D171D171D171D171D171D171D17 +%1D171D17AE8A171D171D181D171D181D171D181D171D181D171D181D171D +%181D171D181D171D181D18FD11FFAE40171D181D171D181D171D181D171D +%181D171D181D171D181D171D181D171D181D171D181D171D181D171D181D +%171D181D171D181D171D181D171D181D171D181D171D181D171D181D8A5F +%1D1718171D1718171D1718171D1718171D1718171D1718171D1718171D17 +%18171D171884FD0FFFA8401718171D1718171D1718171D1718171D171817 +%1D1718171D1718171D1718171D1718171D1718171D1718171D1718171D17 +%18171D1718171D1718171D1718171D1718171D1718171D176565171D181D +%181D181D181D181D181D181D181D181D181D181D181D181D181D181D181D +%178AFD0FFF41171D181D181D181D181D181D181D181D181D181D181D181D +%181D181D181D181D181D181D181D181D181D181D181D181D181D181D181D +%181D181D181D181D181D181D181D181D181D181D653A1D171D171D171D17 +%1D171D171D171D171D171D171D171D171D171D171D171D171D17183AFD0F +%FF3B1D171D171D171D171D171D171D171D171D171D171D171D171D171D17 +%1D171D171D171D171D171D171D171D171D171D171D171D171D171D171D17 +%1D171D171D171D171D171D171D171D174141171D181D171D181D171D181D +%171D181D171D181D171D181D171D181D171D181D171D181DAEFD0FFF181D +%181D171D181D171D181D171D181D171D181D171D181D171D181D171D181D +%171D181D171D181D171D181D171D181D171D181D171D181D171D181D171D +%181D171D181D171D181D171D3A1818171D1718171D1718171D1718171D17 +%18171D1718171D1718171D1718171D1718171D1765FD0FFF8A171D171817 +%1D1718171D1718171D1718171D1718171D1718171D1718171D1718171D17 +%18171D1718171D1718171D1718171D1718171D1718171D1718171D171817 +%1D1718171D171817401D181D181D181D181D181D181D181D181D181D181D +%181D181D181D181D181D181D181D181D18FD10FF41171D181D181D181D18 +%1D181D181D181D181D181D181D181D181D181D181D181D181D181D181D18 +%1D181D181D181D181D181D181D181D181D181D181D181D181D181D181D18 +%1D181D18171D171D171D171D171D171D171D171D171D171D171D171D171D +%171D171D171D171D171D17185FFD0FFF841D171D171D171D171D171D171D +%171D171D171D171D171D171D171D171D171D171D171D171D171D171D171D +%171D171D171D171D171D171D171D171D171D171D171D171D171D171D171D +%1D181D171D181D171D181D171D181D171D181D171D181D171D181D171D18 +%1D171D181D171D1840AEFD0FFF5F1D181D171D181D171D181D171D181D17 +%1D181D171D181D171D181D171D181D171D181D171D181D171D181D171D18 +%1D171D181D171D181D171D181D171D181D171D181D171D181D17171D1718 +%171D1718171D1718171D1718171D1718171D1718171D1718171D1718171D +%1718171D1765FD10FF181D1718171D1718171D1718171D1718171D171817 +%1D1718171D1718171D1718171D1718171D1718171D1718171D1718171D17 +%18171D1718171D1718171D1718171D1718171D17181D181D181D181D181D +%181D181D181D181D181D181D181D181D181D181D181D181D181D181D181D +%17AFFD10FF181D181D181D181D181D181D181D181D181D181D181D181D18 +%1D181D181D181D181D181D181D181D181D181D181D181D181D181D181D18 +%1D181D181D181D181D181D181D181D18171D171D171D171D171D171D171D +%171D171D171D171D171D171D171D171D171D171D171D171D171818FD10FF +%AE171D171D171D171D171D171D171D171D171D171D171D171D171D171D17 +%1D171D171D171D171D171D171D171D171D171D171D171D171D171D171D17 +%1D171D171D171D171D171D1D171D181D171D181D171D181D171D181D171D +%181D171D181D171D181D171D181D171D181D171D181D5FFD10FF8A171D18 +%1D181D171D181D171D181D171D181D171D181D171D181D171D181D171D18 +%1D171D181D171D181D171D181D171D181D171D181D171D181D171D181D17 +%1D181D171D181718171D1718171D1718171D1718171D1718171D1718171D +%1718171D1718171D1718171D1718171D171884FD10FF8A1718171D171817 +%1D1718171D1718171D1718171D1718171D1718171D1718171D1718171D17 +%18171D1718171D1718171D1718171D1718171D1718171D1718171D171817 +%1D40181D181D181D181D181D181D181D181D181D181D181D181D181D181D +%181D181D181D181D181D181D1740AFFD10FF8A171D181D181D181D181D18 +%1D181D181D181D181D181D181D181D181D181D181D181D181D181D181D18 +%1D181D181D181D181D181D181D181D181D181D181D181D181D183A1D171D +%171D171D171D171D171D171D171D171D171D171D171D171D171D171D171D +%171D171D171D171D1741FD11FF8A171D171D171D171D171D171D171D1718 +%171D171D171D171D171D171D171D171D171D171D171D171D171D171D171D +%171D171D171D171D171D171D171D171D171D174165171D171D181D171D18 +%1D171D181D171D181D171D181D171D181D171D181D171D181D171D181D17 +%1D181D1765FD11FFAE181D181D171D181D171D181D171D418A8A8A181D17 +%1D181D171D181D171D181D171D181D171D181D171D181D171D181D171D18 +%1D171D181D171D181D171D181D3A3A1D1718171D1718171D1718171D1718 +%171D1718171D1718171D1718171D1718171D1718171D1718171D1718178A +%FD11FFAF181D1718171D1718171D171884FD05FF3A17171D1718171D1718 +%171D1718171D1718171D1718171D1718171D1718171D1718171D1718171D +%1718171D17658A171D181D181D181D181D181D181D181D181D181D181D18 +%1D181D181D181D181D181D181D181D181D181D181D17AEFD12FF651D171D +%181D181D181DAEFD07FF5F1D181D181D181D181D181D181D181D181D181D +%181D181D181D181D181D181D181D181D181D181D181D181D65831D171D17 +%1D171D171D171D171D171D171D171D171D171D171D171D171D171D171D17 +%1D171D171D171D171D171D17AEFD12FF8A40171D171D171DAEFD09FF3A1D +%171D171D171D171D171D171D171D171D171D171D171D171D171D171D171D +%171D171D171D171D171D178AAE171D181D171D181D171D181D171D181D17 +%1D181D171D181D171D181D171D181D171D181D171D181D171D181D181D18 +%AEFD13FF8A181D171DAEFD0BFF3B1D181D171D181D171D181D171D181D17 +%1D181D171D181D171D181D171D181D171D181D171D181D171DAEAE1D171D +%1718171D1718171D1718171D1718171D1718171D1718171D1718171D1718 +%171D1718171D1718171D1718171D17AFFD13FFAF3A1D8AFD0DFF3A1D1718 +%171D1718171D1718171D1718171D1718171D1718171D1718171D1718171D +%1718171D171817FFFF3A1D181D181D181D181D181D181D181D181D181D18 +%1D181D181D181D181D181D181D181D181D181D181D181D181D181D18FD25 +%FF651D181D181D181D181D181D181D181D181D181D181D181D181D181D18 +%1D181D181D181D181D1741FFFF65171D171D171D171D171D171D171D171D +%171D171D171D171D171D171D171D171D171D171D171D171D171D171D171D +%171818AFFD24FF3A17171D171D171D171D171D171D171D171D171D171D17 +%1D171D171D171D171D171D171D17183BFFFF891D171D181D171D181D171D +%181D171D181D171D181D171D181D171D181D171D181D171D181D171D181D +%171D181D171D181D18AFFD24FF401D171D181D171D181D171D181D171D18 +%1D171D181D171D181D171D181D171D181D171D178AFFFFAE1718171D1718 +%171D1718171D1718171D1718171D1718171D1718171D1718171D1718171D +%1718171D1718171D1718171D171817AEFD24FF3A17171D1718171D171817 +%1D1718171D1718171D1718171D1718171D1718171D1718171DAEFFFFFF40 +%171D181D181D181D181D181D181D181D181D181D181D181D181D181D181D +%181D181D181D181D181D181D181D181D181D181D18AEFD24FF3A1D181D18 +%1D181D181D181D181D181D181D181D181D181D181D181D181D181D181D18 +%FD04FF3B18171D171D171D171D171D171D171D171D171D171D171D171D17 +%1D171D171D171D171D171D171D171D171D171D171D171D171D178AFD24FF +%181D171D171D171D171D171D171D171D171D171D171D171D171D171D171D +%171D1765FD04FF8A171D171D181D171D181D171D181D171D181D171D181D +%171D181D171D181D171D181D171D181D171D181D171D181D171D181D181D +%1765FD23FF65171D181D171D181D171D181D171D181D171D181D171D181D +%171D181D171D181D8AFD04FFA81D1718171D1718171D1718171D1718171D +%1718171D1718171D1718171D1718171D1718171D1718171D1718171D1718 +%171D1718171D1740A8FD21FF5F17171D1718171D1718171D1718171D1718 +%171D1718171D1718171D1718171D17AFFD05FF411D181D181D181D181D18 +%1D181D181D181D181D181D181D181D181D181D181D181D181D181D181D18 +%1D181D181D181D181D181D181D17408AFD20FF8A171D181D181D181D181D +%181D181D181D181D181D181D181D181D181D181D1765FD06FF8A171D171D +%171D171D171D171D171D171D171D171D171D171D171D171D171D171D171D +%171D171D171D171D171D171D171D171D171D171D171D3BFD1FFF3A1D171D +%171D171D171D171D171D171D171D171D171D171D171D171D171D171D89FD +%06FFAF40181D171D181D171D181D171D181D171D181D171D181D171D181D +%171D181D171D181D171D181D171D181D171D181D171D181D171D181D171D +%188AFD1CFF891D181D171D181D171D181D171D181D171D181D171D181D17 +%1D181D171D181D18FD08FF3B1D1718171D1718171D1718171D1718171D17 +%18171D1718171D1718171D1718171D1718171D1718171D1718171D171817 +%1D1718171D1718171D174184FD19FF8418171D1718171D1718171D171817 +%1D1718171D1718171D1718171D1718171D1765FD08FFAF181D181D181D18 +%1D181D181D181D181D181D181D181D181D181D181D181D181D181D181D18 +%1D181D181D181D181D181D181D181D181D181D181D171D65FD17FF8A1D17 +%1D181D181D181D181D181D181D181D181D181D181D181D181D181D181D18 +%1DAEFD09FF41171D171D171D171D171D171D171D171D171D171D171D171D +%171D171D171D171D171D171D171D171D171D171D171D171D171D171D171D +%171D171D171D176584FD13FF5F1D171D171D171D171D171D171D171D171D +%171D171D171D171D171D171D171D171D3AFD0AFF8A1D181D171D181D171D +%181D171D181D171D181D171D181D171D181D171D181D171D181D171D181D +%171D181D171D181D171D181D171D181D171D181D171D171D3A8AAFFD0EFF +%AF411D171D181D171D181D171D181D171D181D171D181D171D181D171D18 +%1D171D181D17AEFD0BFF181D1718171D1718171D1718171D1718171D1718 +%171D1718171D1718171D1718171D1718171D1718171D1718171D1718171D +%1718171D1718171D1718171D1718171D3A8984FD09FF8465171D1718171D +%1718171D1718171D1718171D1718171D1718171D1718171D1718171D1740 +%FD0CFF8A171D181D181D181D181D181D181D181D181D181D181D181D181D +%181D181D181D181D181D181D181D181D181D181D181D181D181D181D181D +%181D181D181D181D181D171D18415F8A898A6565181D171D181D181D181D +%181D181D181D181D181D181D181D181D181D181D181D181D181D181D89FD +%0DFF40171D171D171D171D171D171D171D171D171D171D171D171D171D17 +%1D171D171D171D171D171D171D171D171D171D171D171D171D171D171D17 +%1D171D171D171D171D171D1718171D1717171D171D171D171D171D171D17 +%1D171D171D171D171D171D171D171D171D171D171D171D171D18FD0EFF8A +%1D181D171D181D171D181D171D181D171D181D171D181D171D181D171D18 +%1D171D181D171D181D171D181D171D181D171D181D171D181D171D181D17 +%1D181D171D181D171D181D171D181D171D181D171D181D171D181D171D18 +%1D171D181D171D181D171D181D171D181D171D181D178AFD0FFF3A1D1718 +%171D1718171D1718171D1718171D1718171D1718171D1718171D1718171D +%1718171D1718171D1718171D1718171D1718171D1718171D1718171D1718 +%171D1718171D1718171D1718171D1718171D1718171D1718171D1718171D +%1718171D1718171D1718171D1718171D1740FD10FFAF181D181D181D181D +%181D181D181D181D181D181D181D181D181D181D181D181D181D181D181D +%181D181D181D181D181D181D181D181D181D181D181D181D181D181D181D +%181D181D181D181D181D181D181D181D181D181D181D181D181D181D181D +%181D181D181D181D181D181DAEFD11FF65171D171D171D171D171D171D17 +%1D171D171D171D171D171D171D171D171D171D171D171D171D171D171D17 +%1D171D171D171D171D171D171D171D171D171D171D171D171D171D171D17 +%1D171D171D171D171D171D171D171D171D171D171D171D171D171D171D17 +%1D171D171D5FFD12FFAF40181D171D181D171D181D171D181D171D181D17 +%1D181D171D181D171D181D171D181D171D181D171D181D171D181D171D18 +%1D171D181D171D181D171D181D171D181D171D181D171D181D171D181D17 +%1D181D171D181D171D181D171D181D171D181D171D181D171D181D18FD14 +%FF831D1718171D1718171D1718171D1718171D1718171D1718171D171817 +%1D1718171D1718171D1718171D1718171D1718171D1718171D1718171D17 +%18171D1718171D1718171D1718171D1718171D1718171D1718171D171817 +%1D1718171D1718171D1718171D1718171D178AFD15FF651D181D181D181D +%181D181D181D181D181D181D181D181D181D181D181D181D181D181D181D +%181D181D181D181D181D181D181D181D181D181D181D181D181D181D181D +%181D181D181D181D181D181D181D181D181D181D181D181D181D181D181D +%181D181D181D1765FD17FF1818171D171D171D171D171D171D171D171D17 +%1D171D171D171D171D171D171D171D171D171D171D171D171D171D171D17 +%1D171D171D171D171D171D171D171D171D171D171D171D171D171D171D17 +%1D171D171D171D171D171D171D171D171D171D171D171D1740AFFD17FFAF +%181D171D181D171D181D171D181D171D181D171D181D171D181D171D181D +%171D181D171D181D171D181D171D181D171D181D171D181D171D181D171D +%181D171D181D171D181D171D181D171D181D171D181D171D181D171D181D +%171D181D171D181D171D181D8AFD19FF8A1718171D1718171D1718171D17 +%18171D1718171D1718171D1718171D1718171D1718171D1718171D171817 +%1D1718171D1718171D1718171D1718171D1718171D1718171D1718171D17 +%18171D1718171D1718171D1718171D1718171D1718171D1718171D5FFD1B +%FF65171D181D181D181D181D181D181D181D181D181D181D181D181D181D +%181D181D181D181D181D181D181D181D181D181D181D181D181D181D181D +%181D181D181D181D181D181D181D181D181D181D181D181D181D181D181D +%181D181D181D181D181D65FD1DFF40171D171D171D171D171D171D171D17 +%1D171D171D171D171D171D171D171D171D171D171D171D171D171D171D17 +%1D171D171D171D171D171D171D171D171D171D171D171D171D171D171D17 +%1D171D171D171D171D171D171D171D171D171D17183AFD1FFF40171D181D +%171D181D171D181D171D181D171D181D171D181D171D181D171D181D171D +%181D171D181D171D181D171D181D171D181D171D181D171D181D171D181D +%171D181D171D181D171D181D171D181D171D181D171D181D171D181D181D +%18FD20FFAE1D171D1718171D1718171D1718171D1718171D1718171D1718 +%171D1718171D1718171D1718171D1718171D1718171D1718171D1718171D +%1718171D1718171D1718171D1718171D1718171D1718171D1718171D1718 +%171D1718171D171817AFFD21FFAE40171D181D181D181D181D181D181D18 +%1D181D181D181D181D181D181D181D181D181D181D181D181D181D181D18 +%1D181D181D181D181D181D181D181D181D181D181D181D181D181D181D18 +%1D181D181D181D181D181D181D181D18FD24FF841D171D171D171D171D17 +%1D171D171D171D171D171D171D171D171D171D171D171D171D171D171D17 +%1D171D171D171D171D171D171D171D171D171D171D171D171D171D171D17 +%1D171D171D171D171D171D171D171D171D171D18AEFD25FFAE40171D171D +%181D171D181D171D181D171D181D171D181D171D181D171D181D171D181D +%171D181D171D181D171D181D171D181D171D181D171D181D171D181D171D +%181D171D181D171D181D171D181D171D181D171D181D18FD28FFA8401718 +%171D1718171D1718171D1718171D1718171D1718171D1718171D1718171D +%1718171D1718171D1718171D1718171D1718171D1718171D1718171D1718 +%171D1718171D1718171D1718171D1718171D1718171D18AFFD2AFF41171D +%181D181D181D181D181D181D181D181D181D181D181D181D181D181D181D +%181D181D181D181D181D181D181D181D181D181D181D181D181D181D181D +%181D181D181D181D181D181D181D181D181D181D40FD2DFF65171D171D17 +%1D171D171D171D171D171D171D171D171D171D171D171D171D171D171D17 +%1D171D171D171D171D171D171D171D171D171D171D171D171D171D171D17 +%1D171D171D171D171D171D171D17173AFD2FFF8A171D181D171D181D171D +%181D171D181D171D181D171D181D171D181D171D181D171D181D171D181D +%171D181D171D181D171D181D171D181D171D181D171D181D171D181D171D +%181D171D181D171D89FD31FFAE181D1718171D1718171D1718171D171817 +%1D1718171D1718171D1718171D1718171D1718171D1718171D1718171D17 +%18171D1718171D1718171D1718171D1718171D1718171D1718171D174084 +%FD34FF651D171D181D181D181D181D181D181D181D181D181D181D181D18 +%1D181D181D181D181D181D181D181D181D181D181D181D181D181D181D18 +%1D181D181D181D181D181D181D181D1765FD37FF891D171D171D171D171D +%171D171D171D171D171D171D171D171D171D171D171D171D171D171D171D +%171D171D171D171D171D171D171D171D171D171D171D171D171D171D1717 +%188AFD3AFF65171D181D171D181D171D181D171D181D171D181D171D181D +%171D181D171D181D171D181D171D181D171D181D171D181D171D181D171D +%181D171D181D171D181D171D65FD3DFFAE3A1D1718171D1718171D171817 +%1D1718171D1718171D1718171D1718171D1718171D1718171D1718171D17 +%18171D1718171D1718171D1718171D1718171D174084FD40FF8A41171D18 +%1D181D181D181D181D181D181D181D181D181D181D181D181D181D181D18 +%1D181D181D181D181D181D181D181D181D181D181D181D171D3A8AFD44FF +%8A181D171D171D171D171D171D171D171D171D171D171D171D171D171D17 +%1D171D171D171D171D171D171D171D171D171D171D171D171D17405FFD48 +%FF8A41171D171D181D171D181D171D181D171D181D171D181D171D181D17 +%1D181D171D181D171D181D171D181D171D181D171D171D3AAEFD4CFFAE5F +%1D171D1718171D1718171D1718171D1718171D1718171D1718171D171817 +%1D1718171D1718171D1718171D171717658AFD50FFAF8A4140171D181D18 +%1D181D181D181D181D181D181D181D181D181D181D181D181D181D181D18 +%1D171D18658AFD56FFAE8A3B401718171D171D171D171D171D171D171D17 +%1D171D171D171D171D171D1718171D186583FD5DFFAF89653A40181D171D +%171D171D181D171D181D171D181D171D171D1841658AAEFD64FFA8AE8365 +%3A411840171D1718171D171818403A655F8A8AFD34FFFF +%%EndData + +endstream endobj 19 0 obj <>stream +H_sӼ; m49ҫ9)2E2&|(NBmw9Pj~_~_~_~_~_~_~Ӭ7"M>{{7H߻2F],L +˷/[juFσYҶqj(Z;&aH8]۞.j6_5;vn:f0߷::y+{{+xI$G38:.Obs|Iuԧ3F jE/[cUBҔѫdwBW#Š BbC+R('q_*sRE/{uPr^ǕCW-8|Dۘۺ8W&$ÐJ^^Q*MhxU$ȉ~5Id3)RM5< +Caaľ諽+pDA!9aÄɄ~0ζv3\ur-mTH< k٣BJ&T-+"كoT39MdZ9[@sEYҶg^|(щ="ĭk#ڶ @$fΕc|J)&xJ5"h.d addMԭ$dz{:d3ʗT幺ySOV7S0)p$85~uԛfq1ew1lM=t g[B;mZXDp=LJS~bjDdK?Pd&"Ǟ# 5:٣ؽeۺCvymCV"IGBȡf:`in~8E%'[0Ȧ&0E[ Ę2&)=WKFE(߹YDZ*8cO[]0i{Z.k}&tJ$˵mJwrvk?f?"F Jon196I.%@j-9N QYn 1┼eVgwE%Ѩ[N_+:1粲l%s-$<4x '(\lUKD-]!%\˭mQ)x`6EL" Xv[v܀_14ksCl/ ,M=k@bsh\h[ͶדIJᑨ6L 5Wfan7rc0X2N+Z"aHClzm X5:\/!Tf-[[hոL3n.'[iH8}yJ¶ԲuG鳂L4?) +mLeǞZ '!R/Irmu#bFػ TC8csR]b #R錑#NZfuvW4hƀdiʈ ,~j':Ef/t&}xSV7S0)V$85~uŨ8t斕 K|~D,KV;7 +@$fMQ~X2h 肇my*{eY5$,zYF7Y͢H؋ŧE*VыE/(HԨq?TG}:cd-x,YLdKik>ILV}:ߩb4jrp5"%V uOf"2{92V#vrT>|0u@m`"fS4DZt& 8dHxx)F"デ-Dɢ"5KqL5Qŵ-@s8 A$Q4"⃇-f{$ iI&W s \(`ز-^M;'r0hAc~6<O9DtJ{lIJ2S!$&q9m̗`TG}:cd-8[Wrq\;O{Y֎ ,MJzK.FhJ$1 t- ^( UDKDa /DLBFmنei Z_ 'Bg :AXDmدՄ +^{j*rRTUQl6n}cT $[ȼ}ofwluk)s+]YP3FW3}f,m[ ~Lz +WOiA!zϦ1.{i%鱆̓7džM5sN- 3lY}'^r;*?KGcuֳ/pg2N.b,f 0Ȍ>,:uu_ {/ +endstream endobj 20 0 obj <>stream +HMo9 e 0-;mM@VsB=lҎݒxbV9TSK`),}K<Wp1sVOK?Ly>lE{f|ߣ_S9i40~;Qe_ +9q0{]nUGIm7?t^䲹8c$'QNIeL l neJ]9v/u%27``% gFx!ߋ=V,F1H5bj:MaH&8V,5Ur]֧2.5aMB >.Wpƻ*Dnت ',n:Cl`Bƹt2ֹmĻ2w >m4"mY^L$C7bƴe&QAu.9NLV= nVLVʅ\KX'2vc/=M_?9ڋKL+>/zލһчB}*|/ka.2)ƧO,o~ f0*./ 6~s=BQTA#˔\" &^9^ᶜv~^yD zRfL` L),s1&{La+ܶ֜'ofYJ&n Y9P}̶É3#@" z zI-ZgvNs_tY銥p(Jߙԃ&Ɉ阈&#\F" 49?f$ $&S7I'2.D$?ezpϤG7Mh+YeSĜn"s'kxb>9g]NqV3m&UYW"kkٺQTs#)}q|UIO`AbNR}D+*~Je< +U!?ҷtؙee!!Szz9> \ą0Tb:~pq*#89uf|ŕߏDpB:CdxOL$&h8*x:A@Y۔]{'v$lDSq-Y}-8[7*e3MLDY_2 uAzN(~Je< +)*7UM a~ߔam,+ jeI/Ӄ4[{S&!Ygo>zt#*yX.wn-SFVFk:L0Sf +0X'@Tw' +u~yJ؎BqfJO/tվ#kŃQQ +Ks +.Rs p<+JMd; +TI-X-Ȍ 6V"S7lQ\r:N^uL[D 4Why2TaK'cSwAxo6rPjY2䣓V $|ilp0 !^ A%&[>p >?QQNf̝y㉉ g]d m:El:Hh՚9lUwR D!tP__^e]c1d9BjElNQa?a?$ֹa?~ +|~{inYX;ԥհ&6T8㰠 j25,Â:,;򂊁KkA}>޹̒?c2Ĵ6PV̲->S9>ג,(*ʹg5 +ZC1_U(c4xqJd< +~ʂ\${*Q:W,82%ˤ[FVJ`lHs4m<]b}.є50tdL),sD"aj#*77<(OͲ4tpU#+p1 Q"2&h~$-\Six ]`Fd}l`{hxcU Ysptt,L),sG}2vjR \~mg>KUp13> !0 ԢufYYl$^l;=Mu~+%;p:!HaLQ[Ovrdݵm,dE>_ i[ +endstream endobj 21 0 obj <>stream +HWYo~/`g$v#; +NAcֿWsP#Yd$@$SU]UUm?WONW竫ejrvVl=/Zf-޺,_XܛV͎,6՗d\{{.5OUDk;7S{k#+(2o8~Q}d*rzׯH3D]- EMUfINϯVst2WlS=??:<-'B[/B{meKaZ ^*#iÃ*H.&tH5տA ް)xwxPR@7ڰ–"p0BBm;!NŊPZe`lBsΈKg%/9P%W*+U(LjWkD"6%22xmr:QRr' /*jetA"I,a\~84-(:dp^vVm9@,덛ֹΓt/F05sh:2V !%I{:T$)PA:]w)Q*X褤Qϖ݃W'EGFBH7CJO6Ҵe(uл.ƭ2M%Y.ذ k+TJ7|qlTuz80Gᣄ4 +򜕦RdwBzp(%<4FMv%%']Z&iEEhc+-%PqryY 4/ $ʕiD</'s \׳韫8M=5fa +YOH6$ʎGռra^M/6ySoWM&AhN746r1iOWm6魉3HE 1h}\Y["ɴHHrOŏ^\ ),~&z@U'b g6<aXJ}<,-[Ў UfC8yC p/2h"7,XfE{X*rQt[Ԛ}7ұKl)Vc^OBQ/zm9Iim +6>y\NՔ}ZO +S.?];D7.=Ҥۅ.2PhѷȀnCI"?O)H]q+Lpr ɨlte{|UtMȸ$* =-o&-?6[7e+YCm|&I|CY <.1;L)/MjVͧw1IFI)":h\ sg^QJE"ދHw3/l]<j} ꌲۨzmCS D^I݁dY9 n=ll!UZ(-dkS7nJ֨%(Oqfzz%NESYݝs54[mȣX4z`tZ[~o}ƁqOn=xxMȄiqHwaÆLM-Q>b虉T1a/6,Rz#2Yǹ\ ǰhV8#:}he Y(h{`@CI\6W֐'ܬ(o8#<86ne8q~ ')815NL&[4; JĉzzHGJ +TLDk9.PxOGEɐu.CI;B5E4E4eEeL&QK6^ ^4'<4:,=Ef""꺢T_m/Cz m3~xчd٥dԗl=PF~w!Q_c=3;^YY=d s$ü x%`.a% +he<񮱑1IǜM`%p驮/--T[cV" ]&;oMywYV*k+I)( QP(96ڎQPXwc% + % +.H + +2HyM~X`-d B9 C:)8edAM^p[QIdu;Cdi9 3C"锓\^NTd[-۹)6QNXXɺ<@klYnla5eB +#9D@I-($zNaa恛f+~#N NMWo]^;/׈W@Ƀ +WײzkC$Ǣgs"[Xt*e:W U{`?CjIVGYV:o.{lox>NM1 Z]#qϦϸR{6c{2W'p`:Vlu>WWWi7}ssYd2j,FpP]e-pd:DYbNn.&#(2tr9|&&D=u)uf]4K.R +w Jt'dhs܉C\"&L 1[*)lXb&c@bz("%Qfމ%iqG%"ңmPtB;]|8 6bcg{~~}q< 񴝿[l̎tB]tk䖨vvoy?^g1p!==dv3zmOZKXр^H"CXdžETA2',W]xp"/LձDG1`msfiV)QlU-qw? f DMVg@ +q!|-p%9U)[ڬ4wmO\P܏RJ#ps9KnC7[?<̺yl[8G-Vm.=O +,=]X*<4ͷ<@Ҩ +p +|TtYgZ*R+X(44 /8 ) +kl$0XLԒ +ƬpFs<~Aɡp+$V9p±(G!:Gzܷ_?haJ] 0M6VǠyq*(`f?ݱ!,½i)ןK0Oﯷoln\_KmwjEZY-a1ڐދi7lg ݴ`ֶqCWnݠ~ Gm{{Hwާ[𙃍D5 .?vlRx~6`rV6^]Uӏՠfϯjyw<YwOp%?i"RLB H%D +/!jP \ 94>v()7H'×~@9 +WC<arO"@E yrE䯲d3b% +K @,jp#!ÊZ8i4m#o7~5ٝgbw@{["?n~ɨjmx[>nX"m2]h%G=}ugɒj$3r:XFc& cM_ /m-}t,ҾKʎAXEH)~`?|#A7Z `82 5OhDf_iJx(1DK)%^Pƒ%61+I.L҃Wqdߖ tAr!,ؒrPwZ.ehz-L-;)K1㎰nǚ'4W#B3Zg4m}1aO ipݞW2+W!|; t:vbث!0 ]:84P 2qER> 8"T$[ h_mALH.DEXBz*E Z) ZP>InJCh X,XBe!* +VHNa샊6XTb'XABY<<-,8,GmKH.DEXC +F=?ߟGlVóm&ƍ?7KnWpWFU:{cCgp~2G:?dEW*J2HB|YjG66a=TΎ =,_~{zy}{gx 5?}k *r9W;^}2ҮΟ=:") +ޭf4I|9G+1Z+ZeR_cfJp1!W/>5l%[JPޛ֏Z_hVoOnrxjN~J]K v'9(˾1D6ߺKydZ w¡H/,@.ݍG'F̈́z d6! U* A"\hT + ݙ! mą3Y,/5n@_:A7d7zG6hd]IY׷ZՒO`*1TDAF|9)ACr l0|Էn\xTYM9QʼK'^yމ[AJ j\g`G̛sy? CHA 8p3FgHr$6|&WtmJRȃ1%C04%Y +yİ2,BKaJQ@K5Ѝ f>t'iu,rc٤`>[JU%Rp!Klh!,#\@ŗ{ޣkZ H eP +^gy]Jk\I!p,UC'1#\P.ہGkZ&}OJBU`1ihu¸ʪ]FKfi&DB@TXG%t#\P .ہǝA}e@ bY]X\Mz\Mm%K! D,UC'H8buqgGxxzH}T֎Y,6ڱۆכ H"&jOT Fuߵ6 ͆88zlR{1`*SdNޖƌaacưfC1 YČ}1c} A0N=Cnj2XL+Co!rgo9oLj!F 4CB1`dFQܙa LsRx5eTl3& ΰB̘PgP"Y*|Z kZV$ܞ#%#u!A N3"̮4"*[g!"TIX$""# +bxآ覼d#Q?#bFpF#i,m 4>D#"#E;@M9=GP!t:2g&"L,1eQ2AƏ!ERVVd>h)')DT{ȋ7R͈)\)#vĔETtD O~hpO @PSNB}l'r=[.սzw&jr^v=f_]~-wWy'qyc?wWf{HfCv|Ɠ"?XΝPWv&[:bq{/э;dm}d銚Lu֊-[@G7{7ZUSTZeW7y/Ku6Q +@ݛ֏t i_F\M):ZTEO2kXWhEV'-Gh+RFs#v(^g%uv9إHkQ˦b;Ma53  tY FTx$(Ugt[A-ܳ)$ 2%ɺ +l^tYb\˺&yLh{쟎@s7ӅqQv),K)n~8+h6km"crജ+Fnط $ bsu3 9\[rkPX~>gZ /j<'A+%D|X]*䒊4D3an3.hP>\h߱2z/~c/vDCI7CJ-A(}XAdNRH3gYRonz>-Ye(ؼ8t%tlbx3{V_65'I770=g;e:lq)0Zxߖ1wB8""zqaЕCEc/{F: Im4tXl 66/1$PG= +Qn mq Mi- ^ vh42Y9kY7!4Igtl9ve𼑩~B]P{rw\=y*c؎p [h#1^-xSqm1x\\ 33NX1"dPB=:@gtO3LpY->2Jv/3<(*DVzHW{\(dxW &hGqS9(Zh~RpS3gY ^ߪrhob^tnManאe1cf…cOTϙeW\*Vl o򆣍r8[#*#Ow 33եUȺbX6HFё|%o?`-i;^!BX'@^ȡ8Rv|K *qI*$4YOPSL!P_1kv~ +lqN0F7X3C O?cVЉYV=Z@uCAJ@XT nJJ sM#M i 00BA2Y fP#1bNdU973 ]T*6S*an*#a.k5 e-2\TT3k:!#zn$N n?y|i|<=vJxl;zH|l;~H>gڴ[`?]邊!59 deO ٘_3xoҵBߐ]=sf\TQMX$9p\m!;@]$@Hl gvV‰`sQ1D!Ll[=6vx!,3Γh\ޕ}dKDv8Rܨ)ΟzXn-WbSE^3]&KoE|Jts)*f2kN'^br3. Xx_>0}~0E`7qΡP@Gݞ}&EAtFVLv">BLO$R=t<`Qi "-AiJLB3%F8|eXENhx _8ɣcglu0>Fp}M<52;v&((fYThXk0ȜDo&ן{HWo}8loMk(*2ZWkPaȝ 'ᨿܿ}K|cT_a%v2%1RiYka3, xȢ; ";aEa@l{g3,c`h9 +Yjp vefc9̽m@Vp%=_l?i4&$T<)B2e HT˴er&j28al +ZA`t$/5<Ȧ"-ijO + @**u/}kU7倽a١ +W>@㔴jZ+l~('"i1 +gin'8Z:9bN3jP0y5nI<~n By(Ӻ]jTF-MCsb)c&TIɴ&r~m`q.wZyaq:k<͒˕To\1E1+rZGo. + <[ ]c^ `薶'h{Osz9 B&@Y8.iΈ)ܚHxPkVٚʛ;Iwj,FZ2jX^!%˓5 :ը^^/>.&q1b-vZV(M:aҊb)2MW?yYtY;w 芚6ᕎha 'u;AhJdx8>LsAfqmC E͐2 Ʒ+Vܜps7 {k{W.|$ ĸK8t_[y-wXpo#14pC 4վ0ԝ! L ª' pϣ8O5tZ\u +± cYOon)M^ Gp_V,L á9$fm8G5JgEÃt鰿a2P>DFI\JZnX9o 3ev' Hy~f/~b ]2 āuNlj0gvUd<7tL\*:ktfF\Y>stream +HWkOY/+41U(0a lVQ5vnn1~Onm^`BTթ]Ƴ(y$kըT_zʓdYޫ'KG/nlrRU6JmyċI"h6^'XH7罖ZMD55WY*"}I'~+Gw[a[$pXmI a 85iUJ*G+BAjof\m= VYy4`w$5'ezIOIJ޼I: +7~n/Z X$`ER1e\ʼ#}pR+7b/ +MV٨i,RE4gCf^A3SLn3c{2uJQ\RN&2 bͫhd +獖 +h߅gg*$gբ8s.vrgbߏ6F~&5q3I;ĵm9#pgs֛0z3Eh0"4*OuNOkc6alvv95!UpBx Ia엇YQ:<{Iw4$/@K[|Cfo09 \ͦQZ;^շ daI~ljr|Sk( -ge>h5ʋdY>/[#qxm+Bl%k:xk X#Tcg wLc22,yATۄ ϲ!u[,3݊l<>tsQfq<Ⱦ&yq9^B~ȡ(^X,󒿗{BhG +<9EqxL\pW305әUEoKit{hU^Z|X? &jV娓}.. ؚiJ1)0.<A, I-I5sU.h}h27l2Gm2Ka.MN޽v/KKw2&Fy(@~$H$A? +uK&cuYHt$R5蟚Ctm,uO;$lzSoFX(? lT_~\576{gǖS.rPktʭOz6uׯ/ո9|5fGz8=i8[9L)z<)G7jOӓT`J/6 +LHyi7qE(%7[cx%iPW"~SMosPi解V Qᠨ&}tջ,yӇ0w;i jS :%ohҨUT@8-dxz-{;(A;*cEAYRΑ6Ni͕9`AmRPM̎Z0BNؐA)dX=TӇj s/en}͙è"dPmt8 X?u,NQDN)^Q-vUюq\"^zwYp!/=jֻ2p/dU.ۺ7Say^"$ғBkjՊ̏3[_ҹ%A) 4@$X$nC,O%80B܆㬨ҙ p Z)Ռ"8\BKy̋m8ŀ8:|bd=bnh#I#%< ,oyT<ŝSCB=FC}(PJ:͒#T Q<_v&D0VPp o3d W'4M)cmm<Rp(D^WJQL2M+.kwUAJDd4 X'K6hfG;R @c5 $DIPjaI}B)SI-o(=-A2qS6!\+>! G@(́5<Qi :Fqcimf<y73Ŋ9#)NC#(-%rd2mjdG,y=!Q )']2cƚ;Yk +q`{gtEc; Ő@H$t(mY +; w+aKWah0#DE9OM<'@dA|.w50MnF;M hgй#JM;8wr0`_m~#!uFLb(!x؅/;fc!Xы8فM@qP,tD EK#A&S sA7nJI@0h.Z'bޯ{4*E@2+qY`F-B?4=p<91c1D;QTQϰT=fYK7@4YB ŝLZs}=Ji[(`MLK(rCRnGcԡ,}ְ \rĆCmZQE#̬PW!3p5 B\-.~H("`b݀T5\[ +6!# @@(Zv4 Lnpv&1ٸG$T&a@ݙ#n`db  \Q[@q5 ǧD4WHUB,2"dIN7$N7Cx.0イM L!0}upEh  $LwiV@6x rHbY`wYmx;\j n_S[xYq;mG9]%Uyj?ɴ皽-1v,n IȐ"u@lh\ |N.F1$/=i ;@R}HJ 1C1@X,`C$*(JAiMZiڧZ -#\WAEPR;U*Cio HQlA44$ՂL* 1((?#p>b4F* *R5LZi +YCXAIf*$"Lۂaa@#D@AN\Rt˘7Fs `W+Y>7vyJƢbDe +1Ղ"#\Xmw2LVE.l?C ԘN,Xθi``D A&(~@f0xJkI87`9:$s@C` +f"kA ԃكkIs~ d4//03!C7h;~=FH|GY 7ܗ1`A4V$z|5E@݉{^e彬ͪ]"7B*"tRKћCy\#Sy_: g٤52Cj +'5KA=pgGl+M aot0zzYn;/7Ez܁vxv,mW״,m1M{yI:ϭM){h{^Ye +ŝ$Ԝ2+jvbj)gz'ocܬˤJ= ~*8:y9&[/fD >6p;K:#gk īukl|_.{84, . wWRzgy{Cla+oQٵ~D^.um? wNGnզ~:)n7ɭjjf)a+*;m6f̫:JҴ;}?גQ3cϡ aCib&ʥt Cë(W,YvGC/ kZXȂc""O44 CB5v6>Pߜx^ M"6(F? kA54!Rqט +x}jʽBRauZPG,5Xf~ux8ֵ'W+$`&'թR24k馾ډ-8.ry\mL/VPdMh#11~$aĐa`5AtKlK>lV + ?#C )Ιy ADpd{'ot8C!cr\``19t uƭZw{5^x VruU(ю'#Q/(# ex&| H $#תJ@T#/swn@"C+ȷQа$RV銭q$Eeוq zLb+%v,p-;[+nE^;#?XnVGK,/(.%b.`1+ӧyNˎDZw8puxS 8VbcQ Xȶ/us +!Z<ְ6aONѾqZNGƌJK4/iNNukZؘ? +jY5:β}*~ؒoos s' :qg&􀧕{]ôrEaNxB=dHYsu8Sh(V2UC̃'IU`l xqs5kT"r5` +zLZ[( Y(9Haj4`@Xv6#6Uॣ"(_\Q̂s>7PhԲچ'A4z P+;1P7吀 - gZ22Q?#g@5@ +a9U8[)WG)nC6$̇-pӯEPS.Tv?IRn&ӽHu'9Yk۽ಾ{}밋բ=7\:*֧筭vU]{elU׮vFcZ9Zsz`V5.q紁'}:(ߠP w?𠻢ca-Z[p~=P3U/ҶrK_OƵ:S˫=,]h3Pk۵===(=!۳/HDTysnFW +ћvwreF|F jKX/NN;އާa_n-NT-olpU +Rzd!bwǓ?:':i?u{uRQV_Rz#yٖ}C=<ԫZ[W$Yz[:<=ܬ\tӭ&=Δ>z=kӭ'Gk=Ym>DFA]c?k-И}:7Lfs!e( ҧTm5+]"߬Oq4CC33crF?;57E4CMc"f\.1[%H#4?gcTZM%=R84kLiQJz}y䎃ncdڣf=yD-!+j劊PD#1Mkm0Q?n |)0+ܱ6KRrlh\qygpO:'srUAU 0#f^#:f|qU$, {fGpɽ'äOvq"`4NEI?Ɗ_/ymϐ5=yDj\qAik/{l յXg:5mj<݋PmZT90¬Mbw_$SGQ5VIKU T7)ExF;4TWd)N TB'LZAdU̶fEtpKAQ/g@ŭu$=G %ʭNс pFXm非|oME͞ Ќ- +~Giط0[$&Wٟ> C 1Sg;.M-Ï96'ȸA'눯_1-= NӤLP#]6[w~r2l>GIU,r W41$d/f2-~"=RbߦD`jِBC2a0|AJmEhQgH!Nd#O3tJ [ +i (ݝ~-}sQ + +@6H9h3R5 :2>R1[/_$)w3G/[9 +AHe?"mih2iҴhr+7pъK HxpQIQC+\J0})RKQG2mR*NCj36y<Q2)ŪFL.s4u$|gګnjLUK։Ԩ@Oq'FSyz9?~QTƥnRqaa8`h}FZsmw'OG9;FRq"x;Z{x +d͝=FֻW' +G0Npi %H͈ [A +FCӒ&4䜽r*ellnW_Ovlnm?tb>RҾx\..@.:h5ftU q/&|okS'/ ׆VwxЧg|r_RAށDq}Ly*îAy gBX\6rRrtkSlfQۓn EaV†26f৫󊤮sPg5}s)/xQoS/7 ~k6*_p88S-[rMs;gൎ&[\|NA]@XppTU(Ix7 u4_X'톀;.JV +[Zhe8܊zmW'i1/ԪmkzKGJzOOdmpӢKYd!EdScxΈ啃bW ͳ!!Ϥ +!E#v /yZ-S@0ZW@+MQFkp_rח`2+DwG `lʅ ߘ7.9cZ8J>4!?$O2^%kD^>O* :*N:ZQ5Vz[sa~/6H$I&\"P/tət˾ou| z-ɹ&>E n r?yJ:L$RZMbYleB\S͜\}R5iҫ-qEHŽ $DD0,! BYz0 ~&Vݪ:uK6nM7 +:@ ȮZ}HC-?LMJ+S@rjԅU!GcT3|y۷em$Gd?4;79Ϛ!cUY5TJE"q +%pT:v䈖[&ؾc+`VizYըltA>B Wm󒹮4%3+cT%Gn` 9,=~F[)ܩey{j%5jV-OȜ2^"m-bdqp)w#VkW߮)g-/aZΤxn)Hiաiґ W@do]^6Kq;* %@qQZ qyjŚ9]u -Z֍+ȷCbo^fYr +`9{wN&Ж u R + 9BBm!ȍQF#*xT$> & Q$^:LRD/ZdzHǍN]C: t@[a<Y @ayETW z}+NKJS?͍l Jo!b+Dib`M ++lA=MiivxdK2%JY H.LN.COeqwW jcyNlv/3hD2#E'[>6P/gPmݕ)f1M#1Q0xkI[~p&pMb^< ❾wG|Ymحvmy" w. ˀtdCGNGM9AB-^bWTvf,t_8ęPu0oi7jf^o ~={]3=4oAO}Ҋ~O}ԭ$(ڦenxhIm=ю(=[M=(o_ے̨X}QiA'5mke[YҬ'CUB7tU>8~B Yh. })_/WԖL]3?1dHd\2bAl~O̭t%#IMK2_H܄WyM""z%-o@]>6Y[8VY۞2q)W^׶Qi<h'kd;;Ef%nY@ `hjlk9jxJ(UsS'Qx-ٓY~͊} ~Ue)vz_\Eܮ=ե)bF]ݮ (.587up^)Vf_#y4| igɖb1.eg?[$2L͠?I2i"x)2ח {:Yy'FEg %Y,ƻ]),=h2XhFpvQ1}1;*^d!I&8\Đ$֚ι""]V)%jf* cjP3.l> ?=Fz[$X8H P.|+Ә:?M*Hg8mgLKBa)Y'sviCi ODNݾɊ||x*<-NHtskS( :m:4]ľ}’:;(jձ_:M`1uYN'u+aEe`|մ6e״3D?d4*<=8puz< *5|kЫ'?;8n:ZAޯ*ex#q {} mJ sPLIvEeř5 xTf(j&#{$<ix\D\^On23]q8NXQbC&D)ӱtrQ-يJmIu7\E̕Ԉ#*ώѶk5q0Q%2?{xhTQ][zە 1M4j)R}WVw DSJ4jY-Q޷{'| + +1 +$V*Fz Ek=/z{$0yVxֈ MTQ>`%ua@Q8|Ktio" +F,OYS~%uAzTh  rgiGdfosy:FKPėPAӂ\uîGlku `z`97Eᜫ麱P<KRCEх'1QkMppRsjj s/y,ZWJ\: ,%|X4U{GKU-I5?A!vH)Qʬgi(6`pUtXMlBK'ZRiDoS*?w6R*ʁ*%}^f8bQfR +{Ay6RhRH;abn1vka&;/X !%/``^UyԪ&*Dlpz[sᡚ3HW19 b#xt2^,|6pk|; ^>5Bųړ9Hg+ qyKpp3D8z + tIU 58 +M5:zCRA{ T8ǙZ̫R=ÝXTG] +8\`oMS4^fC>Vf`6X 9 +Wqlv4,ըvM1PO]/ +RHܱ%םK҄i>ԏαe08NUJ +Щk1t :W5(}EPղ +5*@a`%mQ]Dp j|"4%Ge˥=g?u.jUUgBt!|8.``ÄNz:4+ ::9"H>&8c\tkn;`_Bx@#ISC3B q*!`8FCca!k A#}r:"5]1/DJSbz%.Bwdz??DCeB^M#aNJqeV&: 5 +vw:#}%K>`|^ӳ*B.a}a1C7'Vh֒ɼuPΧ3ߕՏqrx>ٮ?IqQqIxj9#Eq!*Ʉ:.oh;Ƒ$m> +{57&2^Q#ȣ g8. +^od+ಽSЩCv#Zh꼠Q8UDտ`A|Hf^8oF"]0jKl|N5K!x:wưB"Z$.Rj%YY{+P+ ǝHgJi2V|*m|Cd;^Et1$&2x#1E`o bwӟq U΁πXzCKʕv)9S O^X}B!x>[q2)'n >txUP]n[{vf*ys͉`TgtC 7ONAu݁AvEfIN[Ͷ{wCnҘLGidCdqp"U4]wHdZ&u}H4-SIʨyhb*pnfИ?`5m0|҅>4g;|eLS:BJe\r.:j"4?sʋhA~uդAi 䢄Y C(ِ@ř.Fg*-.N~jh!kD{\ʜ}#3u܏M{]Uu?tTn$F6-9NnH֒$JN g-%A~a7n0J݄ ݈g7%b7pzQ6jv<*#.k2?+'\uHo(Js-iζ^FA]0U Mr+g\ͅ"IK0m8NNՓr#6(lyҁ@S4Lg)B$&įxFMRoJ/GQ9(tG>. +Ѧ㢰 +#Jop`rq k0Q"q3k |F> 7G8Rjz.$Ib}CrJɶR5 g p Srit>9@`Z +endstream endobj 23 0 obj <>stream +HWY[}w CWBD'#GZVUB*RB|c{^{Z{l2 Ln\_ge^꽖\ 'rm?'r"j/oTڤSr+}TTf.95^OSr*l R)M >)h `r@9ڶ>0ڊS$Ţ| Q'dF'i`ojO:$ 8Ѽ.h'#5e>^&"..6A<0ILrLŚ6! k9^3<ڊ5h*e +֊2 *ZXQiPm#ij&QZAS֢K;BJ . F3n2OԶ@dEXs8Q)vv^y*)h(_+Rջb{[߉ +DW"w?RQ%+h!|-7C?B8t,ռT~]3ARytPގs +Uʱ&,>D)'C V*U=4gѸ/Q7jv'uv +Bȕa\Xۨ*z`_O35X$rqUE {)UQQۡJjFA0D֫ +?߭|; ǧ8]aZ`?[DkOO( $X>yӧ-*kyPxPmS *ӰT-[H~}wN;#BpIDhφ~Lri>"NG;S!;ڳi eV{(nG5`6~ш9K49ƨu!_a Q}rGƻ0{ sװk[ UFvdSXw̑5c„+?\FLHW6Zc˥aқ_nBSiOQȀCO@AϬ. V&ݪ`}"]m4%Z + w:U -G C><7ԸFYw>9NՔg(sS/omua"ij8HiW~~qYwsO m. aÞ8 8EfpMFpM=WJˤZI{, (dh2:h,Q J[TܜknVxF7:%A8^ҘFZBHI%3р5sNe#-5:>K:׎pT'8B Aqݨv߁ +RA.̡l?=(g1rCQxvڵB1/&Ⱦtg׆OMFqjhX-W3+M6lT <~l%PNnTe32.j`:uFby%ʩ`^_vi`^`(vL5A_)gPo53| P`Cշ.UPh ] UBz9R%2\X?XZÀC**Hr>MEYz?ٖ8F ET]]pW;3)S23_"stŹfQ쵑zbhn(T3t(z ¹1!aU}%|Ϋ/o_Ǒѻ}>pLm:X3,yl}1ⓈdMnud|ڌ~:+vPO8+gy'AV'|mU< +=ƽtPA Qd򬩄ŏflLY\λrults\WzNKLXRީxܺ`]~-.=qvb%.gRaeWOHyq7NkH/& xAO߳sɹ{kO[pܘe#+"^Ly{z8cuy# 6*Pᵔ0{96:zjo)rÉ,NnKy3YɵC<.')rK -7pXF-lϺ8D?J[RY8`6" Ug`Jq'4G *(P\D995%}#PCJ_*H9|^ԇI)g'M9zl/Pi˜`6Ag>p' $] +'T#:+]6 ,8T]6%ӯ5+*\\.DfO{61ن.DK:u[pDJ(A{؏q&+W=k_&nhQArE]'1"΍_T^ʋ]@.Ni\Q. aLt[3v;l: Nti)ִeҵAaOO'E!2+XgAtC_Gn&RJ*?4jtTk=,#*8N[4jCe]*֞SskJ!qUj::8R`\[k.@11\ )-֞7ն_o10Qh +; C]@ ]RPbgCJwu*JmlPZq[uCӡYIh<==pIfزm4Ry|Q?Xؔyh4ڼ"bb45e X=1SЍmmM?Y)^Ash$3;m +[8Vbb .P_[|+qhia;%gPvJu_CC(~ۣl7a_]3>eOlWoܬV?Cain8hat2Y* QL/)hC0c} 5jU^k^=ynHp6IyH6BGbƪgLYoןgk>J"|{~Gx}"+ :NM8J,^k/K5ױ sN8r0t'"cWHl%^׊G)6SZ8i}gpcD*&7B +6>NC{3)92}us9:"ۉ}:Z3U\\_P6QE?V<|cWgcZc-#}Bp 5̃=% Fi:!^l.*- + w{I=Yk:K3$7wm2CJxϏ1Aж>ɾ_nQ8~bgZv9Y ٮAM'lÎ8.~: l}icn&BzWCM*|-{JF>DB)f.QdU5^` =m!S. dBI37745BllF>Ʈ8 |k֓h>> +܀uf +! Y7Aɤip3EJ)A4B&CL ALo #CRluvb +ojxE6Xi& fRhJƣ}kIrc'iX]9NEkSiԗ@yzN\y (T,#pڲ#lhk/`;wz++h>AT-Α41s\ mW{/FeY@%0~6|u[Cx#^-gRg P˥RlѴ2oeG@?Y;9s37ar̻TPP57[v$gVN,83z#+-|49 7Ni._;]ҡ C$w:J{TN< +i_!eqC`w㲊וsOWYDY$8%DpoCp;$X%1zh]<@/O}0܆M@U%U>4Tmi򗩲"UZ1l>wO?naxq(W-p' |ǧjz`\#떇V#q-ݳHr ~@=NqʩUS| 7&: ( șS֭&zQ PЃG- -8ۊ/Us5\s~ucim[A q4 ȥT? 5[j㒛1wVAg`U|gkSb7LAk}Djcm +kMZXT@"w=`ʩ-PrUJX.EܗÒë.I aձK)[HP~` ^5?|`cP7=7.ؚvClrq3tfsJVz^{43]Ae'k`*/;24Y+l4Phnj %ko ɋ(deȶtPTSVN+ڷ5Ӿ)}H:gmg orF`K"I?Fpxf;FmE58ɭxFNDW mYc} oRr׉⁰?B1UĦial4@|Dkmw 4$/u6uGL(DmdRήxX7Zoy˘Y>'m`c(>7GmسRx kYQo+h)e<jhq=;v޶v2vRw炛mHUMC׭T0Ͼ/@Qd\h#06<³D;G +Z[6( FPd$)pf +T\jރ/ v,v9]L-3'iM?힣 7>w[ 0XJx ߟ)F Hq4ΰƃq*+Fx+Ftaέ&R@i0CH )!v6!L5ۥ6`j+5_Tk饫T~M91GmlTSF/(ۚ PN3|pi϶h/uɡeDd'>xvR?ʛM!YE3is[]>̗h@t$'^R/8i8qUfw˵2'ʵcr;#OQu7G~szF{(N+zD8v/Q8frxf>UO";$ 5!g`^}oX)L]>o{C +7Sǐٳϴ"s( -'6C6|Ѿˀ ;g˅OZfx5|@nܦnc^_(pUF>dhB6 r8g7 = ԼFBa6 k uz&[9~㔌PO0`}OoT9<k"3?44Ȝ"Kvlɗ͇„wUaMzFѧ&{pdH +p,[V11.Mq2m9@ ,-5s[X!MIb}Ҩ~29 +u>'p6mzG)b obx:9~ڪG}vr$k:Ii؆Z-6 nz-0To)Ӌ"5R9nj4SEaD?Fp,8$/!>OAuuM O" N:U+ / ثл.b2QC +B6=0$2nm +iE3-D?֫+qߵh!!!C2CBYfAT@dnmm%s>Yi=e@"_2Id\S1y ( :>*W[n% aqzf3R[@{ܦMwnq'?"ai;&87qhq.; J?4Oe,=sSs>Pv5)b ^/-ݪF[X6[C.T| xoJj ƟDZe #~8XձBkx\PU˒X]t7 t'0N$='X&5@ ,8< yuC yaG"ڴ*?Ӥeْ CJz0p6>ds2M\?6!|X^v"g ݜ3y&/8ݍ(:PTiCD8:߉69%ͱN yX3'1E&~7/ H |Ƈv}t6?@mu ݼm|ɵF'" +opEho|mH݈AIFaТB-e#6IUq޿Gw - | P'^SCI +[c;_YRT*9_u:y%5Dfq [4l#7T;~ *[)ت@I~M j[GPF؍juc.h%kd4_eM:p%]^/t(͑y\64j$[+x^=~@⤿"nˈd0bQ|r%k:N&8ԣ% -x8Ս,f+X; zfn*,37-ܚ'Q + +d u|[Ab|6ZY2˨A)oY2kJAm,uU6-@G;XȀ @7`O#}on$Ԡ'l\™OD*ϙm7KC,GcJ@6KEW&:s+^h`9T:Fh T:ҧR.mo3oéjCC߸?7 ;`x|)  `$4 _!`̂*??I? P2Nd<3,MqҤt,Pap9IŅu/Ef-( +(SW*_RKDyʹùu(O$C Ğ -&jRWoshm6^0Iꬄwccͽ&WUe +F $D@.xyuTx}oE0 Jx*X[eh@P3T=KYq,?3EYn=/va]Gij⥪/n1HwĕbMԭE#1^Қ\~p/.ݸub +GE3 9->B:~ϖl{@7*+(:.^ 3z +av{ʰFFߔ LKIˮ~8a W;-Ưq3.ֵ\'bd~AA+'i֯K blYbWF&H+o|nG4KWքlٗv-ffi +> +YW6*Gpm"+2e!@يC{"Qʰ0`5uTeN-l>.(Tr16 +rT: |.(y$Ε<n2͓1_$/ c0<Ҕz#m.(R / Uu KDhِE/yu|F5 +6D59[b/Y#0#+v=T:Ts7ו }?+p*>zdkʫ:XnL=M.YȕۗJxݪr # +j \(Si : l@.{ƍY*v d_ڐ::wUI(_i +')XhBųޭc/]o)IO,8sI[@iF;Y|0u"Gb:!l\2/tP|LX&ԇЦ伬*kJdYF7TuPi yfUw͢2;UY|T/\\ޭ-iqT_ĒR]RbVu'rɬsttALe6#V9M;Q{;O gGq=U0Α]I«g\mi!c%=ZA6 8m&xOh$xFmGE K0*x^{c`4~M v5ɨ M`a q +jHw7mRPfep2f K +wsIV;+yob.Gڊ[#yw }pp@DW"v:Y匛q{W<)}Lt[^|FoR3aý/ ,HyBadnij*&RNj":!5v6SASVi@^. yAu\qrsZb8SJ n@bAv'2TZ0M sqQ-8šM0nS ^ d1F\PeD7 {=jyhMC^'NűံGJϿm3ãEzf=3mƐȶazПR4^Y4}jA#~cA׷~֏jB 2ܚ03Eiz6[rx֪wJ7?Y3m-C9,A*zJmmHQ ݼ^{yOHa?+avFkN7ѫ߾K`>ӗ߅9y?3-kH*ф1֘N-|S||bR;'*%d']$kd~k%0׽ +KluX]K2@: c>$'j;[|C(O`U"Uݱ5$St/noQ56lUZ=ڐ< Y:]98Mt{9Ӄ4́.J" bl\:9Νʀy,BRzT 0C,宰e|\M,ec$­)1A$L>Pt/ J HlgYXJj4QP\O AZpyywDk? >ABbI%p ? K*s@:,I(?qX闆ڿON|vW+|(t7q3-;Ȫ-6 2"Qt;f9aGYA,aUlqJef:'&x\ q~jjaUCΪƜIBN牂YGfvFmMC#MTk7W5{[:Gx@LrlkOqEJ}Cf@8v`ꉸt$tpق:& 'yh7q1P ܵ)l4+U_j4sSUЖiѕg!X &[6U@ $EcdU7)8Z#! SXRFq~|7nߓ߁ ʄ8אY?@ ?~pf}CEDıtT賘]2LDM>9~n쐓`7p.5P!^.$TLg>4k?*_{eq>ŜyK[ +؈n_'}~|q3{w||{ u{<$J3~yZ ,YfHQPnK38Oo^kG{b|z"J֝&ٓv@3r8 +K1Nm +^X77&b0ɪ fҲA 12]{@"MgSe3[RZ?a #( WqLd̈hAa"u4݁_Ր>67=J|J(q^(uCMM5*űb Y 0Re.V +芰`AUw5t}0NX御T6i&UYӀ%64S6Ol$IX `!*UtiA$C}K`3ddSˆ_Fd)dy西ԴȪ%Vjqπ(A*8b(P5bÊ)PRY!U]7TsK$SuW7i.3 +  ؍C M&1*kJ4(3]"R j\SBUOSEj&LWY?j Y$3QeMgDORK^4Tk$R@o$+TW՝kT:ap'!~:wiD*"F2,Fe V +b(|L2If• }Si-~g~Q7B͝$眜-[cS!L/5s}[i#i}0@d߭!1CdcEk5Y*`n!Ј[^ rzjXug J#Xr ,)+IX܌t\oE 1s,; +mvYZ&$k뢔Z)+R<2p5!j6t}ئ4}!"ҹ:i;Ҫp!r2-/V6qD8G$qi4Π]OWE{H1QuBBs7LeEL.2 s7Ņw%B1U4'9k,#^36$?]0. 鮕ʗs,C+Qn}g)cX9-0B5N9@@&A;B;{ѝmjttc 5Z[w2&gS.# 8T. +(zmI-V4EVu@N~jh|;lc44їtEP#+y-udQPK,Ak@jc aŅLI]ՂLQsƲ>stream +HW[oܸ~7>HDEQ\ Z, +Chu#C3N~<$%j:^|l7]k34_UGy|e/ŀH3w ~^z{6fS~؜Dŷf]ʫ[m'%,jS;P3"16 vA=:EEbVpZ#b0_z0} +%-pq,#O?@Ū**:,x?iҵ_Owuȇ텥G]>-VP[fU_+|1W iq 7-ٞg4ZCskחGZ_DqL‚4bfh +`~R6ݝh#HSC8{?t7' &zi){+ke"XM&_"0eӿ(A]5d}stw#lլ!|kt_`U=f`sdpSZvj#l %,j}ј?1 zTsWziF9/~Wv5ö^6Y<3=߯RW258l7nf7:f)ժ3}jҋkc|q*-Zv8c.6oOqTzu_wv⨿3Ttk]ȋIB39%^C^ͲCB_-utn"^vxы+h޽A8HH?$&3#"!¡ P-jH=fK 8 fЁy,Rz 9#+!d/@qbpx`q@Ũw=-X3^E1gm YVyvՌ,`wH{ȷ}u9ap0;96gE(wrrrg Z gCIjff;;2bK8+ҫ&IC1i =fV* +m9sGnjk5hg !HȅD uƒK!LRBV~%1Hd$*ɒCHfD[g4AgȂ*t0ljӉJY6x xgXr/Z`="٣P gb:8{7Իi@bLS@F>ϕ?qFW񥃭۬k +3ë%{x +k_ˡjչCMeӼ[)~Ok1:iMIoz,)5YT?^ߜsS'$>VӛL5^!qOw=> H>;J^Hߟ''/7<^=<`٪*HP0UTd$W#ZvF}/z? _H6{'t܃X-lZ_,Evh,F YŪȈkJNbM0Yb`hdfXjhUVѢ՚`F1ZF+Ud-+BE5*Zjh-1\fF1\E - lו+KWE~Y)XI,MmXư|N ͋!ђєіјŚќy.# +UG +0x}GE%yXXSl:N[{Fa;T='J6hYw\S6;q$*,ā ⎞xk!aHD2N(IaqH#&%g %;K1Pd5BLji%\P.WPSḄ@""'< +E>ZcD M]>fބbҍ"ܳH +9OAOHI^Eb @= 3, bHz)^&&̅T:'끁$"Τ]x{]Vq,va׍U6xtcύ579Rٱcnd= .ۘmen݇<;q'9hNŞ%٦\Dgx̢M2HxaΨ`Kk6;ys?\KM-Ӷٛ&z!$Ц{إT}荓Tj|Q2߈?^?tm%AHMSրK+%xFn6,eCyn[ tSmA4 gFxư%sEn6O1Έn9~?˯????}^|_>iB`GeGnBJbKQ&J~z3tN1E-o |O7O7c1R}vѹ! Ady2SxaI:ԍkF*U0[TJMͱhoUˢH3D(J + jM=Q I +slP߂Hl +*'Az7?! D}^"Q.B6,5?fo2` װ,(:Å7tBNgrێ*nt !#л_j*2\EJ([[lk*G:g)R^,T$Ɖ)0,$"Emф4b,26bl4.c< Ԁ" !a!\flcˌ5&E$+Jdgt`2 0ޞ1 3X0n)EY//ʤ.XD˓ 'bDǽsLVy)u.V7Mby^JGơsWR}[ݡot5'Zm woO7 ]{kb m8irrW>^ȿη_z}~w9n]- +jxFm5Vv[m'L/ڸѽbXiJ]_ `i ,m@$(@Łʃ d((T(Pa M.`X$Pd +E:,Q[6ȥEXmhOUͲoal*vi &ȥ'Kl M,h-M&4!F1hK3$j FB +hj df+,AZbDG)[%~ PDsRs1W +HAQк  +`C17Z~jjvY^rUUU2f>N +%ٚIZ^ڨfֺܖlj9Y<ٚe|!:F9Y]{VTHD[Ѓp{Bn600P("C: v=u;\b|ŝXnɞ)SgDtw~4?%jM7C^/;ӗ2D] qgNGچ( W/AXj-͢9kl0lfqb؆d!c86u_ꗢzO^cpCkI84eAoMA[Ɩ)pќA;zȜ6f=h^۾M嗎wO_~?7n ?@b")J51s09l>ߪHJ.v̊-WNz|({w>קoޔң}kȋM^[]VD + Hz9zT#R ?XcL .룘¥3DcJDQ$Υ lRrrH ZbJ'N_ŕ-4'@$ଂFExTJ yfhSn2pEA ǧ3Yٚj8jkmIˊ]ž"-@ajjWf[AMa?ʸ?p Pq>μH@qmMfrdn,@:L턡M'%6#ȶ±O̢Yxc_d1L/@gi봭C9c.n%7v^r5Qhä)Jpb1x;,ok1:IID,._ +B:pq$>g-"` + MZ12#s1Aо"YhΐgDBPF9r "j4#krgIR1Q:S82wե5XܯjK]ɋW_\ZDN۰T($K"ik4=-Id%,9gI5%,eQr2YȒ?i:l+9}qm/]B(>wJr@8A!†£$6:Aҵ]F(oI؛0%eÓm hEe]2֖!*Tt+4v7XR_*ratέ@QBpՐцgLka(\ ou~v |$0p̼Ea©IX3mfṅQ_ݎ.n6һڱ)f{]fsMCR~c~O\rB(>:LV,N9n w*@*=: A$A#Ii BaCs3 '҃(S)`|x$>[կ3@wr HR MhVǤICS%xRyVAU٣)KiQ˭@G5])i)r$ ,M"x4z9++ +~FeO`Χ|=ޛ=o7`pV56pŧۛ? +iq6cv@'5QGA;nr$n[x~twӗCK_no4.==;:F ;|s"bN_ΈW75, cRbNg!ǂ~q5nj5kF{aj@{1`"ĴVNϰ6o]$4܇\kQ4nrW:uʧS_?͓ǩ͙zgx˛(3 ѢWf'z.SUffbMe^+ػ 封hHf{c9g0~nVj&@Ͷ"iݘڎ}ވDQb'JҋPx>P ‰NthU!zn3Y M!`ùE=l8,Z[c'LGK's{A:Įa\X fv~bhО[i`AO_K*+GH %Z9mP#i < Y [v +2Pgۀ__T^+Sb[UE=b#>E%җ(:A;EAYe=2pV_$zS2II"UP>k*JD-rUcFYQڔT(h}9QNtP1KMyCŘ1Ui*ШUj̊pQGTl/"^n*vD2PWWWzO^_P&IDJWխů04'9YBQ4L4[:ɔ̓ԓ&= zMQU鲪J( EeQIp-PaFeTg4PEiZu3 DmGmT4p@GT/|zo{TōbEKjE!E%i-泺 ؘ3uqCMMNf*}SqNHf6NH3??y[z7YEDF*g#*BrQ[uDݘ݌ZFPv,kjRɊRp"`95jBO0s-jih_j sg0`^\  PZ*Eyaf;ӈW߃ i9y +C{\K +1꽘?[ym\ oNz|({w~?>}z}[xJ/7Qңt&$b7~2k1 1RlWcJS91`2IYfC-a $_@T)~h֝nJ *piz4bULnfL4dPLAS,=L;H6l1cȐ#:ר]"~SNѹ [QYَ#R㄄ 8UQ$`c+TPΰZTJvɄ\06g9T?V"S'/˖Nj13 H}bS9JBIYS0Q ͘yzL)Կrqp.ҟoAwj?f7ۈlY[C/v8hͶ 0&dYC&w"-s?ў\މ$ok80w\Yvⶦ˺C*6`秹ܗs +z6]NeD;zߢ}o~G_F ߏ0UXA)`xu V^R_vaP}=^nsqN Thдe9Ah[iLKkn[@#*|FL+hIaoC1OwkXpE(5dɥ| +9Aۯ/i$6NgCfs%O`V`:@ RNe~vPI4zRtv w(ҰRlJnP<3p8j^3i71*l󇳙\~& V¶`VH=!TOZJ>xSyZGKċy~YjB~ژJzK%,CmgR,a>niO ?9 apbOP`%#堶fӛ={OƲJ!%= %8y-@ +0ZG"ߴ޿ؿx@8`ʗZTV:#&C= ro9QmqcQ;g#WSZʼ W8OAOӫ6gX؛E8J<%ߣ^޼ +BZ7R$VJ5fxF@7;w['>]zۊNJ7"fz72*!Tff rUX߰^c̬Y:Gː;$%|tEȆ,֤a]=rƺVP\}WTf.g*bI~E,o+ɾ,\AڧZS.R&9f5; x@8I:UչVa(8TUu^: +d},+RF,-/5g|{m¶Z-SIi0@9؝09-Vu%c;}kxݑ)d(|Ib}ćBDIuN9{ILDqfccѥNt*ЕfQMVu"{K9sHxjzVjhfR[Ԛ3vjqowE&q6}D9Q 5[ldg cTb'PS~ɡkAati[Cf;\1Y[⸩u7]W6eU9gFpl]fy2}F[J*g=wvSaN#*[J _XʓHbV| `ȓ"0Nk.vCOn[9Է)c]H{ CK839S̕dr9CO[ﱡZ91diiTH94Wm^wQ1ɨ>dWT!PYhyº4UO[ -"SˍΌëM:Xۺ`pa *@Vz( WKx7GAMb)҉Ay$d]]A8 @7,*yGImn²{ 4&{}|nCg]!})ffw}=Npo]~; &A+B!**#!5:!F&dIF) Sd Z;չN/K5 nHo +*TDɠO()u!vKߨl2bkMi`o2mxAx9_ς_5#>u !H=辻NSJEެbPDaPp)rRLSJ R:ƬUQ#*kSh*xw;^` ёM*ixXͰQSתk +>*J*hBr*?iȣx5. Hh +F_`yar?/?5scް"è0uy09Wt6H'tX)|lҡ<,Rt9+-˫IaP' aFD\[66 d&LYU+NC)l*;jáE '*oR"z+Wk*^-DnYxgSݨ;/wdq>y?70;.9۠=\)f`mfkSFI63-lC,#'!jɛ! uV-1% :(„ltVn!LHPWbdt?[o?@R$Α34  ` g] +>p\*6l7+-HPѼEiAR@ۻi-t76;Fz<9NT09HشR8K eOe*4īKdM,"!=b $)J ] F F!`UC uؐ3BĚX+żx:s3as41g||#(=>I/x,,&3\(h}˱ny)c9 eKm]d~c,&Y@3$; 4]2C&Vf@$@8 ~XE_Mh * k91MUo5IR;"}/\<$!/">WR(P)<Z'rK-%]jr5M0M'$"qX+1-"D*U)V5?p+|*Kuto,e/lOa㵥e>69tyINhbR7Sdfd&-cH&hAD *QwIyUpXȇ~cc2Mpڠx;+D$K<%@Qt /afܮ{ ӞbỲGY,PY%uE7Iۺ<<Š̋ ྅@z|TWZ^fQ '#4I#ꨆ>@цHQR;S+zp PfNw`aX7X},Fi#fx?~ͪϰ?U*̓]|:n~]}2U) 43a~UrY$uZ%)8NerGi ^|@p#5>R\ð637Q*Y<#`l Ka"xg#T2+)&䱈ӉK&TҲmHO4JKt$L/&V&S\DRI_`J +J4ДD `n҆Jp튜>~kNmw +r%-H)8 G VhixAQ3+d%"DA.žP4 )ł$d +1b=a*&l-P6K-1GYYFkq%-۰nonGTq`bKDN#"",#-p1`b5v5;AMfJ;T>IZqςeArVANЌE\a)N`$ r + : kBe}A9 x5gG]Oв`ԉu{ (E$~G] f@)PBuXc<Ͽ]|'3eĿ̱e1w +T_d#+YWkԲ^ͪ +;=;Ѳ{jR'ꩰ g'-O5:R=Ȓ]4_S#,-8#r|jxd{α4g9Y=s$yK@hu˫/?lnϿﮟl}ɿpyww}}ի7c7vyckX46eޙ_[TuuuQgp8P;ؼCyí|| 7n^n/o_W_=U}ս||Zb{y{]>ݿ͋}^}!oU>SsCPMbѠJQk8m Ѡ" ( ~y5ܱ RNэ~3XQjSc9y#\ʱ{5bc:+o(6{Z]Vr!F*6'\vu(*/0`Edu x{e߻LsruYkUX֋Ɓkc!lxcβn 92 :?xҤ?L͓5,|`{`ͥW 'X z *`ň+vk 5 #WGs I;CC YX.dA:x/.Q8@6 v,tܾ-z +1:Dų͸T&R/PIc>Ȓw,DzRrz!1\YU.*.ז.: +¥Fg Gh_CkLmgĀWS8ic+wkO2~xr0BLъ +^wXOla_{>+GMs hJ!>#\]5xKD; ?T??&.&u, yFT,` s b.QO)pW1%f@ TuYy3BiZHw0@g}H]++a/Ԗ pIfD`Dj_Gih_oӫS>X#p * 7?IW>`Dy"5 Fi.0_Ij5=DsFb2wDkmpBRvg0Td8I \@kѦ%n)qrUͦtr@h]1`2Rl j$+C⻈ڰ?9oy~-ZoCh2W F׫pb&nśXz G,bi@ӞMyaqhg0GuհF7^6lg]piî󯈸*-{iG!z!fg Dі'K g,K +Cz\`%hy#qH@̨W"M .xZ VƊ¦iw1A\9%FZ#?*Cg$qZ+pP:]d*QR1-9=H@R^Я26g/q,NҬ84qEX%J:g<-MZH\3h)K洒 dS$5tx)BL 3A֡!MYO.40HeqpTHMUBڒO@XʈB +&|RO:\vjkP ,B>IA7]7 1ʕ'E=I{jXS.Eܲy 嗪*$BiZ|:$#AuBkFXďt"4e`9NП@cA i$1TdELr"Τˉ}U:A£,rBR:>s;۱kvnKzWUxu@!M4zoVZn +d=u"֥QX.4,nXԢ$=4 /.v|_h^/JJbzo6]u~vMqg*T+ +zDZo mgV!ojD*[։+\B !Zx5f8d%.Gk +`^5*2 '& ZZ8ARQA<ߔaV٣0ƎȹseH0O +N ZB\Rpic?%Xӥ5Kf h*ֳd6b&AhXKưho&%#Ǵ ȚGZ>h>h`4Ԍ$͈4U" 4ޚ7 'GbbEDB +MݐikrSok|<=\vۆ(+\&@HCTJ((Ptqe"sIJ8Mqfxg 6UƈG^ O~29󾾂'Z'#3쫒 !V~Pٔu}:@ $zd4`cW`ZKcVD2j[Ʈ"˘4B 0 [(LZHOz2MQ% h9fɜ +\BQ%;BC|AhMA-P1QJD0q&T  ie G/0;V&'5F69! +7q` +endstream endobj 25 0 obj <>stream +HėM|`]oIC! !-Ao3([${%TWSOűkZyRZ1RRY-qh#R{5N[n>NԪXzϾt|uG {F^R=Ѭ$uy)6zے{ <0˗!X9OZO#^rN˰1yy;յ8G倛i͂цQ&OU͇ʮ}W-\0Gs̫) +F×h%,v;_չaχjd2Slc)?!;ڦȐGŌy|j+UwʁՇ-=t:TS~S 1V+`>~6Y"ݤt4I,@4 1+<9y֋a E<(D MmF^8Kl)ɣ+ETz +i^s8z=ukAQ'IE%O^%9):gh̫zN&gLkʩo%&˽VC)DeCyօCKNlj718[ٰR + V(RKc)J!Pm<ҊDeL=FwUֳPCJiH^dcR#a`25tt[jTb'@tX'L>й^'ŽX4zS#$JjZiL"[HR'$J=>di05-a3G(D:nftHK8LC2Us2j4( w*4-D5!:h$Ѭ `"h:bgXz 5_A{uxѹs +ɖ2\1cJu +ȏ_+H# L}}+]Je,"h[mVH54?n6Z#WoRdT^G(æ)P_ ߽O?uU?O~ݏ~7˷_wz/w_>=~]~|?^26SlM$jQ&{EGY3)62ioB` r"ba,5V6{cS,M(<,a d3@ݘp@-tZJ8Jl]'c`B˚:A%Hӝuc>xNPL*@P*6}U9Ib}i;6"(PT +6 Ns f 6V^Y -O=M"3yŽXŤy + zx=Ca t +̪fQ:cеm\VH6ƲV|[*5ik=JngʄSb=$]$5_p88=u#@ +Z1<1(D©۽cfLN<jZD>%P`bʢ$B.H٩Sh 9"&E1}U_jg؎%u hH6"Ilu}m(g3wBXz]J hx,TƲ頾JLX7Q-0kCi$kͧ %x3V حҜlWp-v@:X'fa1W9>+ ,6)b\(bZ1\,~ɘ NW 6@:ă|S*!L:=Wǚ8+eqB)S$OetqsdF?WòA.=}ˡ}՗զʥۅ g3υbj``}1.gPqp:UŎ1lx-3#4;X/^I+~$b,3P6k AHdžxJibVR}aSVX; 5(5ZV1C;[Um+VїFw&U`3Tq%`oKgz}""K;H'H J¸Z?L}fyP_Kaݺ[rl&TEc`o /yI^ u5-o$٭[ +T웡ⱫQ$}YRK Ml MxAx Qk=i1Ma^:,!qaZ`GYMΨ洮dŒ& +FnΉ+Q=`E.q>jO H\@rf90j>tF7>\uDT<+!fE!K +hxidhr+<^_S7g̨Z{QR>LJg]|L +Zdtl p A6 b;HF-*vEIk X?@[Oa z70osF싎s\i].z d&M:"ϐ͒?+JI\e @ZnDo Q!][+`l + +ܟxZu+o]nG 0&`ʢkOK&5 -R,*(yz1n" zTQ8s *u-U-8&ն$2T&Vhv(HWMƞ+#_<&hfzM4a7!0F!l&4 A%itjd=AW{ƀr~֢ R2 4Jn`TX4F_ELl#;/}Uˊh}h̛ۤyAg hz ڝzJXo5r/51E#* +l*\RAL+ V#b5 Tm)MˤWViJ0v&{VZ$?}%@I?K2 +:thF0f,si {Υs֯YK1~Q-b>MA RQe\⌨jfxM BhwɸU*\f&c#ݫ6qd/ 1$J8h՘ӸbFbB y{+FzX.UtJ^KehBXWվZLA}`+7 ^[DK |*= qaC!(܁bOi^yo(kV7b v ZK>X25;T40G[X +Խ;ҪIa\$wdoX$ aL\4olnD0eilsO89kQMx/Hl#ȀFV枷=j0/f= z4AZ󊀋'b q#K=VG>%Z=:a B2xE?< ,Od<u`e5kRBZklek-&]3L#A[A%(Rej\uF 3_}jzPZa"Ihu +4yksvi*! pAH{fTz{J\΀I3"2472شrwO:"M6./]zU hegi{`& (Ҙ .d9 +-@ 4؆D[?*a)Xȃa>e` +9U "G:˜w{O#us\].:Upll֯`sJ.L~ Y&?uj{,gf@PDŋvq#-zbJhKEoW0iW<|9Aߛk_=_nT +ԣ?7bUc\ӥ[}0Y" +3 5˓BTtUGnإ~&sqj_X&eCv'0YIUG變GyAG2{WȸP,8 fO̊I{\TibrdDQ>KF8b #90P8HtO{ViCBAr8GX~&aS炤bo޲X^%]ekU/-lPte¶ɒD} a[䂐j2zo>!-M5*MMb%41ĸIz|?ɔxc7Ԭ2МYb>t 4|Iz5&03uşSjra\ΈM&6֠)929(w3b_ptKSN_,p=~@mΠdF&7Ժ`#` [f]txIOX[axڪ?֪苗HhكHڳ(y࢈h,\0gb||ƻ^ ~ . _&/ғ> C=Wz\CQT\?|1P@Y24<1eha4P"B`ɬj}vo#6(1/7; Hg"ΧgՂYu"L@ґ>lEݛΈu.+kW>Sh l❜5nJ0;|DFʊ&(NF[%ub+YM1C ]OkeDecێdD#ad*9!b1靌g%XEtÌʇX#ζ/ecr_F틩 u"LeG;*Ȉ+}m0q$6ž43DL/:L *H   +t8m228tC4̠)@hT(H8we"("_V'ޢ e3:AV#[ +NkmIAi%Wr1XJHutbYsp7b ,WZWZHgAHy̠z$[Zi18]xF]Q5ωW,i4?/9VEO5iD #O~P)Y*!&N$xBG'=EjV(߾bsT' R^!$io:3qYt|*WZ]E  #1@BdgfVL/f `!ޡ# ݸΌq mA׃/B`r1|@(X3Ҕ䌳xʙ8g (Էʫ,ыngb@!bdbϻ٩,'N0λ)g3xbS,wL{:ˠ>y-0 - KX%`=ӓU?ҔǜD jNb`gpk7K?41WIƊy |ZȘx|&n;+PIvC8.#$R8RA*DUFX惂je(@M$2Ch_ 8Q[l\\P*LYtTc*8bXl@<Ul ayQ[*s4g[ => toK4XClΙAZ -p,$}h[sE ba7E,]JDI(dGl +IEM!Dz1ha*;%\z@"Ieޝf7~( ; *ĚۙkQwu51m(ԝFXZm\9$6(rFHs/s5PH5#Ì +LG4͕?H`ܹ }/vl`4=h=_L KV*RX+ <JF $4"{爁'xpOk0:J;pJʡhWQ!|}^^'k;HOMn{Û &3hc:M?`K3OOZ")y)٧Y8Rϝ 9|GPo <1/RB9k &FM aGrq >}Xt0wrY)#NYZQZf+,A.x $/DX> wM`"gqxopf_W@X^qM-ηyϧ T1D%@яX 컭<Sv#:`f^P2+2X4A N sRf &-t{BqGT*gCL-̖o)PlX'=hA|=_B2p? d?qx$=PW W!$aYr䆔rf.0c4\b=qSϳ5RC-ӺXieo<$`G "g*8ܻR +Pb:,UwN.·OUDVsC&jpx\'D`7uQ| CSnrKJB%tE<)vKGg6(3&AZM") 2$]K`z*.g?[,0 %"- +s D+ilJ1K^ ]|LŎȍ?LwS2E5k6X컦LEglIOf +diL4fɷ4CxKGp +:Hfgpl#K@ͤ?8}Ꙅ$}*uD?2 +B,V,2[q + Z6e} y5bOe)QdW= :;s)p#y+-.eX }ņ9,3i. FS{_k>{B$^sR0 U::ֱ.?iYoۿ z(jZGLuwo[hD"}!^px?bS -8* rPU^O9oiKE)/q.gg~K MU4PjzAO苁&Ō>ˁ`K\ ((5'H yO0nXT(>RѸ,{rYQ7a v#9(/ݸn+ ;6s%*Qh`E1P3=#1ɽG枱Л#isWĹ91 ~{f\-_^7˧ۛznQ$?6н1YFC ++IbFQ-5QF`KIl75|BCf/aZ^0ANPR[%>5fAЩWuU+ԇ(e;׭נMFu҃wNzgPpr@ 4jRIQBI 4x/c&zMH|t:$贫M(Vi& m||f#TU_5z 3'}wp +l{ +s s_(8WELNIL 29%NQЫWA^.&إ e\t"FK. JCmK҃T&TؤF$9l4`Np݄T}4xIWVܗux'W; ͬS;8IKх|KUt1;NcDi^Rv@|} $l 6CodMb0NMhA]6&WtrQO~:^nR,)A06ݘl40He>u(-_nVՏ7\oO7M;(PoHPgnjc|]|ާ^Ac\Bu^.?9zx1 }_VWyܿvuu{wrS(^b{u牠O**ZV@?-o mzӖX=OLCM-uZ:o$Mg2ԂMA^/s%?61oo//Vz(3QѨ}jG{qo +?/տכ5w_}${G_. +i/fP +h։b,,:(iZD^$iCE;Ed"ߌqJr,0<>苀3[Ǯ&`D={P8QdPdĢPw1g#JZTnNdy3P-dP;RBhe`uiʮ2U+eE +;(œr*T9K@`E"Ms_ u/=EM-2".VArQ>كC$L'"XԀ( 2Zh_iвdT/v$In@+|AY]?nUHR3@nX ^w j;i fdJYX8RzOĸS+Uz,Xȴr|f3NƂ:,{:rn c(\⮍+{N*ȌK +l|7pwus/"kyI s!IenmC9cy +R`uAE$O jNDz&i|fD= EWB9Z#+b"s D'SRccy۝AXRgZi礬g YaT[Hyx[U -ڱH .^ߣj稝A#dո{h'.ӓQ!oqa|U`q, +.ע@\ҚQm ֗1 NO`Z~jz棐ɏyLKD䨆I1Sa u+(m YX~Ĕ?9Pc!G.V<]]Ie]S| #JZ:ҩ4hFRi6l'`R*5Oj!Ŋ,fXcZ-n#c1zɂ=-n;,OoǢZ9‚Y#'k}HF ^N(<)7K 50#t +0N14!}վA}40wD= p,sa e'J'*%auƪL;]d>n @kU$x! xRcՠLA|oO6RIE4s]$w͊7ʺ,0hp\YOOU,y91]cU4@(V,Y(d1 X1,:HƪI;#9Pp1q=]80: 9':?Pj$=CRT8?،n!%(X.aLJp7get5\C ?,j'BD4tD_J'O( ]!>՞yoLih~GMG- jfz~qU/B;\ӊU@>c9Uɐ6)uSsB +E+J9׳L + P8!FpwR8l-\|Rh&fW|f(ne.U AѤZPl*q`ePJ^Svt-rYm(R:J*Μp(ͺPq>"dѢc T# _54%EPTYt|Թco8ۉ' ]ʫȑ:hG~~qǒ]JnKu=pqi f-oZs@R>D/5]j.F&u yjf|ʯK1HU"(R%Jb+## 0R*/59+5@upE?&mgj҂d!\eƼ-J~t?;3p:H hX%^vQF{!o*Hخ%m)ZǼg$n'BCȎlǟ~48hv U+U26]"Yz{ +Uܘ[]FlJi*A2TbK7?J,8(PH_ens$%R1աhAsdtʭhMf7Q?VtA-8@gz s3)ZRP)*(9]U8Te  K2܂O F5ȗ%0QřeImSg&,B@M~L̰;2vZ"`…'clCdRi' +>.&v.!n7z`Χ-1wwSB8I?\?#?0j] 0% +.a%E%I1 phW"wE8avl{2Bc>ywI]4ՕAF#ѲhO5Cՠ xv=X@)WpsrO9h +{>$.0CU*:u="M'e Jزⴒk~2-4 `AbP9*0^-3C+k6 Wu=nݤ{QsB<]&7XFB[ ۝q]^Nbb*&)r]*Y dg"EMu(Mt +V8 +Z]VEpF D-Z@\9Y &&a-f%%PR$DNA |4wwtΜSM7 ژq 8QV. eWf`+`4έqze9?ߋЏwFXkzغB<Y.c +Gth0LZHg1}+rTpX3<ܙп֧ ştP֏<5.i=:UF@[1OݱMU}RL9Pwxl! F\W9])0t.cG +Bj_M_ tH t-(-6zpu:R`~xƊ 8 t8>|FIS%zL^h(0EPB6(Y;)D \,( Qc=^33+Lu'1Uf^.˅6aw B :v/LЭMqIi[XZ|o­B_BCk"k$>)n&~җinWqokz;M?ɞ çu*0^s1 +vC"Fxv?YJ!5W`RN)Sy11qgw9o]YJoSw{4t*PACԆLMhxi>\jg"K7 z IF 'G(j F@L8*;M5$WqQVC$uX۲ŧ2[SnІb r6oPQ= HȐw:ORUjڌЉ.Wr`:uu{̿?#pGPus-ڔ}C5ixNk\;~4 +RY43k%BNgr>3(/nq'=B2*m@DEcGq6!@;`FZnZ%"hҚI`}%clŕ5eM>vNPQ(S}vji@@.`>'NɌVjdcRLo+XzdӢq /[PxAJzerDrֹ[}#eAxrJ!9w. +֊Jjr/mBj{ϴ $ 6,J aF0e=cnpq<ؠs'_2^pgѸ$C9~2x+Hq M:v;>B s@l+kvHwNm@Yb OCwy4A- rk8uQ(p7m(JwȯCA>34>WQIcl.Cu(X>ݡDz=bV `#񭑭R@wLB"5΅k3@"nV`jIrvNOy[4 u]Tܪ>-E)Œ{L-*Qށ;9)}PA JAD|)ح|a>>8R Tb<xje+NmQH"(EBO +[Cuֶ"ۛ6|wش;s#P +ɰXLӺ.o] AfD:7 Ϊpc~yq1]OF(zFhj KΆ1[an"W`Egg@nى4`߾,shLRpsBC2ʭIw~l%=;>44-Z: +k"Wɚ$&OP@/0NPӄ9:;$itpX>r0f>T1#͸>նњмfBEGO|-wԡ9bD=dh/0aAQ|pV:,~@AqA-[`sFqdt{hxZ|=BhߑWX+{ Ф( onO,UhDNJws\ r?wk<\>S h.rO=ڄqG=MX}@-w 3 B5p1ؕn8e㳥!p#3>ifԌ 5\,'RK= P'PqI֣z)YGG6xV`^_[Sfh~߃C>v5z^AtrDd%;ya(3~GVcA猽 +|_ ܽ=(@TFR5ݻȔs\Ui '6㘧 yWl *6 (cOf!^e'="-2X'sq?oe7?C=jaڈֹ3:W +\4AbFUWq\b*_j |p5G=S&r[~[Xa%;DYw!~?w$ +"*Cg vP3bY)r2 R 3ntν.Hgg3[mZGy,vz‚jn̩x;U߼~O2?+205 +M9xZJHŀ]y +bSLP)_wel.+tlC)B0H"!2.Ws^Nq2S-"ak՛*dh}+3y#䃑vsE*H II8cL +vXr&¤+M&7C%!Q{0cViնG.Ag :5]Ǻլ?`tiŜhzv>%%a#_h6A\.7\RW0AXoKMYUY^=ȿACag)\U[?+PB5DA@+_kL E6^|TYoZ,Bb%Yb+6c˝ݧͅb01PvPs)4oYT@$%{&44)^ 0y(Yug'c +ѴjqI4)F+2@B\b֕YmuR8O햡 +8'A9`Ѧ%5GTmOE{2yzE rXVw 2Wox]5wbl=)jؖn]+ ug53 (?J^S^_cdB8 'W{ܤ__N9* a%.Zjk)2SGY@=C>>e+i ho@IkzCK?pTV{mȀ3Fn 0y{"0f;?t9,;*r[ +GC_ג"#lBe]Gp}ƇG$1r6?è~gl1ނAM…u@ T74K0 %|v _\ɰ&j.bPSO@ßFMԳG&CtҭƁeXԗAi Dr灎(+aO@h +!xmzOQ[l&@3+&ɪU]&ͦUˮQ:bb1YR1F:sFOcNÙ0T&_4cz$}](# \4{89t + $ +D%41=\2"w$S1ؚνI'ϴMBV0qA8ȡo栥*FU 41;)&)`/< ?} C3v51Vf3?|p ChF‰$q伧n?]l?+msv< E?c%Fw&萤Pm y& #>yd{Ҍ88"N]^gީ-} "R+)"v*%tUfP70J#Yn]H z4O>j{Җ#V/{h,;-vk6l洠Z8}Ԥ4hKjxSU-2z>F!P! ԁ_]Y43hb=*VDFDq<y$ThFWem m&bFph|l rWw;mPk BEv,:~:|D.Baf2(.2!sWP3BLq%=otΎ vT5Ful^UIRu?iz 4E: O%3'z/<UFX*m@_΋gB/Ig#+dkX*g]#JȭV1v>K(bk Ԁ \ vܠ0S<48NϮ>ZP bx0%<(/а6)d[Sih»l~ [!UGw&8zhh# yԮ+9F'fwDA,,ldw|*oR# \MqǡPX5J۫<Ex6v{[$zݦ>&aDR#gB5ٗF骍0~sUj42Dy\ hvG`ES/9&Ɵ l:)@ZU5  GB,5Gs91-&11[@^ତ.&.UeQmú=zL.1ˁ%'͖M][x ;等k΀B()9ݘ$߼ 4s +r:ש<ԙCSҦDR߫g"!M ;뚅ڦJxRU{V|\%~tIkDAz ETo&1WM#M@85`pŖnS(x*csX)N[`p&w 7D't)0\T{tC_2҇St*hPUoaMNdhЄmhMvFy׎C0yl~߿~_~}e#ǍѫRVW}1o?ÖUL27("j|?ϟL~{bow zpJ%(鲡Aj; 7 Mؙ֩h(Κa"-[ʄ";P$k䞬\G]+0-4Ŗ喊 1JMg50;iHf4 ~w"i4wShH=6t9q3"B_p2)LgGG~ʸ>)W^"B낝r l?_␾bBk*|<[f\OK`|}]&+( ??GfjN)恺3t:t-"]9XiA\鬶rVrQ#:&:kL 4ﰠM8*e +J(2e2EmկjǶB^E+`}\3h:¨LG?$əx` uy}k̩FhјU 쇜E4/DžFЏ@)O~ktڦT&b{ȼ>{߲O+r#LRBSTd{ EAO<3oޗa8 T!^e1GpB-,ֶ\$F{538еYf/p˾MHnTULm(OdWr?-s}@\m[B0r-.u! !$VC!1P #X%QXaYz;漦n #,Ihnq_m+!4   ٫vw Rweom8ߤYjچ̉K5qgxpOs.Zcz^@iF#enk<c-ΌX[` rב8S|0P#>0fezD-P9 +3Ű!l,T>~+/2K%< + +/;>fܵqjlݖYA@W|ɞ>8b,2&59Ji/i*҅7ԩ(fڠ eeb!` rvVhjMQ+z3GWew@ޮq`1B=apidڋYؒ5?*,0e~m^88 +=1?[ܰ)N;ܐc, ҘAhEݝ>"5赶6hhE^4&Lj OdU(w0CQ"Cϫq&l#>똁q:A(l= !CRF][vW[*[~l[МV5; +\V?J Dqh$:*^e\S^}UpI bĈ(ƌ%m -Z,luO@[s|O?=~ٛw"3x/>|o_L?9Æ7bƣMˈ.%GR aQ>."Pվ^9Ix}B-{YX1-M6ׁ@}ۃ>m v% Q}o#ω:gyzE Ѻuna|Ա'[XyCݘ-lB(r0S̄c4I'b9%HgakHfmM3T}^/a¡an=.ANK-_&XlAT .f VVB\ٵQ0{W6NHc0N4@P#LN"0@z0F 9 ^sSt,b rm7dqaS> Mmn,!,GԊD&%:54'i+!;s^nHVj#0)nƀv^k + }=}g|0hb-1\!v=)[r NIDUgf.9 ^ y %dȪnXH?̬Ȉo$#@:Q+h-9$ez› +CV +r:MXX-k}*q+YpWrI5'DZ6 iyϱ9Eޯpu="QXdqU:F `.q5n}[QTSfB Mc: i5a)^ X Uim1kh z)Wr4F19y=M?&rAs^ +2j4f45#oXƢy҈vcSV h5SGM-flFRFSƀ7Hgf;0R<7r; + 2U\z)X=CH'e 󸚁+ 0%H?(I#msM>:b5I>fȡ£IˢRn" 5U +u.ٶNuP'DK :·5sU29nDBVT;ga{{t;[..pmD ™s 6 rl#M׮wrhȆϪuo[= BOkgK&1QZ`&6 TWVIDT '5NtC4M1ӱ0G˪dPGxWRǍ:y"bL*%KP +A)JG ey T2CsHM=2Gj֣HZâظ4wڢH#r8=m0F|$N%""DKé8|􎁮BzDV@ <@Q7F~F#BkLrXT Y ;@vMVEJmQMm"ߖ@/G:඗[}Tےibc"VycWDF[;_PmJC;e/Tqci467 A81Y +5b9w|?{>[J + _?~Ƿo?߸d +g +Nyx!z1^U $y +d9}z"4H7ʀh4`RsH [\4vQ=zLݑE @ʜprΣФW9mU09m(|j|Q<&)f!<_6X dk*vMgd`Ёz^Cr{)h+pIu @m[a8o>9([#r72C*HT.ƈO;Tmt(N;|ORW|(G%f;<Ή-PDy=X*ί'UK-H1#߿>5Dis8Q-&Nǥsi8 u?u?`d[JR2Ռ36/D.'ķ%a)SpY}AI|JIvfƳmۨ|ÿ>~BGckrilDRwV<[ ԸB*KͭrxFǕ$lӕn0=>nDת`ܕTx.A]z +yC5=Aᘫ DM|Yĺ"̘v8Tq;i-a 5# [6xǫrz }K6R`ڛ@&6.SϒܛӣlV]c\drc`#6X<BƜBF|S o)JtQ#ND pGne+1`pYi:+Xo&;|Q\p쭌@<$ +rpc?"uYBЖ5\bh 9+܁dw@|r%p4ZL2YLn_Xqу̎pAy N +䛊Ebq,ufCGLe2-eM,DP+MG뛂 ;s5;Dabev7ؿեylGI5Ջџ0hq5TCYԙ|3i(gO~!J?2;?p +endstream endobj 26 0 obj <>stream +HM^ &d%!x! ,A1hT0.4wԩek_Vu3nsɈyZʈnQ귿CVkaqV?jo+&ȅ?X(`@eʧc5X,s'ŊasŴQ1suZ5F4ŜcKLvoV/s*W0s s|#Z%GT#,L[ok^RGY\l&'m:;苭[ d|zW|z),45<)>6Q~Υ+鄦2jè'Qsaz֌U\)cY|*,3onY`zkʹFkhڨ#(:Wʈ1jvn9XA \5b +Y +Q` cDjFW\$bBg#%ԯ^O<e__w:^|ׯ?q^C_?OO>^oo޿{?߽q-ë?><>Ӈ:^|q|OYo函7qdpL)zh5Zʬ̨M*~"k=h'.?V[%aOWPĈt{6 \nePI+7kddG2!a'rLPXsNiJ 46kv j:-v̲lHCKvHަ=!Z.^pDK #z;;_X"6Lڵ<Ӯz/?L}RpnZ. +m`:hm#{I1Z4ۄ˴܇>\Q +(r8L u,{ fɯł{(n#0uV݊Q`f*1YJCEu +tD)"dkCD3ֳcוV(}w68U#AԻyMH8Q<^;Z0ν-!Ggv~2ue^8۾#*)nO_;n,=aj?XNUlI $8 .$q)R"e^Cp}("Xv(a.pu13&$)d *m=[`߲#44D>ټ.f+gv_cf*skPYG8rIh)2a6f ARQQM"/$8)jdۡAKӊeV%oH-U,IoI0j!.PMY'.R#[ڇ#}  Z*%QtD< G8{K +^P7Bu!|6 +p.g4׋ޱNJ;ک]=Q%͎#;^릌s((*y2O竪 \Z=f\jud1/*:-w UAsf/Q/l1sR:UGu@i%gr0rDǥ(\OydGl@ E#)NLjy囦3ءyKMH6'AWƴzO6dK<'핤Τ`2kҘoW}Nj]Fx$J{oWnI"J<ȓ70g#կ;^%jz VTfc/'u {*lXSv_Ų)㮣wT$BN]Lh +|ۯܞJ[#L6 N@qz#s v\ҥbr+hN&כi`4g pɜ4OST-նFY]xFv+/ѯ TCIKR V:$=['ZU:#xxILA&T2$phu"&[Q 9_$~s:Ǒm:Rak螣g GJ%ձ?6V}}N2n-c*TM٫^`ҥ:1IoiAb߫7o9$ئGA"rUZ: 0e/iG($U& #Xʕݒl޲q|R#U8@VvLrj@g!%k"Nil_v'b_$_ yY ڸ;{dҢL1cNpzȝ +z9a % H=6FXi A{Q~SM~s~#Q[zQŽxΑE C4*J:CZ+Oǡ[āw)#+=**Ԟ?c.][i ߍBW`5[U{i.jyJ08u˲"gfڦR7ɸ +GTy/!_dNc~pw<۱vU?>)wSȏ 2(I HJWRn,{t*vmPE%HU2c庳P0=늽@jLxl ~6Օc u&HUeBUe*LfIDm=џ$u^,ur9XA+r@ j@mfLl/O%ܖKPe;OpH僉w 1N'e0xbi8輒sEZ|5;mhf~~iELeԠqjk {)̙s{3^^x_QxR >DA 7HtR?H3):'r/1]g\ϵ]#恿zxz P_MԣV%WdgjRP5V~ ґ'8p;oe۪@! 0 +IJ}Jހ"BZ׶xv,W }ƒi'ڍh9gbn 鏙 uJcS}H<LT ]"'#_2JKXs}:MM @n}kG5u(ކxgm2\ȉֳjO(dNЂۓ,trbU!Ɋ(8{>fESo0TvpuuhJV :yH-À +W[Q)¿/uJ E5l@Ź= V5}?3@,j PeA1;WȑjĢiA_;}-ήiQ$2tnduc [d]TTɐb7U3vv;h$.jsAFf{BB2b sh$jEYf{%sKcIL8#9cEw + +r&'51ߐy%<<>udxaX0%H';pq!N0sʓ:2/A0gv\DDv{]&[ѿ0}J/seќj9@KV쨶aN)"̉A~aY$j9X +$ 剜ECtk1mܰ+"<&0$%D& Ka8ed#Qʣ)cf 9҆J~弘w,=E&)Ge{y6š)+MAN M$m &H gO>lvJ89>=rCPjӿ BY:K)*8f.5',Oǹzx +gL+'Όv.(@V.J-!8w8Rrb˶~t69ډ̫@~bٔŕ11P!ZS,-K8W 'ؿϫfy ֫ gKr`5r=ۼQnSZ rnZo!O6"n/니?;H\6CH<0]s<\6) (Z[qG;.;@åɞ%l].`:=<Ӊް:7(WF ˟Φ mD,vˆ icIvE ivT1?2(RF2(/H-0.Z2fkLgKtJhFUxV͘gyJU-Pi1MloeO_SICx>jfcŎCL)%2†WIuZ%ܐ_XoH0G@ |.DTudҌqU_ XjkPT=y' #ͺ5Y_}.pe=0(t^&:Qݩ|g R68%e䣠&Emjozxz_W BM߫iؘepp3թ ~ : cHU +q'[Il @_iaՌ*xFEBM!mj/#mD_$c6LV x+Il@Sd'hd5_u/cE+rh3_@ċ_H!TYb= +_!'t~OChLC!Rʒ}| q9@F+%5\Vh-(h;ڥlLi.@֭+e!BX@a j@vW.g}SF"6KIkG+y_< h5IӦG54QwH9BTljS@Mor`=} I礼!!7phGO-;a8u8 c?fDaNO4!P{4:n!6'ca~3g꣠(}K(?3g<)C?QgL\7&?sDbW7X̵u+}@sy3cha^ίZq:&t{ D?&egSry3XD$3,,exHYA5y:9q#]chP ; {[~3NV^xyܷ|Q/徽R.H vjqBnw-8Wb5Ś1bQ4{|h'!+U$FIsWhZQ 2"~rv"J^P@-mr']iBC9nd;7Mpc5NáꑞNWFC'O{ +wzl|>ZgF!Oނl PҒ9 {x%dkl9/x,wp& mc콊sI5lrU 8|VS%K"ITOw2 'dsGVi!є.{9 'Oy#R0 +pPj=n:J!W@PZ`XsiPXP\(Æl'…a̶!8F |j@\ %6PXoCOͿbktqpLεܐJp ܪ\5uAіOoO N<)baD^jjxS  :O02(U̹h\3hfdnȑ4_]>ՋbdRi%Ob-1F.dy`T0},Z)4GMNռؚ#hɡIע$M (%& Z,&s˾{& *)F2MEa]>w T&jfw` +ʍ(ZYf݇G(1Q&C BӸGS (0Ǟ&1?@p(:iS9na:ρƽ&U!&pe􊵜/-y|n\\CY]tNbÉJQ^Y./)1 b/w%JkD{,HN%Mv#5H54dZ&8$$aK 5}ߪ xq=TEerLܴj)n8y×o_/]97>|yӿkw7: H؉Ȑ/c6d'ű :t7BmHv 0Sɐ +edɸWDbP P: +5P\oTr߮zndr0S0qшrxx$e< ~_Mج;BWb292KHsTNX<-]eH3uKGxP@N멓+BS9AS&c}?"Jviױ}/(qQE~J=]3/]v~v['jD BA~kW%Lr-(7ľG{B64J3}?&IF&lk7 *!]#sGCQUIMIn,Їaƻqgߺ" + U2TlEz;e(9 "L!"0CڱOrt4^g r X4gРi Y}bŝнY 2$@/ +Y[G,rOp|N)-q>MToڏvY#V2J\M3m5 YoN@DAL2MASE L%#{BU3de5ANH, +^X#'i.vx$6:Iv# +r47EI`Ο4@CAmɾ@s q.j b(=0O-j<Oa$bm2 i&4xlCG.> )٣<]m +fv +LJ-naKPB76ǮICfypt[4aWM͆( 8Exށ(1g֨|u񘗴!ˑoia0e|xi0b#!#d!$&eh겺2hdmt"e*Hю`;pSI@9KU|TGa*+ 7 ':B 2†}bǼiS6d^R8%*;kA!}qja_Yrl|p8ϪMV$ +7,l[m5: +wDY +ܻR +JB:!, ysƙXXYm.Sioӥo]*ChPjs:ļx8A3@A4E$ۼ*Z"W;V fyC]i:}5%9nd]ߏ?}_?_r_?_뻟?_q8oJ-b^eũk82U@.!Ռ[;pm1Nӎ!*V<먗h>^5ؠ:n'=ڈJ'V}&2>!pWp +HG!ҿAPUc\e$ +:S1+Hrqc#;Wvw`w X(4`8:wMPww N {*>C哬ޟHV@:fI)y kfvᜄQ:U2[x4/): 6ZƔ;k  +c\u]TWx*QIyГJ8^ mͅ`:5eYP-t/VF*h@w7,^>9ӸqkOY{Q̗Jh 8pyr0שջ4㢍 xOjE'HpKq9& 5ED}N6xe`B _WQ]QQaRjcЎ6`3v`bL7!l^CTĚ줁;@&rUr%fUQV<3gB)Yp8:WlDbTۈmʛ悝9[ RkQmn4':\i|^y4i0 EZ{8R˒gfj"T k3 gMENJQ. +!$ΊHuD[NYTIj"\H݄<鯷,@9>ohT|z +xYan([r:;SU-EyK՘#n]I*?6SIh;(ʦlO>X (m ن<fxƚ8Qnr_?٥+Lacq6#LfN BnyrQ ͐c!c#F$A1u?g4.6P&nfPd#K܆5s_WgTlհMK{DtN҉wPihڠT4x,ڞ}V Qwۊg'Bz+FRVpˤpM𫎫&Tǣt#qƑ)G0Ӊ-v.1~)O$" +[VƔHfM]gL nw}Ө& +`j^JlДv6uP:*+5"5y;]͡ڌE +;=%mHM(cKsӗ05;y^ⷪB$+MaZ + 'I/[r~gl?KyyZi@#]OY^6#^c 0DCڄ#W\r ]v98 +y$As B1g>|CN3a8b[?l<[>$A騑{rIJ&꧑xݓBhyVdSSz@%RmW{Cϻ퇿cHE_C=-o7uY)qvҐ]r8 ]&z=|D PL +*`3!D1.)ȅ^wz)|)/O8:`8h1c=ICunqS!LSԤ~y2j]9Ń*s\>n> t}&};.hjݒ$Ƚ.޶H3G9/>ph8M + GM߀X󱡓KA'm* yry#Y( f@ogxX/~? &D# YR1AD hG'YKB,*tևrޢ9YFȜذ4[8_ mv\~"J XO}.O{n+(W r@~{g#" +<1>~0! e: Bf F q{X~ᡇƘJD>4pnBen eٌl[~1+K3 82 Ds-_خmAh pЬn]F|)Om"yrk_sY~޿7׷?7?}x㻟ŵ6۟yy7^tN!n4u`T{}dC| 5MZфhьR.oϹY{4Ohߊl["rkZz4q0ՌjKmUUzl%)C5_(W]G +ʥ ゝ26Cqbf#8p#MR3_M, + BɐzV^*MTj`h(FёH[^jQEP+Vا/X~-ǂ4-8)' A~Lqu,Ա9x% Gz[ G`z+3y866[)$b>A/ hkY7ԍ zXYZ+ N˒3`6H:8L1!n`z1⁐zY=H/h?:f]6hTUXPƸ/+׹ +٦7aAӷl)F⋤spΆ7lM@W4>hFqSijҌj,ك=ݾ)@vFqdM1,@ +o},[ +; +b4O Rg! +OUi ^M +5`cAf9MS]x:KnD9'kFI03D F-&Aтv擹Q`y_`q9llxĀ{2`'ۚhA`|m% îV > +|*N?f׬[x)rY[-8XL'mGs9WHOU vT*5MSqEr*lkßԈU9 TfJv55Y0hhА1Єs#JVLiGk(qf (d3نfkjV"I3,Hذ?f8⎷@%6Z -na 8& Ȋt% +t$|S: GV"20?z0?y!=(;<ӲmB H|/`>Û +&I dpMD؀jy%7.&R@UyS)qȏc0A&0̹msRJeET%c6C{jmjl4{ rIES +`kGQH*hŨ>BD`J0&p|to*dGm +vIp)QCU_DU8aIq894Έ=}(*J}7`+Ymb%;)٭$ǢPW̓<爼"q=皍hK1CrZlߵ@wur_;-Q|k +rjT!Q=G23..6Cw$ɑ@+|A˙ +Dd9t/pAosK `ƃW`.%̖o:.񙫱r|SR*0šlc\a@W^r&Waڒ#aj ҍz'V؉e:tc NuZ"6E;ǚ~SK[{EKo[#N.qE7S_Y_}b|cib :A{ ?ӫ,IG֑2,iX̏m֚! +g{vnEq+ j2О{|\6.O8+`n}`~`uy*fvx \1"__Hk[Ow}iyB8xƻ`|"]d.}{LN׻'A7.KKd6Qd&%]lprLu= \g,fd%czY?6/cO)рr5{Aua1i;-S>@X,F1! fl<[yEb@Etsn 戍LFթb{ʕ~Z9Vj/qYx *?5o̠`6ɵ42o Z,VtWp0_GS`]EQHxqVO"`\0a<=X-!-$lu Qn? 1HļhZk"V[od*+u m_R|"Et"S̥iR%D}[MXYiYeic!.05-ࢁ+favذhO6w~8~w"JόVHO +FY){U _=/åsN%&+>+L ԲAxaC(sF lN%wR:j`eء/3°@Ԛ: ;(Dк2PGr;$AIӭ|K8ID Jk([yIxj |[`=$x^uo759% )a2:0x12ͯ7b2$Qa!l8;[g79 s֍ Wнx K>8RDM+Hgilk=j߮JQ Ybcm]M8Zf9_Jׇtq<"T+B׀TZT.62NbFnAlÈ?O^6>@?䂟!G /N›=l|>pT,J[VaVIM4]l9“dmŻZD咝8nD)!rTsf a@w\y#F& cRGLz6y喞 +d2:`6_7} Hjq@0<[L =)GbF-xNi8x }O}Y3+Wk_ dDJI-"A D$n0׳P *h +2;]Cln(xL21u +^wb4P k)0IH#kEV;t/4T E x??V B](sbœ!*6C?2VKQ#I/੷rkx?8 7ML#-n"m\Cɍ:& &nFGP,\%V_XDexRc += +C6nv 1+e&pFiqU7hF'~uJA!@tBpI ա+ +1Xu߰lOP@h0–o퍅)p>WD|C xFE *%yͦXy^aF#OCc^7X>/V4d x:BZd۰;!K;kS iW!,,}珻;szbvT[sVN8r&IJ@̾ÔDÔ,i Vb`_w0dt*Q5 I[kO[dFSs!?!g~Y~l^+-}ҭBƕ\1;w^QmE|wtAhӿ!ƞ^F Go"x!*=#IK=wB'x肳3 +ź'R CνmQ|"n2?a} P3!4U*gٶ#D`ܓGr:n͜|Mpr?fMXܪ }{#˶==κ.T~|'؞:(F5jq/ "@{V8r2[A;v4ZI"TT;O1R +3 |V<2 yTSv#i*dSYz%;~U\Ue&Jyw(W݇.y $c\BžxLXO  BP|a?}ǦsqbQC3:Uw'e\@I~ۏV󵔰s01մ$d#0dmD"?aHwt$l8܈8оA62 cX8s(14d?'l==0>Mqb#h+Np_ z/QxL}'0ԭ 'u TgϠTW}8c?PMzT uV}|rٞP gNHAgݒ!~Qf9pzIe'RxبKr$'ٯz9hXюi2W4jnMuOV%S*ShMYC)=D) .?[78E֘]NBzcRwvz`'t%5aXu)s@y y`:@ 5]!-2A=]1dDĆf(Q>me(̌yȕ~˫w {f-ؑ!=r&6 t5N\#i6M";uI@ͤ)3B H>gIflچ\ ,(W.Yfޱcs 7ˬybYN#-x /=we6g^̷0P ?Wƫ(v׉ +!+ #O kJʸDvGBF 4.Ez ;ݱ6ʏ@j ./&&sL7L);y FJErzI~j1;/tbdZ8a]j +%YQc)V hsa3(?69Θ@J$dZ+T/)JY0O@K4]f10`';p%O^Pχ 'QOt1tL"Zb@8-_Zn @"ֈP|UPq,yf,Cx}=AݎRbT}U]; cb}&68]L◖|^c"\38g| :bX)K6>!q@~@ZZM XNA^a@4 8g!q` x F9x}_9G >[h^R:E$6mW8{@c +^pBU4|:4} +Qvi᫚ۿDLg%S%*EӨW/lqfk*7=5NZj ( 7Aj[,$z]Tsj};q7k-ꁷ$,4v܃x' kTJe$zVt|W@|pɨBn _7 ЁNnXz&P*ȉ4mnF3SD@JGn6G!F|JKJ `{Z#n%VQ1"@ G#!vS\'#0@86~Y"=ۺXH(b׍4JF0H?ϡXDQ<־GvX[QW*vP[Thؿ``RvЁkmJU4W[$ux~kM3שׁ(ƕvyNV) W3@5_p"C%ȁ+#xzOd_҅܄\J CrZtvJ-츴S_l]ܐ!nPL`S~qU V ҭ |~tr/2j2{e]OROF >ʽ2n | wa4 2R-CӉԌGz-̱aѡmJ#QyHM K>e@>` +:Eŝe?]8bWзw*#8`{^S}m O`9@{]XAgՁP0lvv9dL ҭ"ʙKur!ꕯtDYsϢ>;t9TEC= rǾ;#ooOlIXnt-xa]_ 5D +9Z [@&,56sk~܉I-az3aUD͵lk}<yPӟ[5\ܟ*D:znQt•X!AHn[F> zѫ2stJ&MKV;ZrBe2'wηBNr*Ų% +(>gf.Y<00fxd +>SeVӼ8 C%O!Yޮy()n8 +YOE'N )>DSr,mpģߥmOiMF^Pj!3ߐ6"bEjt \X-+^mjN3tT䇅˨7ihG&0˪cS?4 +Q=2˧yq³k\DQizi$v'P[UJ:(dJ0ގ}m4 bMSdzF bVv6iyUBr ydo*-ClB?uɕ<*qVŧRшAZ}ypgZJ +|N:ݞ>:.ZxTܜDZ(FQ/OfZ˴Wx.80>g!c@! = [W^f=s 䱤" }4 1Ӄ5[ڦkR'FqMD {L[{=!Ť)NͫM l"4ÀQ\j!@%5LFa[Tc7$R +G?5WQ@Ńq~}\9&N\5ueE`v-HQ/Ā@>DT.p*{ ,cOaAzUuE:2* ЕIɈ8K4|.5TyimH4+udbJ|>S3߇@ .Z>ӬYP?Oy"iM딓b^}^ ~|RVr\`tI0ʳӴ [0)S">Utw;'=zN|Мm15T,Yw&e˫"𙘘f9i$$>yA_:I|/XpBN,ƃ@lb ҉@-.[DT~o{6c][ PHD욬B}l!D.!];vH_ʧBHCp_s}GİK!K&Uyå'<-oݩg!Ḥe0Z|eO R-a'$VQLctX_]ir[*RCjrl&W8֊'O48RD-1lRgc +e'O`_` =(̅~U;,#@߭®N-1Nog#pPÛpYU4l0<1)w&tm(صs0ZK\-{gB\Kn>Fw CбTz +@ny73y~}6x!`0uZ *N * LeH="qYpyy8d3b#bX z v;G57*syg֔KZai8)3Y_߄8Ӄ VX?>2V|m4ȎB^]LP7ֶ76<9u;-[ ZE81k# Фih7!>r."3ܞ1}tCDZ %tޮ6ip~¿7w{ժ 2p7:/YݹŜ!$-C֎Зb@IiDWh`W\5F%Ŕ#KǺT'tϧ]/`g@0.DPXKcYgK0,ȵgAD@qinu1YSQ-V#Ɣl:m0 mּ3{h{Q(m.ڵ'5=#òYaBE/GP(<4lyaI}PK]2k`6UziwY|o܁l@:_ k?ԋCJ8rm,%1 g]&E7%<0rVI lKVy߳V9 'Ȉ,hy\ߛ17,Ou8QƱ{qqrI>NE6PJ>՟xc>}ISP>~SN#aչ# xSmM7&:SUy *'׀t'Td3Tv:^g僉^Xl;f]NO&ZFae9z.uz[+L{s9_|xW[se|W33&iWހs^ % zYPPÈx83/Rp+ 6+s;IYMY$򗩃*YgG`vXܝ޴d@ܭ*\  R* 4BٽI[` ðGD{bٻ'yyzRy2h$"DI l -l~Ϩ*ƃ$,93찱 Ogyvzh}k[O֎ lm]cq!# +'),w>F)6n5J ޖ6ZDBP8ʈ9*=$؄;O 6 GO3] l DŽ{'M> w&Q)H;I5IA4@p*ysÿ׷ǟ?|_?oo_%۷ϿÇ?~o_cNֳΙFy~y`` XxK vQ}%#-U3o#Blr' 9NHIqf_oɑm)U5oU;lʼn\eAR>CVu1r%B/,tlJd;^Oz͏LC''s0=)VTRǂH@w%z#h {"_LI?С;sz'/'@Q);@`'߭J&(%^ +%U+ _b_PYQj ͭpnuNl0oZ5^~7Xt(4>U +y5,RQ/~<ςCoM4qMzm1x"%ZUc.HZ>oF8e !%`e:&Ű~| >-tە0Iy#lGjFڠj 躜$bDW1h|FZl8; 9TfÞ7s 4LwJq9/[.5<kx:>"Pp42AQvka:zSC.4CbhH&ڮ Q!ClfD0;]n3Ae3Dˁd\$rmL,$OY-VM\|v,׏ o[cLqvm Ӄ鏪Y瘏B/-X8~Quw$Wh-ؙX'Fa<xfs4Y#}[ˡ2%Ѳ!&tbҨHnUl9@Ɔ+`Ѹ0+@=+1d0\Eߝ'02@Ksl +Y.o/<_*cƵpdRF$ipAP}I,|"_+K11XN}&٤;Y+#L%}N5[W4q-r7jN1XX-@S4q +^:4X+%VAp$N70aNP ++)"B*ƒX#ڟ5ʻ]C)ei 6Fzn]3[-虑9!9>Q&BX4}NV%C|0<-F#,ܰ-YEٿ"2g(}~ٱQQGnH4FȜD-q.@0F"R"'jFs18l3>oR%kxĻQF䫈leYH8P̞P-Qff}kv9xfD>J/~بMi1Qef˧u}V̱n#7ԀQ| LQ +:#c3sϕqda=wߞ/szo|ߏ7߿|} B/\<.8n2w;Ƥ̅UcUN'D5x)~yݩ9T%-Kx ;3BMItӈEq+:aIHnQ3"6g]BЇSD\2vagMIaS]*x`if}jbߵIk( +Uiq$~g"zsQP%sFpO6jgB ^;K7{Hǯ-DɖQWH{821@U#"FU!Җ>W_!!Bg:^FfdРOF| *0]N!NɏSA 8 X;΋hXD + 'VGVZ#w&3j e^3h Ѡnյ1 vbB8Mj%#hwctaHXSPC5FPu\s(klϫΌF娵K3FsC܈Qkؔ"Gg Pki$2F# (3FFbYuu<KXnKΗ%#̛ZRwIrQG?j/+ : DAM]r bda1nU.Vl9N/CCPvC)& d׍)`SX߁%5GavQGDqP"FU9tdL=C4i܁gOp^# i_pv˚,Rݒd[λ BVΓt3 lJ׶l'"MEW +IéCW?HZ^StjJ0bj"0b‡/[;A\qXgN*}2P`GNpʾIW=ehu_vi>ZL!$gÛTSSX)pv>`@#DuX65|XU8hzb B@h'v7\G/FsL'@t*>H~x6E^$c!K37;ӣbEc,jT/9"j8$a1`&d7:H5 "Qc1CmJ &cH+Y{ +`* +IN 4I+ +}aa\u  nfa w"=se fYqEeqVV,Oae[BpJ<O؍f҈U`݈A3^h=7l\>vʁ8̓::Jwմ!8S&zlƨ#\5`tU}W6Lɗ$H},(X &9Dm0xR ߝD_j&A{t M_;aGFY3l#nbT',[5<(Ʌ BҔjR)QW6cӋЄYJAN(t:oκh 2 S**.`jcT &ħ\ DMfL-͏ BKṣ%9b`Av^ʖ@ *aҳ\f2hQf'=';za{4H#k#OPr"؎ߓ/}O?}ۿ_/x?=ӗo޿?AO[5cPA=> S&9{YŒyKAQb>stream +HM^ ג(RȢ@w}u_ۨ]cd칤(QkN;g)m >k?jg&bo;y*5ö Ei:ӷQ@kYjg7ΙUVMOC0 E7i yUqFvynމΒ1P3?2~x!N#Pk/P ~bORfy<(AzY}{0x4Vq5E9}q>Bw kZ5l];˲&527Cj3)d~jٶKPGW7R`'rVgF|VLqVR'Kk-cx,hMY/[PNҏVP7rW1ΗEzX^4u#n/Ai,JY#A'TZ۾4c=/Itϣ׹%oh%}áR%V@BnHS)S~(]i,B=Md^QS9S0DӯdR|U +e\Z5oQg,ˊb2JA0N +n +Z1w-&̓xJOs1LR?]L'$Ac2n70mRXXC12216ѡ!47J1p"+&x/"}x_>~xsG}{PQƾ97<<x/oKO4Vu$= ,G1;t& -=\ Z'Z0ͿRQYʝ|G +;ӹ,} z hj=T)?ٞ%MS= qO^`iQZI\HH¾OH92)0=.J^ƼqDKx%g]TA7[T:͆EZ2-q1)a=U_ܞf<HaJ5{֎&H7J } ~ +uP6% Ƀz\ohm{n&綏ţw_MN(f$H{B%*B >*"@*}jOv:&]ķav.E$JIT0%=eJ&4k j.QLt\s;H ڕWk8=r;l!5:xXdCs)2̆0A]WpgxUBlfp%QEXB[\Mo}K" .ձegZECEl;>YYH]JwgI + W^!JA>CзrnBƮ8Y$%`mY/";f8ҽNJF`R,Lh }/ uF{_b olNj 6jٛf8ΤJ_-&0]&r8J^eXRzt8܋|DKNiuL5;3Bl3c 7%RP-9+3v1#U(- <48J >XL |AD:gR0,-z,5W챧97dn\FAj)f!hl lދWRt3^i0691=$w;O<1Cp:/Z31^z".Z-}XBpZ9_- +=^^=&T4FpD⑇փCL7Uo+8lr#AKDv iHHNu ]C@X$<_zy ,'è؀haEeд6-]6|\O40eQw{hlޝq84툣0)䇍2i8uQp_'Cf5*Sv +ˋr $FUl}% gV~G<|j}g3 /"fFOFt5ذz]e ޺rɨ-.;ACi;M'kV[k&W[0ޱok;:ƭ>x + \dK)ϟw?ʜ[T TDʌpPU ףvIܱ2D%k75?v%SM)+ ~90GRt({G[ [iw~U[g\5]ohi$2lj?~Ԍ-<3֭gIuxb/pI5C3<7zԓXt#@vִ=&>8.4w[ @DB([\?gaTo.{.e@Xv(!m"5Yʰs, +0JrPL׺F>دϚ,4.Zw)/?F"%Tѧm?.*LC[&[j'UZ]m8nGC +*Th1힣xL n\P}//\y$ܽEtHla\d ' -V r?_=DUUl3wťu!-@l:3ޯF1wG:x<~Px2p)߈ګA:OR)UI%o&%Nlɦ?e]þ`=>Ƈ6f h2}}]̏~ Á Q5BKv$oOkǯC\S@[xI+%k"x~;+quۊ{҂RI䬼&ZR'GY9,7X-+m1@6mw&v} +֭*jUlEZ8'rGxT#(i3I h3Y@fJѣ~?m#AY < l*o$yߋc#2iDwQ;8^H0ڧ{6,Xeiia>Wm~yi[b2f +\?ѡT@ %ߧ۞D +Bݒt6͉/bUX(, +h<߉}}ʵ:@|CHR%B!k)=E.#cL 9ltu1C ˨Y?/Xb +3 I>s>a̼n6\UF0\(.07|JK7NO"B.o/ αYެ1km͘ + ʠC%fE Gd e<,ehbJ]%zR]Me@&95d;,Xk|V~NS'1oy{o{<$O SQ85}V~$(]VueQ<(o^0s(p*t>r2sZ˴+}iJ_oʡ7IUU@,$X>=\_"2ZL"WK5e{MYf. +շ 5P"@S0P6D5۔MNqDw+Rj2GҶB(;2Jgl +%& +Ɖ؁o+o>!J}<l !7kҸ-ۯ( GN@ MmYodAtDJ0(R15yiJPú5ҧ/hCWdei+p  l4AǸ4Wȧ8-.鄣)I[8Q8T.QH%ʟJZOEljjϺ!2tCvזZHaM'5/VsfE"YuރuVQy\ʫLiv&xMm1ϥPh0|GAHRM>pPb$UO7d& !eSh$ jFebiv~S\o@~@֛Y}Bx//TؘA$GvHr@+Fˇt2#3̈KǮyODkG;9*^ D0-Or H=zQLՑ |q"lїPT^E{xO5it2lp X;g:ld% +CSNLԄf7|rW c9aXH RIzz"^R1C@.(a؍V,CU+Fr޲pL'Yu-Ij L!@[pXVGkzVzN%Y$ 'l$Nt_=(F07lPa6it~o`e: /D`{vΌ)4ҭٖG8ALuFPL0?R +|ƄB?/qh>tqba>6X߈=- H!;K 䲉CCUD]diߍ`qMg0*Ok+j 3iX#8Ig#w7)Zt a)n%O@zXlBQ'G& 7 r<ǀPA_$7 8?'5?#o?nOx1(F@@$jq(XH m)ݺ7Σnz㒩ԭh~,y,KJƄ}(ZRړȳŵ(4dr{9e"}[wbE +$#SJ'*6goX:saerKndWHB2N+֔NS@fbTT!ѭA4ra }tzCs UZ&๡`=Z*"2N̚nE7(n9]=O08$޽t #` -W30yAXjX=}ǣ=fv]6+/XËcFZ qΡؓ=+Vӣ@7,ʲQLS SG<W67ܔjgA h2{5PU嗉q]eT^@5_=@WxL8T03DuP7|]sX>ip;2 Y`aeq<ޛϰozq6rBS8]aa@.t }IK8/]CEY6 r={fU*$M9Ts{ +z-4s0JFGePs{ +2]x4Y4<'=*8^5SZ窛߃g,ˌq@|+Ħ:@dea [ y`i"ƒnA - ADa7Fi bO}Bo= psd꠺ҡɄx(5&F0vh{*A ȼ*0+aM6Րy_^nzGfsE+0bsnpyZ,@sj9gC OǍ\0u $;A A:A iP;7N( +TFe]fQ43_э^y 麋Hc !D$ͨZǜ3*7|+V2!釪cՃhGizLm7nX  B]O-,JROEΑ1su)Prif[Ԯ۔Ŕgl\z8z%#jPWth|~W%VCԣWsK٦|7ݘg r2C.o-֡/!EVĽv@k/=jpF1^a 8T0&eP!}EȰ^!W *~{Jq6S|3j ]=TF3Q֩t𪵜g%V Hސ|( <*+d*)k2:d ႚ[)R< +tg >G!_/X):vc5А^ p{>G* dy˼@1TH;&VͯP +'~']&!`. P= k ̑exE^,aE6ZYߤA='HG܍jDK t"#TCŊ5YȩOj2|XJQUP]؁< ^Fk + MrU >Ee-"i`3 1#`gj@XYtq +Hv mLY;*2쟅~S V?;RCUZ{ZOQW2Û=S7\yU=#`o ffRDC +ݮE_e.%pe$ĭed >kUF +G>S5=.VHfoGR{ZVtC4oD<Y;!k43U#x|4vihwp(xL;`{TUF\9/^brFyHa9fhlG h"y$酱!{Oy<0R~0Y[%0feN_6@0_T-ZLkA=hacyU?bzqHEykg QP;xcJ2qa!V<)5\ѓ+B~nH}1 qӎ"yy'-ޏcVrF6?#oYILKu5Nb>)NJYۀRbi)4cG [ dCXU_AoQ?O<=4>nVhT\qPlbx84 ul8@5IbEYJ5lsֆhIwsʄ.=y1))^UNal3'zRyBMKΤ,4g/b?=tOG|z_ԕ|t1XIJh 7Z1GI߇]>vYhIO-3`K|R/\V OQX㜙CGV[xPWh=N / &9U9Q .ĤP7hHygfsUGZP1p eMKY$lCa&,Gŕ).HFb^g:k4X#zex[lSq j|$HQlPbd3H1x􎆎6FsH]18, MVB5@@𻛤ʆ) +ZyhrHg(bŦrWSқ|FdMBi`s:2ݩ^C ˒.!..CCCK%CFŕB%wd;P]+>4VVmmۜR_ NNC&TH6nDŽqal8\:K1G@pw`-588lVwqrtM&O_-sMm>< 9ƘLx|Tk)r{[ޭy9M?,[Cor8x h=vk=_jtn}n'mGJ &t>i}qPM%Y +IlTP> 4KT+>(!qaY \U .2 @ꙇ7u,k9ZIGVCvj bG<B_ka)9 O:&ʾG├ηCS6>OBwKǑtD* o`{9 @K#=9ý#kRuny# 4_5vgDMY89V=y>X6iƕst[5Mu'wD1c~ۃYPUiPzkqucqXdX Rs ,evB@)"uS]mc>8 x1 #4>&$JZk9:*,7,fj;6ap^Jkqya? ԇe,K="`%'"D xm.KYAZpfبnHbg0SĜdytz~mZRM4ޖ}A4Nc亀a+ h8:sN0Ū[ycَ.DP`yK B P;d1].IGT%A +,lNʉo8b׈of)mZ7"]Hb{3p$V?| .\a( |ôd5$^ɾ0\+}a0 \xz]Uc^Zķ TuwNt>@N*.#p;Z/=Gr,wӨm:n [/ W= +KnW)z +[n*>O}>%?:. T;BF#mCqEIKw]_e:k8s g[+` +!ۑV:Mfrׇ&&7-&uM~qxdϵr|Y*ZBgM]rtyv\&٣ >:UaY??}wo^=>Pק~͏?y_w:{Mt}lV FOE\_(zX1.wxmJC\.sq~8*}-[a£DQE`Ry,QP$)_WR,Yqgb"ɧ_TZZjL4({ǁ OHl3d1 +ҽLmսrY0x52ɕVr7U#lVPY2}}߉{3A87ބEL+tgGe.t cp^%E!& "R^•QbW3՚9jb$;8Y^}oNBHgKz2ָ$4'PYbBK^ ܟF`^~'Pv9XjS͵9yD ٢\9H$Yd,؃~qKqG~Q:R[#319o%aH`"rAoG  @A`YM\œLl I40o+#(j" +! }Ը 5']€U2&0Gq5cɖ48 +LލtyY蜋1%A>"I"Dx䜌%sp᮲J\{C@}Y6M +oreleߑf*~4BbMbHQ=a!^5sl$ĂÂ;)@Su7NHX#\}Qw|(sS7i|sȟD)Rq8ߌoF1"К,G"%WQ.}]63جulB_$ S VgB&ܖiQiU0Ş~ZG|с]AjO9eZ 4 ":s?imM='xW7n |(L, D +ե#"g9aarFIn B=Eag΀_`i r}S7:#ŽHeF.LEl"(w8ʼ˚'6'whQ$ۢܓOK!Y`hX|mD R(j\ѫ>kEh?@1\8:Y@2Y=O` :a|#u$?—f;ŀhqr +r`&b ,scQME[MwS) ۹G@"EOA+npo,8:>& +HC O+JI51X\P~qEla +7Hy.3E ܖOx?aZ6v5ɳP.V!mߢnԈx 8xfBE"ډh'&yGJdǁk! HU)*,'j%6¥jKyQ{^7:iٞWӄ_hCDHÓfzO7lAGcDeBY&Q24 +xLWYHgp^ˀ*rW>U*:=x5"' $6 +Ҋm6h-H$d5qR}ߋD002"Bt@-ADT>'LQ m5uY6ш}AF.rg(EOUܤ3tÀIbd܎V쁰:0 LAKsxŽMrhюEG'YM0:LO`aF ҂bz 1`eiQiZ;z96}-y.wu' +jOQqUf$`G" IkjyfU-5~i#g?OP|"W#1.QeoUgvv +2"S1 \D_}`0u!HOuDvC&F,a$)f O4 犎,\=$^V_Qyq +Na}͐1¾Y5ELS,?v*^WH:)%,=*mZtQ1 ܈όBRgM=F? 1].e@ Q#)sl7U(uH|&֓(oJX=>Z=}U$@xbB'@b*6SN1M'h6qF-[Y:"^AU"G۱!}-@ EU5>n1:\PsA<#7 ~_>~O}?_|鿏޽Ç?_~3?>KʀwƮ=~w'm6ޢ?ziRx+/`3@ͫart("U$&vD eQed%"w:=1Sd3,Iw|g\,,%\@3=e|s;8E,E{_cq/+bLYM +M( ,uHMjQ\3}ͪ?pN5z+!\c.>R{nkh5VWF$r#f=R =IuUbo{--( \O$2ސfvA{BF'kJǮ85sV(PΞ}̕Pdku^yHkpܲGTAyB2HKmQs?{ VG4CBf\NM|=~ј݈YH2,zX`hK,@U)g Gj};n}? _ԋ/pkҥt?T\ߑf+A6qEEණ)u-R׎ùe_ v+E)]=$Lop_ڶOp+ ;>Jp 7-{U*^CtcPjV>M ۱1:ش@Fp ol38h>Ya5Q6‚X?#UrD%Ze { #=Jm(%BMGNDjmfX4{n捌8q_QH .f@aqQ >ͥMd2LL(r X#u?Q^@a$%#(Ue~Y_wdA䉮aZGy ;ck V{#@%vgy^FpC*hUo,l,< +kuW݁4.SvlIS(L#!z$a L#77 AZ3Um;QLbԂ?Y/!+cP0"/rM5i0Z=3ຬ +TĈ$t@ZR@>3GR*JB64 Ϥwc$aH3N'!bLMY`β]^YrBͦ,‰?h:$^ +KxUwuձ"J~2T;ktN@\PU<i +* (9F)Q*z:!u*FhJ +p",N {u)Jq̔Ta()U[Qm&4W))j յaE@l=&[зKH=5*14q%xP&MIzV9@ב6Ơ* Ƈl'KU5Jң[?ibn(~NJQHqh8%SCݟO< ĸMٖMRt cfr#pZ_=D1IX7](>QuŨڲĩ$aH +h֢wcT컼л;R+*cZd05fk6$ y ,lk'Pޛ)E:@ +"@s* UИ\)e#`<*dAGhjXcet)(6h+Bg!z.zhhN2eBT^5f.HCR~3tj_i*_,o<6BɃleG K! gh+ n(P4(#qꪬxĖÑ]#ErY6BdC3R()ZW엜@qvCa t,(XkHWs~cJ I bG"@/ ]Jeˁt`CP>B[rnww?qq:?qqyzy|oo:wݿ=:};^_\{ye˧7}z|˘Iu=5Tr=zyx?aӻ' v|=/wA<|xsyZ./>QWϪ^_N_,{y?zvU?^P'-w?|4l8O{WƫE?/PLfmYJ)_WWfaO#yKo_ן߾|K='_or/M`޾N0FQ*(KsP\?X ;Vc_# lX$ǀT7 \thPv"d~z :-+cf"*'l됝~0@+ G?K؛=݅>o W' 'S?6c: ugUU{^3ݵj_L|;T_an%+ؕGpCH{h汁rg bYxܩĠH@Y81x7ONHj?aKv{(:8(Er=F㾌53I8S(eiݸ<`ƈ~TTMQKsؾZ%"ysL`\g^ĻIa ҃'SM?z\^"3#^8teiOډ-x o֦[2 +OB~x5 S pկ8y1Β7]xY39Sլϋ1fȕŪ`n[ ز̦_xgʑjtdXsZ='R AiLb6%1pLTT9> Xy43/_q-&8:H]暷>0`.jﮤg'c7sw\. v }i o}S|'j9rpwD9IƮeU$<7M. & 6/nX ,{x'>FX^je4nK"VMdw8&ɡıͤXYh7My?>p+Ui!TB:`pRm0p)C qdH:]t5pO~jn?FGzʇ_')"8i"8z2xWK-ċ  +x/K'1qMKp0D20];JK]I,,A|.-6OUoc+?}i}o/5F㮀12GPʊ9},L a{Oq8Z2܃ƚ> lxVM]J]qYdXL l֕*~ /{`[9W"K2\xFLOluBə-L-~~ӕ%estT@Lg4n閝Rz`OσNOG3#Z9US^[vJ;#JL Zhu\"2^p]T{1sTڏɽtټ2 :b2sEk&UlDߕLE N=\gU X|B0=թq:ʠevgs_?jF6 +IY.%<-n@Rs蹼&xhV^[%44e7cRC)9rhPKѫX)GXEmd &Gѿ.c/̰Lh)OPt]v20>$$;-XN̛,XXD. + RUg:6b`cNOxcVjމג)=rp(PU6`FiFQv$ٖ ;i;ǵ=0׳LccRρ up/0״F,fOPzOEt.P?ĥjIwPvB9Vlo2'ۊ73MmnYp%y&js;y$k7` Kx/d(ݰ,,EA;vN ql)':45[zUƩlk1C $ɩf-);"YΦA q. HP@eux}҉[;+yV2A<|}kZK{Ov¡%4Q9y^_ˎ%4IЅWRu{3f eoJ>23\{ޞFY?K+{ؠ$u.U'jyOG-Z$$S&BZJ]48澅ׄgu5eU2{jѯCv~Ѐ8X!p9%JW+q?347 YVb[ZGwm;@g  +omākepP*mGlmpŮؕYk4{ +z@^a}C2͡Xi N)O̿vxhg-@exX\a0*@Kәa}1OhAHkz.pNc,?Gqy 5xڿ-#$LV789,! +dd r Cܪ<יތ,5';bՆOC9#0|8::q~k#K+QFZ4~p!0 o$EM\IIҊ$ 0`:W Pe)G>')@^o1æj^s/wCAem +A{[ad6AshR'e4L +e6jeXO*j~za?>~L @JNLJ\W3Yrqg0V;cKd;j+242_/9V\:lc h> #o1 +Cv5^nskwJN[TY#zw]vj3cB +.%8؟ */} ;F[56Ae9h!mF.q4ܱqspt0.+3_ٟ c<`)E~_<:t286ʴV)dT)˾8GHT>,euF-d~2,Gڦi¦]VRJ$k|M;{j3m[_Blc3՚},i6)dhUeRP5TE+5|\p_//O v9Z!\"э,ukq +&#<Ş\1$ۀXjtbn%#&\ZV2KdYPHZj%%VJE\kmVx_bd 3xm<-}nWᮑVA-Ƣ3@~ |g{v5Tݜ2sѝ{,jԝo/0}@ou%?|au!][]=H i|jݸGkBK\a8^Uxdf) mːS]i0X+儖2t]3]mɖ(p*5 +TD:׹?wg.F`ƦPo +1GϜtRp ۨp7J[6Hy͜)/6ggTe +a^4O){KD'c+ -#߃v>|>tjG}i'39m! .%KlLH#U1SiF,,9ٙȺ3G=+|ةO8b{5pangfDsO[ +|~k[۸'A\ شjZe*42L'gU&?rKkʳ7Ze}â*R4ȍ-.\OrlQV|#L#}Vȶ❽^ +}ny_ywS@M:>/]!䓪(;ApZSvꖫ*9eY˱- U460ݪE~<`V sΚ$ސ'+󉚼=ۻ)fnӵ2m{5:8TT`X5׀aHPI"B?;0\=Ld wmXI8jesWЩkhp\_$-r rc /΄bb`/Hoq}MQ']܃)O;L.}7sZ;1;*ԻnѦ$뻲'xKV6*foz_ C~ `Wǐ&(&YOS,Ϣy]RpKO?uK5D[P?*+Sy0лyY]\;8O#"FeeYa|\Ό 5ɚw9BU=zj55R ޥ&uʮefXV|,r=Y.>F+]|3i>}?W+I68;u"j±wp!ʭQ p̠"2؟uH;mKQ= #m-pYR|[9뷂q: + Ηr'T eK^Ό}lJ8NSQ>`\mޛ$THΈk=`ſ8!? +-ޭÜVK hd:,^M=>2޻2+AtyIGxU ++VT8|cb`gK;2&Vdd_dYc#+A]RUPXr7qmENΖQ̼qK..ъ]G< +r1yBIyuRY8%55ry~g=;8ҋ2J3N^&xD +'ָڊᚽ_=XAܥx:z=x%UX]j[y"QHRu.GS^ާW^2@[J:'NICM<~KL%M4"W 3ʽz?-4[H>x˰נ ` IȻy0x33?%hq4Nq8q6K]M6Z/^tH#]V@<+ #mKw R[8Ue2&\ک+P{',X;so _ (F{8'!#36C9qefppQl0XǧFX&_+Zk +tvذ3'݄Vx vtÝ +['){ó*g;VvVY#U;G6d贲O~,` +TU]rVp'EBYHN*$ 8ׇ nj_ ,jڑ/[ٔc\< epTGCiE0XHݟUg<;/3O9$e0z/xj'y]g ?m&z*dP~!Ǧ,MHu.9f]T;YAxrf-4=P [lV3]POm8?7AWFp]cO;?Ļ0 g5_<\W;K\a4vh5N#Si8%1ĖwvE)]XD(#}ZV X@:`zJb=H+3 | ].r /]|$oʕfΚ\<]]+U;R{_WMzؓ#[]f@l)FYA?8x;Z~sm+ԞvSԘf97l;Z/5XFv)x# p;sЧ [? +endstream endobj 28 0 obj <>stream +HWko^;` N/RP%R;3Rظ16vM ^c=@(nUJM>gfΙwm-ۜ3ʫ1"TIji([*3Ԡ5zTZq4 dʸ+ Mz2(cN)GSAeآ N6$ c @ +gP+C>C! a,~)m4OC8:3!YIɈј +xt“C%i`r*P^w3_>9ЊlW@iô~hlOV$A4KCS4{\ T[!o6%Ёfs GG. sh"@;W IB]8=JrǰZ}g9ij/OfBv@m Q3<񲜑,gϓJMb9tZKQS)g!w>z MJ# l<)`LJHXH&! ΏoTa|tԪLF8> C؜%ԁL(UW!BV{箍4@f{,"ػ"jSq+xWAD@MitepD*cC>/ rj% +˃#2|Wp_b)wY5'7ޔ.mױk*^oDB.JR" f~ZP@Red~anl1n%:zjf27wIc>qҘK`dRP`U+0U ]+)nv^.2^Bqk ڴ&Rp0*l5ЇrRB=]TddJL^ȃQ;ӔSesIFcsKGV=()ICq%\1!$`ؽcI#O 0@! +[3 +Lmch 17HA|=fI4dU|IAvHˌ˛8b̡Wjg_bӭv0O8L9;SZ +?s=6BjڤA +}Yr93UxyɸT9Q g(mZ"ydqyB׬Ǘku,Y eIm}ds<I5зm[#\Q)*[[qg%BbB 狠An%pv*d&$Q'xYMDBPb>#o tE5&a4?mTֈKIE*b\BB + +k{"d~zBcP5|.1zIf쒩B\xAuUESx}c1Y5sĕ)lY#%j!%s&RT*2sEЀ%aSu7 Kڻ^R˞.zٕ݉jhs;ĀΥsI3T}1 7 MKEÏ{6>@ZG K٥͡KgvՍK7f綆.̬6&g CWFm}ujq֋G'/ k| 驕ƥk_ ]Z7|iᩋˍƅˍ鹕Bw7ߜ>;9p1;w1v{'_Zɉ84~/4Wn /zqjFy_ttٟ]/V^}{fyGV~w_n'/U>l?n;uf7ۯ~W{^6W|V^쭵_:a?_On{ɛ[{#뻿{s?OWKw_j#4*A4cl'lCrNrw BD*$ˀ k['Y?$}ڇ?s{wm7gt絢ۣȜ̆ěj7@yF3/8%f:8K@' /ipClQ(oj8\օg|ڨќ77XՈ̚&i@+ϥ $jk}|-X@x<[$Ɋgd*>S6%7Q#˦IsDu(Vc;. tLbON22x67THFӂ XIvP.]w !TssQ@ Dt"=1wHOhY(@"6,jp0!39=!aKAAYb_?uBgF׎Jj0I¤'߻ CW^f1S4XHFRb37BVvjpzhC,AA -Y=OXqo;hQ5pDힵKߛ3?{8[YB2u>xiM%43H{* 8J%Qfy +/N_FˏXEp"/,Qpل}QxH;YǶ%kydǑU,s"-]$oGi1=z8rίG#Vv_C~%l.xHj/0NW;bޢDfvJmt`D2yz PY+88)Cމc: ImE3)hEϢN, !y4Nل^/HsYPqHIٯ^-(}؍fK?`$ +f~EĢ:8iT&ZEӹ(xt*'U+Un453uד۳=3pނv5xXht@vvHt I6?{?|B~-[;Ȳ$ qrRY߿c[ޠ(cF?wCӁO\oL8W8sr&|əau`騟ލҝonеixw6y/n7MntD{E{͜rU(~=k{8GdZz +5gJse l[52^%/^nXU۔pѢD!/.a}$AWIf\rOO(*j+jfS#D,~ܽш~Nr% " ?PzW[8?LPFtDw{BL*]6ʁYInEr`=Pdk gĎkpv/ZnXdjv֧i$Ht+i:(xx{y.䎠$۹qpŬf/~l ~3DJCr: B +&yCCy+~ 0 Sp|3Jwn]2]BcyZPJwzN'6zzd㽙jbn#GT<$~~pڎCY8C;Nt;Z7{ߐW~>i0_[ׯٰ8CmdqUtOXQ>Y}u $3F!uÃ6Rӳ}VE}t٢S(jfb4iyn Zxl~7slUlMGNz7xa?-miv4kPT"Ql*rPY ˲,r.7 * xMԤS;vZm:L;N~krv/I6C) (տnhY2<ɠ.s~rٵ- WYM|c z1;h?N:w,on2i GtD.ß䱉vsy\o_\G=9ps xYEYHZ3;6phۘ|{lAc/~Vqw'5&cϬ!8Ji 6X)7ս2mBCE'<M0{nlP"eQ~3Hvj @STZn+FPVRW_V]~+fz_k +1L\%`̓է}\^?dx瓩 >VϦ;[F \Hg5?RYZb^Jy# +hu^˽)՟U" ˆIեc&+F~QlOQ:@WP?7T^zzem[j o t4ǨNj _ySNٱ2g)2{Gl D$NT!'n/f8:g/B9ZudͰܢz A/17Tc;w("KXB|K9gjm}6j/_F=wzyG4M;`1<3zY[Jifd;gdT_e\ͲZKz-' X)O9:;0s*/RG:G ',=C6*zcډą6kW3C*\x\3:gdu2X8F>#Q7&=3K'>4NTYrV沨>%i_tCq^aE6X3Fmw^Y~( y)7[3WxY 8ZE/JTdfnqZ߭-‹ڥ?E9<][?nE^ϯUl^m;ps%o1"rZ2w~ƍđ+OIaNfao;bf}Ll7b?M@ +3;մŒԬP#E`SQo-GR` o< NZ~KUl@GfS|n&tbmGOٵoo,ЗzXm7ξ$kCw%0U=G&줾֫4[>ee5Wx&= p ,~-G3#{jTLzڎXiSe,`v\RtR_DY(w xdXmLъ+0T)fD~uMӁ("4NjJVS+5pU@["@^C{Ix4PI>[ǐ!;1YTI0ա"FSYյŧ_#W3#f\cGٽi=}<8N>9XQBy&wgjK_m}5'Qex6Iq+c)sV}kBE)y ^e=I)CrX2$j/dGev옧\_ov-l t?fo|O W6s}{V1diw1G>f>c[~|dFQO];Q5J6Ϯ!^5?{O*^H'>4-Ѳk9۸_&Wxpq \z __,Xx~{O٠zc.Rv^vKmskElǸЃ|.6{G~b[\V}]X'R/M!i$ț=Buc:9W;<古N|3i3Z8G)wdMgBd]5 SadY&m%5P{}oٹeo^kz5mȋt3t7O~kXkN&֓u+tEϥ,mڝ$STWƑ}% (k4Lʔ%=s% ҍiiFA`4cRcbJ?T>?=pM{y=5#7ӾER%9 +^|B6چydZxL΋Z|}6P)QVycojLE~4{pF#X%+PsJEkWa QmM[NJS4.}m 13_:RwH]k_]7Җџi\<'W yaN;_ ?n'3ɘe$ ۨ|[v+u^ZEL)<#pNt*yɣ/vI/ޣzs1{=U\SSVt8ϓ͚W;i98T>n\'EN7IЛ;K9+tg ,L"y8: Bs1/ixz"W鼝yHI}WtD-n4 ]R~K@+^za'!&o~iZ0Q_Q\:Ϊn'AT˥7ʋo[褹OmIM-Xc cSj1ƋKj'rSGzv`yrWkѿ~G{ 'b` ?o%vNd5t,|7=]ϯ=O2A[I{b[DC龚Zs^( l =8-QR:fWs*ztyvs-24*t FCv }\Vz[pV/^ gFa;45v2nt22t}Z'Sȋ\7ax;:[{c=*|W~⺕#XԎ=ߥ}hnDgL]XZlܷ7mdnn'zX'd*t:^LϽ,Ľ=6켚E;I\_P|dh]Z[-ETpFuox7K^ Gl ^8 hӾRukTf }I"Ю ۋt0אt#:QDZc i&xbsatgȓڙyڑvMz9uDo~O>ʗIGЏ jh*z\\x8_)<|j3pRb +RNYB%#p6RS+kaa7ҶT9ӷ {:П`1նL9~7 qK=xR58#x]gޛ[P"a)s!^<ڂ,DkOTB0?fLO3}l_eP|/=9D3S EWH<t.}dѳ eR'ǹѝtL `woB)v+j;Cg|v?{lFg4~)5‰‡caӦ` Y?Ӿ(IQ$DjQH5k}/k[%RIT$˔JM˴8{?۝p><|>0O?43߯WZL`m'ctu~]H̰Iܓٽs&#d~bn$8ݘnvM. +Nx<͍VAe Ud)< y|vOLdr t^|>8Z0QΚ=2F#QCiJ!L#|KizD3A԰F7k`sQ`eN'Pg% gg'R|'LSȜ>ddfJ[#g3ȞЃ N(U-|W K~_afXÆ =S1!t_veC<% |&t 7 =XG<@;nf('mv얲mhbZ}bX + oHW>Z\.D?ܛcp0$tQʴQy{\y-k}wGj4sDuӗeK,ghztać-u!JuKpb' /ʅMndlN/8 +lp~ˁ2.#MAO!|69",ۚK$LS r'~E-hvJ"tMzL'. +|;J2ggr8j~&Ca"pL^)t ?hx093B9=Nbem#e:9@2{@υn-"7 k8YI~pz.O9cDS%PekU8%sxQl@.T*o范~*U9z tyo1Z\B|ػqtx0p$6Y#9v/BmRԌMgRQ'M)oY-i\ 7턮 M.:EjMjGcn^(n)~Su:^zB>Ƒt7 ޴fsspTn|{n +.~?=6ZMVؕb^\RGވ&Vn:|_x a-ryGtM%ZIjY\ҹX< cpcwj&Y~gG|;wx5'vt yj"BXXʊ\ixt+ RA>@?x~(%7͗/Jk1d=)8VךqtO]VT"XH_u)̃[ h-c+tšK(>Fi;/lte,9 ^e="J;UgyK,pb6'57AoHup>V~~*_NN[9O-n;_n^ƒ1 R3Tů~L"_ӓVhhM⇦7kGy5xFk bQ6_{/=p/08V=eh)eko$eeהC3Y9t9r? <݆5|?<(+UA"YdQQ8[7 EdQd]A!( (.q(:gTM*}7܂~ws~!0D|G(\dĩO0=zDtϹ(=A/qg,DOAfd8K*Yl928t3 XI6"U˭7UxNvrdhy}CxvF\%35W-ɳjRV*Zl|j^YX$ywr-٭rR:1_) ςDQO +ZXnP=MNRճR#vF|F'#x +.tUOΖc swwF^I-7 pS)t/8Dlv`wƅJB+Y*8;lIjk9yf.e\ +k`0n/b6EaRLsYI>nO!e!D3{#?gĕ";C%M)6ƞ2J ,_g-|!V0j{G  B_)!&5Jo^\ EV% ~זE ¼5{Øo&k5W=^m&➁c5d̴yĽ@7@N7~*U<VrtyɎ~t{xv;TiE}Nk^^JyZ+Xm-g]s$%]ቧ|  ~Nmsuv,ʟzO6;=ǔ̀pS/dzG cO⍖ӕ֏ӕzʇNO%[wF/"A1yS/)1e3Cf| 5 >{ւͅOCieMMo/G,=dȶq|W/Gw +vm 9608|4~d7A + :r~dyx,Hctjب@Xb-scg8ܛ.A T +86'z˃t{TVԵS438|' JZֺH}cbTԌVG.9,t ߐn1//g^x%ٴujd]uW4ර8jYmxr'3Rd9-pCVOMю(Rrn?'ŝӃ8/iuiuٍi!avm"Moj}YiT}H}E*})^n셏}5'z;)rGMJr<5"sV_l$./fa''MƐacE%ڐ~rd?rsDdMvLo ,Ƣ,si+?fkR[N0V"2d ӂLZk`ۙ8YoQW[lG !۝xڕƒ[#ڝs$4Z 6,^*"FK$hAO~c%}D:a-~C_u-n ~ &iÇ/g~j@m~07*3/zb +H|q:ͼ +W5]ʖ.is/jPșgg˕%Z؞ }ɪW~ػ 5t5ou˥Uš|W/!Iiě2ȅ=p^, /m}˵=ZkVd^ޭ Į b=UCjyHGt|R7ӝLMyf|7mWzӚ_ܻJrw'mfǺ}q;fGKgPUFjd̘K: VRrƒ>W׵#>jnw>=ߖ߶ҡ#é8n?|.q'wY?j{ڞ7l Jkƾb`5x7>WC+n#% 3c9.;=}C{ӗ99>9JSuT(([rnIX¢ADȈ:Ȧ Ad j0@=d#,:E;p_~u~LTDZ!k;Ao0mt!&{ƚ!{QNLdd(sGm$9dCP#+BFZ֎mscT|q1wxv{R'fm^ Sq.`+-ЦhڹY{@,ַ l& 'l b'gQ#C))PaA wN:Cf2}CK|ji@8p՗lxе˭ VƹW~)7iB`4d G#'y)9?IN)*)9ҽuZ0;z-!}Bg>'IKmĉVkK8X:v\5,#^b{QB~˞Cd3luO}'HS0msݱ}g?l\+'b԰}n_"<5Tm d;zzscJg&ӡ|DaH3;)ڗMi")̀A(YIQF5*vmFG1d>?=\=7_eK`s(XED+UoӐcx"$kٴҔ΢>dyG.fI? 1Ll Zұ]&,{%cvjKȕ0 c')ItQ]73|ߛN EĤCġx@1z1L`pN:|ewz/Xq_^(0~r!h!>kIF[>TTX%<þ3m]Fu0OǞEbCok=X3燓M󯯈ǔW覵hP9'~ߛ xK?4ͻG'Xˆ5)"zpԙtc{zcNEti4yjvSqM*-‹DU +zKm*Tm\}]XMC +X*3\>m a+{ +;O *tm fpn+f̖MLoda Lx)Jk*i*%k1c!5N#w|=5=]onk˲)FM 5ons2)g4,\W2Ӻ#ULJ koֶi$֖z6+r5J*h-K)t7A;)0\YhSh ;Adj0v}=՘oՄtS|ji h9\DKB&}xe?t|İ ܞyեd1&u' ;?óe:>}}ᑮPP|,7ۺԖ8]{YOQgniʶ?ƪ2Nۉz>C(O +3;dl뱡u~53 ^:ֵ.3eܮwQwn[r 'A(nb@n$H0F(PQ. !!7rB "Z[{?wyyHr|0}mh&'d` RJ 5UD"ۏ}M?Ym s I>C unl]JM[cQ6ZXVO*٫q߮tTɥNRM1RWVi L`o8#peR*}y>̥YY^|O]qph2QۘiWx>+{q1A3=qM6;VE?EOޙ?wwu|&?-3¤@1I^sG;K}0D78f2k;ME.Z5sc9m+m u]JFMECd2D,6 $z} XZC5뼣H=Q\?@x Խ#= +R}i8'h~":bӃZKq!jl //)i_|#SE@)aQiAmAŅ.y4Y9p +$N!a{xV }ܩ,i70,P$c (%N߇kGQ/QiCi1O^^W֤zu U㺒ضt߼xN==GsF1MZKOn +L|uRh!Eב8k'IA*b_s5޾4ӥJ j1.,@K(c.t[9U(s[ ggez+5 Cz%: MCOueȀїB#vȨ+MZnQFfshz:dЌـt<#O^a#1q#_I㟣}zD˕7i9p]J%6Uˢ5hts4:ѳqeWZd騱vt5Nc!8y&n٪E` 6ީbPdHN|+Ӻ~(iLX1~E;"vU uV6|~ ^g(-*ңQJd%E//Ѽ75&yrV=ܟ=@O4o2h:OaR=ݫdZeeJe$)t!jY@qvonfуNaYUffs~):Q0*??ߧ|;O]>%U^mU^"|Lʂ NCEl[]A6µ(aE1^`CchLΊ]QŽ3:X;ꦹWѱ!LN-`<BK#PHzzLWXkcD 7Ƃ5,. z)9s~Lc-}ȓRhDW"i8Ko.-׮`b\DZ?F7k.Tu;>gL6R7&kf.AЫAhLSr'궯q4#~p♿:Q֟' k޲Rl;sdQ WwF9xD yˮV T'ϛf{Ջ j i`oo"[>z{1yeXd*0~p1$3 q!%qVEwe@;GQ{ | +€±_ g " +o;~D`aOSm ضN8^ /=a}UO+ڂR?]#h EnojpTcJG E'LPed.Ri(fQJU|[ +D<-6obD1!rL_b3TȎt;6<ͼadAޝQ5k===|سO@ϰ>~ 5 czFEqe-2(DT( +΀8$8#hB4"]UMC6115@w&DP\8Ss8&s&z^WwwBA`Dyb7-̨u|GލZ` }ùw +|WQEeS.y̤%>d)suɪ ZNZ5$1QkQ feU^VӦȎ܊CJ z>˒^zbm`~^Ό0y + ]xY)a} 0<戸x8}wwD_9s+0x#r=Ĭ+Ɵ4gu$LyޔG[9*;8ebG;gCg\CU)x9$Bc!j1nb/囇.:Ӭ[ۧ]7k(Ɨ1J^d>ugfLJ׎;fQ\$_e:ˤs~7:g6>vEko+0en:Dp1,0m?vM8Qcl5"*cEsUinTzOYMs 3lͭ҅׽p`r k,8][f?E|JmفyԹ}]dRLjUYUkV2js!oב̭(&lbйCl#_] yy/qsqؼ8R#O$_ p C0xuHK`Dg#[LM U'w Pc+XFaV/<@Ub0I\B@9|  { tq;~'ȭ0gi^:t=z{ڭsE;N2҈%x)ĝu̓m;Ut+;6sZNGy> ) _ 7>T~\_Ԓ9y\A Rx/Q G9Sj1^+[RkhiT*CEml0EeТ"MZ3N)qeL7pcyh<(q W0 ij@ ! dA,x5H|@4q)M //&:"UEb-%@oF[L=s8oB^UNXYA"Fb?M/͌:gbt҅Uo@{n"dn _i,oo~N)7pH'i*Ig ;F= w鷒Fm[n]ؿMyV[ېm4Ff9qQGV zt_WO_ogG㏺G{cr>灳^ vOt0{c<Pb ~A–xsK^CY騤'{}IAk봆&uy/tÖELRT~|ɦD=K_;6X叩g\xy%p, OCl Ea|aL&O&!$’+!l]r;?E l_?f1=[t轺Z%q6ˇ,CEJ]B#l{R y8MDL>Bi59 |űfäLNTCbrBΣR"AQy92&3Ř:'QPSq:HtMSP"rZq7ӽ?Zfo͚?ٟ[,0l2W5̗S|l[\%龷mÃZY͊=%One=q6Hy3goj=\\GV?HU3 {k\Kd{i]eNo  $U%_=5}R ,AWJH9Go vQ`nT5`VlP.mgւh}EvVem:?&bUzf{Q 6%FcՓoMI%i2HOv۳/a@EfMP̀2A+i H͜"l0 V@1 +yKa'9^68^Vw=|i4QikbZOɬߗ"C5ĥIF?I>4YRг=SV$9_Vr,ݿo/&4j 3+|@߀ÀFI̚-S/XpZN&p;؇UjN;s'zV#:t +FNP{⒎w)xTq.;;B/`r_ /w{qF\໻^2u}mTvDe)7>Ocg:z<̤-`1L /ّy`bNT[!PIazgg\VTw}=EWN/Ɗel+SjvSc#Qo6ˎ[)r?H/da#k7kgj7OZ*R*DK=MS &d)0MSAO L!~J;oYsm>S^ezHO\9{#rx;k%wF]ۿpFfLg &:`1 vA&<c6\.N{&8>au ۚ;]Orl|[|8tdJoF/hӮp1قaP:n:$S:SĝS(d$W߇q/H~ci\zt +a_П.X{Ni.8ǞU<:>IcPo~3'eqީd;da%:?a0Ә=(ad5 +lmJ{6Jo}\~qvO?yҊ((ŕM#X/z1̮GoD&rҦw3s>P.KEO;~26jֶ`cC[7 Ƹo籥~,O0 Bz1J~x[L<]?Ƈ $2_qmpY-s#Y0Z4؝ҍKwMq A19cYu*hQwnY + _M8ח7/T憀(:VSWq%\t7{|ЕDj|՝6BZ\NmϣH!sg a8FFpE0F{003JhȊ; +Rrdҁ`=z5҂*Ѕ!G: ݸ.tkǦWC*З$V ^yPx0QIďy:rKLIyCA´ ēfL8['.ORqT'Ek XJeIWN T;q?A\|NXЌ ~Lu*dp :M9z\BWޤx~ƲΌq34b C5: +rl%݄=IGDF͒\,+h{6QT[uVd<EÿZD)&0u)LӦ+P0!$hk.hv?u/8Qތ* +#DV +颶G^OKG*m"''SzHzs.^Ty#K.zȖ]E,[֬} w&'_E(fww+{zЛ=2뭰 #tQ/Q7-:Y'6{ %CuQ}:`bDiz/I42A!G2^/).a̞W4[ 7Gmp zrۙg_2jIf̍GQ74uWbFD* 2k24cq=<g|HR4S% Ѽhm;DMB^Ə_1LVhIhA\ચsD-3~~jMլ.DXO)7ᤵcٹd`30s$u?RT21! iH,0oi%[0#CS P,7p؋ <$~"&D\&挙\f_pL)v³mԉ:kť|]*:ۍ63gkD/YAnތM*HL Yg,-v''5wEY:: "`P0y Ѕw[\eQ3;up44Ccx@|2-<Ɇ}}FcG2yX/PeFl~-ĎZߐ׆%A yX3QYl%bV ȸ&h3wzBWNp'$mvi#H렿rUXy +:r@78C%wOP _@vҒV(f7%Y2mW_gg=H2 v'0e F}Pb3oKlHV,R%f’ \by2`%yJa^mW1;^;eD֯S6V=zב#kQ+yz-"t◾ =8 M K9zL?3GP'>\n los'6s [@U Ϥĥol_B)b!간Ŋԕ'JSR:`]̪Dd``_~*y&&z=~փo*U-]6' w`ECT$[3t/᧚l377 3BY'\驆V< xס(:.YKRslp\Tf{_vTQD +tfc/*e1BS$Ƹ(vfFz1"5S͜˜yNsXw~??5IdoFZQLBօp9 pii9qnz KՅ5|Qo; OIb" 3ّٔ?t0 ڝ`Y ,< +ZreOi]jQt?XGjtu EFjXs܎>1<X;썺'8,c$zkp~BfwKt%hK) <:+޸ >dGv#opШR #\e:[}YBr"4ͣ%VD F2i|69-cVeNm~4.aw`薧azt4*!s\_^O͎t!;U=eͥ +9ZB6(I 8F+O7#IP~L2.UxGzۺŗ (WT]w +{7gv4 [RaRtR\ F +kcId +V9(9V\nK&Ak~i J U%nRFW ,YsC}L榄Cw cQ La WK[>SOB^BV% +N:(+/ s¾' $ 'DJp5}bV8@?L|!%ܡ3|AlЋiۓӚ h6sn[Yf˥ +k2GWAT˨X`i/S~]ǁhk,vPΣfh%<1&cbc*Y:c#*|v2,:B&.'2RdDH@l( mw@½)o' ޱ]#ha.NՕU8ֿ `J%L+[<x~>]>6(-㡸Y }zN鞇I9>{Tˣuԑa/(_O".K4_4jȎ7đ^DhO)m%8|uoBae;,?CPLf6`25qb0+aB |b@Vev֖6[.ȚK2i҂F "HDJ[|{QjaVˍy$WLڥ;]} ;l70f? +6vZS ق PΕ–?n1֗wt!jAPR`@'g`.`*Oype'ك|i:{\HNo# ^tv=-MgV ͑klWG}PJ\ҽgARqFEqah@@@@%"PEEE]un}h7 6첨Hl+ +ƉˌKhqę_3斿>O}_v f鬥a(Y"7i:YD|XVG6:4(3a8&X7N|)0諒m`n(Y WZ77lMm:(?'\}/LN篽A7㤃e.e7rlľCgӏ臫GVsIAMdTмm{%=7>VbvofΥ e!$\u2+^(M5FIDpJSjt ǯnn=R(KRG%[$^"ORYD\q]aj3_9,j] +u0{B=s +COX4\"Lxal>s%@ȇ@aY4~%G'lŠ^|wW3qb/ryMۘ>Lh#O62A+vBePҕf0>\ Q|0<$[mWl'nM}/Ҷ娠΁T&-ˠ[%N|g\@}0vqI$q R[,Oa͔ǧH*4j3?D3JΙMV/"qz%_5V (by喤rq_#@+?ޟ;ю}Pm~pÓ׍$3-8<M/Z+zWZކr*;ڰ,Sm)Sy'Aw9$5~%.21`nb ӍQ"+w`)0!Ci6#o&29A&<*:+aO"S%05*Z!2B(<csLqvG ў(\JfZs\fQMei4"(BR $fDP@3ed2H8Ujie)4Z]t^U/}Nr_nV8CLL\lMdJ9+HBYV}5 "y^8.F/?ˏ*?Wb"~) >\`㝨->xY9д|oMlljvEYp`H1,9_"TQFJ KNLT`o5kFG6)W"gO .hv R;:|sX[NN]mWob>/:1pSŽLZ$F*z[vabdEDLD}fWYS%$ӵkpz%rz,:șűr,+p6X^&s)`nofȓ# Uʭp.7_f,J&7i/pz{[>N(lt⪍84Jٌ@[; 8Ùtȍ|> }B}dNiy)ٺQw-*,jK&b .S̤X0,4XD:Oh׾"oEGI4&6g5m ^$"Gzށ/muƹMLJ{$ʺ]Ѕ-+v8C0d EmMޔ]jr_rw_::8tc'X ;)TɈ(22Vؚ@kdKHL34 /D1J*5Q@aYn~5H͕%ΰ?Mq wwf'kc ɽ*\*xKh7篿#FZE\ŵBWܥ +\Y> +FMY?0[ٛᩚ50C!'#/ 5=fqۛH(Θ B/믾o` \M | +Ɔ1F0Cdܹ>i܄:cfC@k?TyT o41I/9-} CKG2L4Ke|PµM| S!xM(~5W=HmG zisdu ȿ1c v;֠Ƹɔħƶ0@{MYg;0Fk&"&>& + E3'@F|;f ^+ArFW[9uc: ~] z˻\ʭ= Uae!n~:W,-LX.d[I]ԍv.Xwg+]-ib)+ܠ,׹+˻6&R39&Rx3N@s%T] )g%\V5u?@SB~HRIꍔQN ?2+ŚG0{rGa9)4ͫj[[i#golN:Nŕ l}TEC." &&aîY!wptU ulv%Ⱦh G/UPDKhdg]pVvٕE(; sTW~f)~s==c}ڃUԭ3 gp7-8Oym)W(bW8pb4 ++_'|!?G<ޱ꾙Z'^ _[bUW)!:JR5Um|ދqi|Iuo(qmmñ!&?6w.A}] 2{]]=^5\-C9E= ),kW=u^J۠ŨO;=@q> a~'b u D; u?`!6xqT=sdd:eh>1FUZN>ΥD=/U-P4ٓxggHMvy3q~zw2tw'Zf^-PFCe6/:1Dg P +EbXne.ʏMf0+ggܮ:P3p5Xmf dF/:\y&ߥsLg\*J-o dg2G2mS[;R,nsۅ)hj=tJyX<u(}qp#[ϯ$T4OtgkS} ܒQy}t? [bm6NuiWp]amd -B͠k98rơ02/<9̣OT3 od h}Y>yR"V" +˪5v^q͍?a<.K9aA}t }v1Qm~We"7#_ 9Eb l^i6Y^{pMq6딨PgCt.<:Ⱦ 1 DiB(C5M1Ĺw|zv׃d9b&L#E)7y#]Q8~[?[&W40qE5^HuD}˨3 uǁoNߟcQȈJk>m !Ra fޯL˱B7'IE)Ћse*WrEfo!c͉'jD 6tPb'F*[{杷wk)_?RP_EFgRzR!) PB>Z&63&ʲբT9KI(nJ-~|PI/ѰF`b'71޳"S'y+Onzp܇.SnDѭj\\f~ v}%0>:CIq{OtRU1\>?B8 +1fYHs &WW'9˥R!Vy0bùpgdP7 v ipnz%>?[<_HTdf K >/9۹Bۥ-y$ZmvEE6py(vtV[gg:i9[{s{jzGO0O,:`<,䯔- |C9i*#YX4_+: ɱ7_өtgJr"CG/w"/w#S'IxPގނـ;΅ry"z`>} 5r,1+;'.S7:IL.g%r~I"yO\$Z.BoZ;N7֛E:B#FJCC/h^blU\ˌyȳ,_6 dK܎ :ZofWbIDRft.= S߇rCy>z)kCNfTo>-WKT^K~v6 -'y~?Ty\#M7ۧ8!GHG|^W/^ެ#8=6e kr밖Bn#U}ҎOh3&#{0s{DӵG(5;UD*'j0 Ω6!o*"d dMabM|)&"VX&9C;XӭȊv 뺾8֫Y,wJ(6r'=b]ZkFA?1S}d-:aGyD.UrÍRDKA֍{ yjjRZ ϕG=|F&fDXFW1nq}=.+5p5A }궹W0i\%hw*QֲP#r1ܯwJI;J4&:NΫī[ M?$#CRF86,>MFN0K2Τ~k8^?W@5*}U! HS:mY(?V;~ʬxhIp^[ ́xmrYcyhRޱP!Y*#ƛѤY2=sk-Fޥ4ެzWOmD JƗ+t|`[m*P#{_Aq%v@xzpKXǭ8z~5MpQDq*E\zPt$$37 `pȭwBuw{ܺ]鶞۞_<|"Cc '`\ <=(,z6yq`?|[Jy4qƽ({6v/r˿?!S>a RPѶoߋLb2chZOdW7@^G'yѵꝋ{%3Ja}w82gw- +z%MY;<^zC%9ԳȠ> ӌ3|9 x4ZY:ԂkF<ӱ'1Q{re-?%~xq5x=On|2s ߉#>)1ko92T=;гw%/ĈۅΉu݃ ' +zf0[6a݋, *z5oXsNU\5'uFS_e FSͺcKe097a-!8GCsw~D3ӑOz/cqddq~  W٩.Zв޻qe~\O:!.-`}wvb-,Wƹ|r,y8H+^qC"@C}k6d5Gѻ??1 5Ww B7v@m oG-Ca$׈nT%FhMuG$M#@mGV\3ڽ7 vFD=, 螊z4;^wA=y㠮+}n0}$okގ<@A%~r9!\Dn{9}nm. a.sKgC1|ϐÖPS1h]7'0Jᦞ%>' c '֚ soBE/Rouoq!h?DLe$ñNܖa0}lD( !KWK.>N@`^)NDZbG&psnwUo5*=q?++`p5mjM.̀YKmqdRaəҥ2`@1zLɭ6'˧[6q/?Dv !YH?&ـ(?*=w#{]pAD"&Vv,XltL5Ԣ1:\jG-M/hiXr$ peA6 Aܓs+5Q$ ,Uj~Lq=y&6גXR?#R$7hHm>eTFuY1fR] j9lpPJå1|֣ߎhlc:+qcF9&)Ǩ[gތdh5蘯POg3Z~YC΃jٗ3oCqjXwEdܗ4^+g!qs&#Xևl!1J=EM"ƣ['Z'ycu,v^S}~X=9';, qM{9Vy7, +FEՐHW; 5?ѹot& L 6`d_J//_;y ϜJK7ytE 9\̵#st%8(ZWvv-=GP답w-,Y8g^kWY^ a]PA +d(!𨏭렙j[*6a}zŷZqNy3O:Zy-GJ W64e` Nyps:Yj[Cu>U;}2>jW3*ɽ<9jw]ˁ1dBKk&-\=4x`bf-%wBGÛM,:jk_~pp\ k<*اхKYd H+ٛ6Į?7~@|Ba^8W=&<~eOz\C~Wt<<畗`>r9oFF+(φX-gD + |y-nd'-=.fUU`){RA^ԁƶ[LXK9gJ~֞:qyq-x' &d倻7j,nD|7(O\%i.r#I)\dq,k==yԁ"-@Fٟ6Ԝ +%1ͫj|3tv!'e&M)eoFNAԓ;Q7z>x?\Y*&u:`pZia +/Y0KQ-b`MN\tlaڲ[#zR k­z³`_/їܟ́<->f@tdzyfIog`GYp"qKiPt qɪ'7bc[f:# X.쥘\oOf7+%or+ /x9'Ɩ-DE t+ #a0grUy0Iv':coIy]mA"k +b|OsEua\徸 ""hU4:&f:&F.{yT]{aY],; 5jij֙fIv%[5p]y~y?x{Sx q51`>`] +vRX +x]_q~h]ֳ|U8cDrxiN6Gڄvt=]/s$ґktޤcuIr&:b!ЙdRMzw8fN{[X~Ljf|: ϢI ɑKf>e˰zm~m'{ x<Ҩ^)W>[7z! rX8җ*'j6>|huTyd{{b#t|;RYz=0nsp*yV|=Hf;on9棹:J+cUXJ +^/fUkK' DWߋyn*c<|Xf>;,A=}2] 7,>fֳlX̦K /mTS앁"֙ԚDNg+)k4KE'+W,oܦ7MK +( 4#I&2җ jMU??,~:zfҧ<j2 + e%}'&78"r >*g/B>,Z`L"s A/jCE("4I>)gѿUv|6e!ҋ;)Y?C#Z3DVɜ;d*g4Y̥u1Ni5W:R]vm]${p&"Y#x侱4͍ĜuȎV=\K\HϦu^RLGLgt,dp>/fzGp~CdYr߰Uqgb ^g*=`SYz + 1'J1S47{;;QEU"A:ޅۋ3"Nڡ-8\r"Yʕ?n|}~-C>![jzk6B>׹ܦTzcj/fNH2&e#2zŹe0=hlI$4BgDsw$NɮP#i`Sؑ-[2` ތ: +N]/|ːl@g#3sz>Ƴ<)d+_}zY[lQ+5s!}=),0aH]䝔jm(VϓWv^fTH!&ظw4MrA2X|%GD9p5H&ȚZCW"iSz,v躒+"~й+o 4z)Wxmg"6(eNI2#Cto=WLE;hpbciJk:ysBg⠷b_C{7Fѱ8C{,w \3x'Yi&]]Ri\b4U=K쓃egDvqv@c!fS;QZD6ewG{YӀHĽO[I]y 2ٹX;=w|}V!ePd,yq#ǻۻFX=2e&27 #/$o5C=>-sc>[^˓|KyO<=kX'tq}+)VS+> o9џt/MELoM UN?OӉ6]\&J4e;~o&MDO~Ĕ&$P^'mfP?^u6o>r]r|t=+H/^)N#S7\>u`ϊ +endstream endobj 29 0 obj <>stream +H$Sg-.AAֻkպ;qV\xT#$HBB G-mkvֱc3;Cg}3yyo~}tA b@-Dlt]B+%Y)v-bb#~)XXHE0p{nlîBDAmdleLZK˅wC|d¡w%8(Iwpw)e< F6v*_ ]W +@_5Xcy; :ѧ{p#C?'?Pmf[(x+v9-b' wˀJR>n냰FwDvf4Ftn1jTb& l7 ! $ad&7hOxhv1hMA}J~2Zc G LmS&pp: iOC.yM\DM61ā(9ZD)Ni ++[/:DmI:F2kVe0E} Y7 Xa@ +-#;7'_ +{yUW'ZbsOZƝZO:dzi(]輔)t =yBw._/ ÙBWJCoeÖ,s'D'=XLB^ '3hle4&9ȽN>-4lO?'R +{oaB|n|S7Y2ZıX]ԊŶ3{S<;DE9Z~^ ]/@ĺ]:NŻnLr%&B<(QqgQH`g~T.h?x/hVLe#d:7dbaG?^zs}Æ`˔4JUhm8d't /G;}6Jeag3TI+Ww;Kr9 C)g9g~3 >5CIsȱ&w a +[wfrcix^,bA,h=YYRH٤ّEXBsKKصR昭mowSrx| +|W|{Ә_0z7GrDKd cd6MHIƚDdK`5 tٚ VȷQֶX6nH!&Z8k%FX44= ٖfhR!>ǾV"bbA`*i l`0 ÅǹC{\!t*V(k"`1w^lVL=wǠu9Zqb% +]rIrHL;yYsR*wqM$X#h1POѠޯJ}.`\/rG{2^yqHw\>|/iu$!3Zw&_h':8p'{>? B2M‘]п^hʫj~Q耞tߣ +سi հ.ⴢ>YYORX:DR kc)6 ۦ?P3.b9~.J1)8tw\/oCl>rzMIjs|1OќOd"elr$vV])lgYo^6 0'|)Pߗ V1l0~NfCZ1+{/z?#yKG2ړ w'`)>˘Bl-bl=r9ARa<߃GUl 6YK'I;YSbtIk$:¦SȦpM\3?@9뢱 +f*s7ek@״GX2_k]`-$F`_)ĞOe*9%yxPx =?lqǕB F ]aF NoL_?߉|GK~-~>M7`dLuM"2AC0FӀ#Z$<$kkq~5FpZ7|LT'I.󧨲3 _Q@P@ADBĸ :2ↀb*C"" , VF⸕NJqX:I%}7?tQ]tsyۥw x^nO7B2V[GZ7~$ +j8!hϋhyj5hgF6beC ŧv$iGbd`.]ɰc{M% dSJ&LVu}giGχ̮<șUM|R7UfVM97,r./=:!XmR;5\ZAgP~["cK d Lqޭ߇ӮQmA sr(v=kLJZ}yt_(} ԡ wȵO13:oykǁ] ++=DEFuiLN̥qJaGԍ˅?,oA6.G*+o]wFwv7BClbI^VX|ފ>ent+>n\f% X8P>ݠ P9.+Y;RR:cGs]@]fwŝjx"mݔ$}e- ?u&ے&G?x;LHYNȻ܂}>ZL!*\hZy8SQu'&JW:aι/PfD"%'):b,i6wY%.x,gf,<+d 7(sHg;QGz."sYHp=r&+Gl ]w"^BwƺD_ f"/ݗېsRr-Dz]}.F>O֗HX5N=#m7_=ҵ"Ǥ]RjËi۽5: +κTقCƪ@x/r|O+ƃ,t"c.yraz3;t#dm#y +?>ne#LT+ޤV_{اo1Zz:R^,3~[/%RY=ɫI^-9mPNhK$2(*tb-6O;D 6#V=㮔^ma?&*Ԍ}yDn8uŢmr5o)Z< ,>29"kjn'7 ;P,dM{96oQY=`V^ߨld- '-L!0tOR$_;Qڞ鯴z:s"kXN +0DJV8mFkGІy?R%=QG2xy] k UnJ gCVS3Xn?Gh sJ;5LN֪]ۮ?MuF xWi4{lq<"tGf/ś{+镙oWSY㍕IҎ4!a϶j3)|uxf +}O=]ϫx9<xv2=NaSknئb"k=DLCAn rqRpf)96 w2BXBE5;SgFɮiד a_+=Pe[[w*?{;^Ʋm>%oгRP*gf#m +c'lmu}J+qB\^)F|Oڑ3sJ]އe#vվE/3V~_ZP[Y\ѷX} zOLܚ,4)M~s#`3vS55]<{MyˍpVV9uz#"V ޥ +A>UEic0"d%HDhQn[Ggz{+PmV0 EIVA(Э.m0cq3k\ksnxw>{;.n.O̜wgPRjռJ@?5)E?Vײa.]V@6:ɍC@ϛ|f@ˇ3}e\2N +_m f׬ϓ09~7 {yΚd,?eERj!ˑMXqfgOf#>ّhT2z.j2<5 O^k*G}%ӓ ?jpV CP@|Pʆ}Y٠7r +zE&;N~Y,V}}S9yԊ{ xe6k_7rO9%}YUr`fjד|phF%4I7'DKGYň/vޮ*T˄cM~f7:]#~jWr\aO|Fj* xiuHr;Zk +۫X 5ʼngV۪y];Q#ޤZT5.,hvtK/ЖJNUoitTpw +=Hׇhn/xbV%:Cow ^"v.:%MoK32 .?agM E{S?3QӱVX|jؓ'Ls,.  99JN\,8,)} +y~A~}l3ost}FcE藢@^>ԊtϖBC`_7r ZM|UO49(CQ Z0_h*h^ ,QȘ9Vl?xYN\w+H,.O3%O3f³^J^9\@=^ksOgCXŽ"EhqA7%EK̄2~t&>qRv$ͷS4R'O&NfJ iӀAAf9펴rP)>eFM \3 w@.a6,i1 cgo^OGQ*x+Zhks?PFVp7-NK-eB"؊ ApRBNU_ n)C+P0W/@|1 k/~AnyuANn ΂ H5r`dm9c6q-J?m~A WIP~t+DnJkW/fiuE~oٰ䬷~ HZ`v%iZ_E_C[_'azl^FW wBi,2+} AOD˯*ޥ?9j09+wu={gb[-XRl4U[iɐ5 SiɎs;D)I8kuRRJp ӂa< ?TlVr]ΰ :v?VtSxp0p啂9]n {Kp_dִ&)0dɹ?:KJ}6'F3-Ɠ)ɽ6IO6XDI*5k_/}`_~p_"u"Qyb3',(+LT!eULL$p{9n Jldqa5lBUA}]M'f^˭{{LGc~θ_#^׎Wky&;b nh%v6Ӊ _`Uh\*{EԌ3MaKAܻB/Bf0sl8ճ"FnH}J܉W>b@1L.gMٶJWm3jAF/j _jYce;߀wA%Fԥ=.RAF<(I3uwd۰z95 +yw@_}ݟ3-}FTf뗓zowv[]q=p}pe~O1BS?HĦS*\J| Ɨ&cer/! + izx2d:uil^T-19?Wdh;ҍq2A{C;(V?so+!`, +~ų/Ju4l ;B=V ;R RiRh5ϱw,zN;❡cb(lP +R8Q,4ӫ?.VR}Z1|O<kuk 4["lr'rͣl`4$lͱzj1xn7 @h$#D{$A 9"b4 LםJ=Qa'^#KYRQnHvo$a=G5#/b|t$my6_Y/-$K׆ς0dAY1f+\o8yvdQFuzEd#LD{5y݁??(p u祳'Dޙ{nͽ1S[u/:h($\~uؼʄgʲmth:.cؽPiWV3>`l#fѳƋtRXC/.dKgs.7a5h-d{ݏuAM$G9Jfu6]Ԧc qB +R)S9c`(ȒSrܓ, rđ&Yp|5L3728v|Nm0:ILJG ]ȇ#N P0 'uxA>.@}5KP A0:@6ێ ^I +sO +hu%v@wHahH$nlj2t+C \?u Ѱl"FFvYH0a>~@_(A/L-(1UxXܿ]|0Z`݇U5T]8mΥ,vjh~jzx|=?<7SܑUCO Fcc4_^UBry!WMwF6 CY6yiK} *2]D+t\~(hq28"U8k~2xr?5|ayhZr^l--%23 e-JmcT5m+g%cTZif&rImyO^ +:'J!fx,ʳR@,w`s^,$^K1 afuT髠WE@<T^>O%/B|g"Z#gԈI*R-e 2G(L"{)MX~;,>AHeRӴrKzvpWIx_HE*h) +kH<3f95ٶ֚Lwwez@UAtwh&5?^HmAw~'2K}e򬡲JÓ6y(4^þ1S7Ipx[;ꫦm#g h %`"m|vaWF%?Dp1]f/&aŴqJ}TUUuFhZM.LN{}l a % ^_ۀ ma  x7YB tz>stFVȆ% +oȜƟd\Q:kֲ͎l`hi`E6t|CS^L^mkSa/~R> {yI%9mLh)R,/sOgRvbmr?hr._0GXD--KCJ8i~1'zC:}2 ~˸w'jX))M>O]r6aQdm$=uwwp& +Y.ljK(bTM8QMMiYc'?\|M<{[VWi<吅? vt uD][R>֋`/xCA1'";2XSJ^g?0S+0OOh.*<3}4ȵx +ԉןP4''4is"ё{bBjjmgFQ۸g:Pu~rzd#.EK+BWib=\l¾v+Hq>)=kS\3$+̗ +`pJS#v!?xWJ~Wggwה%ج>(v`kF-Ol['&MWlY/[5!Di\lt yXU9{.2`v) +x & 3y}O~؀@;ޛ㫴I(To{kY[ٰy[li6?G 7(j_SCyy5&POx"Ȁ^2CvWW&䞔qqK#|_ϐ-R 8[s*cNMSCjЊI / OˡӀ :r;P謃EQW_Ź t}E~YYVþ?o!aFtJ%UvVEI({=ғ8~ג8qI/*;yaͼݦfpuwKx}e7meu]>"7wyJ ǭbmgʅ=*Ϙ͆->h sI-EQlYYhE.ѯ.M(7;H{ 4*+B~J:\!/^4#d''_~. +9zi)x(=tFC;Lb7E#@Xt'BLo4"QtL iBes! 5LqLf{ysK +!(4 +HXVmsbpqwm/H%MUcqYuՒO)1ŏ{-6;|FAY3Ն6ZkQT).<>U^a-emʌJy{c7oɏd;t?+ 5:':(*X{(Jyx͵BF%ޙ)|D-";` 69.D[‹̼eu̸/7MЇR?}lDr) +~VzdiC.~N>gcj]qM5U_hS[z)֬ubuer|ٯzqP + UIgf)8KVü"X;]ABvkla,8=X%[.NЌ-BmvYE7vaqvvSG/Lo +1 c6'pJGTr Z%~EkeؗZz\sf] ULGǻ O'}?n։gCdi )?B_>!?tFgfNeQ0sR0.܀Xr,h" 1=MQxBKGI,ԢuZڐ_2*p1Vq*ͯr䉡¤ⲞՃR͆1Dҕ@dG[2};y31}м^8,WqsPelgbqa>,ǏL/Xޛ4(Eni҇j^ʺM65!-|!#;^kR:)k7Kkex_8Iyv_ںa:i8o5t\4pB ;u#̯ԝ ,}{=eB!vT7uJVFhQh}a Z9oTM-r1wJMdQgQ݅]bI0)MZרPrXfO>e)}ä'ZuK6&9+Tg V!8[ g@sܸtߊw~uU)j+ U.* F1J|=܋k0f]DnoVɤR-v_ÐHTYt׻7CUs ^$n fl ٠F$Nro +5 X"ޯ5Lo }cw~Q_%ۢ(o;Ȇw[ez'1d?:9QޚK}BXj\%^x2 +zʦZPE99f#Ҕ3tڞ*چYA⬩MZqK'Lܩ*Q~llHvἠ71Ɇ<̼WlʙZA3Puϛ  $Og/8=o8Y/ Af(-4 ݂|\֩"S +S78!o"<5&Uv\FoάA 'ù ;@ȳq%3L!Vb0Y~)xrp/s-W@pLf l>Aex15߆a:==T6ZwO]59(3*/<ꐟ[V5+9;}da]ѝtid1:Y(r=X.kp W C/N +AIpB+ᅣN/]ɣce9աQzyy7ʨRzjZħF 9tԂ72͛ Gذn9շbyzf N.%w􍅙s6ٛ`Fj@Q_d;`m(:@=]oEm0f$>C)<ЄF.h͚In azc̍˅-cGƆv6ŻDqyY._{sVPG䙎tHxCsgNd&P E W$0qh~BzmW<{Cdmeەo{0{.ִ֌ +& %c2v^햁zqEl7#k~5pbk)Ykf}  X`fl0 9Ê^X +@k'nnn.-W\yXGG~ 3+lt=aoosYR( +"225s +ӱϛW.)h`]5F|Xz!`n +v@sX^y`-l*yEtspf -oV.,µI;*J'9}&i7[~l/gaU5=!EqH@ֲWhE6= +T܇d0]=F!@)`gW +jx@ |e5'w{gMҸGkG &3;;T&l8YX2>K7hZ0}[/W:|b揽 ? ]g9ófL#7}83좏ڋJ]}Gx"Sϖ9_'bM nhcvWVq^Gѱμ+jzgᶡOx&a.S(E>{tO5m +:$\r6@p[:ò,t] ֑@7F:T7̔\mn;N)Ii5eD}OJN_֌"sxmڳę?J+ij'ȉ_}};ʦdݢ)-Tԣ/5)~ !RO31G24mB ^S^b>G O" 9 + +Bac6^u9~E7.VAB0Hb7\Y*;q'RIdÆ>``dfXm`qz d¾#wV j;NgR.~[V|O9ꇄ7]Qbp$zQŌ=1"Q| س9~M8i65LjZ%z)ѴӅx>I%$=ҧsOkELbf}Gmy +0\l,`;łM0`.1% ܶ.rѵį_~2x(5ln +=QeE>B ۄ4O؄FN(+Eq2冄a @A?__fEs%<{,*/k(7]>/ 3\#XA#X\ߨ^yW}- Cfoo7GȎ8 Eܠ:ޢ QJ .e)V J?0D4ɏny .!^cyzNe9c\#'"2T+;$:2QzbO_M)Fa 7UkzAd{n3(6=J C߆̢$ۇhm\\u/O,7Ne rte*49SuNZ=T%NMPB{r̔vT )e+S[\\uzs>$|j$^jx=r 18tw&E6ts._Kf?rƋɳM6>ZZ$֐% ڏ(ɕhlpVR`xgn69Z ˟9^S%P+} $OYB\ q#( L ce#p iI=K,=v )G~ENE'a|>8JOZ-@N fg dUG@ךԎ/luJ5@ 4fuB: eB\\3zḰ0([ćӞJmNj\+?Wz#F&ȥdžSjZOSdle.DFȾ`ms% 56(`FS燥oI`7| \Pg83U#>t`lQGD톕OsaOIS"hy(^ߕ,4_rc=j i<z݅P)WЏB.z:$X6xǔ.svǭ6@#~yqa@Ε/UIu"/'5iIᰭlfO&d؍xT>%aWٗoLƗ+|OoqurWr)PƔɤ\S4 j^j'kKT M{k&0D9xŌ(U|뺿J zܜ0Z'-G"S + 'VCWy>H^qH'W͐U@`/#ScYucf,ДE^-;pd;0T7':@T?1Xny+l|#<4n>{-|BVEx+rg5o@*ȕVA/IVQGGQ9mWP"x <  H +%P({i +EV%̑ii $â0~  XP}lKN{ %WAJTpФQ4u5QQג-I<{{u+ܞ8˜ Sf0PbFzeLQ*EqJ6ĆR̥XwOPARYeU/$8/d/ߍ=\_*^<9*.<Ύ͉xxDyf*z t;* juF!s9st$T)6gY!?In>Sby6?s10(JU<:v斂TOx3K~g屏^sdi +_dJ&kא-q1lxMwNH؋?b&X,> BOgT{a +xXSwZKd(Tto=>r ʙ#") l!v0^[ 8"~ ;o"m@FDr_$~ P(FejVذͩUVOC"4xG^c Z[K\U:h^4o((}"o]aNSQѰB7Nyg>HͫPXYӎ. 3kŽʛ$O[*x>`EH-o,K`MKc{` ]< n@{ )U"#fjG'V+0eq٦ش%t/!(E%YuDj5y|d֦Ó*bx$7H [!#Yu4 +Kp#A@je +eB_:?y%=X;P*0%8H w$f$JLsO:P56IX@0(=r'茺E6ZZv ['|ɶIEhq78e_盄Xmvc ;4'Hw7yXR6(wgGv=w +`)3=)3eQF䎤2'ޜzoqS?g\8GqBJ5a(Dcrhc\W KYPg YcuZD EI +<&`/uc(;;U+B9tN:`+*s ++Qy NX'>V}Uz>HZzy9_k.r&E|@(%Zc1ƛVsyxq竰1g]j{hЅ ++t0B(^#fo Xd19UzěPXEntHx/'o;e X9<5A6yP-ozyjteYsg3 "E?M?L#.v;<&XD%@BYa\Lk^{g, c~Se˝kP[!xLkV1d\\y/ǃ(=-ROԙJH=btNuǞıaS'OC.K~.ix{/؈ C~x0>J3F%gWm|q\AQioQQ "Lwu " ܥp9\"r9 %A-͊ĨDݸU[lwꪮ{y~W?1?A!ӿo:\,wP ÕB%>~,%TQèTxT1¼yRK AFF#Njх{H?ԿemAr)f$']#kASowF_(ؑY9:?/b%6=fV[ʫv w4^ޚ#Ή93-nwCx)p6ED/?a-YӃrq>oPF'*ל'9O{gnׯ)F2FII5VDĝp`+e|ؠܺH}i=sy } +=amgմNRt9j$k7?dmQPgl{` _a;Y"S8P!81D2d)Y҇2Bb* ,q&8e֌8 S}+lPvL, A|%ߎ8 }ǜ=ٮ0>"D# +'Iqp蝔=v|3Uc[?s|矾tLQ5}tG?~{&6(g0yeկ'dW<`~oOg'uץАQ"&gB3Xj(S 9DAUՎl%>Gez?A[?{s7)pEQcw6se5[ׂAk1 eo +46K莏jl+.( ~8?rV ªP{f+oCg|@y-wp:naC76 <R!,ENT,'m\~5QmgiI2j-L舓D}~l~\y7q_ۮ^G.6˄Sp \inzF_q7j߉/MzςS4iI @]7rs>},[9R9b<7]E kuGC97 +Cj$T+op p;(h]hF왴 B A*Nw;)JP +o1rPoC,eh@2ήN9:?Z+t8^4Ozk'нΗT;KseJRTв9Y* So +x>JZΙ+"3RIQgZ(ª썭/hۇ]HQ gyiTUL,2ĩSpZ2V{ceoˇU)Z`rׁU_ŕ\H*{pYFʖtؑfks=|ӭ^ΐHBboF \}K|)2ъxt +%@b"L!x9g<Xc>R>( P)K4#V}Wpa /٠SũQrH8P,G|h1L5 +Qgs8~?\yijrpŠ#S Z,Q=rΉ1m:TgMA&+~,Zt2h~)xOL]L 9.Ө<e2,.Ž +EE$$w7,aٷ$lBlH Ț"OբՎROgf9~39Ǽ~Úg*>Q0QÙ ;7Zݓ\IDT:&db`V6=@wq +ZܰO9h4V|3Ƴ4'rg ,:c]7p/%J1Nvr*If 9`UG)vd5\Ts.oUxf,"^?Ɩ 5!,90E Q=qXn!0B,Ef/\k~]ܱZh}C,wS+^|:7Q) =e}.t}|Wla.dQBYɎ<yP$MA8SeH/"1"0O/ˎy4T`ה`ѓ%Ѳ Hy$wq:+pd ]>P +HY҅PI;j]LS$sg۳ݨ[j꾱u|M>:4x]<{(;sr<^P. +<Րed/w^GrPWN/!͒Jb"ֆ3{evL.uH 5vdi!R%Qx75Uf9U)S񇤼BZꁸxGvym:_k"s:(ƛk92 sy$&/~qH:%C}z\q֙)tDōnBm?y4E)pDcVoXNn0H}6 +dFwukyeXݷ/.qZpyi`8 hM>Tb[]c^"s;ċXH.ꂐvep ~ 7=82K. jz堨ݍl z~`KHjrgC*=$t=;=~$6ϬAep9љ?8Io!o^ ]*T *y^?\#VG/W -!f<|xBOJyN?>(L|xȜzIƎ.Dp~;A5>7΄bp o q=AqM.B˭v +,D:O,ۙ_0{ch$B^o.NUѨRdX8WǕ>\v, L)8d{,ؽޝN|snNJ;N/3vO#Yajx\9rJTuwF~x! +[@>J]d4_ ăSN֏@-О׻f7c.Im{Ͼ$"TwQx0{9?z8$ M+0?%O|=xi,{&#O}:C&O]phf1Pǃ/'>my bJ\^S +̔c" wH +Z]qT͏/ψZ2|NLl -v˟ +Z/Sag 8< >7 +@D Sj]MBI+Y_DWn. ƂreFCOؖ'2+ Onkk7| +IQn,yZXHZĤdEAK).[Ϭ$9e2g"\ Je4"m칽I5p;_+8Pfd/3?Aa;8tq^ou-P+[F>kyym˪qK$qf+Ӽ`}Ϗ}<VOkA( +rh<*xwt 4F?$cb8RK<Z{},fxM7C~س@hn;lM?DOO)c@˭,@v~l-έ%+_eVT\O{XK}E͞랣0YND0Auھ>ӝdϜQ_D0W* {SP{`&`]>JlK@hb +j3`Zl@uސ,cD歊e7?3ωn4yɅqGJ毃Ef/T1P=&z`e@(ePB; [XTC VZ|Qdx9^um)88p%Hh[ChSi:Q 6 ^BE{; fw)kBz;*x( 'b}_SgcϾa;l_r}Y!&oEe+ʿy2 : pue!o󔥗;ރ^~n+6o%[HN +BYqD.6C-,ӴLdmҺ@Z|W3qxnسe3߳h=HPi鼝1#b^9MZAu^2&f9@j18_D4 ~ad9Ae{'8`[:~vy42nCU#κvyo_zD;_#ABbDŽ{/<(ȑ~ɟjXH ތu#|I/kXkKK*CSx:jޛ*tbݗ`1KhLAsi:cl`xvYnp`SF90]^(8a:9 FUnB[P<_Grgx>ߑPGf6 W!20WXư~劓(5z!Mwn/TXβi N=V$6kbj]tǥ _,B OzfN0 f]6ah9I.@ F#݁c x~4YC+ p|$O=4ߊPܡ|mv2T1,L/,?yTK'QnfǏ3/~(Puňr9ua}2sT;syu+'O2ag=AaN3)" QS.S6;WG>lp;esJ"A_=#=E*mLwT@IK%,nEP:<Cw p? 4GUCWe"-OᕿEٝ>261lü2SM(e.z5n>6tk7?!H6|QF$ 0Qу,~ +?_| +o8ChJ2r\P/x;!sfܯl 5Bi?Y2ʪ0 ZY?AhAErܡZJ CAԃ#ќ00H+V7x/+]x)')T9G0<8lA,p~'wR)Q8*OTj`m.o%WC\Ti]Jbn2=巨7\t&(%1Y$AN ᭨z$ebrR'%f$dV +7A]3.(ҕy4ѹʋr1] e ++X3GHm=mcJ(SEt>5fq8,*bҀ*(6Mi. ;$]n !, qqiGkFqFǙꚚaιe>[ 9ݏb}va3o#>%i.1 &ex&+m]O&F'ewYn+ϯs-kIq;o ~'/uBi:Mf#v8dȳ50g*qݷt mO2/OTg!oYVw{錃_H 븡YzD`WEt6=+YeN$ԙـ+5najH3ZTJdͬt֦}>x^R̀w1}v+X1:[3-AW.NqA003鬣(tLREl|"ACI}BS$l;)$>1+"bK" EBOLUGr!P8O:<+eoҺ _p|c^ #{j~%ߧ۟>H]B$z{A| 3D.C Ri'/ksװӆ4I*Z +*ɴVV57kB&ĐHMc+}t3oy/:O.:N%ݏO 7-wlLf_oVW;'wK7 5V_9*ͿV뮯0vKU7S` iz@~)|PCjǾHpy=w+i"P2ٶH3[uM!7J%:cJjۤ/j^>K˜}Fny} 'is!RsO><6N͠3@~绗OǍkԻRg1jFUkƩ?}M~8htZ`W{BA6#!>)R9v)3~'1YHӝU=^ٖ5o\M)k#*swӰ|cjE\Z-C[UJ(dnS̓6\2E[悄!K;L> :.VQ+ xNWZgXH^bHSgs˜%om՗4y*WV{Y=FE:Cp7CNB7UJZe9(?o +cc?ǐpe䟿>p2>>Hח<|G]^+5?2"@zKS #S+1/q~@WwmvsjN*MnжKvc47./ٶ;yW{x,GRǝpVۯdŵ0#.$0 ߋFER.:Evx^gQa3<2:3 JpKlO;%^ҹ <ѝm^k 3[^˭jN֙t4ZeUx)'*'TmQBG L&}›hRs^)~ot'gk!9]2K!ǤM2#knԗ6zdPֳzKcv.apyo*U+YyT7ѷw3誴g16a/BƠɞjI-Z.<_߇5}9gCġ=tW芴vكZrkh[pEt!O2 rc,]˘K '/0&%$)1۞>NoDpTM"+nՎ8@f>}ԗqHm컱fJwʭǐ;HEVź`>ͳN0#y6u\?Ǟ|`K|n_vM}ȃHmR i~O]Iz]Gj(zy^~] +#] !;MxN]RJ.XE{oI_Z"6^(N/]GoOݷiBjf?|0,XG< 9uOZH^Z&u-DG/-M.O25ƱHW!VoH4G‘Y"k^M½k=Y ϡo G?OCոnqOȵ$&Gɗ XH/l^Zx#DZ]灞5RJ]YUJα qP/BRF4t*9uē~{FY&*V {xf wIoyQ:b؀5=rß> ZR@һAWՄI^ +֚mg"GArp#+ms*61'-Aüy/z #X8)R/(8h*j9kh{+^~[@tiq彨y km 㕿IחO)d ÕaN}pV|-7Ň+m/bCKa)ځ܁ڍ> ==޴}PXvCAo*{ V킞CjJ}Pg62 j'yӎ˾d辖L=OO*Tkg8 iV0?iR|ycط`Sa(|ɇUW"5myCK׷\k%FfgUݳ7w!'r2kZ T%ŭxYkmfࡰg xc$fqHMҩT裱7~>]tYZPFvQ3ƌ7L3VP~~Ezb?5Oj5@^:ǝzօ#i^)NW^u]vЊAv~x#j 7YBszv siEhKgC1i58<<}*/5Ϡ?A_(\|sDO(?b`'=IWV %x"qq8)Y^6oC"5rNwX흘rPz ynn,s!N}wB~bzeuvk{헶=ZRnXݜ~,-Û%esf[a-+,sN⠜#nass_ vT +4*)}9}EVuipx JFm,f2'>Mwx)~k]*g /܂q9v7HJI¼xPަnYKÂ}1ID%3IbBVb17 +GC T7 4^e ҳqR{yyOIvgiӛ82~я%((; у@J[U|y]n[=֡^?KrԠb7oi:gi#Ji-*l/܁ܝ9 + +A)`^tiϯ*,!_//TZYXͪ`$6ű,+A~?vS5x ׌]u+$d웅Jɟ!_(ҎG*q `шY=ɴ\{5εP<;uFp`du@f7 NMhE_JR':"$74A^޵w71eΐsCǁԟ̹̙0\31`t$K IoRNFp4&x{ Ʃ)o$׏[(V<©v cn_ܠN7Ӭp-}{<6]gl•.띵ByVLzvl9WQ5z6__EQ?%1ieu@C0A- JC_ 2ۍ>۟9pG.KQS#^vN32G`@Cmv37B_A/z[/d~`lA?֓7r:k}S_FPt\n^V7\x4S>x4'MxE$G]h5s,mq-. Μ 1ﱳ(~7ZL Rt e:nx>%xs }gOO~ msWM'hBֻ&rU0%ѓH d!@4kL[ǃ1;!\R +M"֤Mڴr $bs5cs3`@5YMmM۲N*Ӧ./ϛe }3G2Tr+F ξ5šzU"kg M;\Zu:u4< WVɪ*3q̍^hMѡQv]ILґ@W5x(֩~=4 ^<X@ $-i2r?(чҿ8בd5q[׷# +Y)'YM['?w^6Ǯ4TzsRF`ghާSr'2k9_fh<3 ,oxˢ&mPFB32|R ڝW BM~%_ +Ľoad{{́BO:[G:tFџG#:ɏOR޳{./Dϔ99v[O,$E6.|z{Pv#qWe?k@xyυ\O/Bs-(g ]tPBSvMZ`N2!ġbCS6:n"v =(Kp;ZW @:;_)oalT~{6b׌Ϗ_~{M=:$ٲu,VCl/'ӣy:TtcZȑ3^ə_Sg7|^mzJ.>Շ]U[)(wX+^ԣF?(aK.qMh7 g~1͑8HhޤqMLhO5=tyyc,%e)#c3*&) j9~ Ku>u f=M̘a>~Yz4jTVλ'xO/ac9 gT<|{/E#p +$jmjǒE|aٻ^$f ֽS}ˠL5ЯiW.rM[1ɕ5ze%u> +t)Url0c&>{t- 1e>B6ϳYbl+|^cዹ,Y.z|5'f.hEoN,6UbRбU!C6b}.;沔ԏ& /Uf/NsJVj_ֿzSaMT0YLB\L%NPHY$}'g LALY@P+怖>Xx!vXnQ=MC)EН("K⸏ }tg= ZS=wjgt.tM;Y[+igd;32I/2t5vYMz-' )O9:sF.Y+xS s iv&v&ͨ=8,<t:v6\?̢N 'C/fNa48h=VGmUA{,Ssu MxA6Y*eg5'-Czz˖SF1ڧ՞x7q Zq$Mu2w3"N9`?;][_cy?n;Ng#7]J]v:rn1|cWGܯpߴ]&8p*y@QnRg8{ip-c&O34Ed}))+4HT9`ˮl"?wb/TYBe+6 #~>L<(=7ʣ'Gٍ(fD~՗"/ٌp4_terq.Nsz S1[ם%s7ړxM›D򡣦ݪy艑xr|#xfH/Ћ-lv6d+:+ 쥤Z`TKB:| ov }2w1eA8n@+9h V>-E9?q/&=9 +^ℌmb[TDKmr1-;닥3Yi}y[ @IRʼ:G@9O Or<"jnp%ju= E;Z8/N/'q{ʥC'|1Z͚ow)p-j]<q?rC) ^KOH>|4&h =yHK4^Hy*]W +2xJFaEpF2֏3K_'ejJa_B{^Qk2vu$_7k+k.=C:Z{DnRbٟ驙 8gȂx^Ju-B""Ƒkc/7|2i%3F)̳'{UzIy1}AN] X }'yZɜVv8߆ c6B[(?z"\/\ $2'M؝Pג'ug ,Lڢm TA>b^^ծ3i?Ԅ}f;5e cnވ e/ 4V LD2;O"hG~ ^FqVew;l ᕎ(MVnt,PzQ0)S6 %V  !9PUƃ/i̡h{b_ǖy=[s@B6$)g4\-FayqS$s^Y1eL-S+!) SK of*24.u F@v!+u\O|kg?8fa/$)\<&SSq}$GA[8e3kkD/9*Gp]Sjwk3?|@wV]6L&o2?}HKNE#OxI>*v7@OËQ@wzqDh֦rp8'{' _dwg+ܐSK3*G+Xd2OŌdzgcOwKkwnPۯB94@N0&(qFrd?`*쏘8üW98{7M +g?6Yw|GZP}{z(0YNZ/.EBv-scӗ@ڿ^ ?ڡ L=u*ⅹ>h:䏽[s Yyn"l,P9[]A^[j#O$"+ת4b>|+yG̍omζuXJƞmS|?ļC?+)gS}b'i:8J]Q860XJ z輱F}_䥨ll^t`IMtRw/rN 7,b7EF[.W?FFTB{߇T)6^*Z~ڭ-W©^޶ڃ sBv==Bn&%M\"g9 +: r,NFNt8jO +;W 7|Vx vޠLSfl(Ẹθ7:#8Pq7z 0o #dr#^ [})X;CގX z_NYa;5+9}JbV '1qp2׏pp?.$viRdнSsV;Ok\%tJJ$M)o6'iRvPp2ŃahmIq?+t&z'7]n>\*`g"coJ}[kqډo#S4i4 4>xI|<=h?+iy l>Kp➜diϬi?pr]7\D!#ۑv=r&; =@l7#qu>b],מf#țdn 6 ')#o3OidB <wC~wבO7[ةJsJnMdpC j^P7p ސ>\355{lEV <8UrT'g}ͺldz?7byϾr˩S<g8X>Idy 3Зrec\`,y"'8xG|aѤQ$3Ie}RD`Zw-Rͨ++hp1Ne +waH0~/byq<QY>vn_%ѱ){Ks|;}Iapу@guE3=Ep/K087˯Lɢ߇ـ\_]^L~?C%[Nuej<}ן;|GQflK{U݋tAJ2j2_( +t/x)od[(h 7퉮o~<,=܀BG'3Oʡ@~$x2=rjϫS#7V [Qu=q)Vّ`7V޵@ZJdc=?@neKB]yl$vjvgOPH.Ө4 hm\1(Y"K\gu7EEEnvAV `Mbp .HDǙ3Ι?k4t[ykW%{aT> i.$Z7E:94@n< ;9nfsBgY ;岛+0&7d? i͋AjzA~NnxA9gY*վYͯ o/݀ק'.Y5/(Ct1"pkm=pz3y]KubQ0%]'ÓkQhȗ,ŝ19ЌVWsyĆ$_%s#YkvF[qr(ah Vܦģ&Ci}O9Gv@':l͢&sQqf1{b;Vp[2= Ev7rJdv 聐Jn7DPAdIkiোsŊoqAT#:)xN<>}|2~@bAWrU}#풣ze +e%-̌Zmxw M0zMRp`"ErՓ`3],b2[U}bgAS{ߨgĬҢ^Z뭤~Oi GJz(g %sIltv؆e;+Y-5ʉ'SDzWH|/lZbfȇ`A'<ۋVn])rGg|i_nw*\7AgU.x}+`W ـs|&å0 .?cggoYӨ"ˇ?g8}'c!3SwXaO5A:,jq#wzͱ娳CVbWQxn:[ vw7zs\Dz,/EB}JN7>[~׍& Eü5{o:+ ޫpP@,>sxG +pjrDw]`'mFhQuu=@R$uI"5:*YݔޥySnP/za  8ty)|! Šnw{8W%;/@bz"_]a-av[d uyFjo@Vƞ=UdlRеV> [~~ f?iqreR >-20)2f׍_o~Q8F3 nlz$T }HUςV<^ +\@dV0=?ͻ`-ek0gѵ՟q X2idщ@;eWR.ԍ+.j'ǗD;ײP-\ij'%l|Z0)O̦tS0VWL3l~݇e]uѷ\0Gz ӵO̚Pa+fj;9튳t鑟qakD˿kzHr޿3q1c=kzc8J#C1*m̬v8U{4c&<<394 +o9Z%4v=ڮT C j +lvc$硴*u%~ +Z؞YƾMioz \q T\HvnxT( ߬Go#WS,/%cb~9';&n,KHc~8\6oTͯF¥|{σqi0uMެ&9f>wkL*Mh oK{Ŋ&YO͡}H 5[pbuK'*gp/ᓥPt1}= +=m)iUK1T'۱N>Ȭh܅S0l{Po3w0-4? vu38B5[ME㾳عp@_~2eSD lA<c* s)DaXi=LךcŵKUi r]Q|n?~OtCH,V?rA )EJBJ qTR"ɨ&;A‰ +X0zavSb+6@,Rhw Q:4eHӨnVñgj93 G\ZW8Ycuγ6YGC8t>T_)<{ruGF_hvGmω(mhCk ^M\qT+ +AvY-4hw _s+ +SnE}%b?-3 _0 += Q~5?z87 +C*= zG5MQ ._{7Dc99J/K)J0F@V#zO4Dޏߢ?.7⾹/]nO離T#<6l?4|3 L;gMH+ 7l#̛o&m V;met`)8rY@-J0K[i0GziV|RX tͦĩG/IzS *&2$/NnIK:Ӡ}zyb(@.7佉RD{|a&)-t[,*N3qSSAأ)W-yݔ yHPA)V(^8̨V/^K29:wӴf2OϘ3s +5mMc5JcNgԋrjrQNGj hQŒiSq}*yAan燂G +&߉%|xuBʽEYg: +?Jĸ m 2ͬߗuS>9/@i$\sXլUꤌbYvNjcQ-2jlZzU@*u jq!5$okSE\SQlڢM.aiȼ)L~U<ӍKί<}tP" +טYA(7Nkz.#4"Q?6޻]1Bx0Il,Is΁p)9oMDw-HnTg~N}{Z.IVTWtWټV3A5RJnN# vXL1LuWn5q̕=* J=Ţf}0~(c- vjY)q+u+OZ%%p9pY,#}Jmm*}حT*nbgc15iec$dBҌF@ !`BB-!t"cAUw?tWw9~E~sYm&XDkxtev[€+3f>rDP"tN&TL,/&m;kҠL&meBʀԡS_&%ALm*MM"[KwM*wЮIzowD=(/rOw_Wx #[ +S'^n7{1:fW`8N Q3w,M|6d \`5l˓X!U~:! +g ³]ACs2nꚽgoSZ8ަ|fc<;p\fgB*- ȰN6fʛT"L-wN"'zfD<>~ ̆f\9W)hʚq1n{[4[@+C%Ѯ2RHOw +Ztݳ j~S&=ϤMs_աyG(з>pEO*0~yǖ.˲\OEkyxFV)|x؈Lky;[bp_k-USTocw6+z}쮡4pVuCܴ s2YL&y?l}OFBt`jP||Fh֎QBߌrgAF\/͊%Ͼ"Sģ֩f(nȊ/!\WlQ{nfQ(m=U'; JۖZDk.ؒndy{dRONmEf2c8ŀR8dݍ@S\֢,Kkri۲:LZM+.ch\-oKt9s5uN[ ұ'I8v'?c)/u8]顟=v )?׌=e oEo,S:.00m dk$n_ᢣDnPa9~s!s{^ojDq~d"1\-I;GSoO%2Iy_Ez?z'*/%BB[I\}G^:>stream +HV{TW~ P@A**ZZqUi)uw+h yPF23a0"[*",(>ݺgϺ{v=;w>̹%L(5':)|OY  u.rBSzӏ㇩w6?>NzN)wnOW>Esz%1nW5AMP?;\|OE&*K_PFje ea50w/ ]`L[~ 1k왹ZqBW6ha֜9 +˷cPRs"/1 +U(NՆ<^׵* F/\kk4ʰ+&uo3ZztC-ϿՖ5d[RkڵTA8<>-:AAs:}R76gel.h&o>,XbIC[SAjoHQV%nKKkr + ؞sJ[L]]R^&Ǿ5J4_vH1ҔgOy>ZNNIyf j,hu vm 3<1'h^o,ݫYhvs`^>.+L$>>L{}ŻG5飗$ʚZ'iI1jh2HnIkz~$NtgkR +6LPɛ}H^IG&1K7#{F̝ m/gB7I}'*JtMĕ^F\oU]o!6ZcB*Ov0yĬ#jzex]Ov,TDf*".-&;lA\Sϱ0~J_`DoĞ%Y̘^C|Ŀx A3 b9vrvM}{R\Sl* w/nW=jPK*>2k.aNȩS[gTjL[,woX_x93{68r@eע `gO8+Be@G_Wڎ3~U@Biio!eapT7Нmni rBמOLm_j;Ll.i{~Sdv ~.2.s߇C( ke )*TDmcEAnlV7^irz48xub{&kO&憿1r3s^5vX"p"H!o Di֚ / m5LlMֈ#7 AqOt:#U1exE6 u 9na\B:1ȉDmԞAM<\r]cn>yՃd$#}:Yr$%WP\//uG)4_־rG3.b(=*o +nN A`^“ϲTM6>vLHȦG;ZdAF庞. aFzM^':5cTLHXcyF80n;3ct~f< n'Ob.WxƁfko t:GQa ҆b[kj3̤*L +/R.Flߩ-(a/DuI;FE-1T#\E v1/ թ5y7-Xgl1r<⊏b΍|qEo[8?cO&GN8W@`b!'p 2S y/=O.EB"θDq ̘T+vۍD(2+l=$R&B( +l';~t5sgy^S֜9KԊݫVzg\Iݝe"hW}#xO$, Hɸ&<we*! =jz1Lb`GRo +zƃ>0K VY%ㅪ m 72+QwXy{gS8tOoYizEJtrSjabU}>~ +Ǣ`d d/6ŬM--'Lgx@WI^g}sSA= 6[jd( + ]됁[լO#FTºO.xܘ~)ƽCE!3Q |_4l8zbmh:Iu}6E\(,"u@ IStqF +j9ml06t}K%V!2tS2l*!, ,敂[W#7%Oh(Kk^l⨡?DK4-w "_?*LIs!mЎ/6_Z4I%+I? !x!D# hX6&؏w˹-u =o! '=됹[_ڮ9j.)E.{܏?VAt!o$ɜD-#Ǯ[V@sױgYGܮM)E&&K+3ݗܪN0̦OŒI tAOWXp<:,k2̧oUK{`My'9d,rHz׸5MS|edݯ[? aPL?vg!:9I^ DŽ>? Ҋ'^e]8 s)#$Z^i$ &kaa.\u~v{y| 3o +^wOiWx]NoM`? ֳg'~OF M?ςb})h-~ =_YڙHͩHшo:3d3e5d{WĦ-Q cק+׃6XF+v:XkҞ ~`aAKG8r29[ಯeOտ7GQ$gz@<#!DžN&О#~ÖK>1mǖg]Foo[$|;c'^vKI}+&F_12ՆS]tu̎Xp⤁ 7Vo+sٌVPg&v 'op>HX3=G bfPD;69<dףŊ51W93/j"՛K?m~utwoUer:3¢bmë{a7GÙU=JWKϽTp HN*{Qpmw>>z9L!n46Πp>-Fv~#/#%*'J% +R:E‰8SCIxw-#*Eю;/"/o;gVgNU/V^wyj4A:)^䐄s$ZtK +tMlbŽj_W!=No͔t_60gP,O=}m=,BqZQ0m G !!F|z= +` X1~g|{űPLP#3TT?͖[o))KX0 Ԩۥh:| :1$z{{-Y)<]/lj'Bljz0uB884X]ebUǐ/L_KC|( Z]R ^²`ԥ>򶡣tp Qirzfٲ7D\<-_ejpfOT^D_hJN'{{v|=AdtRނqbyxT&bWQ M]#!&Q,-vNQ^jFw; t~l5e x[O<ͨ$imW_nzZFK`%a5=yd` 1(A?rH  +qc5d]w35[bl3w;wl܇d}+bRH +eԻlYiCyȫh>۟=h>9s|9)~ 85K7՝5W,q-;Iǫrf:)/;kć8z*ܘC΂4`3 21{!VI ?gH]o#c{{{#2Ͱ mm3vPl [ьfD ,0a +נ +6~M=|;^D-rWk$ҙ}&媥,%0cp+lresv`~ɸ'Fqe(Sif[ĥ6;hV6<ĀӡO dldHj/`;"rRL鿔L*0B:wybmT0M<"MbBS*1qgh~owob`O{oSqrMUo.G'rZ7Urj0{:CГ?)bn6B!%nSL.#SVc9͖ębcdH&8b:& [+O*}@&Fs/eA&~wYd]s*+~` +:ۑ֕1ostԝ,s&.6nz(*FHLo/nXRZؽo|~/<|F!kZ,qʃxXQbdFM4ӕ$?#߷3,o\,E>qJ'JKe@$ooA\|;WjgTl&kFŲa y{Y;M!q+˺hɀY5-+9HQ-ӾM\nr$uST OX m8#w +;/P +D@V(f7xԤj|8XXeTJ8 UjH@"/#-qOZ|"!C +MҤsu~'[&`yaH@WY;W Mo7RyKNh+l&%m,g I'D𙹄Ź 9Ma+Q k9嚼zh>d"`tY̟;8,,vU9,U HC,jUBehjԭ,:W$rH4}"T D Nn@TLJ!#M"r#7l\[r'a[[S?w"jb׾j}+*5dB|7!wy2`t^ tv5cOvaB6/BA8sFߖ;aM_D%r y/FC+"a-^cAn;'(6UgXtP%YgI7= `4է koyq/vNK)8ъN+3Gi ʅT]d㝿(zBVV 4\~iI@4e/ÙH=yD"Nk.ضDZl?ng8ɳRYɘ,Om +р;ܶw":Ʀ!ջ bύ|\dw0&"7 tYLHJ9hJH7f1 /Sr<jS_e\TM6agϦGeDA)nn4fSlNRjxnH5{,?YwˍԼ f^"ږ7'ᳳ ^q‹n ߬‡ھ57'-Y$}e*}3L]d| mH1ʀ*qYsQR)hD9c}U2˟+kp+1~Jj`|Lʛ<yz1S,\ ozI,> uSP~A}327ulԙ +ie& Z5Ƈ86L|ҽJ0E~?U;,X O*-/<}?fi$X +{gä 6C b(A!r NЧvWѺz&嚰i:M(Z {`UlksgI?enr3uӽqV]ӎ9Pk GCr +|)oŰv;t5Tf]ȆknFyCEB\ZEp7x(_y}6 C2QBɣ 2b۾9ze+z }P#P%onT[:PI4$(5P02ʣxӀ@AETPDE]WȎYچجh#&.8Q"㞉3f+?y9uEdlQ zs'=٪NOqe'T58C;aj#*luAk$/FMNXղsa+[ׁ[`EjF7zFD1^`u6"ii4LtU8S(b?hpAu<< Q VbTˑn0& m\Lf"qh ygfFGw>/Nrq;>#9k3&n~隉utߟ!\<5(w:LK;\yh3*ЩyᤛDq Wt~ڏQT +/طkK b8QH'nSb(RDt!:In(q ~|= zIuNTe\jB' %;j}Mlf:W3fgF3=w݇Q-y7#/DT ;}xMATTƾ>qbzwOk%R7/a lO ϫ=H=f(-ofНx+\@KsLQFOmbr4ֱ $zYVX}n9?/T{u0B-k=Qi*Xp nuLݿz}/CqhtFK,{ _0. +n|T\daE'y>C'H;r568ӔYK4;>q.i R5BY҃ "Y>Re\Q+t$g32[RqGDYHYiy8N?} S>/.ВMSXrƕ\E'YAwy&;ۙvOH)P8~DZoIE'%|܂۱%F7\DKL,SҁgTv)ũ]`B*y_a31OA~!osWV~ۗRY/ +x 8cN#Y=Z ٬Rk&܆! R0tJ%DZGxz>tA͸ݍU65t\Pm + `HVfnb3 O x%\UzUuL>ly.nseHOb?ԅٝ8SYҶ|n]!p2tQ !u=@jaH{>H|XNW[2%nd`WTL $0T +@̜՜qCْn0J$>,i + SiCCñ 2!gU {å<3x1iJ IڂdXq PŠYZn<378',5R+IN^Us +ZҩrH@ X M{<W.8e3ڲ:._ ejkV7k[.w񺥤?؋Vbٷf=u}+m|[ngsKmpN Η]xliTSwoULVEARAPEЄ${!("CP QGkE=N 3gNe|IN>@޷֪T366 QbIR54烴R)Ba%X A@a<&tV(3MsQTl䞵M4#(s ϕF[S p"s[Yn RK-\0HXf5k2 %NHUָk=n_<.-Aë}e]x_\P?xIzsPx?QU4˹ +Z%dnq̠~#kgmAY#QHws<$H,(9f᤻ KSr6A$WʼnFA۴.zU(j| W&7S*w\V__ԓːk [#?ƀQS wr|`*wbS +$r +?١e?2pC}P@$ukdHX*ҋNT_2%U\Xa=#p)P^-O~ԝ' sUZsm{֛Bq#H/]R̗k]p5MknF{uI#_gtó|e3^8xZNoE`>􆹬z̒4aaIf[5X(]RL1zr/bӊɝ 8sW_o%|B[CLYX' wJo$:s v>?-䙿D6%lu\Q$4!4]Qnt.D oBЅ_S]r:y~A_>ȏ<Ŏ gȜP /WWYnnЅrOuVlZe%f : $\I b~z~?I<'^$0NxVXغVM4OzA8es$S<bI&T:8XD3uk3֪_qw8p%;~:+.:f[rҽNFj}RuE}24Tń.δm@s@H'fS̢XQrb +X˭qN%pieS@=T0@"w9hd6o W9X0'{٭tjn|5u\`੯p +lUsC_'vF 8ai^ފRנtܟ]K\˃X-U3M;C׶"@ts(7tf .&^o{> BՆjHuܗ+_*5-0^njl9xp#O'ԣodc +4g?r^|fue3952r/6 Fn'ng5rN)Y:'VN WzSnkUYVˁiaSJe(Ntp03"^ +QӌH+2%B툫 tc'iqI,"Q3-Z[ArCl5fV?%wWb X.Uf4m)b6`Դأ{PPRɥqr$ʞMx1ȓ +NJxMM醇VDx$Jqb[`BHy7Qnjr7z + rN.Ef43x؉_?6!86(GgvC5j luVI5\IbYp~n\1?~<|fvd}-9ؾ'g +=gVN>Ͱo3t/~#ΧnENa{GyJLs2]ܔq0eAm{[bVbB3Wn#zodwȲbUɟ埌Ќf;`)ϸq!7odloT֚}bGdqqot+AqLtbp2ԴIË c+Yډ9xj9LˇS!64\skrO~ǻoGt*tܡ!祊/s{C0XH u,X_%<\KG謌l ګ +H=4Ϗמ,*Z#bc"ʮ(ق[QS +u!lH])gSes_]]>Vhi~VMP9 * ԯS3bT".['6KZB`Qc`& L>>WJP臨Cw21B}=p? -g,8v͏~F8{CCg 踷6O$t40Lۂk9^܁ZF7̙9k 辽xhޠ(2+%bDq]|-g"쾀>'b%)Aaie 1:I;&hn,=1ٷ'U.zmɸ:T]=8.mB_+zDA=dJB1wiC2׮h\25nZW !vŇ;!XB9H(mc_̥ >ICsہ^2JY9|#urHOg=/_{L 8d]=7vC]Fo<04pUHeTsK%F$pqcv$EsDJjpyirk(1XBiF#k\)e(Q>`Mx2{i3hrKQ#u8v [;crM"dv!}X&yCooeFUK{iǞgYM:nk)_'q-gs[ٻ9s/ݿk1/M>YUb FV5ќŃh1z9.9B?[l:Yb4M]J0T>Է{^z5E$cq= +& hhk $-0/KKP+]Tpy7V!WʾcՃA:F>Ec0j{<ޡ_~ȟS"ѻ +(pNO4!<4!z}a鱜ea ,t3 5*ɴV޹_A ֕zO"?!ǖt5Pu7:^G'W^D/=ǚ^̀ Q{9Ad֋}:`Tc[5NFJԺ M3U;~u08a8s8.VdE^JjCD˃mufo)I&zѹAW 4}T࣫j V ҬA![rvd^`z{mD&U]J i = CٰXFfӀ@wfrkΒ,E~9b>!^=Y=' l7.M f59'{j3N>lds$UP鎻Ҋe?p_M]s]5?U}hְ<jE!y[)=֫h |4B?0&ݎЌc/pXA>ϰDE +oB)xj0w,cyGpsJ<%ؠ/a+XeeR/JM |!7NB66%}P?K#gIځ4IĹ'%*gP|];O4_xl4}/ݩ/5'r+ `9,L%n/$ѱߒٵik;n ,GzvbMNt'\3DD^eY^jrl[x]wuP=zJ-3hQB7'8{ U{v+k w;XMjWw3\9k^^ +RU]R %xdZ#{$bPM[%t|DJm<^辻U13`7MGJJV'UCwvYx *혍Վ5cQpdNދֻۑзh%YY'iӄ_@ j3!;hȊfJ}khXZ`{?_+хה~|є +Ar奝0#k9 +D(bUBσm8p@^Pkus10R /JBC컛IY/+kgmПQ  XˍHNq] f =_Yt`w?Znc59܀5MNK[tE!@>b-"zSeثNi߄}ZJBVby3JxƧjB +x8lK,^zt4=CՕ,"e{?S3S/՟*}VXR"M -H +1+HM)3隥$7d +GkCi"L>ڎl믭_Z ^L7r^bXhTֱ[&fɼ g"ľg -4F.q|Ge] cai&Z䦩p^'VJ  r(oCFCٗltkTuBҢ&SUg~̼NtG)GU=Jf_-sX~7+2Fj~h{Cmu2<zԎQg2{[*P#s@]YGpv\ gkHbj88zl7=z8_ۄ{LF{mW77Z5/xVV 5+[UOYWx֏Y4ʫ}?qP%g4 +}/dW{p:8t05+n:CA|EqV U +UN!d{ c]B;Yٯ+Ѐ(QHA I{{#F6  [A䫀+(Z׺vuخnԵvܰӱ;/> }sysn~ҷ`E܌z@V凞_j{!N%^xLNmr#CXT5;y6;z!to~7/4Ccc\>:(tøk<s7 չGix7id{o(V7-Fұ*}n1ʝ Qu^,7@3B^2$Nf_hkzޢ?[X"ϣή@QN4u.0Xߍϴ?|nVyf9%z<ӑ({2C@CߒzB?e[|q4\jy^(2 sX"K9 oX2-c;P".OE }1O݃!'~ +"Hol¾k,ZjW@9Eb5M|{հe@ReFh.~q<\G@Mކul%k!XQ#L=wЉϓiz rk!T]RPhw_;NRG6y{(k +-k&k7] QC=5B"tDAEmFS[{ _7^^5wָ1WG h (qƏ.yUDW'/# )p4>*] :DG~2|}TDrqziځ9u(Kj{2sdʰBN|>CS796a/;QsԙP'-OFp7j1n [SUz*u0)8?׷B<\^㺗zawiٯ 35\R$+*qC&H+V P]RVCqczӅw_rٟ8zơ &p.4LbGlY G'+,2PGco.u8Arf?1뺷hb$Wӭ- +gZ+k" ABr:iw}?7Hu`;m :,SũP꾹ܳڧ{ԭwAK~Xs6: Π?A]1=P;T5!;49`kNX'Io P[&b?O+o>6ކ|=෾ "grk $~Am:qnXgl $o+wB~||e)֥q|/03׷WQ'CbID09qo7t5r51?q1`}>E)b>6ս[uyi?%H A $H A $H A $H A $H A ψ>e/0כ +TDgi3;-uJE>3%jɤ6k`Ҙ\RW*>\abҳԪb2ᳪD mLgb~g +V;J6ƳGTĥ5$UJ2&k5)LkSRhܡO?c/B` +RQTfbbGugv=`,hf$*ULRbH#-JP3*8.*ȨULCM*|m +#]iP}*@vh,4'7~;_l"H/e>#*dgL:C^hdjW+fyXWp^V@+J7-sMs +wg`"٩Yp +roWorl='>SYeN058z\$6Nc`o Bn{6Rhy}1u\a=xv3gy©ͥ+[paɽ[VggZGk.ӖP{ir-M!BE_$WW[ +jt݌ N&dxg%$3OyKHH眽Kwj ':Xт6œ78] d^T:I( q?Ղ< ^2ِ%w?epzśԻ (X0XMNiFsu!mP4o>uVy5r)()= Zo~@gb;Yl_D'JѦ&n;Q|hQ-h؆'|b՗!e..Xh0gbٽ^w~φ3BހB3*ܷﮒ/L+^nMnwdHQoN~'t߲ ,Kr~/w/5wlept ;;GVZQ_rfXfX:wctt%Z|2nM,M$Qik^w+{9y~cx[xg]?\{5JO W/7,Ilŕ'lKzHJGxJ|ݪJf5 +u(1LOG>\<#vJ2].7Ԓrgp>/ݢz\|:&jmFu +,~v ߃\ ļ"-fi6uy:i&5) Sꂨx lR|o w90Qx{ g G̻*/ZnOր[$5)Q槟 2 $F}U<[`R JT[N/YĹ^=qzbn'T:$fdqp61^7{ߐW~>i_[ׯوZ9C mdqt?Wc|Ľ"{Ie 8VoCn ZhGK杞;㽢D e*NעتUAty=7؛jPs-Nխq(BdvtRy#.L"_ zc5Ӝuoa%0qD ȆTߌ﯒Ƈk$sث'\4 M~S'6|8(zD>;rO3qyx,^Ǹ/i(ۜil>5g@,% +M ^{w36> XtTdr \ `K[2'nw +FBҚѳCg;e{}RZ䏳/wm[;͝7؂rO4$֡JNhuL&ZkYdx8Tcc7.ubQ^犡{}~kS{Gk\Q QۈO<¦~Y/0{X ii(31>7',3r+bAxp@]uxRhNl_qnht\*]IYE*j"Ezxi=$2 mé2S/s$5:o҈M'hN03lVE ^J<yE WS9`-rbHq^fG+Q;R]Q3M/so̅|6twYfJ}P[ y5m*d6>p ,g3'ί:Kg@$4:f6 迹7(SZ+Aݴ9&f-ҊHK'BJk;YtIU}1o(e1p18TR =,`_֗<*sP&ϤqXLby173&Mc]YtiL)hDb5?{Sc0Z.)k &U ]!0\f2t~40otƮH.?;VyS ϥ=Nu"׺Q7iS/+[I*Ws)C V r(k[ɤODu8b~[Rs/{"/Ê9eZϤ +-sC/^wA/¿I!u/)g1']|ݺek=߬M~wDt_cК-&E>}gTZ^I3#Ci8#2C3i;ԬG"Ic3:Iy51kA57<=b>eU?i=֦c ^9ÝΧ+>sFfQ'du3oN#8P"e?~dj6Y*&pk:e >T:n^7%hn)/'h}7Gx0j{gƈ/q] bnҗ^_c ?n-?,MsNS173ԯrZҪ{y@8(73lL;l]0[o- ;7Mmğo(ڗSjVłuh`-Ґ` o> +Nݤ~KSl@Gf๝|axoyP6cmG3Ooo(~\7͡dkfCzg¬#]wQvR_kuX-z|Bs %i5!3w(.__%!bfd4}\I2Y@'6U FJzE/osTxJmHB/ӊ{g",*uSnsdHMbFZ"znޅtx)F?kV93ͩuzibOjs[T1#=錴Jx4PM>TLcXfϐLT'-fAjWLAW]sO㫙3!΋޿KmOE<0A>9diٺYKկ'g-EZ% ~RJz bnmomh{i(7q]1%G/,X#tqm˺+tGk&zb7$͜$ygutPܢS'6Z ?i`;|:w*c);gDd1DY%mj &Pk Yαued[Cb&-46ZRd`qleՕqd_d  ( +jь{RZ@{m64[7H7iFA`4crIʼn11dj*Uc0{]}>{yYb>~]x{PÚKqri| {{/t>!86%5xdjqR <%x2@򺞥2 ceCͦ)mC{ +^a0Fy&nf:c2Y؏;hZB@nG!-|v^_C'EwHlbX,9AnE4M'`.*XwvN길z"SyF$<-!< 'abV2Z黝8X&)̭pίp5'4bs ;i|^Kn7awB] :s6ZY =Yw3P,71uD@>b^tUq+Cvǐš/,CMCq[7t!Fgi|5RV4Nt *u!B/UNX5 gS)}U7ZI_nb7I6ģV4(;R]_Z֝Uk9!7G˱M+9Z-6k(;-g^~,/s c1QJ*}m o, O; +Et>+e:׭ul⺿t#L7GHHQmkYS+OISd-l#2gfx/+*J|蒐RyU^ M /L1&rO{/{Y5#L鼼ZiIa੘xijl\+-mrו4Z {~X8H׉w[(C3̪>p%yPw3Ynsdz3Q.;JԸCIߝM^v)[GV\ +ٵY (6O<\x4}:j4z'0I&<)q}CVW[xo҆<)|WʌR8aȸ S Q5jHd/v+c)3]W=LqX|v4_3t G&=/FԲ0Md溵e;b{Љ2YXz2>>2Fv$Jm +Bq~I$C<`|m7+M%9b&ij-D{܇@s=7λŻA^*p.ގ`?zF` HQ{c|fV6Ě= zDjxL/aK(tQd:@3GݓBNì 495B_;ezbYxPRc ^gF( kS%weo t[]!EOE%~To}Q<C 9T| .:_(P1{琂E]4/A`!G隆t؉#F^8M2{#f/5:¥N$Wy# "*"(6* *-UwD%AK@@vZ[lwn}3sf{k><.4WVs}Jtiڮ(^=8a+Ys.M7=G48iu9~bnL]B.!ˠǔ!*W5nz]mΓ)l["W.YJpto]϶(c籬::6Վ/%y.U~\sqz#hՄ?m&̇(=YO0!U21Q&dd6t0srDR;to[k,W9y @㬈I$ylb(/"X1b[2M +w߉-?UnQ?,WӚKt*町;GYHf}C7 A>s[`'xu\{W;IՃ7$isR)) 8e fyU8=Q0 :p3y5n^ +][ ozF:{7n/:[eD\1=GW#w*:+UJJ<[(e]:~즜2ϖ }-]5<<\+u=V^Iۤv4wQlTN533pvRX ++Xeώ˒kvt4VzJzE`Б:^6OoDFYgynL)8^gtAD_e4~%= 9=x01v&YhR%wya{8w|W׹^cxcw Jǐj`/˥˕ҡŬA,%q]cѕgR~\t{ l9f!תZ7⍻ +kf%Vu: +V/tSWֿxwAǞRWtl_^`_0}Bt=/~etV̀񦦫Z:3ZU:|xPn{8Ǜ ñXʗVR[[dM rT9*ؾFSd6ZS]n =|MjZ-b+̣q;Pn½<@7CYl7_B.;@ijd\AQ]i%GEEAA!Ȣ(J%q{iw7qAeYZYh QHDcRLTe[w@#G)}kd`ů4 #}O9G6FB'3Gw{D@EMFF#Riw l,ǝó=zzz 䃒3."Wdi(OŊ7S՗9o+i\?ӈq' +! !q/~@H8g_.by^Qݫ}DS*(E;GDhq?lvk|%Q cM~,&f4gJbP9K%Rq/MLc>!Qi6P$ -⹷Ԅjb WWRp[ʿɍ7=Y<*u 3:O7ڴJN?U_`\v&+d˻/eC*\Rj3x*rG`0/b6EaGOƥ$Zfp# .|d*ߓ6l?ϘW6l?vr F>oK&۬i=i.|X:Q C>n&9*цjw[ <:F=߇v@fiؙ6wP7 3GM6/HI?M8Ȅdþdg(wj'b~L*2c7##Kopa iL$vԎ0VTIJDt%:}|Z0X(ϚȶGc6+NJ<[<M9hrȬRq9n _;H/._I/[zMӯk7t?78{FyX~kݫ~ZeUt&簈cƃ/N3&aezaudcm)mx^)|FgM ++g> dϗ(Ym IwY hWtN`{^3}߅g1.Z(|<D^h*H}ѫx֍*BXEi悏9DßT35,|`bJm_eˋ/h@TbܣT.Gޮfˍ/d0$\)j9k){#ցp8Kګ=:~_\ JJSOC*xh}hm٥m~YFuD-oWijaWRX?k8(+V_ |\3 ֜A蜉l Hwg[i/{EI|mV$+705a{Xk3TpH~"%;- 3Së`ƼYПh|=Ms Nz=]-Dw#a6v<<^]ow:']=qrۏ͟};y氪w%+_ϕd1Rz ̢8ӞgNilNGǩ:*-$,aQ uQgA6YY  `{ƪSSz?[GHd 7+ +o5~D7œt|L<C\7cy;V=r%e$19HiUf z$-ct'Z1d/vb`pWpx&MGo -{i?KO üyҷQeTpz +Q̺}6 +tXS>X詣BkU¸BRUCH-a§w/3*ERf~P4R_h>4ُ>3lH~{ro0PӾS=+ 7o-Ӑcx"$Ҕ΢#>d9G6f ? 1L3LLi` ~ xbӻDM`"ЎBک-"K\F;>G6Oq qpR԰EMHgIό8sD }DK;pj,"&"g'm^ɝ|tċawRK.s{\"^VK?8I4=xTynX#Bv -pH?ߴ{|f:mh@_,+nP > IJJ?ps5v*HoTnR2*PFloi*;oA(p +-VdڥU-5w_?X.[O,iBZNK LPNE:_JD83j$0ƽ5_4Ԣ5f{=|^[Zsiժ|*PӞ$Jidye3 r6y6~Ǽw7H.]dJk.$Bg. 1]yDTDo:jR\qSƪW#3;r$^]fKFKe&3O"0A'lbrtR}I3Qg2*{*B<bJ#WWr;]K֔enJ էTudhT@lh]^|W֞s6J-Ʀ˫5͏Kk!u%F o{l sfLJX.8w!&4oDp2١Ζ'jSSr-?3/bc?qɯz~GגS5YG I`q^}V2'rvkbfJ- tr:ͷ;u,su;5 %jAx>9W[zϤt!z`fuFbmYnS Go\Bxg1(={}q%z̝v;VhD&wcݓα_nQcae5ї; G ^=1Z>w&)re1/Q}T|ʧ qoũ,{YO^ݪkԗ + MMbPOmˉMG)k/M)s 5v<%EErS* j*Kog,ۡ)g)g[gJig[q@֐=aWEJѰHa ley@dYDeI @HHBXD@nͣV{:w!tb~!`S;lօhbkt8{e|>/ՠcRurH0=MT29M>[R+-Kc)>]yN]ՁNA6[M mmPR=mfzxb8jR)0U@]P9,G5JeҌ uJ:uq:ȧNoSF9A7НH^sr0H&\~КVZ>3yu5I5.k|Bb#g/Kxv<3_H%OKzo9J ,^$?fi,8w~[zq6`귖V@SSB4]EBTXG;t qwܗ_+̥kfI=C䧾 RZ3n?0'|ԐE5lPJIz处´"xe"EZHj4mYR^YaIK"oV2k#7\jha&k%k*`Xi|zG5m9X\oᒼ.vTM\ȣh4FћKwzTu]/9*4 `%x2%Ir"WP}jkli%)R(ZE.6jE +&nܝWM = ƞa)ѡ/&nVY;<`X`jMtХفn;H"9?%m![i\cY[%PĥFU\`'uwA)i^f`|ꢳ琞ngzǐƶ/etW򺻫%{q@^ּ"B($o}I2B^j{5h. ;؝jun,Hq<]۸V?q!?8&3SryJ|܌#%&PcRt!Į"w-.qtQA(Ů?'8] ϖ Ie@TP KzɏzZC=lԔ Pۍk(Ԩֱl*.x-VOQ.SF%аDs[[s -#wW!LMar2sWw'殯0Q_ǿ~pWOG`:S:љΟ!gqފrldES "><D~p (p7| #5!dp*$z曁T,ٔ.RiI] TR rߢPe)jfkgַ}b:2C{n@lq={cf"{6\~, r"Zf;x2)ca?*·ٷXdQ\AQ+AqQI q-7hPEwǘc4&fnل(K43j3ɜR޽߃`Ib'6! 1!r`jG=[˞.^ Vzg(7mN滟(lȥԧl +y>ea.YT]sfIk|LU[TTYUfnVmb܊޿Jz6-GRv9>i&5IT}yުlT V~?Iwފgivk{1QQ)~C?ٛ3ϡ qFvʃjshȗ_)yNŞ=y\̹`uGVx;4"2 f1~r7뇾.:Ө[-}ۧm72D{%j/:{:i3?ӬֵfuЮuw6 _P_%,u;`{9W0fĝV!vFs`+QQ0~F~p1,;!vצ/J~\O57ҝYfZ-ڰQmdiV;[l0\f#v͔k@)bxu ?U&:au bNc߅qsvԴCY%OM7?Wu59W244SUUUm2#JѪIj+1Slj m c^ ܞ/ٔ7憫Xy2 AȓDI@B0DIj}1F7)QPHNխǶµkc+ȕG0!$rٚIԝ;^vSc~ĕd"_8DV". +~  RtL_79|'M~'I0}iA6b9ֹ'K6p56bm>k9qIs~͉;Gz36wmec__ Gt +18IG. C'uzD, FGKVATa޴UGqCKV ) /[T1ՆVz]ih2 pIMO5)24Pل opyHQ~h'Q4HAI 9 ldݫʲ4[,Kf5ڌ)j)ԙzr8oBZYAwYTQI#Fj?(N:g"<1׷s=85"1_r{G,'o~!2jY 1+@$ϭE.Leg]avӆ[nk=Zd^6ZMYFFua[fΏ 2u^W&]/vM贷_fdBN Gԣ=سy7Hr <$ޓ0fg&HE3Wa Xi-̃3J\d#Z٘{}IѠ5Z tÖI۔$t)w(w|ĻDy] ^7Xg>|9q%˜\x DΡ آ@"AL@Lu FU@xJKq+`ʆ+)mcuZKYYH׌ګY\g*~|Uao4[T?ߨ%5qr϶ۋx#|O2@ +Rш=1c{ u@>D۹G"oF$xGCqRJ)3,Y-c2ƘJtq{DZ +Cq{SJtM#!"r{fztz93g|}>K<^V`f`LR'KVy>a8qmTeK o\SfvU7~YN٦r'tȶJ(X.hm`ͻ\MZ +G$K2Ms~KYΛDRfS|uUH}T״M*a#y3悞.eǀC,P'XR}6ͅ")N~Q Y;HmEכS/' NVT6gJc}?Ko>;%]x2&5]CU5tGe(&$L0$94uBY(T:P/3/2s'vړӸuĥIFu?N;Ije{jHʾ#i5d/~lKso3?H->sS/&uY a/~3ɜY36#Å`Bc 6y m#WCT=ūM4sp/#FS\ٙ}hG]`½Dwp_a"nԡLj]wd*(9EiY mڤOzckXKOk + 30!4iOb繀qJ6P@/2p\jU4/15~^}0ġ/I;O]VygO]ǼyP&=*m90 O.(UHu_mi<(J~h*_Sp/uMyLu3]-{02җZ5Y>g*|ҷm4*8Ы!깹Fg#mj ø3kw \ݿXq]%pڐq4E]M%'M~J LoQ0BorE>fLd`1 62lb .Z>緉{&dc-u)ntLuKT?+K>-ZwF6 fv`DhJBP>l-*EQI-lhT?l8vC_,m|Ke\E!>zfA@M:p;.pKUZ-Sh{ZSkN:J뒒 g}2j-y_xQ :?a0eD3}(f6tr4F:7myaqO鿹+1#1KjZD}^l8Ո\\U]կ=u+ŤԠwSsP-KZ̦'=/g s <6L8%_Ҏܰ~\d"FRP8]HU,=D<9D<\+lKlݾơJ޽ &0:e;YTn9%?X9D0w\(6dH:V`d.\pN? <#SaABg}m{8><m _!OaCħdxW~GEvW +{zq#13Q:986bhp%'~ +^͈9W6vq xuaaSH(αa7 +M9 wڗ>Zo̶'hu{_hyA +ɞc#Cc*d_.A} ۤŧe2|d=m_AW .⺈Q[E_~*.B͓Mc~:'IQjӫ-'?1ՃK#O 2SSNPy9`4Jɍ/gSxq`&ץ*=: +-Qr(&r~q)aAY26aWD b +5&e(b"Scń}5\i`MF;L`|qwNes6S# !Qxi֎_q]~TqkL\1ʡE"9MwQX%麝'c?Dӂ/5pq10ZT0s\&8%-m2 ld،Ȯ0N ͸D CQJ˳KvfF oa5 ^ްi";=_T{8WRH5Ik{o{VAHnJx(uʭ*R2CDˤ4א$'9գ9yZ~]԰)xpŕs\,,sա:Jk_w[wFI'ݰY^6M\fI^_ g23 7Q};0D-Lh] zMC O@r1]bf6J3lXvaXl˲+Ao.ӓ.(No;f"hP(,%>l.n% S5@õ$dWH#ONjLȂ~rVlRvtGES.Xe|}~E]WA80[k?Gw] zZ׌m <.Y#TbsC 4/O=QٽOrTg YBC7LB#?O?uvJE9#NH +:m߅Oq}n7S|#O+1:]zIot>HQHҊ?MqQnXǯ\mXyk0vsϏp+w |,x&!Rq7 ˙7A( ̥9?m`8oZ\c:rx^nwdID$3X|x؃8TG/m)2’]d񨷼z=,{0~nCk󈎩dHk\\yb1O+YM﷑SIY?hg_߾Awqꌣ$7$Q\]=u`Uȡb O.ޱΰC]]hC?:s,>]?WjN t˘ZR󙅦jN\5 Pu?\_+-4==LRȥm7Jv o0kx(9u[@`^ս}nA7N;u 7 8 +I0d/$*sF3[Dd}ИL4!c&Cҗzs e+f`PKto Rt1QAJR=WKLYe7['Lh!){- Н-ipYt<|c@yc̾\5ig6Xi-ӵ/x?tN3e(eΒ7-/ە8׼+VT '/XRzo /i<|B#CG += +p 铑(i"rVs Uę;xz%U[-zyZ+(cC=el1Q~0@s_08HvOHIkHjbD>ӵ)\-!c/.d$d#]HvQɛBaŜn~%c߃Μ (}r  ~GB?~V THӒBx\prp<AQp.?f1CUDD +h>Yo_{(ntŀHqɷWENȂ=X+U2 +V' 9g[Y3.xTP%L[VշNqw$yTTGƋT,(# Mlޫt ¦ ,D@YBZVv$(q\:j%3uߪ{}RU!^o.@DDk$ (X$R1D! cSb +۝b{6l]*orY^ hx)ZR5lg] +<}@4?w(1w凞rw*̒-tco&;sc2)̞z@➗v7`.eym^u+Ocw5p-A&"B \Nf/O'Bf8!^US&>iaM/~IK^ٔ;l"M8Y l5W(!+GdmLdA_0? ~؜~)@Ae:b:c" ہtףp}"8gtq=.ra.GĽȸc96'?8ѺBK)]qa .sWۧ,>:Vu7J ʩY?#rZˆeGnPg +.(e TzeYp8̷KCo#%M b j!)0U&2W61mzW*SUJHYlt߉V O)R٭`5/{N :($\$V[G*X_GX{vآՖ>')d+tŊB}A-y))1%*R  Ҍy/Ce0L{X2@g"g͋9,Ra;lP=?yJ?n6YmOBCD ̅эp?>S"tz{ +o3-QfbUs畐lWE:;'Ϩ4\~ ϑaht#8`5$E=o#T;6|@J~a7kNrXcVb6 ,Db+XMBeA &۔Kki/2L#^l}r)`I SiAvNa팄^3+9VsRָƌb3/LP1uMqyP:5Ku;2y/7Hy$T Cay)W +tb|6v9Sqҕ+=rVvsﯳE_RQM9Ng5YaКEtFհh':sn~2Jpр@Aq' +Yn{}Y46="l4;+ +e%N4JDC`t3q̟:SykaV7y #ArYK0Y$5:K4]$H>gj-qn#k`&H7 |-0,%fm,lzbvx5*qcg};ؚMLǏ\B_xnrV&(K2;F4?FLzf@| '?n}Ζ)rx 2tT1I6 !n!ƥtCo8Zm;ڽї:2*f)Z'b[f[Œlӯw7to|7ǝizj=pl`]ݏaoWB|IJ?@̭j|y27-' >7:)0쥘V*u~&23K.z0 QVd);1)+wY$qYz[˜RS%q#Oi> &mAC_»Ct[/hG_|rkaʬ/`Q +XB8Kqn<)xƎSXYe,ap$("1qZ!uZn B+\ҶR-T6NFDH*(. "&09EI-0g]nÕqT *TQ Nii]P͕-\Y(HSD0Ym3+DiPa#r+QըJ)YdEM$sxa=|;Z6Q՘hM~$qoNݑ k6WOo_zweU#~6+}ֻ_3 73A>$|${*8]YVLQ,C{$$"fԋs[nju[iSڞmZFftR8)J3۲?c/=Hŵ[QQ3N-?x8qz)oE{4Hz|M,c&Ll 48liEKNNU?,gTaa q\$ho{-]eXUb](-1Q`l ӌa+G;C.#0% A-NRgNSt|sDFut!tHY\ +endstream endobj 31 0 obj <>stream +HYTiŋ@PvH#;+ضm+""&$on,!"l +B$" 8hۮ8a23T|u@uH尼cL{T1r+V۟h:9B>ʀL=<N ͣaXί¶I<}E[}]>Vr5ARq+>|yʘ週{O%+ֻ$'˙ݻ 9d]43*q+21Q @v,RԺ +6K+$5.Jl@i\m?xFѶ+kN?8ObSz&K8m?Fvy鸲 ݏξ_aY\uЉVUǧGc8 +;Dcrt +,?)\qb) ̯cIX')> -G ̧dMvޠKq(<~~LVXߦ)Ҡv>_aCm7`e6|-D>Qg?҉>Qa(DQ@{fFև dq"vK4GqۥUՓzD`64TU,0T وd#w77,7RBt]",tw9\ +_%t|n-(o@f/iThyfoTw;gƉ0[}ooMQX& ۻd~]Q@kp*Z~oC]s3:LGw@ۧ$|"~]Y.sOVp]i<7\1g;Уh`wQ|-WW_R' u'kgV;LE@ۇD|$WWCdV67Ѫuv +*`ۓ {$`@q İ(آb:tsRjtzN s/}s>s?o4ܹv+khUN.9Y[$ =_XL| ͡r=K횒A\-w-5@k5 sDAY˺Uu_ɷo?rcaplݠ:{U)L (3 Nc=]ܖqEQC@ +뜑mv#HlHǍ- YJ,47jR6鼻մE*aIITϸq9 [26;{ C_Jc ƴkt鼿8"P8BZ18UD RlI5׹xb!LC g^AΙMe, >˟s»AVԿ +_=|X/N(F Owwaύt9-S,QœVxggz6LA+=0]LSehh|f7T$JX +L%X<ۧahJ*ĹU+T"MVc5Xm"7 & [O@3VrLJ-o`kjM`>|32k,Mb-g2R:v.c#4!=౅L4 Gxټwe.pL5p'6Kե{Ymdmۊd1X<z#k%@L3 y웟8'6|T +S5D$uhz.Gņ`1S7ۏߟPlGRq7ujj{$q\JF_32\.z b {i +W"]j +X\2 nJ[+7>e߄4iє1K"̤ulCw7y̑S|[3U9%HkGtgeaWyh5[y[;?[b:kkHMT}˩-br"` +atqz#"P|*Up!3ӌF1WӰu8I;ϗ4Ke<?ܣn3htKQ#jCy$v Q=}(FS B]]Ge)j +|iVM1gB{sjt!wyk+v_K[Atg9?,☃w%r|{32ebeEs42I8WU()qh~Kd:GW r,9 $-)FW4np?W"V6޿07>3T;<*) P"}>%3&ErʗR1P*mc.1ҦPѰcDbYNl˷qbpu5ZȂKy_Že7Fw .|wkm*!(X NF0c#dFTUdg8FZ"hցC(g:Ĺ[l<.%8v6+?eyaQ,(  +AcX5mfy3MeVa߆fkmqk1իCK?o}g<},c +󋗘D]*mV+拮׏TvI^;?߳=Wj afH3T6X^TY#!7dz4rc'UI0a?/̼ ] 4] +܏!;š1X>κhsbamvk(3)HS2":awL''2*!1YX֦e+RE sju.蚆iMr;[~b3Fbt/Eh쬚_j_Q]B$zgA|p + f=ࡹwˇW4{ k[ֲ34M)YŊ!?Uim׏o^O V?~=LI />Vu `w/_E'_G/>'CØ-R3_ #3^~3 Hgdl*}u3_u7V4OGoㄖ-ӌ?/?Ǿ;(NK MRKj8@qڹ_!پUjul5_(x=*q,H_ҲyV |ݒ#O;;hB2 7)J Ӷ$@^6I͠3{뻗#f8rk5A~bՍONRO)= s +]`W{BQ6Kr"SdhFw|DF;Lkz|sb`}5L^1HW;i<@ +]Ij_ ~lUΫdU\ MOsPÀ/0*t 0,l_hXI?@WyRAjq"2w: Vy_.*y(6}Y7+d%a0,NA6 x $t#^cSҚ~`0tQayv ı[t0 >NSޱEl_s+'}JxUWP }-G#e޻RTqvj%Į}H^qru)m3Vi=0q\fy;ś74L"Xc13'1Sa(/f.fkw{;;y} +)$<؃8q!ȟqwH|]xa;ײN4L;[~S,])vVs1;e>a=IDEzI'TTdYVWBG CO&}w"̻қxRwQ)~i@',0 5RKZI4ALArFB<L#H͗X +v #%m"a%1w?tUڳA؋0cQPs2ZR-Z;,-a<_a4z}ygݐCġ=t'ȊULV%KXF`:9īC8d/|f%){-%\g^ zB[֋M'nEkG_'Ǥ}7wL01_ %Mi:ҺAh }o'}Y̧e-3!Fz8f-aKc2kC@F8B7`Mvn| ]YGzЏXǵ20=L{ ]$] ;ORJ-ZE{oFJW^Eln%2f~lKKQty rRjFBp%Un#?۴x3OӗLwDx9LkЇӈ"] -H +uEՐd)e+nXC +<%XTv>0y+d|mZ LhgZgB?^ܴV̩y.#v1L(m[#O{>*bHo ml7/n_8'mCr:>j_"=XQR5攖yNIDDωUA?݁gap7*itaE-^ 4B;8蝿m{[X<'q.H[ifV3}-=frAM>O[#AMHد(,\ %,bED@@bE" TW `6ieTب %1x2fd8c̙[a9VW^dr98[y"{pc`LJ]{xzY]~dz8w>zze>&z"?='໒* o?+ʿC?r?Ci8g"ɽ{vV(n~vځ!N.O<5=dA9n$6O11] +dEΆI]sa?b:Z`]C*|SRTI=%ww-0G[jP vi1`8xCaOi\߱vJS2+u/F+R{6iT(?\\CWhuK/Ss̷PzM.aV17τ{qca^d:p;JXD cnMwg:6\;UGIK;QKQGS6,wnrYg2Gluoh'ocr^57=woqO" s[O'|q7Λ_rKOSˏ  H\k'Ia8=++k 2"+y8'BLKCM|hXWa݋9*Rs>P.cvA_`IoqMDduL Yn(6Oo(t)rzFZն8/ fAGJ=K 󓺿R֤u2P7a?~}xx-4kK E\Xw>4,/W*0 +u95ME-;mS+ZxHTjj/\;t'4+]1L>ۍ$P>ig)t"_>]xgcE +7OΌ:1C!^|ӆ+y*܋3I^n*tf*%D?zm<3^)BzoK!B02z_KfFN^B$䲎y4,zwFC;cLcHRM&&& +CIij%RXC·#o6rhY_c\~Gp5Nh:c޵Ϡ>A_(ysDOO=NN5E$F_!uK-_%W^oC.طvH}Ef +/$NmyP~ y-njIyPѳ;!d{zenqyiN 9 ,\&z7h7 ԍ6T'՗+T ΁ ͟nŝ[k^(9kŊ帾 +(P@ +(P@ +(P@ +(P@ +(P@3HT[4jQe +EoW[4h*MNV*-U6Un#GLl*yVD{ȴLm\<wo\L|"ZJȕYVيmh(Z݃ut6~wLBrMbvkٲkOLkR} +Wzg/FkuFb \ƱiVԻ9d+ ++e%hԱ[@A;/x._M`ͬ6)M{W +צGYSNeII)pΙL9x l |'g=sMB",9ggQFo驷UyUVSkWe L.-4V6Z۽Vg>g1 }gbOh:r'OypE/kXfp}~TM]3 +  Tӂ[9==>WMݳ+ԧ3Q]ӊ6|VC;(-fAfH@5Lo{:saA!b>H,UM,X1dkϙ'֠"}|^]G,}x`œQ@u4t;Jy+?>O#3A +e (Zskl<9˘Xf<8^a4׊K\V;fji1A`_1ʱ20`YztqbB%胖Vʋ 4Ӈ%!_^]}']Q- 7,$lZ,?6%k~8e.9^ &hI= V9j4Ϯ~?I+f;Zr <^^ 7Yd ](T4=v:k]ȗ֓v/~yݤZT|9+&+vny +ySaXJcP˄Fˆ}mY1#+gH-+ =No*a!bV+[f0qrF(gKc{nSt_3awI߮lYXٟ eVO| @#j;Ά5W7̃~wvbZXPhDŽ5դxi`u f'lfMtHTIT<;?~Jw~ [9$ ysRu߲nP`MwCہO\oL9W8rJ23ҫm[ޠ+.Qp3n^f^X&99>Mk^k|3{6=u"Y}_zVA<=ײַ]iIbէ*^%7*Y}sq~EYrPyrԞ .d7sAiA1#/$W^'y+9cWͼpd +ySfߋ}5pt$vz +T$fQjD +'.{>]A9#z[nľA $sB~'QR91pEȗ>R>u6&i$-<8m +1 +o'p7׹Xz\h-w,~xV?ou~? pzb$o& +T>!vDqpr8cDo/폟:wя0?|-W~:&GkfwkﯶY\wSz_9/\$OZ#M bP|ś=" Q mkQn#Pe8ln^<r.;6i^){a_`TMhB=ެ^W/ A1Djuoe&p^å!9ٍ`\+BkfR{;6~FZh.8;'\!YHLhf4iTvZ&>ǻg`eYXkY#!$G5QZ:Sӎu~O첻=>$,_Jr72I ;2'E?$O|vy4utҝP5~YA` f4f;MYP!jBɡjC3\nΰɢ?-{ËY=Vq}~jL3{5|px 3 1 6j{ pF^'[T{P{C271PcW$zjCIJv>^W*j7O8xŬ9 U |.dY">9f_A{leA|1W\퀘u~y^"]rEN^VtjgxCI'Ex)j"C 9? 'U$Iea&@~FRNی[+؏ l}/]Efsk?(ֿ~L!u}WDwk_?dXE} Z !Wc/V 5,ꓜhM#| ݳ~a怖] -Մ1ŰS~9ك^I:ozF.3k{/f8dwTB#5s϶`}}=(ΪU^ٔșQ,Ne~3['OcnDz$> '[cރ>et݊>UBͮ|M e4t:f1?9gb't,;$p,O>mtyk[]jA[<[NԴ'tݔP`Ӡ>ݟjNTIeBQ&cgJyE/GOm{I@"f<6A6oanKdH]aF[C ]ۭ&bESNkb)f啛Jթ^_%9OA5[RG^C{:&5PەLz\cXQ!;1Lpڧ܊Zo."Z/g?>&~tofF̸_˽ikOGeR>}9DyŦ2}:A\*^Q{a>.qoVqj1;쿴,u~{[drXf9s;kE:]:!=pZMս Nd6ׅp cY?b&iԲo}wǬyº=>f]9}̽ql+֪x{RtU{˅=ɇv}w(/uog"ٻ~^<.׾ZjǰG WV 9sRN5Bc\zݻOO=D?x)ϦgBW&[UM.~/B ͙&ћнhD cc +sNlJ#zC~$sS`r .Veԙq~KAPAQQж*ZZvb!(!\@`pGXun/պv;;әΐ sy>ѾPw:gl +gG@9Oφ~l(ΦrU:c}8E[*ʝњ~qnhۮpd=~֗īEٙ1zcru@ 7Bkr9s+Gc8"ZgkMSi8/ZU3`fQ\*푢{V hF2WI١Zg2X'e"WlQkN[ٸNHcYqXb^s?ժl~;xHamgKPS`0_L`,SP0~fhhVixQR!@;ZK2IYDuElrW po֯e?dԊ1B G+ӱʞX-bb|V/`-V;ӷ4B@krнӂF:u +; :[ZgWѮkEgd[dC鼚ɝйnGOTϷH?tsՅU10jMS.>ܢ6~cZL䥎Hf[R1eL-W[蕐٤) n>rs-24.u FCv |\u=Sep1}Y?fnݟL!)\<SCiC$@FK8ew7Ta[ΡW17 Zd*oIWy^=;}gmЕht +*[/<}E7@O#ːQ@?bЪoiqN5^J wg+㮙wUgXǕ5e:bٳg{y+=}GΫx!p/ PDE= LHL3g}J^xZa&q[d;`ԸCI M~6kR؆ɮ\ +ٵ &0/t~.PyxW{$ }{T%?.lCr +6LO偒K\|Y Z"P;a2X}Ɨ=>RJ +՝ٖW݊V0oU'S1ECI=B.< olޖy)x;"/sE=7Ӊnٗ»Gṱ1΄bU]XD"4HœA=b6螢rfMG[bh)'E4 &SJy+O>Jfx#X42^މ` 7PۅDG]Siȫ; 5"1'Eyk4PT${[+ٗd~'򄟜)8k\1ZÐSxIm29f,RP6=_m9 N\ۀ2ՁOsw6[axnS#e1Kr7:vIҜoLȗLO\+]wVԯvȻC՛BAFkz㨥?"IEs\;^`^ON+H(%vUм^`ehJ5@b'RUVʖ@i򌐩y?f {=ٮzK?㶂W#<۟맣bAz57kj~O|Vzgv,]4Cxq]7@e ECx` 3Pk\x9%P1w6\j&Wi}s}3j[ 8Ere`֫އڮX2`gQ s>OkjX'u Urf[ACI.4GDE,"("HpX rQ@ %*c*Vxںouhgw~rnO:bzt`mWct b(^58鐴;IknM*7=G28iuSĜ٘!8ݔns&q'']<V6VCsqBg2؋TMˤvLdr t^ L b7VN$mOg +R ӈ9tJ|<#hv$yIoy l3pެdii#?\vqR!-JG CFf:Lvz܊ʆc\9̬lM`-6䜳dz+oSOI6b;aU#B`5㍴շ\Tc=]ށ/;x5%Kt-ࣵȵ:Cl+̀YM'iRʻ=oX룍 E 9OS.?Ų&=l/6I//7My: ,x3 /M=8*A+e228N_̲_ײh,r.0L9mMDi%kXɮ +t"v7{h%`&#g?F̼, lGmGz{W +7OЗ8j ŝ>~ {|YȞdpR dJn!0iYC*ܓGڼv?t?CeNM wQ>eecJY?fCp7Rݍ`ľIBN6/L+|m#$8_O(ÿ:::Ռ,']0䂺yr`V3֎вŴrԟ6MF]&HCH' bC`%RQT>(|#Z~cP5j~r 2%.*(A$ٞE-H& xz9-er2LTث\+H͓5 kfَ4mJ2):{YYnk +NO5>BJ]@3n][ 7?X-kgˎoMeh\غ=GW+oՕ-Q*{ڱUi$LӽH]:~䦜2ˆ~``7iWt ^`r{kJR7}K6] Q*t?xrUNۡٝ"kEXX.>"Az3#͏~?~~$x2=zΙTƙS'֛6pSpy( |o" #A׏-syN>W 4(;3Ul<܀&+yB詼f8u?f=/~bMl6;at-P/7ҪVdZ_jRO.P~lt(]i9|U܀ ^j#I/ŽR.+o4]I V1t]VX.|Ilo)̃ټIV/]iϒ=fϴWL|` ttf-Vz8IօrY2ZCi~J^l%o  ʇa#IAe., +9$^e=~EwvUyţFgH,Z~-]EaFJ/7X)5cOqկIZl|8OK i-rmR׳X*A 쟢d6~zy. +~-]}G).$yPTWkD46j1 +BdEQ%1{ l-"-,͎ EEhDǙTM*}7܂~ws~'PčW=YNlnhƛy}ʏ,.*./yV8VnRY"B+wj΍ÅB7G[Qjm*g?("bؙ<!HD-yDHc\dZ$ =H ϻ䦔? DV'Jdv聐jv>^ɆlaSŪo\逾AFf9O#9{ 쌸J_17OiMyK23jJq^cXd=a&I˞N7R =QWjfh%},hJvxU`^NvIտSbtflsWj-R'[8De x*.\˦ZRtWWR|[)ɍWV>yoM'۩8;iH2a~0;cV\S5T9CYF~.;%;rf2.5<ou1T;cٴϒEϘ_iK݇O+Y=c׮6jJlU!(rI }4=y2s`>4^d':jllJIMO0k\ ā܉,*~48f[!0[{nE!E R4XKq[99u s\j{zZrRrRvחɵ")SLD O8<㜋8ySrnv -6_|@LPl~t +|\2 ϒٓ0/Kh +]y؟3lf^D¢,֣yi]ԣSY6Z|: 3ZKcVeMht/G|A.)vߖ0 +x]amt6XGYG(񣔈# *;sz/΂=\E E.'; w@~ƏY)'s랿AT*8߫t/)}:[QSÓ+g^duR;7-N5Et}$"Ş, -􅄊i}>-fAùҪ-202U2-d6̮<*f/{I]@ )KO=旫yh9)(uwɝCʻ |Їyf^,皛jhe\ÍaWKyӋ5O͎<2q Yw8i)p֑Լ &C7##lsaƻiL;RnO4͠?u_g"b9Q`< g*:}yQjBvfc8Th /8K̑[YvREf-VH\ M^9ha9my:]ǯm*2bg?Pj=}7Op +ud~oxxf4)qxZ؞~ޭb5CXg!.j>W3Dބ)ŏ|ѫxy:U ,^L]:2Zg9DßD5^G(u? h@RׅGNPפbVց`mJh^+ u ;ovi] =|N={}x[FzJkD;r}W}2o{j~BWj v%=ʞLOWұXYe]'},ΟH+/>~fZfyzDER5\c]n 9vAn*95-~VӻFZ3R/΂dChRbt\So!_uGr魏[t!YP[;m}hif>$u3mMKj;qfrt"C,L:&b f66f v@X :nJy9/ٷ +6E\8kǼ\F WI x&M̘1GԷ´t8ݸ9xb9iXǠ~j&&K"{zۇă(Pg3bu?gL + +ǝj:tHN's>s +`h_(oǃ$LJr'ypFV4JM9̂fg4xma9P1 %&Z챠CȜOм@áĹ]T`=4AOj:Y.u<5֔Wf9jjs0nS9Yp(ѫ7:Rv2eQ%Fe>Km4k`ŃK9*&_aH}?2 G+K9JdZWqrkMy,O6ҘE+9)tx ǂ 1%,0g$&)pƑԑ4|.|@뗂盙4lzGl6"K^)X,;Rpq:1cǧhK@CNa$>Ja]:73tǛw^N qڎEqہb +b24'K<:ח`} Q` xo&$BB[ |ƒ +L|c% +jPasbݕ + ̼yܗ? .(Z:Y;wU/\& t$eΤwYp`s0˄QTA:?׉f'o^"R*PJD\1m +ܠBYvYkSupwWcRKyk!VRRi)\=`:,p3)tVI٥[110(֦)Ĵ5 +ZiQJ>7J)YrKiO24qC)WF3CB/T%NW@W +r;mKސcl ԥTdhe"  \h])d}ϿM+kN;IMusƺk˪E!u@ Co@*{ն'CxG(cym )x?F g)u8[IuOzed8~`˿%?%XMIqOU8zך3mC "kNJkIӴ.ftyӒHH^gsZCV1WH{xg0W% +ܧ#s)C-V-\j^Zi^EU.SATC0٫H1Gusv%=DC˵{ ;% hȯ@((;#oq^سΞv2wIg\Y,HR|"eo}?WXdau]q·|4iL2OfI۸b\۵1X f18b"ӓ VCcH ; X[>̽w;9wa*[fi[jEC#bƓfpsO^gIh]CV,_ڊe,_;6&m׻R{Q0wxo .{"Ծtf"=B8ŘXK1ِXG&ڃd' Au2dd/e[}&\ +5[ckR1I-hKiY)s$N\XY~cLWZg9oGueB`Z(I҅/QҠ1Eh™X2Ɯ9۞$-O<}#ŴXt.E3QBCy/9qG[>KcMg Ǽ 4wzg FiC7p +Ó6H*ИKR,LXLbLrWςf}.O ?׬Ql2iOD&Q}ލ8j^ux; w3_(l16 |%uB]_cdb{ +a5¶ +zUfV4 PKV`@V˅z\hli'S㤽X4i+Tjsm7Q/Oޜq0N;@ŏkt9h8Θ5NMSW ծˉ=k7V3z܉KWc`V:d]UDA [/ΐ'nA}qqq@!pROŒZFm*LnQ/tC9!g ָ8FV%1d*'41_"w}D'~sppH:pQT2{%x>o~ ͏0pW| 3ڌPx~Ez%QkY)}, V2ՙ +IئTsS62[+?ԕ$_1D`A +*(XQQ.VhW +yPބ@7kZݪRy v:ٹ;9ǝ;;wJZhAO{wlT~xigoZQwB?)RkW,ksVKW7W=FEƗޙd] +|AN`ظ8r7ނ9KҪ{Ro}F}n:aMѦ܍=37d'KpG~Yyc6yF$>5fȋd!K}$![KΛ$1Qmi%tBMg9Sl@؟v *IxNezّ`T^0>-6 '& ؁-XS+ c+qB 8̜8M`C.;25cXS&QGhsIBCh905Q5dJJˈ2յ:s&el4*9z;L%,!]qlqWlmGlNYS_-l %}`l.87Bw@Lvp#) rΤOA/-l$/4+H䘒ڦ׋Z h(Q},%,2u/=$=e{ K5ϩ1z9V11}g=W A2nVv?⌨]=AEjz#ġٿ`{*J֩RաEm5QvZT4H\zM/#$tUS./yvԁ-T\dB2Bv1'N~ZO6|} $. ajZ?{PwrRsC/S_oP^o},MAN5Bs툺]mETUaV}O퍫"b/E'dDe|О9U Ou[&,qbVGdUgrxo[\8~ "rX[GQu XĜQmsr ykhM~`J'zTιO^3Aqq%=~/8uFV1eLv1 pk897Ppu W0N0v-81UT!*%RŴ6RE03fg,?=ڬr)w%*܆ ̘T+m7Q袐˸PTݳ.JR +DE!"giΜs>-nՁkk=P\3qr@{riRYӃݝbhW}#xO$< Hɸ&=0O+VI- m?8qUvZ8VݱU2657IIfٞf|'H$PBW\AZ#RYbo$Syc2記 +?WYDm g +3@+r.T S{p\`CyEW {pmmM)7ےį:&'VI:**ّmDgf'VR{+{䍀n' Q"hU8f +̓GU;(;>WutB"m1e((4ђSCϒ*GW~Ǣ`bDZ\aVx懎 f&3k$00e%L)[ ؆-rC%ȣzVϷQ3l?q@{'>i8ҜRrޡ (/g6_bFq6sEjź>e J.Yzn 8-c`-,\6r{!z)@yQw-#le<%"HyB E7cΏ>W=8|ٙƣe}wTԛE]3ɾ1iB^1ijQeb "s~' / }$E1-5%ϼ-dEw=S׽pT5wX-Umo'Z|M'N$.n95vB➻=pzvmN-(Y޶5h\R$VM߿f6|fLZ`6;1`T`p>xCC%7(:,t:ѿ)gS#%RUzdۅ!t3yf_K9Y/%amc Ywr5؉!K35 S[խ8~S4-gjW Mj{5$;Zϙ^0ݒFA6>+Vn9md-+sR5Eu3QSQkwnbgdF bDdk9qM[DƮO#W3m6 [a#,ʊBpfs]m_B:_P7b(́GGB?|M@sXU [/!ǐvb!>Gžov% ^uUqn`k<ӊ"Þ~+z֑[!mߗ.) 8\WvgNMsw 6Pn$XB68,4K4y34]a;8s}[ bL!}EG!h}`}zX9 &g~p3 + +hF͜++Nhb_MWvVW^`B <:V1<>~s4U=DSb|NK ͈lBFEj ϞkgѦۇܩ?C>- ]ILz03G,{;5gv!@ 1Pg3G0hHa:B.r{}ğ "Ԛ kM~&&#cXSXj.gS{v,u+0wJ9Mkd5X0&&ŝ.eH7zưqBۘ$`4@V +CPM7PNL?)-އK N =P9s?L&'bP$J/ + V}wtHJpq!ISW3sD+r+XX*, V^ZF&o:L_%)v*Xx٨'qc%mz@t@7W0Fϰځ:}{BjЍ? w0\0}^Ō璈dṋ5na'EhH'eTG^D_ hLN'zwsv;l&jBa.f3yo8)j?s<7\/<& M]%t0u-bu_3 0`\v{ |]xG9L~?zq&&Q,-vN^^jFw;='v~l6ȥ x/<ͨimU_nzZ[zKO`a5K~[d`wp?o҂1(A?rK +' +$qc5ͤ]{̟ 35Bl1l;wڞK6hDDj2Y1) JF܈ %ҫ,iiEyʫhC^xN6GA^֥55rur K~ZJl.NJ^, +>!ΧƬۋ!KrkEU 01!GI ?cH]k#{" ƣw>"hGZe֛afDHLJjd̈kX`40R;Azv<[)oHϛE3{OZN6yUݔfí`? YݧϾ qڡ z%fNĥzsL%%?lۈn;Y["2O>^d@&TU_!+=W1b2܉߅^oCٍ]ioNxR4ˈ3I +Mīĭiٷݽ=홾]UgGVV\Nk]xZ +MW>"b A/8I')It(X+ fri$N3P'CR4Qa57`+ Hje]yRF R +{ 2k<j oSuk/ye+nG:.[Wʼ%3k̉Qϙ8߼9멨!2CO2$_ܱb#{77yx#? B*D#^S8`E >"h,"w+YCkg4YP߼XBw'T B9g^@ A1E/n6 U-IBx\p<AQKp&q f)CՐ^P%F8[uSYZl"!C +MҤsu~'꫽&`y:aP@Wi;S=Mo7 +RzKNhKtm, q'D s:ְʗsV#5dsd"Oad7|?+ppXX2rX(׆ܰ#bT  B ++eY ۩ϹՏx,Ą:`]r]9FSx;x+W.@-K7bZRiIΡ[~ƒF4Zkd5-G`v5ʪQWW󽏲zx M~W`ӄrw^yw&<- P))ΧSUb n{fbR5S-U+`etj2;wSbcXu!_~gYS:_f!:UH jӍ72͏]6Oz.AڗϘ&[\۲GQ_N&ak]k\K9Sx _?UQ ^2}(So'5$N&>Evto?A06@ m1jn`)^´(tQ[XpۀP{gPPF%fO +6 +C +df>+T)ɇ,i\6*HE [2V@ygJWmvYPMeR©sUViT_uc +IE'lv.뎋ίd5,E%Wc=+NXIv7]idV=T5W"WO& 6{qy&˝yKdnզ̯>Ϻm#l-MyVHZv#n{)rĂK:he.btSշ\) Z{mqQ!;˭PjX; F=prx=sf\ύoF0cדXDӾ1!v,1vfB(% 挙c}Y:W#cWƗR*:¸ ;g6y#`yw׏ᮧ! \JPN0&i P03L8(4 (@D0QAA {W}[U7Ȏٚ͆.6;+ +hq%!0:8k|:uNSN}}s/WP@tnWUZJ9 Hsv{N81Lmy^8Cc8Ɏ[}7?lJQ:xc 5LK_0& 1Ded`HњEJsƌ61sC('"Sn_W~~7N([g _E x.raxaFGow>/Zvq<7>#swd8&m~)ulߟ\ 5'>gb;a yjӟˇְIK(q Sr~ ET +-ڷkK("F'fh,2El#ITX0z[Rum_OnQ:;i4:)w!OŲ$¨"3n}MlJ:WCu=:/:t=:gEw\{m ?2h䅄}'NCxL\ ꗂ/١AdO=E5P/vʱ0S~ٙi ym&" +$(ƀa蝲Js +kۙosD +mY7qF3|8ETq%+`ԵM~nRزؒ5XRŪ&{ف?h̡Y\ӵ7;/0Cc&SX9.?e D :SsdU بLCZdF*tS`<1"A ،[. ^f} ~z>!îg uEu<8:|/A@{(e241K~zb=.^%pm C +bxYXdu3/#dW,2Rki rǃ{ Icz =h($#VÙ/hu$,HNpLqS&$.jw3?uy]ߗfCZ>'rJSP/w5=.+ֻ.;PVrg;*jAUJ?@Jre<݆ކBo0s"_"6{^ XV~zE"@K-4s,o M,Ww"y vlT!nHrNr?zzDYGL[?4%̗T UJ2 TؾS~Wc v$kɫbtƚK/6Y28OGCHui+kmɫA{?1Ln&N=eŒ C\R/KNwu>JǞ4#trbS.Kϯ"U} {p.Ҟ?̅ٝΐe$-.0'm+^åC.Pi$2B'cdbv4J.t0L1{2bo +n!cnaq@QH$'qrcNs \uBfa1JVr &QnLD=r .^cVG|F9.FZϕvt#yݥ r 4*pt LKw%^q$fa "ՓY/L܏1skdd_O0qz+ajK;{&9\j/BkvQbxgnf qD8q"\[r@t} +K16|n=J&e~0Nn)GM8|F?H'&T:Jgt4SMmHip٠YVQcĦF + ` bg4uŜ#۲MRRDh' @۴7~-pH(̂WE%st6T".#ߜ~a~VYzn)tT-pN]ֻ۹b Yjr<]437(* TZ I{" JEdMA$"R׶:jhzgΜ|y|ȹ1CsGmKXF0Fٯ&H>@n, U D *sRL9e.ZNZ).s]peBxv= ̹;qp?>qm=>tfq|J|!J.Уso`lGu,'w%sѭjҹ%&Q/pN0s;  +4) +Ckx4eH\uI2@Q46@B Ar*aӥUISODI˵uC_K~nO/NE"Tŧ*6DB4b4E_8UL7!i'5 TNy1}3UZY$j]2@FĕBr24JΣlVBZol[.K̲PPcYzkIo8V7{H :| O36=НNۅAep5} b;Lr]y:K>!RJ]?WbP".T#=^q({MjMH̫wٳ>_4YGǖ Ֆw1ՙ.hp3Z8sŝIqRZ"6\Z-/jl2"R<7,"_3]Aw7<B_v>(~ G:)^ z c~-&"`f\>9^ȑi%|Q´s[ +G='YNrOx +X2OՈN=)i"-!9-I9pz[$d;3?CIaZBB=Г0Z\ٺZPz/'4_𗚮#-WׁW0[,hp%gJϗN M{X{֏Lm(4x޶̞ԧ+!ta.<uzyͮ|z +%vmBC¸n?P3k琣g}N`ij @jB;m/ ͞ %lȋ8(Tu{c\X&~OxrW[:~_[&CĿTg#)59*!f[O* dy09kI(*O-W ꫺'bq:( M,7KRH=rk\yl > -<<¼ n.G7^Rc3Xf^~A^ݴո"c;*q Գ26לk/ao]ZFJ ͷퟻPT0Ր/~|vY& fwfrQݨӼ48 mw#ۻݨ˛WFGh9Z~ ӍLoGm7ףvesn_,Ox}YB|@^*vnlїWPǁ*pwIYpm0[,g~>+Y!_x*^{~`={yfuiTDNe|N'A@kLwtP*q8lTqGjQ_w+lh'*z02u<"-D +sIng\8"Fj.mVb.snO܃ HI$@ U;jKw[3.R]jvwٙaI f< R2GdݪDl<"~]hOd<#5`+o`4$6M7.%qB5a(oaviUlD'Jrz~y&u0A踽NSTs6%wƲNc~R~ofz +Ϭd>Ͱo3n>`GH3BU@݊/>; d2c=71)aº`+o΅m1-`=GeK+??y]$wv3UbSi?q%626}}{Vi1#2JkdiU5+@qLbp*ԶJË c+9xj9 LJGTS"v8 F>ܒݏNU +踹S5½%_(`/݇2XH /DSW4Kp&ՕZ2Ggg^C~ &P>TuFVҿ պPk asGjRE96(1<"GKkrz̹/e5TQhW(nNP퍢z\POI3 [Kx%#P$$n'EA}d36!Է`d2.Yjn7[k+@]ibpL'.=MSn-,̾];}t3h[L-/ŀۻy_)#\O} hY>yR"V"UCs8Dҽ b=͍;A<6[9Aw:;sOQU:I͈`ܣ9B1@ȩg)E,~`,K,gyn)mP둪Pg Asl|.> 1i1ERFil ;. &uW̑3?ۨ3U$H E::Þy7e + !SzU{#UmRG,~dI<waHI( v >c +k(ћCiF#>I9b[q< >HU4z9j-:@;X1D9H$$FWm<7^M|ͪ-JN4cϳN,I7vbϵd/T8~3@s[i`=s0^Z+70c_n"#sjx*\,Acw4c9\.OI[C}/2E։?o, +>O."Rj\RltG:wq\Nzcq#7ޛ4G(p/>A&oT dqQ^VH>֑mil3cbnZ.|)3> B5Dևbc*if A\ _H ˉ?!-qOlgƎh歄>]dܳ[-r_UMr}%h>=Qpts椸OJtv tE0'yW*41j[Y<_>&_笗^,cY3Fx!A0:޻fW7v3~CSP|?Llva1tE3b+\N"j@|<!˯ı[qխ=N=>0QM.llz.gkr6ݕLWRg);}7;#Su*Z$ +\%eUH#Lߢ0ހ"*F@[T@A#`!jF K][4APfiQ%M'fy?9:W1dQsd4C5Y[f(+}T?i'7 WʾcU!:F6Mb1j{t?ڡ_ȟ+DbpD'Q'QA|#Dw5g""/1zczXV롃u'<dt]Es$UP돻@e?p_9L`ffղ\EAy[.=+Ql|4B&[ =?.ފԎcH>״DM +o/)hr0wlxOpϳn%c8`(c}X>eER{ '#0CjlJZum84&:$ŸpeFTXvnok&f_8B/|Pb O.T@^ %}ϒdYc.XJL%8l /$ӱRi;Zl 5:+=t6[h{n]Y^jtn[xmW*5ͺI31lS/78V=sۅ>*855 ]=oonmU] ^Sz%owBF56 J^h ζZ_v~vHE]n/Z& ƪBeXE H#X' ]w +?&`w(GJZ0CÀmCY*E1(8s2Q[,ɭqB/d 5xbiOmGȊfޏJushP`sE?хW~-s[`v܈&^7:"6pt/r^bm7YhTھ]&fʼ f"ާ-2Nk/q|Ge] ucq&ڟ䖩^'ZWJ r(oCFCٗTpr6jI*:Oia)*F]3pif^/^%S4 xFɪWn%39,ޗZNt?$@TtŷI*FeoIEuQrE"[ 7va(ȑr> j1LZe*Ӥ``?xw)> endobj xref +0 33 +0000000000 65535 f +0000000016 00000 n +0000000144 00000 n +0000045633 00000 n +0000000000 00000 f +0000047399 00000 n +0000409570 00000 n +0000045684 00000 n +0000045998 00000 n +0000047698 00000 n +0000047585 00000 n +0000046636 00000 n +0000046837 00000 n +0000046885 00000 n +0000047469 00000 n +0000047500 00000 n +0000047771 00000 n +0000048235 00000 n +0000049343 00000 n +0000077174 00000 n +0000080460 00000 n +0000083861 00000 n +0000097559 00000 n +0000114424 00000 n +0000129371 00000 n +0000147188 00000 n +0000173013 00000 n +0000202438 00000 n +0000228326 00000 n +0000271451 00000 n +0000318605 00000 n +0000365428 00000 n +0000409593 00000 n +trailer +<<79F5FFF12FF11949ACFBA29319C290E6>]>> +startxref +409791 +%%EOF diff --git a/Nynja/Resources/Constants.swift b/Nynja/Resources/Constants.swift index 82bc2a0a8..c0c1cc939 100644 --- a/Nynja/Resources/Constants.swift +++ b/Nynja/Resources/Constants.swift @@ -72,6 +72,7 @@ struct Constants { static let blackWithOpacity = Color(hex: "#000000", alpha: 0.6) static let callGradientStart = Color(hex: "#2c2e33", alpha: 1) static let callGradientEnd = Color(hex: "#2c2e33", alpha: 0) + static let callBackground = Color(hex: "#2c2e33", alpha: 1) static let separatorGrayColor = Color(hex: "#3f3f3f") static let sectionBackgroundColor = Color(hex: "#3f3f3f") static let backgroundColor = Color(hex: "#272a30") @@ -135,17 +136,19 @@ struct Constants { } struct LocalizableKeys { - static let contacts = "contacts_title"; - static let byContacts = "by_contacts_title"; - static let byQRCode = "by_qr_code_title"; - static let byNumber = "by_number_title"; - static let byUsername = "by_username_title"; - static let history = "history_title"; - static let selectCountry = "select_country_title"; - static let dataAndStorage = "data_and_storage" + static let contacts = "contacts_title"; + static let byContacts = "by_contacts_title"; + static let byQRCode = "by_qr_code_title"; + static let byNumber = "by_number_title"; + static let byUsername = "by_username_title"; + static let history = "history_title"; + static let selectCountry = "select_country_title"; + static let dataAndStorage = "data_and_storage" - static let search = "Search"; - static let save = "save" + static let search = "Search"; + static let save = "save" + + static let wrongProtocol = "wrongVersion" } struct Size { @@ -162,6 +165,8 @@ struct Constants { } static let commaSeparator = "," + static let spaceSeparator = " " + } diff --git a/Nynja/Resources/DevConfig.xcconfig b/Nynja/Resources/DevConfig.xcconfig index be91b0379..ce27d9da4 100644 --- a/Nynja/Resources/DevConfig.xcconfig +++ b/Nynja/Resources/DevConfig.xcconfig @@ -14,6 +14,8 @@ AppName = NYNJADev ServerPort = 1883 Config = dev AppGroup = group.com.nynja.mobile.communicator.dev -ModelsVersion = 8 +ModelsVersion = 9 +isServerConnectionSecure = false ConfServerAddress = 35.198.118.190 ConfServerPort = 80 +ConfServerSecure = false diff --git a/Nynja/Resources/Info.plist b/Nynja/Resources/Info.plist index 638040a07..640cc1e3f 100644 --- a/Nynja/Resources/Info.plist +++ b/Nynja/Resources/Info.plist @@ -21,11 +21,13 @@ CFBundleShortVersionString 1.0 CFBundleVersion - 0.2.154 + 0.5.2 ConfServerAddress $(ConfServerAddress) ConfServerPort $(ConfServerPort) + ConfServerSecure + $(ConfServerSecure) Config $(Config) Fabric @@ -114,5 +116,7 @@ UIViewControllerBasedStatusBarAppearance + isServerConnectionSecure + $(isServerConnectionSecure) diff --git a/Nynja/Resources/PrereleaseConfig.xcconfig b/Nynja/Resources/PrereleaseConfig.xcconfig index af2f4cf88..14f6d7d54 100644 --- a/Nynja/Resources/PrereleaseConfig.xcconfig +++ b/Nynja/Resources/PrereleaseConfig.xcconfig @@ -9,11 +9,13 @@ BundleIdentifier = com.nynja.rc.mobile.communicator ExtensionBundleIdentifier = com.nynja.rc.mobile.communicator.NynjaShare -ServerURL = preprod.ci.nynja.net +ServerURL = im-fallback.staging.nynja.net AppName = NYNJARC -ServerPort = 1883 +ServerPort = 8443 Config = prerelease AppGroup = group.com.nynja.mobile.communicator.rc -ModelsVersion = 8 -ConfServerAddress = 35.198.188.251 -ConfServerPort = 80 +ModelsVersion = 9 +isServerConnectionSecure = true +ConfServerAddress = call.staging.nynja.net +ConfServerPort = 443 +ConfServerSecure = true diff --git a/Nynja/Resources/ReleaseConfig.xcconfig b/Nynja/Resources/ReleaseConfig.xcconfig index a42e7510e..556cb1b2b 100644 --- a/Nynja/Resources/ReleaseConfig.xcconfig +++ b/Nynja/Resources/ReleaseConfig.xcconfig @@ -9,11 +9,13 @@ BundleIdentifier = com.nynja.mobile.communicator ExtensionBundleIdentifier = com.nynja.mobile.communicator.Nynja-Share -ServerURL = rc.ci.nynja.net +ServerURL = im-fallback.nynja.net AppName = NYNJA -ServerPort = 1883 +ServerPort = 8443 Config = release AppGroup = group.com.nynja.mobile.communicator -ModelsVersion = 8 -ConfServerAddress = 35.198.165.190 -ConfServerPort = 80 +ModelsVersion = 9 +isServerConnectionSecure = true +ConfServerAddress = call.nynja.net +ConfServerPort = 443 +ConfServerSecure = true diff --git a/Nynja/Resources/ThirdPartyServices.swift b/Nynja/Resources/ThirdPartyServices.swift index 1566522c2..9de0c332c 100644 --- a/Nynja/Resources/ThirdPartyServices.swift +++ b/Nynja/Resources/ThirdPartyServices.swift @@ -14,6 +14,7 @@ struct ThirdPartyServicesFactory { static let amazon: AmazonService = AmazonService(config: Bundle.main.config) static let support: SupportService = SupportService(config: Bundle.main.config) static let testFairy: TestFairyService = TestFairyService(config: Bundle.main.config) + static let intercom: IntercomService = IntercomService(config: Bundle.main.config) } enum AppConfig: String { @@ -80,12 +81,30 @@ struct GoogleService: ThirdPartyService { } } +struct IntercomService: ThirdPartyService { + struct Config { + let apiKey: String + let appId: String + } + + let serviceConfig: Config + + init(config: AppConfig) { + switch config { + case .dev, .devAutoTests: serviceConfig = Config(apiKey: "ios_sdk-3f0f8a4f52e4ed08a2bf6f1a39a1e9eb8b0763d5", appId: "s3isdm0n") + case .prerelease: serviceConfig = Config(apiKey: "ios_sdk-3f0f8a4f52e4ed08a2bf6f1a39a1e9eb8b0763d5", appId: "s3isdm0n") + case .release: serviceConfig = Config(apiKey: "ios_sdk-3f0f8a4f52e4ed08a2bf6f1a39a1e9eb8b0763d5", appId: "s3isdm0n") + } + } +} + //TODO: - should be created different configs struct SupportService: ThirdPartyService { struct Config { let mailAddress: String let faq: URL let privacyPolicy: URL + let terms: URL } let serviceConfig: Config @@ -93,14 +112,17 @@ struct SupportService: ThirdPartyService { init(config: AppConfig) { switch config { case .dev, .devAutoTests: serviceConfig = Config(mailAddress: "support@nynja.biz", - faq: URL(string: "https://www.nynja.biz/faq")!, - privacyPolicy: URL(string: "https://www.nynja.biz/privacy-policy")!) + faq: URL(string: "https://landing.nynja.io/-temporary-slug-81be145c-f4aa-4787-8d71-b3ab51a1aef2?hs_preview=XOrlQzBx-6108791186")!, + privacyPolicy: URL(string: "https://landing.nynja.io/privacy-policy")!, + terms: URL(string:"https://landing.nynja.io/terms-of-use")!) case .prerelease: serviceConfig = Config(mailAddress: "support@nynja.biz", - faq: URL(string: "https://www.nynja.biz/faq")!, - privacyPolicy: URL(string: "https://www.nynja.biz/privacy-policy")!) + faq: URL(string: "https://landing.nynja.io/-temporary-slug-81be145c-f4aa-4787-8d71-b3ab51a1aef2?hs_preview=XOrlQzBx-6108791186")!, + privacyPolicy: URL(string: "https://landing.nynja.io/privacy-policy")!, + terms: URL(string:"https://landing.nynja.io/terms-of-use")!) case .release: serviceConfig = Config(mailAddress: "support@nynja.biz", - faq: URL(string: "https://www.nynja.biz/faq")!, - privacyPolicy: URL(string: "https://www.nynja.biz/privacy-policy")!) + faq: URL(string: "https://landing.nynja.io/-temporary-slug-81be145c-f4aa-4787-8d71-b3ab51a1aef2?hs_preview=XOrlQzBx-6108791186")!, + privacyPolicy: URL(string: "https://landing.nynja.io/privacy-policy")!, + terms: URL(string:"https://landing.nynja.io/terms-of-use")!) } } } diff --git a/Nynja/Resources/en.lproj/Localizable.strings b/Nynja/Resources/en.lproj/Localizable.strings index 0d0e29024..dfcddf270 100644 --- a/Nynja/Resources/en.lproj/Localizable.strings +++ b/Nynja/Resources/en.lproj/Localizable.strings @@ -33,6 +33,7 @@ "Go_to_settings"="Go to Settings"; "your_device_is_rooted" = "Your device appears to be rooted. The security of your app can be compromised."; "coming_soon" = "Coming soon..."; +"connection_to_server_failed" = "Can't connect to the server. Try again later."; // MARK: login vc "confirm_contry_long"="Please confirm your country code and enter your phone number.\nBy signing up, you agree to our Terms of Service."; @@ -41,7 +42,6 @@ "code_send"="We’ve sent a code to your phone."; "code_send_isReceived"="We’ve sent a code to your phone.\nHaven’t received the code?"; "have_u_receive"="Haven’t received the code?"; -"wrong_sms"="SMSCode wrong"; "alert_sms"="SMS"; "alert_voice"="Voice call"; "alert_get_code"="Which way would you like to get a code?"; @@ -124,7 +124,6 @@ "Alias_Empty_Message"="Sorry, empty alias is invalid."; "Alias_Max_Size_Message"="Alias must be at more 65 characters"; -"Alias_Busy_Message"="Sorry, this username is already taken"; // MARK: edit group name "Group_Name"="Group name"; @@ -262,6 +261,7 @@ "add_participants_history_count_popup_title"="Add Users to the Group?"; "add_participants_history_count_popup_message"="Number of last messages to forward:"; "you_cannot_remove_all"="You can not remove all members."; +"add_participants_no_contacts_to_select" = "Sorry, no contacts to select"; "call_maximum_participants_reached"="You have selected maximum amount of call members"; "call_available_slots_count"="There are only %d places for new users in the call"; "call_no_available_slots"="There is no place for new users in the call"; @@ -473,6 +473,7 @@ // MARK: Replies "replies_header_replies"="Replies"; +"deleted_message_replied_preview" = "Deleted message"; // MARK: Message "message_status_typing"="...typing"; @@ -501,7 +502,6 @@ // MARK: Auth "login_wrong_country_code"="Wrong country code"; "login_choose_country"="Choose country"; -"auth_something_went_wrong"="Something went wrong."; "auth_sms_code_is_wrong"="SMS Code is wrong"; "auth_attempts_expired"="Attempts expired."; "auth_attempts_removed"="Removed"; @@ -582,7 +582,7 @@ "wheel_privacy"="Privacy"; "wheel_invite_friends"="Invite Friends"; "wheel_item_transfer" = "Transfer"; - +"wheel_item_help" = "Help & Feedback"; // MARK: Main "main_undefined"="Undefined"; @@ -652,6 +652,7 @@ "support privacy policy title"="PRIVACY POLICY"; "support faq title"="FAQ"; +"support terms title"= "TERMS OF USE"; // MARK: Theme Picker "theme picker title"="THEME"; @@ -813,7 +814,7 @@ //MARK: Calling "remove_participant_from_call"="Remove from call"; -"question_end_call"="Who do you want to complete a call for?"; +"question_end_call"="Who would you like to end the call for?"; "end_call_for_me"="Me"; "end_call_for_all"="All"; "call_info_banner_members" = "%d members"; diff --git a/Nynja/RoomDAO.swift b/Nynja/RoomDAO.swift index ddefef3d8..7a4b730e0 100644 --- a/Nynja/RoomDAO.swift +++ b/Nynja/RoomDAO.swift @@ -73,13 +73,11 @@ class RoomDAO: RoomDAOProtocol { .asRequest(of: Int64.self) .fetchOne(db) } - } else { - guard let phoneId = StorageService.sharedInstance.phoneId else { - return nil - } - + } else if let phoneId = StorageService.sharedInstance.phoneId { return MemberDAO.findMemberBy(roomId: roomId, phoneId: phoneId)?.reader } + + return nil } // MARK: -- Mentions @@ -105,12 +103,20 @@ class RoomDAO: RoomDAOProtocol { oldRoom.files = newRoom.files oldRoom.type = newRoom.type oldRoom.tos = newRoom.tos + oldRoom.readers = newRoom.readers return oldRoom } } static func updateColumns(_ columns: Set, room: Room) { - let columns = Set(columns.map { $0.title }) + guard let dbRoom = room.databaseModel as? DBRoom else { + return + } + updateColumns(columns, room: dbRoom) + } + + private static func updateColumns(_ columns: Set, room: DBRoom) { + let columns = Set(columns.map { $0.title } ) try? dbManager.perform(action: .updateColumns(columns), with: room) } @@ -122,37 +128,43 @@ class RoomDAO: RoomDAOProtocol { } // MARK: -- Fields - static func updateReader(_ reader: Int64, roomId: String, kind: ReaderKind) { + static func updateReader(_ reader: Int64, roomId: String, phoneId: String, kind: ReaderKind) { if kind == .other { - updateOtherReader(reader, roomId: roomId) - } else { - updateOwnReader(reader, roomId: roomId) + updateChatReader(reader, roomId: roomId) + } else if kind == .own { + updateMentions(with: reader, roomId: roomId) + } + + MemberDAO.updateReader(reader, roomId: roomId, phoneId: phoneId) + } + + private static func updateChatReader(_ reader: Int64, roomId: String) { + guard let room = fetchRoom(by: roomId), + let oldReader = room.reader, + oldReader < reader else { + return } + + room.reader = reader + updateColumns([.reader], room: room) } - private static func updateOtherReader(_ reader: Int64, roomId: String) { - // Remove mentions that has been read. - let unreadMentions = fetchMentions(for: roomId)?.compactMap { $0 > reader ? $0 as AnyObject : nil } + private static func updateMentions(with reader: Int64, roomId: String) { + let mentions = fetchMentions(for: roomId) ?? [] + guard !mentions.isEmpty else { + return + } + + let unreadMentions = mentions.compactMap { $0 > reader ? $0 as AnyObject : nil } let room = Room() room.id = roomId - room.readers = [reader as AnyObject] room.mentions = unreadMentions - let columns: Set = [RoomTable.Column.reader.title, RoomTable.Column.mentions.title] + let columns: Set = [RoomTable.Column.mentions.title] try? dbManager.perform(action: .updateColumns(columns), with: room) } - private static func updateOwnReader(_ reader: Int64, roomId: String) { - guard let phoneId = StorageService.sharedInstance.phoneId, - let member = MemberDAO.findMemberBy(roomId: roomId, phoneId: phoneId) else { - return - } - - member.reader = reader - MemberDAO.updateColumns([.reader], member: member) - } - static func updatedMentions(with message: Message, roomId: String) -> UpdateResult<[Int64]> { guard message.hasMentions, let phoneId = storageService.phoneId, diff --git a/Nynja/RoomDAOProtocol.swift b/Nynja/RoomDAOProtocol.swift index 2dd4ef83b..0cd19c451 100644 --- a/Nynja/RoomDAOProtocol.swift +++ b/Nynja/RoomDAOProtocol.swift @@ -29,7 +29,7 @@ protocol RoomDAOProtocol: DAOProtocol { static func updateColumns(_ columns: Set, room: Room) // MARK: -- Fields - static func updateReader(_ reader: Int64, roomId: String, kind: ReaderKind) + static func updateReader(_ reader: Int64, roomId: String, phoneId: String, kind: ReaderKind) static func updatedMentions(with message: Message, roomId: String) -> UpdateResult<[Int64]> // MARK: - Contains Members diff --git a/Nynja/ServerModel/Model/Contact.swift b/Nynja/ServerModel/Model/Contact.swift index 22f979fb6..c72885058 100644 --- a/Nynja/ServerModel/Model/Contact.swift +++ b/Nynja/ServerModel/Model/Contact.swift @@ -15,5 +15,5 @@ class Contact { var presence: StringAtom? var status: AnyObject? - var lastMessageId: Int64? + var lastMessageId: String? } diff --git a/Nynja/ServerModel/Model/Message.swift b/Nynja/ServerModel/Model/Message.swift index c6d66da65..ff382502c 100644 --- a/Nynja/ServerModel/Model/Message.swift +++ b/Nynja/ServerModel/Model/Message.swift @@ -8,19 +8,24 @@ class Message { var msg_id: String? var from: String? var to: String? - var created: AnyObject? + var created: Int64? var files: [Desc]? var type: [AnyObject]? - var link: Int64? + var link: AnyObject? var seenby: [AnyObject]? var repliedby: [AnyObject]? var mentioned: [AnyObject]? var status: AnyObject? + + // MARK: - Local Fields + var feedName: String? var senderName: String? var senderAvatar: String? + var localStatus: LocalStatus? + /// Property is needed in order to handle gaps in message history. /// Property 'isTrusted = true' when sequence of [message BEFORE (serverId = 1), self message (serverId = 2)] /// form a valid history chain inside a p2p of muc, diff --git a/Nynja/ServerModel/Model/Room.swift b/Nynja/ServerModel/Model/Room.swift index 85575efc0..99a85499b 100755 --- a/Nynja/ServerModel/Model/Room.swift +++ b/Nynja/ServerModel/Model/Room.swift @@ -19,5 +19,5 @@ class Room { var created: Int64? var status: AnyObject? - var lastMessageId: Int64? + var lastMessageId: String? } diff --git a/Nynja/ServerModel/Source/Decoder.swift b/Nynja/ServerModel/Source/Decoder.swift index 58d381134..bf56d8420 100644 --- a/Nynja/ServerModel/Source/Decoder.swift +++ b/Nynja/ServerModel/Source/Decoder.swift @@ -233,10 +233,10 @@ func parseObject(name: String, body:[Model], tuple: BertTuple) -> AnyObject? a_Message.msg_id = body[5].parse(bert: tuple.elements[6]) as? String a_Message.from = body[6].parse(bert: tuple.elements[7]) as? String a_Message.to = body[7].parse(bert: tuple.elements[8]) as? String - a_Message.created = body[8].parse(bert: tuple.elements[9]) as? AnyObject + a_Message.created = body[8].parse(bert: tuple.elements[9]) as? Int64 a_Message.files = body[9].parse(bert: tuple.elements[10]) as? [Desc] a_Message.type = body[10].parse(bert: tuple.elements[11]) as? [AnyObject] - a_Message.link = body[11].parse(bert: tuple.elements[12]) as? Int64 + a_Message.link = body[11].parse(bert: tuple.elements[12]) as? AnyObject a_Message.seenby = body[12].parse(bert: tuple.elements[13]) as? [AnyObject] a_Message.repliedby = body[13].parse(bert: tuple.elements[14]) as? [AnyObject] a_Message.mentioned = body[14].parse(bert: tuple.elements[15]) as? [AnyObject] diff --git a/Nynja/ServerModel/Spec/Message_Spec.swift b/Nynja/ServerModel/Spec/Message_Spec.swift index 59a1eea24..ba04a9473 100644 --- a/Nynja/ServerModel/Spec/Message_Spec.swift +++ b/Nynja/ServerModel/Spec/Message_Spec.swift @@ -1,10 +1,21 @@ func get_Message() -> Model { + return get_Message(recursive: true) +} + +private func get_Message(recursive: Bool) -> Model { + var linkTypes = [ + Model(value:List(constant:"")), + Model(value:Number()) + ] + if recursive { + linkTypes.append(get_Message(recursive: false)) + } + return Model(value:Tuple(name:"Message",body:[ Model(value:Chain(types:[ Model(value:List(constant:"")), Model(value:Number())])), Model(value:Chain(types:[ - Model(value:Atom()), Model(value:Atom(constant:"chain")), Model(value:Atom(constant:"cur"))])), Model(value:Chain(types:[ @@ -27,40 +38,29 @@ func get_Message() -> Model { Model(value:Binary())])), Model(value:Chain(types:[ Model(value:List(constant:"")), - Model(value:Number()), - Model(value:Binary())])), - Model(value:Chain(types:[ - Model(value:List(constant:"")), - Model(value:List(constant:nil,model:get_Desc()))])), + Model(value:Number())])), + Model(value:List(constant:nil,model:get_Desc())), Model(value:Chain(types:[ Model(value:List(constant:"")), Model(value:List(constant:nil, model:Model(value:Chain(types:[ - Model(value:Atom()), Model(value:Atom(constant:"sys")), Model(value:Atom(constant:"reply")), Model(value:Atom(constant:"forward")), - Model(value:Atom(constant:"sched")), Model(value:Atom(constant:"read")), Model(value:Atom(constant:"edited")), Model(value:Atom(constant:"cursor"))]))))])), - Model(value:Chain(types:[ - Model(value:List(constant:"")), - Model(value:Number())])), - Model(value:Chain(types:[ - Model(value:List(constant:"")), - Model(value:List(constant:nil, model:Model(value:Chain(types:[ - Model(value:Binary()), - Model(value:Number())]))))])), - Model(value:Chain(types:[ - Model(value:List(constant:"")), - Model(value:List(constant:nil, model:Model(value:Number())))])), - Model(value:Chain(types:[ - Model(value:List(constant:"")), - Model(value:List(constant:nil, model:Model(value:Number())))])), + Model(value:Chain(types: + linkTypes)), + Model(value:List(constant:nil, model:Model(value:Chain(types:[ + Model(value:Binary()), + Model(value:Number())])))), + Model(value:List(constant:nil, model:Model(value:Number()))), + Model(value:List(constant:nil, model:Model(value:Number()))), Model(value:Chain(types:[ Model(value:List(constant:"")), Model(value:Atom(constant:"async")), Model(value:Atom(constant:"delete")), Model(value:Atom(constant:"clear")), Model(value:Atom(constant:"update")), - Model(value:Atom(constant:"edit"))]))]))} + Model(value:Atom(constant:"edit"))]))])) +} diff --git a/Nynja/Services/Aps.swift b/Nynja/Services/Aps.swift index c60c44acc..6323b5e3e 100644 --- a/Nynja/Services/Aps.swift +++ b/Nynja/Services/Aps.swift @@ -73,12 +73,14 @@ struct Aps { return "Contact_\(contact?.fullName ?? "")_\(contact?.status as? String ?? "")" case .message: if let c = contact { - let msg_id = c.last_msg?.msg_id ?? "messageID" - return "Message_\(c.phone_id ?? "")_\(msg_id)" + let lastMessage = c.last_msg + let msg_id = lastMessage?.id ?? 0 + return "Message_p2p;\(lastMessage?.p2pFeed?.opponentId ?? "");\(msg_id)" } if let r = room { - let msg_id = r.last_msg?.msg_id ?? "messageID" - return "Message_\(r.id ?? "")_\(msg_id)" + let lastMessage = r.last_msg + let msg_id = lastMessage?.id ?? 0 + return "Message_muc;\(lastMessage?.mucFeed?.name ?? "");\(msg_id)" } } return "NynjaId" @@ -97,24 +99,35 @@ struct Aps { } if let r = room { chat = r - if let member = r.members?.filter({ (member) -> Bool in - guard let memId = member.phone_id, - let lastMessSender = r.last_msg?.from else { return false } + if let member = r.allMembersWithoutFilter?.first(where: { + guard let memId = $0.phone_id, let lastMessSender = r.last_msg?.from else { + return false + } return memId == lastMessSender - }).first { + }) { text = notificationTextFrom(room: r, member: member) sender = "\(member.alias ?? member.fullName ?? "")@\(r.name ?? "")" } } if text == nil, - let desc = chat?.last_msg?.mainFile, - let type = desc.type { + let desc = chat?.last_msg?.mainFile, + let type = desc.type { + if (contact?.status as? StringAtom)?.string == "friend" && (desc.payload == "" && desc.mime == "text") { + return "\(sender ?? "") accepted your contact request! Send a message!" + } switch type { case .sticker: let stick: String = desc.messageRepresentation ?? "" text = "\(sender ?? ""): \(stick)" - default: + case .audio,.contact,.location,.image,.video,.file,.place: text = "\(sender ?? ""): \(type.messageDescription)" + default: + break + } + } + if text == nil, let c = contact { + if (c.status as? StringAtom)?.string == "authorization" && c.last_msg == nil { + text = "\(sender ?? "") wants to add you on NYNJA!" } } if let text = text { @@ -123,6 +136,7 @@ struct Aps { return notificationText } + // MARK: - Message Payload private static func notificationTextFrom(room: Room, member: Member) -> String? { @@ -184,7 +198,7 @@ struct Aps { static func getSound() -> String? { if Aps.notificationSettings.alertSound.url?.pathExtension == "caf" { - SoundService.sharedInstance.playPushSound() + SystemSoundManager.sharedInstance.playPushSound() return nil } else { return Aps.notificationSettings.alertSound.fileName diff --git a/Nynja/Services/Audio/AudioManager/AudioManager.swift b/Nynja/Services/Audio/AudioManager/AudioManager.swift new file mode 100644 index 000000000..a7d339feb --- /dev/null +++ b/Nynja/Services/Audio/AudioManager/AudioManager.swift @@ -0,0 +1,234 @@ +// +// AudioManager.swift +// Nynja +// +// Created by Volodymyr Hryhoriev on 10/17/17. +// Copyright © 2017 TecSynt Solutions. All rights reserved. +// + +import AVFoundation + +final class AudioManager: NSObject, AVAudioPlayerDelegate { + + static let sharedInstance = AudioManager() + + weak var delegate: AudioManagerDelegate? + + private var audioPlayer: AVAudioPlayer? { + didSet { + audioPlayer?.delegate = self + audioPlayer?.volume = 1.0 + audioPlayer?.prepareToPlay() + } + } + + private var progressTimer: Timer? + + private let audioSessionManager = AudioSessionManager.shared + + private(set) var playbackOptions: PlaybackOptions = [] + + var currentUrl: URL? + + var currentDuration: TimeInterval { + return audioPlayer?.duration ?? 0 + } + + var isPlaying: Bool { + return audioPlayer?.isPlaying ?? false + } + + private(set) var isFinished: Bool = false + + // MARK: - Init + + private override init() { + super.init() + audioSessionManager.configureSpeaker() + } + + + // MARK: - Playing + + func prepareToPlay(with url: URL, options: PlaybackOptions = []) throws { + if !isActiveAudio(with: url) { + try setup(for: url, options: options) + } else if options.shouldAlwaysPlayFromStart { + audioPlayer?.currentTime = 0 + } + } + + func play(with url: URL, at time: TimeInterval? = nil, options: PlaybackOptions = []) throws { + try prepareToPlay(with: url, options: options) + audioSessionManager.speaker = options.shouldUseSoftSpeaker ? .soft : .loud + audioPlayer = try AVAudioPlayer(contentsOf: url) + play(at: time) + } + + private func setup(for url: URL, options: PlaybackOptions) throws { + stopActiveAudio() + currentUrl = url + playbackOptions = options + } + + private func stopActiveAudio() { + if let currentUrl = currentUrl { + delegate?.didFinishPlayingAudio(self, with: currentUrl) + stop(shouldDeactivate: false) + } + } + + private func isActiveAudio(with url: URL) -> Bool { + guard let playedUrl = currentUrl else { + return false + } + + return url == playedUrl + } + + private func play(at time: TimeInterval? = nil) { + guard let player = audioPlayer, !player.isPlaying else { + return + } + + if let time = time { + player.currentTime = time + } else if isFinished, playbackOptions.isInfinite { + vibrateAtStart() + } + + isFinished = false + + player.play() + startProgressTimer() + } + + func resume() throws { + play() + } + + func pause() { + if let player = audioPlayer, player.isPlaying { + player.pause() + invalidateProgressTimer() + } + } + + func stop(with url: URL, shouldDeactivate: Bool = true) { + guard currentUrl == url else { + return + } + stop(shouldDeactivate: shouldDeactivate) + } + + func stop(shouldDeactivate: Bool = true) { + guard let player = audioPlayer else { + return + } + + player.stop() + + audioPlayer?.delegate = nil + audioPlayer = nil + currentUrl = nil + invalidateProgressTimer() + + playbackOptions = [] + + if shouldDeactivate { + try? audioSessionManager.setActive(false, with: .notifyOthersOnDeactivation) + } + } + + + // MARK: - Progress Timer + + private func startProgressTimer() { + let timer = Timer.scheduledTimer(timeInterval: 0.01, + target: self, + selector: #selector(updateProgress(_:)), + userInfo: nil, + repeats: true) + RunLoop.current.add(timer, forMode: .commonModes) + progressTimer = timer + } + + private func invalidateProgressTimer() { + progressTimer?.invalidate() + progressTimer = nil + } + + @objc private func updateProgress(_ timer: Timer) { + guard let player = audioPlayer, player.isPlaying else { + return + } + + self.delegate?.didChangedCurrentTime(self, currentTime: player.currentTime) + } + + + // MARK: - AVAudioPlayerDelegate + + func audioPlayerDidFinishPlaying(_ player: AVAudioPlayer, successfully flag: Bool) { + isFinished = true + if playbackOptions.isInfinite { + didFinishInfiniteSound() + } else { + didFinishSound(in: player) + } + } + + private func didFinishInfiniteSound() { + audioPlayer?.currentTime = 0 + play() + } + + private func didFinishSound(in player: AVAudioPlayer) { + if let url = currentUrl { + delegate?.didFinishPlayingAudio(self, with: url) + } + stop() + } + + private func vibrateAtStart() { + if playbackOptions.shouldVibrateAtStart { + AudioServicesPlaySystemSound(kSystemSoundID_Vibrate) + } + } + +} + +// MARK: - PlaybackOptions + +extension AudioManager { + + struct PlaybackOptions: OptionSet { + let rawValue: Int + + init(rawValue: Int) { + self.rawValue = rawValue + } + + var isInfinite: Bool { + return self.contains(.isInfinite) + } + + var shouldVibrateAtStart: Bool { + return self.contains(.vibrateAtStart) + } + + var shouldAlwaysPlayFromStart: Bool { + return self.contains(.alwaysPlayFromStart) + } + + var shouldUseSoftSpeaker: Bool { + return self.contains(.usingSoftSpeaker) + } + + static let isInfinite = PlaybackOptions(rawValue: 1 << 0) + static let vibrateAtStart = PlaybackOptions(rawValue: 1 << 1) + static let alwaysPlayFromStart = PlaybackOptions(rawValue: 1 << 2) + static let usingSoftSpeaker = PlaybackOptions(rawValue: 1 << 3) + } + +} diff --git a/Nynja/AudioManagerDelegate.swift b/Nynja/Services/Audio/AudioManager/AudioManagerDelegate.swift similarity index 100% rename from Nynja/AudioManagerDelegate.swift rename to Nynja/Services/Audio/AudioManager/AudioManagerDelegate.swift diff --git a/Nynja/Services/Audio/AudioPlayable.swift b/Nynja/Services/Audio/AudioPlayable.swift new file mode 100644 index 000000000..b3c5f3590 --- /dev/null +++ b/Nynja/Services/Audio/AudioPlayable.swift @@ -0,0 +1,53 @@ +// +// AudioPlayable.swift +// Nynja +// +// Created by Andrey Reznik on 18.09.2018. +// Copyright © 2018 TecSynt Solutions. All rights reserved. +// + +import Foundation + +protocol AudioPlayable { + var fileUrl: URL? { get set} + var audioDuration: TimeInterval? { get set } + var audioCurrentTime: TimeInterval? { get set } + var playStatus: PlayStatus { get set } + + var audioStateHandler: (() -> Void)? { get set } + + func notifyAudioHandler() + mutating func resetHandlers() +} + +extension AudioPlayable { + func notifyAudioHandler() { + self.audioStateHandler?() + } + + mutating func resetHandlers() { + audioStateHandler = nil + } +} + + +class AudioPlayableModel: AudioPlayable { + var fileUrl: URL? + var audioDuration: TimeInterval? + var audioCurrentTime: TimeInterval? + var playStatus: PlayStatus = .stop + + var audioStateHandler: (() -> Void)? + + init(fileUrl: URL?, + audioDuration: TimeInterval?, + audioCurrentTime: TimeInterval?, + playStatus: PlayStatus = .stop, + audioStateHandler: (() -> Void)? = nil) { + self.fileUrl = fileUrl + self.audioDuration = audioDuration + self.audioCurrentTime = audioCurrentTime + self.playStatus = playStatus + self.audioStateHandler = audioStateHandler + } +} diff --git a/Nynja/AudioPlayer.swift b/Nynja/Services/Audio/AudioPlayer/AudioPlayer.swift similarity index 100% rename from Nynja/AudioPlayer.swift rename to Nynja/Services/Audio/AudioPlayer/AudioPlayer.swift diff --git a/Nynja/AudioRecorder.swift b/Nynja/Services/Audio/AudioRecorder/AudioRecorder.swift similarity index 80% rename from Nynja/AudioRecorder.swift rename to Nynja/Services/Audio/AudioRecorder/AudioRecorder.swift index ff3faff32..a9d4af5c5 100644 --- a/Nynja/AudioRecorder.swift +++ b/Nynja/Services/Audio/AudioRecorder/AudioRecorder.swift @@ -29,7 +29,6 @@ final class AudioRecorder: NSObject { private var shouldSendRecording: Bool = false private var audioRecorder: AVAudioRecorder? - private var recordingSession: AVAudioSession! private let audioSessionManager = AudioSessionManager.shared private var recordMeterTimer: TimerHandler? @@ -44,6 +43,7 @@ final class AudioRecorder: NSObject { self.init() self.recordingStatusHandler = recordingStatusHandler self.endRecordingHandler = endRecordingHandler + if !setupAudioSession() { return nil } @@ -62,41 +62,41 @@ final class AudioRecorder: NSObject { // MARK: - Setup private func setupAudioSession() -> Bool { - recordingSession = AVAudioSession.sharedInstance() do { - try audioSessionManager.request(category: .playAndRecord, with: .defaultToSpeaker) + guard audioSessionManager.isRecordPermissionGranted else { + return false + } - if recordingSession.recordPermission() == .granted { - setupRecorder() - isMoreThanSecond = false - - recordMeterTimer = TimerHandler(interval: 0.1, repeats: true) { [weak self] timer in - self?.updateRecordMeter() - } - - let interval = Constants.Duration.resendRecordingStatus - recordingStatusMeterTimer = TimerHandler(interval: interval, repeats: true) { [weak self] timer in - self?.updateRecordingStatusMeter() - } - - updateRecordingStatusMeter() + try setupRecorder() + + isMoreThanSecond = false + + recordMeterTimer = TimerHandler(interval: 0.1, repeats: true) { [weak self] timer in + self?.updateRecordMeter() } + + let interval = Constants.Duration.resendRecordingStatus + recordingStatusMeterTimer = TimerHandler(interval: interval, repeats: true) { [weak self] timer in + self?.updateRecordingStatusMeter() + } + + updateRecordingStatusMeter() + + return true } catch { if let errorCode = AVAudioSessionErrorCode(rawValue: (error as NSError).code) { - switch errorCode { - case .insufficientPriority: + LogService.log(topic: .audioSystem) { return "AVSession error: \(errorCode)" } + + if errorCode == .insufficientPriority { AlertManager.sharedInstance.showAlertOk(message: "microphone_is_busy".localized) - default: - break } - LogService.log(topic: .audioSystem) { return "AVSession error: \(errorCode)" } - return false } + + return false } - return true } - private func setupRecorder() { + private func setupRecorder() throws { let recordSetings = [ AVFormatIDKey: kAudioFormatMPEG4AAC, AVEncoderAudioQualityKey: AVAudioQuality.max.rawValue, @@ -106,12 +106,13 @@ final class AudioRecorder: NSObject { ] as [String: Any] do { + try audioSessionManager.setActive(true) + audioRecorder = try AVAudioRecorder(url: getFileURL(), settings: recordSetings) audioRecorder?.delegate = self audioRecorder?.isMeteringEnabled = true audioRecorder?.prepareToRecord() audioRecorder?.record() - audioSessionManager.startPlayback() } catch { LogService.log(topic: .audioSystem) { return error.localizedDescription } } @@ -133,7 +134,7 @@ final class AudioRecorder: NSObject { recordMeterTimer = nil audioRecorder?.stop() - audioSessionManager.stopPlayback() + try? audioSessionManager.setActive(false, with: .notifyOthersOnDeactivation) } private func getFileURL() -> URL { @@ -173,8 +174,6 @@ final class AudioRecorder: NSObject { extension AudioRecorder: AVAudioRecorderDelegate { func audioRecorderDidFinishRecording(_ recorder: AVAudioRecorder, successfully flag: Bool) { - audioSessionManager.stopPlayback() - if isMoreThanSecond { endRecordingHandler?(.success(recorder.url)) } else { diff --git a/Nynja/Services/Audio/AudioSessionManager/AudioSessionManager.swift b/Nynja/Services/Audio/AudioSessionManager/AudioSessionManager.swift new file mode 100644 index 000000000..3ec40b652 --- /dev/null +++ b/Nynja/Services/Audio/AudioSessionManager/AudioSessionManager.swift @@ -0,0 +1,264 @@ +// +// AudioSessionManager.swift +// Nynja +// +// Created by Anton Poltoratskyi on 06.08.2018. +// Copyright © 2018 TecSynt Solutions. All rights reserved. +// + +import AVFoundation + +protocol AudioSessionManagerDelegate: class { + func speakerUpdated(to state: AudioSessionManager.Speaker) +} + +extension AudioSessionManagerDelegate { + func speakerUpdated(to state: AudioSessionManager.Speaker) {} +} + +final class AudioSessionManager { + + weak var delegate: AudioSessionManagerDelegate? + + /// Determines type of speaker + /// Default: .unknown + var speaker: Speaker { + get { + var temp: Speaker = .unknown + dispatchQueue.sync { + temp = _speaker + } + return temp + } + set { + dispatchQueue.async(flags: .barrier) { [weak self] in + guard let `self` = self, self._speaker != newValue else { + return + } + + let updatedSpeaker = self.tryAdjustSpeaker(with: newValue) ? newValue : self._speaker + dispatchAsyncMain { [weak self] in + self?.delegate?.speakerUpdated(to: updatedSpeaker) + } + + self._speaker = newValue + } + } + } + + private var _speaker: Speaker = .unknown + + private let session: AVAudioSession + + private let dispatchQueue = DispatchQueue( + label: "com.nynja.communicator.audio-session-manager.queue", + qos: .userInteractive, + attributes: .concurrent) + + private var currentCategory: Category? { + return Category(string: session.category) + } + + private let notificationCenter = NotificationCenter.default + + // MARK: - Init + + static let shared = AudioSessionManager(session: .sharedInstance()) + + private init(session: AVAudioSession) { + self.session = session + notificationCenter.addObserver( + self, + selector: #selector(audioSessionRouteChange(notification:)), + name: .AVAudioSessionRouteChange, + object: nil) + } + + deinit { + notificationCenter.removeObserver(self) + } + + + // MARK: - Configure + + func configureDefaultSession() throws { + try dispatchQueue.sync { + try _configureDefaultSession() + } + } + + private func _configureDefaultSession() throws { + try session.setCategory(Category.default.nativeCategory) + try session.setMode(AVAudioSessionModeDefault) + } + + + // MARK: - Reset + + func resetAudioSession() throws { + try dispatchQueue.sync { + try _configureDefaultSession() + try session.setActive(false, with: .notifyOthersOnDeactivation) + } + } + + + // MARK: - Category Options + + var categoryOptions: AVAudioSessionCategoryOptions { + return session.categoryOptions + } + + func addCategoryOptions(_ options: AVAudioSessionCategoryOptions, applyImmediately: Bool = false) throws { + try dispatchQueue.sync { + let newOptions = session.categoryOptions.union(options) + try setCategoryOptions(newOptions, applyImmidiately: applyImmediately) + } + } + + func removeCategoryOptions(_ options: AVAudioSessionCategoryOptions, applyImmediately: Bool = false) throws { + try dispatchQueue.sync { + let newOptions = session.categoryOptions.subtracting(options) + try setCategoryOptions(newOptions, applyImmidiately: applyImmediately) + } + } + + private func setCategoryOptions(_ options: AVAudioSessionCategoryOptions, applyImmidiately: Bool) throws { + if applyImmidiately { + try session.setActive(false) + } + + let category = session.category + let mode = session.mode + try session.setCategory(category, mode: mode, options: options) + + if applyImmidiately { + try session.setActive(true) + } + } + + + // MARK: - Record permission + + var isRecordPermissionGranted: Bool { + return session.recordPermission() == .granted + } + + + // MARK: - Set Active/Inactive + + func setActive(_ isActive: Bool, with options: AVAudioSessionSetActiveOptions = []) throws { + try dispatchQueue.sync { + try session.setActive(isActive, with: options) + } + } + + + // MARK: - Notifications + + @objc private func audioSessionRouteChange(notification: Notification) { + guard let userInfo = notification.userInfo, + let reasonValue = userInfo[AVAudioSessionRouteChangeReasonKey] as? UInt, + let reason = AVAudioSessionRouteChangeReason(rawValue:reasonValue) else { + return + } + + switch reason { + case .unknown: + LogService.log(topic: .audioSystem) { return "audioSessionRouteChange reason unknown" } + case .newDeviceAvailable: + LogService.log(topic: .audioSystem) { return "audioSessionRouteChange reason newDeviceAvailable" } + case .oldDeviceUnavailable: + LogService.log(topic: .audioSystem) { return "audioSessionRouteChange reason oldDeviceUnavailable" } + case .categoryChange: + LogService.log(topic: .audioSystem) { return "audioSessionRouteChange reason categoryChange" } + LogService.log(topic: .audioSystem) { return "AVAudioSession Category: \(session.category)" } + case .override: + LogService.log(topic: .audioSystem) { return "audioSessionRouteChange reason override" } + case .wakeFromSleep: + LogService.log(topic: .audioSystem) { return "audioSessionRouteChange reason wakeFromSleep" } + case .noSuitableRouteForCategory: + LogService.log(topic: .audioSystem) { return "audioSessionRouteChange reason noSuitableRouteForCategory" } + case .routeConfigurationChange: + LogService.log(topic: .audioSystem) { return "audioSessionRouteChange reason routeConfigurationChange" } + } + } + +} + + +// MARK: - Category + +extension AudioSessionManager { + + enum Category { + case playback + case playAndRecord + + var nativeCategory: String { + switch self { + case .playback: + return AVAudioSessionCategoryPlayback + case .playAndRecord: + return AVAudioSessionCategoryPlayAndRecord + } + } + + init?(string: String) { + switch string { + case AVAudioSessionCategoryPlayback: + self = .playback + case AVAudioSessionCategoryPlayAndRecord: + self = .playAndRecord + default: + // Just ignore other categories at now. + return nil + } + } + + static let `default`: Category = .playAndRecord + } + +} + + +// MARK: - Speaker + +extension AudioSessionManager { + + enum Speaker { + case unknown + case soft + case loud + } + + func configureSpeaker() { + speaker = activeSpeaker + } + + @discardableResult + private func tryAdjustSpeaker(with speaker: Speaker) -> Bool { + switch speaker { + case .soft, .loud: + let port: AVAudioSessionPortOverride = speaker == .soft ? .none : .speaker + + do { + try session.overrideOutputAudioPort(port) + return true + } catch { + LogService.log(topic: .audioSystem) { return error.localizedDescription } + } + case .unknown: + LogService.log(topic: .audioSystem) { return "unexpected case of speaker" } + } + + return false + } + + private var activeSpeaker: Speaker { + let isSpeakerActivated = session.currentRoute.outputs + .first { $0.portType == AVAudioSessionPortBuiltInSpeaker } != nil + return isSpeakerActivated ? .loud : .soft + } + +} diff --git a/Nynja/Sound.swift b/Nynja/Services/Audio/SystemSoundManager/Sound.swift similarity index 88% rename from Nynja/Sound.swift rename to Nynja/Services/Audio/SystemSoundManager/Sound.swift index 3d91a612b..88989e2a0 100644 --- a/Nynja/Sound.swift +++ b/Nynja/Services/Audio/SystemSoundManager/Sound.swift @@ -9,9 +9,11 @@ import Foundation struct Sound: Codable, Equatable { - static let none = SoundService.sharedInstance.soundBundle.silence - static let defaultPush = SoundService.sharedInstance.soundBundle.defaultPush - static let defaultCall = SoundService.sharedInstance.soundBundle.defaultCall + private static let soundBundle = SoundBundle.shared + + static let none = soundBundle.silence + static let defaultPush = soundBundle.defaultPush + static let defaultCall = soundBundle.defaultCall var displayName: String { return localizedKey.localized diff --git a/Nynja/SoundBundle.swift b/Nynja/Services/Audio/SystemSoundManager/SoundBundle.swift similarity index 77% rename from Nynja/SoundBundle.swift rename to Nynja/Services/Audio/SystemSoundManager/SoundBundle.swift index c889a65b6..56e800e12 100644 --- a/Nynja/SoundBundle.swift +++ b/Nynja/Services/Audio/SystemSoundManager/SoundBundle.swift @@ -27,4 +27,10 @@ struct SoundBundle: Codable { case silence case defaultRingback = "default_ringback" } + + static let shared: SoundBundle = { + let dataURL = Bundle.main.url(forResource: "Sounds", withExtension: "json")! + let data = try! Data(contentsOf: dataURL) + return try! JSONDecoder().decode(SoundBundle.self, from: data) + }() } diff --git a/Nynja/Services/Audio/SystemSoundManager/SystemSoundManager.swift b/Nynja/Services/Audio/SystemSoundManager/SystemSoundManager.swift new file mode 100644 index 000000000..8291eea75 --- /dev/null +++ b/Nynja/Services/Audio/SystemSoundManager/SystemSoundManager.swift @@ -0,0 +1,129 @@ +// +// SystemSoundManager.swift +// Nynja +// +// Created by Anton Makarov on 25.05.17. +// Copyright © 2017 TecSynt Solutions. All rights reserved. +// + +import AVFoundation + +final class SystemSoundManager { + + private let soundBundle = SoundBundle.shared + + private let userSettingsService = UserSettingsService.shared + + private var isInChatSoundEnabled: Bool { + return userSettingsService.notifications.isInChatSoundEnabled + } + + private let lock = NSLock() + + private var isVoipCallActive: Bool = false + + // MARK: - Init + + static let sharedInstance = SystemSoundManager() + + private var cachedSounds: [URL: SoundInfo] = [:] + + + // MARK: - VoIP calls + + func voipCallStarted() { + lock.lock() + isVoipCallActive = true + lock.unlock() + } + + func voipCallStopped() { + lock.lock() + isVoipCallActive = false + lock.unlock() + } + + + // MARK: - Play Sound + + func playSound(with url: URL) { + guard canPlaySound(with: url) else { + return + } + + dispatchAsyncMain { + let soundId = self.makeSoundId(with: url) + self.cachedSounds[url] = SoundInfo(soundId: soundId, lastTimeSoundPlayed: CFAbsoluteTimeGetCurrent()) + AudioServicesPlaySystemSound(soundId) + } + } + + private func canPlaySound(with url: URL) -> Bool { + return isInChatSoundEnabled && isSafeIntervalPassed(for: url) + } + + private func isSafeIntervalPassed(for url: URL) -> Bool { + guard let lastTimeSoundPlayed = cachedSounds[url]?.lastTimeSoundPlayed else { + return true + } + + let diff = abs(CFAbsoluteTimeGetCurrent() - lastTimeSoundPlayed) + return diff > 0.25 + } + + private func makeSoundId(with url: URL) -> SystemSoundID { + var soundId: SystemSoundID = 0 + + if let info = cachedSounds[url] { + soundId = info.soundId + } else { + AudioServicesCreateSystemSoundID(url as CFURL, &soundId) + } + + return soundId + } + + func playPushSound() { + guard let soundUrl = userSettingsService.notifications.alertSound.url else { + return + } + + playSound(with: soundUrl) + } + + func playIncomingMessageSound() { + guard canPlayMessageSound(), + let soundUrl = soundBundle.defaultIncomingMessage.url else { + return + } + + playSound(with: soundUrl) + } + + func playOutcomingMessageSound() { + guard canPlayMessageSound(), + let soundUrl = soundBundle.defaultOutcomingMessage.url else { + return + } + + playSound(with: soundUrl) + } + + private func canPlayMessageSound() -> Bool { + let isAppActive = UIApplication.shared.applicationState == .active + return !isVoipCallActive && isAppActive + } + +} + + +// MARK: - SoundInfo + +extension SystemSoundManager { + + struct SoundInfo { + let soundId: SystemSoundID + let lastTimeSoundPlayed: CFAbsoluteTime? + } + +} diff --git a/Nynja/Services/ConnectionService.swift b/Nynja/Services/ConnectionService.swift new file mode 100644 index 000000000..f3f2c8d8c --- /dev/null +++ b/Nynja/Services/ConnectionService.swift @@ -0,0 +1,81 @@ +// +// ConnectionService.swift +// Nynja +// +// Created by Anton M on 10.09.2018. +// Copyright © 2018 TecSynt Solutions. All rights reserved. +// + +import Foundation + +protocol ConnectionServiceDelegate: class { + func connectionStatusChanged(_ sender: ConnectionService, service: ConnectionService.Service, oldValue: ConnectionService.ConnectionServiceState) +} + +class ConnectionService { + + static let shared = ConnectionService() + + private var subscribers = [WeakRef]() + + private (set) var state: [Service:ConnectionServiceState] = [.mqtt:.undefined, + .networking:.undefined, + .call:.undefined] + + private let subscribersQueue = DispatchQueue(label: "com.nynja.mobile.communicator.connectionService.subscribers") + + func addSubscriber(_ subscriber: ConnectionServiceDelegate) { + subscribersQueue.sync { + subscribers.append(WeakRef(value: subscriber)) + } + } + + func removeSubscriber(_ subscriber: ConnectionServiceDelegate) { + subscribersQueue.sync { + subscribers = subscribers.filter { $0.value !== subscriber } + } + } + + func updateCallServiceState(_ service: ConnectionService.Service, state: ConnectionService.ConnectionServiceState) { + var oldValue: ConnectionServiceState = .undefined + if let oldState = self.state[service] { + oldValue = oldState + } + self.state[service] = state + var newSubscribers = [WeakRef]() + subscribers.forEach() { subscriber in + if let _subscriber = subscriber as? ConnectionServiceDelegate { + _subscriber.connectionStatusChanged(self, service: service, oldValue: oldValue) + newSubscribers.append(subscriber) + } + } + subscribers = newSubscribers + LogService.log(topic: .connectionState) { + let result = "service: \(service) changed value from \(oldValue) to \(state), so connection state is: \n" + return result + self.stateString() + + } + } + + func stateString() -> String { + return self.state.keys.reduce("") { (result, service) -> String in + return result + "; " + "\(service.rawValue):\(self.state[service]?.rawValue ?? "")" + } + } + + enum Service: String { + case mqtt = "MQTT" + case networking = "Networking" + case call = "CallKit" + } + + enum ConnectionServiceState: String { + case disconnected = "Disconnected" + case connected = "Connected" + case switched = "Switched" + case connecting = "Connecting" + case reconnecting = "Reconnecting" + case undefined = "Undefined" + } +} + diff --git a/Nynja/Services/Debug/LogService/LogService.swift b/Nynja/Services/Debug/LogService/LogService.swift new file mode 100644 index 000000000..56708486e --- /dev/null +++ b/Nynja/Services/Debug/LogService/LogService.swift @@ -0,0 +1,78 @@ +// +// LogService.swift +// Nynja +// +// Created by Anton M on 16.07.2018. +// Copyright © 2018 TecSynt Solutions. All rights reserved. +// + +import Foundation +import os.log + +class LogService { + + static var enable:[LogServiceTopic] = [.userDefaults, .wallet, .keychain, .fileSystem, + .audioSystem, .MQTT, .amazon, .callSystem, .locationSystem, + .system, .network, .galery, .videoConverter, .QRCode, + .passphrase, .arc, .connectionState] + + enum LogServiceTopic: String { + case wallet = "Wallet" + case keychain = "Keychain" + case fileSystem = "File System" + case db = "Database" + case audioSystem = "Audo System" + case MQTT = "MQTT" + case amazon = "Amazon" + case callSystem = "Call System" + case locationSystem = "Location System" + case system = "Nynja System" + case network = "Network" + case galery = "Galery" + case videoConverter = "Video Converter" + case QRCode = "QRCode" + case passphrase = "Passphrase" + case push = "Push notification" + case userDefaults = "User Defaults" + case arc = "ARC" + case connectionState = "Connection State" + } + + static let allValues: [LogServiceTopic] = [.userDefaults, .wallet, .keychain, .fileSystem, .db, + .audioSystem, .MQTT, .amazon, .callSystem, .locationSystem, + .system, .network, .galery, .videoConverter, .QRCode, + .passphrase, .arc, .connectionState] + + static var allValuesStrings: String { + return allValues.reduce("") { (result, topic) -> String in + return "\(result)\(topic.rawValue),\n" + } + } + + static func log(topic: LogServiceTopic, block: () -> String) { + return + #if !RELEASE + if !enable.contains(topic) { return } + LogService.executeLogs(topic: topic, text: block(), thread: Thread.current.debugDescription) + #endif + } + + private static func executeLogs(topic: LogServiceTopic, text: String, thread: String) { + LogWriter.shared?.writeLog(topic: topic.rawValue, description: text, thread: thread) + printToLog(topic: topic, text: text, thread: thread) + } + + private static func printToLog(topic: LogServiceTopic, text: String, thread: String) { + var i = 0 + let text = "\(thread), \(text)" + text.split(by: 30000).forEach { (part) in + let log = OSLog(subsystem: Bundle.main.bundleIdentifier, category: topic.rawValue) + if i != 0 { + os_log("%{public}@", log: log, type: .default, ">>>>> \(i):\n \(part)") + } else { + os_log("%{public}@", log: log, type: .default, "\(part)") + } + i += 1 + } + } +} diff --git a/Nynja/Services/Debug/LogService/LogWriter.swift b/Nynja/Services/Debug/LogService/LogWriter.swift new file mode 100644 index 000000000..1a1453996 --- /dev/null +++ b/Nynja/Services/Debug/LogService/LogWriter.swift @@ -0,0 +1,76 @@ +// +// LogWriter.swift +// Nynja +// +// Created by Anton M on 15.08.2018. +// Copyright © 2018 TecSynt Solutions. All rights reserved. +// + +import Foundation + +class LogWriter { + + static let shared: LogWriter? = LogWriter() + + private var handle: FileHandle! + + private init?() { + let fileName = LogWriter.getCurrentDateAndTime() + ".log" + FileManagerService.sharedInstance.createDirectory(dirName: "Logs") + + guard let path = FileManagerService.sharedInstance.createFile(folder: "Logs", name: fileName), + FileManagerService.sharedInstance.fileManager.createFile(atPath: path, contents: nil, attributes: nil), + let url = URL(string: path) else { return nil } + + do { handle = try FileHandle(forUpdating: url) } + catch { + return nil + } + } + + private static func getCurrentDateAndTime() -> String { + let formatter = DateFormatter() + formatter.dateFormat = "yyyy_MM_dd_HH_mm_ss" + return formatter.string(from: Date()) + } + + func writeLog(topic: String, description: String, thread: String) { + guard let _log = Log(topic: topic, description: description, thread: thread), let data = _log.data else { return } + handle.seekToEndOfFile() + handle.write(data) + } + + func closeFile() { + handle.closeFile() + } + + private struct Log { + private var _bytes: [UInt8] + + init?(topic: String, description: String, thread: String) { + let title = BertAtom(fromString: "Log") + let _topic = topic.getBin() + let _description = description.getBin() + let timestamp = Int64(Date().timeIntervalSince1970.seconds) + let _timeStamp = BertNumber(fromInt64: timestamp) + let _thread = thread.getBin() + let bert = BertTuple(fromElements: [title, _topic, _description, _timeStamp, _thread]) + do { + let bytes = try Bert.encode(object: bert) + _bytes = [UInt8](repeating: 0, count: bytes.length) + bytes.getBytes(&_bytes, length: bytes.length) + } catch { + return nil + } + } + + var data: Data? { + get { + guard let bytesString = _bytes.toBase64() else { return nil } + return (bytesString + ";").data(using: String.Encoding.ascii) + } + } + } +} + + diff --git a/Nynja/Services/Debug/MotionManager/MotionManager.swift b/Nynja/Services/Debug/MotionManager/MotionManager.swift new file mode 100644 index 000000000..b0829e81e --- /dev/null +++ b/Nynja/Services/Debug/MotionManager/MotionManager.swift @@ -0,0 +1,117 @@ +// +// MotionManager.swift +// Nynja +// +// Created by Anton M on 17.08.2018. +// Copyright © 2018 TecSynt Solutions. All rights reserved. +// + +import Foundation +import CoreMotion + +class MotionManager { + + static let shared = MotionManager() + private let motion: CMMotionManager + private var timer: Timer? = nil + + private var axCoords: MotionCoords? + private var gCoords: MotionCoords? + + private init() { + motion = CMMotionManager() + } + + func startAccelerometers() { + #if !RELEASE + // Make sure the accelerometer hardware is available. + let interval: TimeInterval = 1.0 / 60 + + guard self.motion.isGyroAvailable, + self.motion.isAccelerometerAvailable else { return } + + self.motion.accelerometerUpdateInterval = interval + self.motion.gyroUpdateInterval = interval + self.motion.startGyroUpdates() + self.motion.startAccelerometerUpdates() + + self.timer = Timer(fire: Date(), interval: interval, repeats: true) { _ in + self.updates(ax: self.motion.accelerometerData, g: self.motion.gyroData) + } + + RunLoop.current.add(self.timer!, forMode: .defaultRunLoopMode) + #endif + } + + var res = 0 + var i = 1 + var y = 1 + + func updates(ax: CMAccelerometerData?, g: CMGyroData?) { + if let _g = self.gCoords,let new = MotionCoords(data: g) { + res += Int(new.z) + if res > 300, Int(new.z) > 0, i > 0 { + i -= 1 + res = 0 + } + if res < -300, Int(new.z) < 0, y > 0 { + y -= 1 + res = 0 + } + + if i == 0 && y == 0 { + guard let nav = UIApplication.shared.keyWindow?.rootViewController as? UINavigationController else { return } + LogOutputWireFrame().presentLogOutputView(navigation: nav) + i = 1 + y = 1 + res = 0 + } + } else { self.gCoords = MotionCoords(data: g) } + } + +} + +struct MotionCoords { + var x: Double = 0 + var y: Double = 0 + var z: Double = 0 + + private init() { + + } + + init?(data: CMAccelerometerData?) { + guard let _data = data else { return nil } + self.x = _data.acceleration.x + self.y = _data.acceleration.y + self.z = _data.acceleration.z + } + + init?(data: CMGyroData?) { + guard let _data = data else { return nil } + self.x = _data.rotationRate.x + self.y = _data.rotationRate.y + self.z = _data.rotationRate.z + } + + func getDiff(newData: MotionCoords) -> MotionCoords { + var result = MotionCoords() + result.x = self.x + newData.x + result.y = self.y + newData.y + result.z = self.z + newData.z + return result + } + + var description: String { + return "x:\(self.x)\n y: \(self.y)\n z: \(self.z)" + } +} + +extension Double { + func absolute() -> Double { + if self < 0 { + return self * -1 + } + return self + } +} diff --git a/Nynja/Services/Debug/SMSCodeProvider/SMSCodeProvider.swift b/Nynja/Services/Debug/SMSCodeProvider/SMSCodeProvider.swift new file mode 100644 index 000000000..01f980df7 --- /dev/null +++ b/Nynja/Services/Debug/SMSCodeProvider/SMSCodeProvider.swift @@ -0,0 +1,60 @@ +// +// SMSCodeProvider.swift +// Nynja +// +// Created by Volodymyr Hryhoriev on 9/21/18. +// Copyright © 2018 TecSynt Solutions. All rights reserved. +// + +class SMSCodeProvider: SMSCodeProviding { + private let host = Host() + private let deviceId = UIDevice.current.persistentIdentifier + + func fetchSMSCode(for phone: String, completion: @escaping SMSCodeProvidingCompletion) { + #if !RELEASE + let path = "http://\(host.url):8888/sessions?phone=\(phone)" + guard let url = URL(string: path) else { + completion(nil) + return + } + + var req = URLRequest(url: url) + + req.allHTTPHeaderFields = makeHTTPHeaderFields() + + let session = URLSession.shared + let task = session.dataTask(with: req, completionHandler: { [weak self] (data, resp, err) in + guard let `self` = self, + data != nil else { + return + } + + do { + if let todoJSON = try JSONSerialization.jsonObject(with: data!, options: []) as? [[String: Any]] { + for i in todoJSON { + let model = Modelka(input: i) + if let smsCode = model.smsCode as? String, model.devKey == self.deviceId { + completion(smsCode) + } + } + } + } catch { + completion(nil) + } + }) + task.resume() + #endif + } + + private func makeHTTPHeaderFields() -> [String: String]? { + let username = "nynja" + let password = "nynjaTS" + + guard let base64Encoded = "\(username):\(password)".base64Encoded() else { + return nil + } + + return ["Authorization": "Basic \(base64Encoded)"] + } + +} diff --git a/Nynja/Services/Debug/SMSCodeProvider/SMSCodeProviding.swift b/Nynja/Services/Debug/SMSCodeProvider/SMSCodeProviding.swift new file mode 100644 index 000000000..c54e4f1a4 --- /dev/null +++ b/Nynja/Services/Debug/SMSCodeProvider/SMSCodeProviding.swift @@ -0,0 +1,13 @@ +// +// SMSCodeProviding.swift +// Nynja +// +// Created by Volodymyr Hryhoriev on 9/21/18. +// Copyright © 2018 TecSynt Solutions. All rights reserved. +// + +typealias SMSCodeProvidingCompletion = (String?) -> Void + +protocol SMSCodeProviding { + func fetchSMSCode(for phone: String, completion: @escaping SMSCodeProvidingCompletion) +} diff --git a/Nynja/Services/HandleServices/ContactHandler.swift b/Nynja/Services/HandleServices/ContactHandler.swift index 84e0d24f4..158edf56f 100644 --- a/Nynja/Services/HandleServices/ContactHandler.swift +++ b/Nynja/Services/HandleServices/ContactHandler.swift @@ -77,7 +77,7 @@ class ContactHandler: BaseHandler { do { try storageService.perform(action: .save, with: contact) - if [.request, .authorization].contains(prevStatus) { + if [.request, .authorization, .ignore].contains(prevStatus) { NotificationManager.shared.handle(bert: data, type: .friend) } } catch { diff --git a/Nynja/Services/HandleServices/HistoryHandler.swift b/Nynja/Services/HandleServices/HistoryHandler.swift index 03ac100d1..4b36b4288 100644 --- a/Nynja/Services/HandleServices/HistoryHandler.swift +++ b/Nynja/Services/HandleServices/HistoryHandler.swift @@ -22,7 +22,34 @@ extension HistoryHandlerDelegate { final class HistoryHandler: BaseHandler { - static weak var delegate: HistoryHandlerDelegate? + // MARK: - Subscribers + + private static let subscribersLock = NSLock() + + private static var subscribers = [WeakRef]() + + private static func notify(block: (HistoryHandlerDelegate) -> Void) { + subscribersLock.lock() + subscribers.forEach { ($0.value as? HistoryHandlerDelegate).map { block($0) } } + subscribersLock.unlock() + } + + static func addSubscriber(_ subscriber: HistoryHandlerDelegate) { + subscribersLock.lock() + defer { subscribersLock.unlock() } + + guard !subscribers.contains(where: { $0.value === subscriber }) else { + return + } + let ref = WeakRef(value: subscriber as AnyObject) + subscribers.append(ref) + } + + static func removeSubscriber(_ subscriber: HistoryHandlerDelegate) { + subscribersLock.lock() + subscribers = subscribers.filter { $0.value != nil && $0.value !== subscriber } + subscribersLock.unlock() + } // MARK: - Dependencies @@ -53,16 +80,16 @@ final class HistoryHandler: BaseHandler { } else if let messageId = history.entity_id, let fetchType = fetchType(from: history.feed) { markHistoryAsTrusted(before: messageId, in: fetchType) } - delegate?.getHistorySuccess() + notify { $0.getHistorySuccess() } case is act: if let jobs = history.data as? [Job] { updateJobsHistory(jobs) - delegate?.getJobsHistorySuccess() + notify { $0.getJobsHistorySuccess() } } case is StickerPack: if let stickerPacks = history.data as? [StickerPack] { updateStickerPacks(stickerPacks) - delegate?.getStickerPacksSuccess() + notify { $0.getStickerPacksSuccess() } } default: break @@ -72,10 +99,16 @@ final class HistoryHandler: BaseHandler { // MARK: - Data + // MARK: -- Messages + /// The first message is new, the last is old. private static func updateMessageHistory(_ messages: [Message]) { - var stackForSave = [Message]() + var stackForSave = [Message](reserveCapacity: messages.count) var stackForDelete = [Message]() + + var repliedMessages = [MessageServerId: [Message]]() + var visibleRepliedMessages: Set = [] + var deletedActions = [DBMessageAction]() var systemClearMessage: Message? @@ -84,42 +117,92 @@ final class HistoryHandler: BaseHandler { for message in messages { message.isTrusted = true - if message.isStatusDelete { - if let id = message.link, let action = MessageActionDAO.fetchMessageAction(by: id) { + if message.messageStatus == .delete { + if let id = message.linkedId, let action = MessageActionDAO.fetchMessageAction(by: id) { deletedActions.append(action) } if !isHistoryCleared { stackForDelete.append(message) } - } else if let id = message.id, MessageActionDAO.containsDeleteAction(for: id) { + continue + } + if let id = message.id, MessageActionDAO.containsDeleteAction(for: id) { // Ignore received message if we have local 'delete' action for it. continue - } else { - switch messageEditService.getMergeActionForMessage(message) { - case .override: - if message.isSystem, message.isStatusClear { - systemClearMessage = message - isHistoryCleared = true - stackForSave.append(message) - - } else if !isHistoryCleared { - stackForSave.append(message) + } + switch messageEditService.getMergeActionForMessage(message) { + case .override: + switch message.messageStatus { + case .clear?: + guard !MessageDAO.shouldMarkMessageAsDeleted(message) else { + message.localStatus = .deleted + fallthrough + } + systemClearMessage = message + isHistoryCleared = true + stackForSave.append(message) + default: + guard !isHistoryCleared else { + break + } + stackForSave.append(message) + + if let id = message.id, repliedMessages[id] != nil { + visibleRepliedMessages.insert(id) + repliedMessages[id]?.append(message) + } + + if let link = message.linkedId, let repliedMessage = message.repliedMessage { + if repliedMessages[link] == nil { + repliedMessages[link] = [repliedMessage] + } else { + message.linkedId = link + } } - case .skip: - // Ignore received message if we have local 'edit' action for it. - break } + case .skip: + // Ignore received message if we have local 'edit' action for it. + break } } if messages.count > 1 { messages.last?.isTrusted = nil } + + saveMessageHistory(stackForSave: stackForSave, + stackForDelete: stackForDelete, + repliedMessages: repliedMessages, + visibleRepliedMessages: visibleRepliedMessages, + deletedActions: deletedActions, + systemClearMessage: systemClearMessage) + } + + private static func saveMessageHistory(stackForSave: [Message], + stackForDelete: [Message], + repliedMessages: [MessageServerId: [Message]], + visibleRepliedMessages: Set, + deletedActions: [DBMessageAction], + systemClearMessage: Message?) { + if let systemClearMessage = systemClearMessage { ChatService.clearMessages(before: systemClearMessage) } - try? MessageDAO.saveMessages(stackForSave) + try? repliedMessages.forEach { id, messages in + if visibleRepliedMessages.contains(id) { + messages.forEach { $0.localStatus = nil } + } else { + guard let message = messages.first else { return } + + message.localStatus = try MessageDAO.localStatusForRepliedMessage(message) + + messages.forEach { $0.localStatus = message.localStatus } + } + } + + // Save new messages after old messages + try? MessageDAO.saveMessages(stackForSave.reversed()) ChatService.removeMessages(stackForDelete) try? MessageActionDAO.delete(deletedActions) } @@ -146,6 +229,9 @@ final class HistoryHandler: BaseHandler { try? MessageDAO.trustMessages(before: id, in: fetchType) } + + // MARK: -- Jobs + private static func updateJobsHistory(_ jobs: [Job]) { let stackForSave = jobs.filter { StringAtom.string($0.status) == "pending" } @@ -158,6 +244,9 @@ final class HistoryHandler: BaseHandler { try? storageService.perform(action: .save, with: stackForSave) } + + // MARK: -- Stickers + private static func updateStickerPacks(_ stickerPacks: [StickerPack]) { for package in stickerPacks { try? storageService.perform(action: .save, with: package) diff --git a/Nynja/Services/HandleServices/MessageHandler.swift b/Nynja/Services/HandleServices/MessageHandler.swift index 2b20d6385..b585642f2 100644 --- a/Nynja/Services/HandleServices/MessageHandler.swift +++ b/Nynja/Services/HandleServices/MessageHandler.swift @@ -28,28 +28,33 @@ final class MessageHandler: BaseHandler { // MARK: - Execute + static func executeHandle(data: BertTuple) { guard let message = get_Message().parse(bert: data) as? Message else { return } let types = message.types - let status = message.statusString + let status = message.messageStatus switch (types, status) { - case let (types, status) where types.contains("sys") && status == "clear": - ChatService.clearHistory(message) + case let (types, status) where types.contains("sys") && status == .clear: + clearHistory(message) case let (types, _) where types.contains("read"): updateReader(from: message) - case let (_, status) where status == "delete": + case let (_, status) where status == .delete: deleteMessage(message) - case let (_, status) where status == "edit": + case let (_, status) where status == .edit: editMessage(message) - case let (_, status) where status == "update": + case let (_, status) where status == .update: updateMessage(message) default: saveMessage(message, data: data) } } + private static func clearHistory(_ message: Message) { + ChatService.clearHistory(message) + } + private static func updateReader(from message: Message) { if shouldUpdateOwnReader(from: message) { ChatService.updateReader(from: message, kind: .own) @@ -70,15 +75,12 @@ final class MessageHandler: BaseHandler { private static func editMessage(_ message: Message) { try? saveIntoDatabase(message: message) - guard let link = message.link else { + guard let link = message.linkedId else { return } if let oldMessage = MessageDAO.fetchMessage(serverId: link) { - oldMessage.files = message.files - - oldMessage.markAsEdited() - oldMessage.status = nil + oldMessage.edit(by: message) do { try saveIntoDatabase(message: oldMessage) @@ -165,8 +167,11 @@ final class MessageHandler: BaseHandler { } private static func saveIntoDatabase(message: Message) throws { - try? MessageDAO.trustIfNextMessageExists(before: message) - try? StorageService.sharedInstance.perform(action: .save, with: message) + if let repliedMessage = message.repliedMessage { + repliedMessage.localStatus = try MessageDAO.localStatusForRepliedMessage(repliedMessage) + } + try MessageDAO.trustIfNextMessageExists(before: message) + try StorageService.sharedInstance.perform(action: .save, with: message) } /// Play sound for incoming messages if chat isn't muted. @@ -180,7 +185,7 @@ final class MessageHandler: BaseHandler { guard let currentPhoneId = storage.phoneId, MessageDAO.isIncoming(message: message, currentPhoneId: currentPhoneId) else { return } - let soundService = SoundService.sharedInstance + let systemSoundManager = SystemSoundManager.sharedInstance switch message.feed_id { case let feed as muc: @@ -190,7 +195,7 @@ final class MessageHandler: BaseHandler { member.shouldNotify(for: message) else { break } - soundService.playIncomingMessageSound() + systemSoundManager.playIncomingMessageSound() case is p2p: guard @@ -199,7 +204,7 @@ final class MessageHandler: BaseHandler { contact.notifications else { break } - soundService.playIncomingMessageSound() + systemSoundManager.playIncomingMessageSound() default: break diff --git a/Nynja/Services/HandleServices/ProfileHandler.swift b/Nynja/Services/HandleServices/ProfileHandler.swift index 8528b241a..9556c75db 100644 --- a/Nynja/Services/HandleServices/ProfileHandler.swift +++ b/Nynja/Services/HandleServices/ProfileHandler.swift @@ -83,35 +83,41 @@ class ProfileHandler: BaseHandler { } private static func prepareForReceived(_ newRoster: Roster) { - guard let currentRoster = RosterDAO.currentRoster else { - return - } + let currentRoster = RosterDAO.currentRoster - // Just ignore last message from newRoster if user have local 'delete' or 'edit' action for it. - func deleteActionExists(for messageServerId: MessageServerId) -> Bool { - return MessageActionDAO.containsDeleteAction(for: messageServerId) + func shouldSave(_ message: Message?) -> Bool { + guard let message = message, let id = message.id else { + return true + } + // Just ignore last message from newRoster if user have local 'delete' or 'edit' action for it. + let localActionExists = MessageActionDAO.containsDeleteAction(for: id) || MessageEditActionDAO.containsEditAction(for: id) + return !localActionExists } - func editActionExists(for messageServerId: MessageServerId) -> Bool { - return MessageEditActionDAO.containsEditAction(for: messageServerId) + func updateRepliedStatus(for repliedMessage: Message) { + do { + repliedMessage.localStatus = try MessageDAO.localStatusForRepliedMessage(repliedMessage) + } catch { + LogService.log(topic: .db) { return error.localizedDescription } + } } newRoster.userlist?.forEach { contact in - guard let contactId = contact.id, - let id = contact.last_msg?.id, - (deleteActionExists(for: id) || editActionExists(for: id)) else { - return + if let currentRoster = currentRoster, let contactId = contact.id, !shouldSave(contact.last_msg) { + contact.last_msg = currentRoster.userlist?.first { $0.id == contactId }?.last_msg + + } else if let repliedMessage = contact.last_msg?.repliedMessage { + updateRepliedStatus(for: repliedMessage) } - contact.last_msg = currentRoster.userlist?.first { $0.id == contactId }?.last_msg } newRoster.roomlist?.forEach { room in - guard let roomId = room.id, - let id = room.last_msg?.id, - (deleteActionExists(for: id) || editActionExists(for: id)) else { - return + if let currentRoster = currentRoster, let roomId = room.id, !shouldSave(room.last_msg) { + room.last_msg = currentRoster.roomlist?.first { $0.id == roomId }?.last_msg + + } else if let repliedMessage = room.last_msg?.repliedMessage { + updateRepliedStatus(for: repliedMessage) } - room.last_msg = currentRoster.roomlist?.first { $0.id == roomId }?.last_msg } newRoster.favorite?.forEach { extendedStar in diff --git a/Nynja/Services/HandleServices/RoomHandler.swift b/Nynja/Services/HandleServices/RoomHandler.swift index 34d7b905c..6193bb931 100644 --- a/Nynja/Services/HandleServices/RoomHandler.swift +++ b/Nynja/Services/HandleServices/RoomHandler.swift @@ -39,6 +39,7 @@ class RoomHandler: BaseHandler { switch status { case .get: RoomDAO.updateRoom(room) { (new, old) in + new.mentions = old.mentions new.message = old.message return new } @@ -49,13 +50,10 @@ class RoomHandler: BaseHandler { case .patch: RoomDAO.patchRoom(room) case .add: - trustLastMessageIfNeeded(for: room) handleAddMember(room) case .remove: - trustLastMessageIfNeeded(for: room) handleRemoveMember(room) case .leave: - trustLastMessageIfNeeded(for: room) handleLeave(room) case .lastMessage: RoomDAO.updateColumns([.unread], room: room) @@ -74,11 +72,15 @@ class RoomHandler: BaseHandler { // MARK: - Add Member private static func handleAddMember(_ room: Room) { + trustLastMessageIfNeeded(for: room) + guard let id = room.id, let oldRoom = RoomDAO.findRoom(by: id) else { try? storageService.perform(action: .save, with: room) return } + updateReadersUnreadAndStatus(from: room, oldRoom: oldRoom) + if room.kind == .channel { handleChannelAddMember(room, oldRoom: oldRoom) } else { @@ -93,8 +95,7 @@ class RoomHandler: BaseHandler { if let features = room.settings { oldRoom.settings = features } - - oldRoom.status = room.status + try? storageService.perform(action: .save, with: oldRoom) } @@ -107,8 +108,10 @@ class RoomHandler: BaseHandler { addNotExistedAdmins(from: room, to: oldRoom) filterMembers(using: room, in: oldRoom) - - oldRoom.status = room.status + + if let lastMessage = room.lastMessage, lastMessage.isOwn, let messageId = lastMessage.id { + oldRoom.selfReader = messageId + } oldRoom.last_msg = room.last_msg try? storageService.perform(action: .save, with: oldRoom) @@ -131,26 +134,24 @@ class RoomHandler: BaseHandler { } private static func filterAdmins(using room: Room, in oldRoom: Room) { - oldRoom.admins = oldRoom.admins?.filter { member -> Bool in - if let isContains = room.members?.contains(where: { newMember -> Bool in - return newMember.phone_id == member.phone_id - }) { - return !isContains + oldRoom.admins = oldRoom.admins?.filter { admin in + guard let members = room.members else { + return true } - return true + return !members.contains { $0.phone_id == admin.phone_id } } } private static func addNotExistedAdmins(from room: Room, to oldRoom: Room) { var notExistsAdmins: [Member] = [] - room.admins?.forEach({ member in + room.admins?.forEach { member in if let oldMember = oldRoom.admins?.first(where: { $0.phone_id == member.phone_id }) { oldMember.status = member.status } else { notExistsAdmins.append(member) } - }) + } var admins = oldRoom.admins ?? [] admins.append(contentsOf: notExistsAdmins) @@ -158,27 +159,27 @@ class RoomHandler: BaseHandler { } private static func filterMembers(using room: Room, in oldRoom: Room) { - oldRoom.members = oldRoom.members?.filter({ member -> Bool in - if let isContains = room.admins?.contains(where: { newMember -> Bool in - return newMember.phone_id == member.phone_id - }) { - return !isContains + oldRoom.members = oldRoom.members?.filter { member in + guard let admins = room.admins else { + return true } - return true - }) + return !admins.contains { $0.phone_id == member.phone_id } + } } // MARK: - Remove Member private static func handleRemoveMember(_ room: Room) { + trustLastMessageIfNeeded(for: room) + if let id = room.id, let oldRoom = RoomDAO.findRoom(by: id) { - room.members?.forEach({ member in - if let oldMember = oldRoom.members?.first(where: { $0.phone_id == member.phone_id }) { + room.allMembersWithoutFilter?.forEach { member in + if let oldMember = oldRoom.allMembersWithoutFilter?.first(where: { $0.phone_id == member.phone_id }) { oldMember.status = member.status } - }) - oldRoom.status = room.status + } + updateReadersUnreadAndStatus(from: room, oldRoom: oldRoom) try? storageService.perform(action: .save, with: oldRoom) } } @@ -187,21 +188,25 @@ class RoomHandler: BaseHandler { // MARK: - Leave private static func handleLeave(_ room: Room) { - if let id = room.id, let oldRoom = RoomDAO.findRoom(by: id) { - let phoneId = storageService.phoneId - if let _ = room.allMembersWithRemoved?.first(where: { $0.phone_id == phoneId }) { - room.unread = 0 - try? storageService.perform(action: .save, with: room) - } else { - if let ad = oldRoom.admins?.first(where: { $0.phone_id == room.allMembers?.first?.phone_id }) { - ad.status = StringAtom(string: "removed") - } else if let mm = oldRoom.members?.first(where: { $0.phone_id == room.allMembers?.first?.phone_id }) { - mm.status = StringAtom(string: "removed") - } - oldRoom.status = StringAtom(string: "get") - oldRoom.unread = 0 - try? storageService.perform(action: .save, with: oldRoom) + trustLastMessageIfNeeded(for: room) + + guard let id = room.id, let oldRoom = RoomDAO.findRoom(by: id) else { + return + } + + if let _ = room.selfMember { + room.unread = 0 + try? storageService.perform(action: .save, with: room) + } else { + let leaveMember = oldRoom.allMembersWithoutFilter?.first { $0.phone_id == room.allMembers?.first?.phone_id } + if let member = leaveMember { + member.memberStatus = .removed } + + oldRoom.readers = room.readers + oldRoom.originalStatus = .get + oldRoom.unread = 0 + try? storageService.perform(action: .save, with: oldRoom) } } @@ -214,4 +219,14 @@ class RoomHandler: BaseHandler { } try? MessageDAO.trustIfNextMessageExists(before: lastMessage) } + + + // MARK: - Update + + private static func updateReadersUnreadAndStatus(from room: Room, oldRoom: Room) { + oldRoom.unread = room.unread + oldRoom.readers = room.readers + oldRoom.status = room.status + } + } diff --git a/Nynja/Services/MQTT/MQTTService.swift b/Nynja/Services/MQTT/MQTTService.swift index d3c995c20..902a3814a 100644 --- a/Nynja/Services/MQTT/MQTTService.swift +++ b/Nynja/Services/MQTT/MQTTService.swift @@ -12,14 +12,14 @@ import CocoaMQTT struct Host { let url: String let port: UInt16 - + init() { url = Bundle.main.serverUrl port = Bundle.main.serverPort } } -final class MQTTService: NSObject, CocoaMQTTDelegate, ReachabilityServiceObserver { +final class MQTTService: NSObject, CocoaMQTTDelegate, ConnectionServiceDelegate { enum ConnectionState { case connected @@ -27,41 +27,73 @@ final class MQTTService: NSObject, CocoaMQTTDelegate, ReachabilityServiceObserve } var mqtt: CocoaMQTT? - + static let version = Bundle.main.modelsVersion - + let host = Host() - + var push: String? - + let deviceId = UIDevice.current.persistentIdentifier - - var timer: Timer? - + + let semaphore = DispatchSemaphore(value: 1) + typealias MQTTServiceSubscribers = [WeakRef] private var subscribers = MQTTServiceSubscribers() private let subscribersQueue = DispatchQueue(label: "com.nynja.mobile.communicator.mqttservice.subscribers") - + let showHandlers : Set = [] - - static let sharedInstance : MQTTService = { - let instance = MQTTService() - return instance - }() - + + static let sharedInstance = MQTTService() + + private var autoReconnectTimeInterval: UInt16? { + willSet { + mqtt?.autoReconnect = newValue != nil + mqtt?.autoReconnectTimeInterval = newValue ?? 0 + } + } + + private(set) var isBadProtocolVersion: Bool = false { + willSet { + if newValue { + autoReconnectTimeInterval = nil + } + } + } + + private(set) var isConnectedSuccess: Bool = false { + didSet { + guard oldValue != isConnectedSuccess else { + return + } + + if isConnectedSuccess { + notifySubscribers { (delegate) in + delegate.mqttServiceDidConnect(self) + } + } else { + notifySubscribers { (delegate) in + delegate.mqttServiceDidDisconnect(self) + } + } + } + } + + // MARK: - Session State + private let userDefaults: UserDefaults = .standard - + enum SessionState { case notDetermined case notAuthenticated(isLoggedOutFromServer: Bool) - + enum Keys { static let state = "SessionState.AuthenticationState" static let isLoggedOutFromServer = "SessionState.isLoggedOutFromServer" } } - + var state: SessionState = .notDetermined { didSet { switch state { @@ -75,63 +107,33 @@ final class MQTTService: NSObject, CocoaMQTTDelegate, ReachabilityServiceObserve } } } - + var isAuthenticated: Bool { return StorageService.sharedInstance.hasToken } - - + + // MARK: - Init - + override init () { super.init() - state = fetchCurrentState() } - + func initialize() { self.setup() myConnect() - self.timer = Timer.scheduledTimer(timeInterval: 15, target: self, selector: #selector(runTimedCode), userInfo: nil, repeats: true) } - func reachabilityStatusChanged(isReachable: Bool) { - if !isReachable { - mqtt?.disconnect() - } else { - myConnect() - } - } - - func reachabilityStatusChanged(from: Network.Status?, to: Network.Status?) { - guard let from = from else { - return - } - - if (from == .wifi && to == .wwan) || (to == .wifi && from == .wwan) { - reconnect() - } - } - - let semaphore = DispatchSemaphore(value: 1) - func myConnect() { semaphore.wait() mqtt?.connect() semaphore.signal() } - - @objc func runTimedCode() { - if let state = self.mqtt?.connState, state == .disconnected || state == .connecting { - reconnect() - } - } - - var queue = Queue() - + func setup() { - ReachabilityService.sharedInstance.addRechabilityObserver(self) - + ConnectionService.shared.addSubscriber(self) + if let token = StorageService.sharedInstance.token, let clientId = StorageService.sharedInstance.clientId, !token.isEmpty { mqtt?.delegate = nil mqtt = CocoaMQTT(clientID: clientId, host: host.url, port: host.port) @@ -145,23 +147,16 @@ final class MQTTService: NSObject, CocoaMQTTDelegate, ReachabilityServiceObserve mqtt?.username = "api" mqtt?.cleanSession = false } - + mqtt?.enableSSL = Bundle.main.isServerConnectionSecure mqtt?.willMessage = CocoaMQTTWill(topic: "version/\(MQTTService.version)", message: "") mqtt?.keepAlive = 0 mqtt?.delegate = self -// mqtt?.enableSSL = true + + autoReconnectTimeInterval = 15 LogService.log(topic: .MQTT) { return "setup clientID: \(mqtt?.clientID ?? "") & password: \(mqtt?.password ?? "")" } } - private(set) var isConnectedSuccess = false - - func mqtt(_ mqtt: CocoaMQTT, didConnect host: String, port: Int) { - //sendPresence() - //sendOld() - isConnectedSuccess = true - } - func mqtt(_ mqtt: CocoaMQTT, didConnectAck ack: CocoaMQTTConnAck) { if ack == .badUsernameOrPassword { if let actualToken = StorageService.sharedInstance.token { @@ -169,43 +164,38 @@ final class MQTTService: NSObject, CocoaMQTTDelegate, ReachabilityServiceObserve LogService.log(topic: .MQTT) { return "Bad Username or Password" } #if !SHARE_EXTENSION LogService.log(topic: .db) { return "Clear storage: bad username" } - StorageService.sharedInstance.clearStorage() - - self.state = .notAuthenticated(isLoggedOutFromServer: true) - - MQTTService.sharedInstance.queue = Queue() - IoHandler.delegate?.sessionNotFound() - notifySubscribersAuthFailure() - self.reconnect() + StorageService.sharedInstance.clearStorage() + + self.state = .notAuthenticated(isLoggedOutFromServer: true) + + IoHandler.delegate?.sessionNotFound() + notifySubscribers { (delegate) in + delegate.mqttServiceDidReceiveAuthenticationFailure(self) + } + self.reconnect() #endif } } else { LogService.log(topic: .MQTT) { return "Bad protocol version" } - notifyWrongVersion() + isBadProtocolVersion = true + notifySubscribers { (delegate) in + delegate.mqttServiceDidReceiveWrongServerVersion() + } } } if ack == .accept { LogService.log(topic: .MQTT) { return "clientID: \(mqtt.clientID ) & password: \(mqtt.password ?? "") & clearSession: \(mqtt.cleanSession) state: \(mqtt.connState)" } + isConnectedSuccess = true #if !SHARE_EXTENSION -// if shouldResendLogout { -// logout() -// } - #endif - - notifySubscribersConnect() - //delegate?.didConnect(self) - //sendPresence() - //sendOld() - #if !SHARE_EXTENSION if (mqtt.password ?? "") != "" && self.push != nil { self.updatePushToken(push_token: self.push!) } #endif } } - + private func fetchCurrentState() -> SessionState { guard let sessionState = userDefaults.dictionary(forKey: SessionState.Keys.state), let isLoggedOutFromServer = sessionState[SessionState.Keys.isLoggedOutFromServer] as? Bool else { @@ -213,7 +203,7 @@ final class MQTTService: NSObject, CocoaMQTTDelegate, ReachabilityServiceObserve } return self.isAuthenticated ? .notDetermined : .notAuthenticated(isLoggedOutFromServer: isLoggedOutFromServer) } - + private var shouldResendLogout: Bool { let storage = StorageService.sharedInstance if !storage.hasToken, case let .notAuthenticated(isLoggedOutFromServer) = self.state, !isLoggedOutFromServer { @@ -221,101 +211,105 @@ final class MQTTService: NSObject, CocoaMQTTDelegate, ReachabilityServiceObserve } return false } - - func reconnectWithTimer() { - timer = Timer.scheduledTimer(timeInterval: 5, target: self, selector: #selector(runTimedCode), userInfo: nil, repeats: true) + + func tryReconnect() { + let states: [CocoaMQTTConnState] = [.initial, .disconnected] + if let connState = mqtt?.connState, states.contains(connState) { + reconnect() + } } - + func reconnect() { LogService.log(topic: .MQTT) { return "Reconnect" } self.setup() myConnect() } - - func disconnect() { + + func disconnect(shouldRemoveConnectionSubscriber: Bool = true) { LogService.log(topic: .MQTT) { return "Disconnect" } - ReachabilityService.sharedInstance.removeRechabilityObserver(self) - timer?.invalidate() + if shouldRemoveConnectionSubscriber { + ConnectionService.shared.removeSubscriber(self) + } mqtt?.disconnect() } - - + func mqtt(_ mqtt: CocoaMQTT, didPublishMessage message: CocoaMQTTMessage, id: UInt16) { printMessage(msg: message, isSent: true) } - + func mqtt(_ mqtt: CocoaMQTT, didReceiveMessage message: CocoaMQTTMessage, id: UInt16 ) { printMessage(msg: message, isSent: false) HandlerService.handle(response: message) } - + func mqttDidDisconnect(_ mqtt: CocoaMQTT, withError err: Error?) { LogService.log(topic: .MQTT) { return "clientID: \(mqtt.clientID ) & password: \(mqtt.password ?? "") & clearSession: \(mqtt.cleanSession) state: \(mqtt.connState) & error: \(err?.localizedDescription)" } if let error = err as NSError?, error.code == 7 { LogService.log(topic: .MQTT) { return "Something went wrong" } } + isConnectedSuccess = false - notifySubscribersDisconnect() } - + + func mqtt(_ mqtt: CocoaMQTT, didConnect host: String, port: Int) { + isConnectedSuccess = true + } + func mqtt(_ mqtt: CocoaMQTT, didSubscribeTopic topic: String) {} func mqtt(_ mqtt: CocoaMQTT, didUnsubscribeTopic topic: String) {} func mqtt(_ mqtt: CocoaMQTT, didPublishAck id: UInt16) {} func mqttDidPing(_ mqtt: CocoaMQTT) {} func mqttDidReceivePong(_ mqtt: CocoaMQTT) {} - + // MARK: Subscribers func addSubscriber(_ subscriber: MQTTServiceDelegate) { subscribersQueue.sync { subscribers.append(WeakRef(value: subscriber)) } } - + func removeSubscriber(_ subscriber: MQTTServiceDelegate) { subscribersQueue.sync { subscribers = subscribers.filter { $0.value !== subscriber } } } - + func removeAllSubscribers() { subscribersQueue.sync { subscribers.removeAll() } } - + /* Notify all subscribers that reachbility status changed */ - private func notifySubscribersConnect() { - subscribersQueue.sync { - subscribers.forEach { (weak) in - (weak.value as? MQTTServiceDelegate)?.mqttServiceDidConnect(self) - } - } - } - - private func notifySubscribersDisconnect() { - subscribersQueue.sync { - subscribers.forEach { (weak) in - (weak.value as? MQTTServiceDelegate)?.mqttServiceDidDisconnect(self) - } - } - } - private func notifySubscribersAuthFailure() { + private func notifySubscribers(with eventClosure: (MQTTServiceDelegate) -> Void) { + subscribersQueue.sync { subscribers.forEach { (weak) in - (weak.value as? MQTTServiceDelegate)?.mqttServiceDidReceiveAuthenticationFailure(self) + if let delegate = weak.value as? MQTTServiceDelegate { + eventClosure(delegate) + } } } } - - private func notifyWrongVersion() { - subscribersQueue.sync { - subscribers.forEach { (weak) in - (weak.value as? MQTTServiceDelegate)?.mqttServiceDidReceiveWrongServerVersion() + + + // MARK: - ConnectionServiceDelegate + + func connectionStatusChanged(_ sender: ConnectionService, service: ConnectionService.Service, oldValue: ConnectionService.ConnectionServiceState) { + + if service == .networking { + guard let networkStatus = sender.state[.networking] else { return } + switch networkStatus { + case .connected: myConnect() + case .disconnected: disconnect(shouldRemoveConnectionSubscriber: false) + case .switched: reconnect() + default: break } } } + } protocol MQTTServiceDelegate: class { diff --git a/Nynja/Services/MQTT/MQTTServiceAuth.swift b/Nynja/Services/MQTT/MQTTServiceAuth.swift index a53b1c992..293ceb17a 100644 --- a/Nynja/Services/MQTT/MQTTServiceAuth.swift +++ b/Nynja/Services/MQTT/MQTTServiceAuth.swift @@ -40,12 +40,13 @@ extension MQTTService { func registration(number: String) { let settings = FeatureFactory.getAuthSettings() - let model = AuthModel(type: .REG, - phoneNumber: number, - userID: nil, - clientID: "reg_\(deviceId)", + let model = AuthModel( + type: .REG, + phoneNumber: number, + userID: nil, + clientID: "reg_\(deviceId)", deviceToken: deviceId, - settings:settings) + settings: settings) publish(model: model) } @@ -75,19 +76,6 @@ extension MQTTService { } func checkSMS(code: String, phone: String) { - - // TODO: - // This is Dr.Strange fix. I know that isn't good solution and we should write merge signal manager for resolve this issue and the same issues. - // Best idea for this i think is subscribe to signal about connection is connect and after that think what we need to do with all signals which was send in offline. But it's not quick. - // I will think about this later. Makarov A. - - if self.mqtt?.connState != .connected { - dispatchAsyncMainAfter(1) { - self.checkSMS(code: code, phone: phone) - } - return - } - let model = AuthModel(type: .VERIFY, phoneNumber: phone, userID: nil, diff --git a/Nynja/Services/MQTT/MQTTServiceHelper.swift b/Nynja/Services/MQTT/MQTTServiceHelper.swift index 734736e23..41e228ac7 100644 --- a/Nynja/Services/MQTT/MQTTServiceHelper.swift +++ b/Nynja/Services/MQTT/MQTTServiceHelper.swift @@ -12,15 +12,14 @@ import CocoaMQTT extension MQTTService { func publish(model: BaseMQTTModel) { - if mqtt!.connState != .connected { - queue.enqueue(model) - } else { - let finalModel: CocoaMQTTMessage = model.getMessage() - mqtt?.publish(finalModel) + guard let mqtt = self.mqtt, mqtt.connState == .connected else { + return } + + let finalModel: CocoaMQTTMessage = model.getMessage() + mqtt.publish(finalModel) } - func printMessage(msg: CocoaMQTTMessage, isSent: Bool) { let desc = self.prepareOutputMessage(msg: msg, isSent: isSent) guard let bert = desc.1 as? BertTuple else { diff --git a/Nynja/Services/MQTT/MQTTServiceProfile.swift b/Nynja/Services/MQTT/MQTTServiceProfile.swift index 0ba490286..48f53411f 100644 --- a/Nynja/Services/MQTT/MQTTServiceProfile.swift +++ b/Nynja/Services/MQTT/MQTTServiceProfile.swift @@ -12,7 +12,12 @@ extension MQTTService { func updateAccount(id: Int64, name: String, surname: String, phone: String) { if let _ = StorageService.sharedInstance.token { - let model = UpdateRosterModel(id: id, name: name, surname: surname, avatar: nil, email: nil,nick: nil) + var model:UpdateRosterModel! + if surname != "" { + model = UpdateRosterModel(id: id, name: name, surname: surname, avatar: nil, email: nil,nick: nil) + } else { + model = UpdateRosterModel(id: id, name: name, surname: nil, avatar: nil, email: nil,nick: nil) + } publish(model: model) } } diff --git a/Nynja/Services/Member/MemberDAO.swift b/Nynja/Services/Member/MemberDAO.swift index 9af0b48c9..9cf7ed62a 100644 --- a/Nynja/Services/Member/MemberDAO.swift +++ b/Nynja/Services/Member/MemberDAO.swift @@ -60,4 +60,13 @@ class MemberDAO: MemberDAOProtocol { try? dbManager.perform(action: .updateColumns(columns), with: member) } + static func updateReader(_ reader: Int64, roomId: String, phoneId: String) { + guard let member = MemberDAO.findMemberBy(roomId: roomId, phoneId: phoneId) else { + return + } + + member.reader = reader + MemberDAO.updateColumns([.reader], member: member) + } + } diff --git a/Nynja/Services/Member/MemberDAOProtocol.swift b/Nynja/Services/Member/MemberDAOProtocol.swift index 379bd5a8b..a8d461120 100644 --- a/Nynja/Services/Member/MemberDAOProtocol.swift +++ b/Nynja/Services/Member/MemberDAOProtocol.swift @@ -19,5 +19,6 @@ protocol MemberDAOProtocol: DAOProtocol { // MARK: - Update static func updateColumns(_ columns: Set, member: Member) + static func updateReader(_ reader: Int64, roomId: String, phoneId: String) } diff --git a/Nynja/Services/MessageSendingService/MessageSendingService.swift b/Nynja/Services/MessageSendingService/MessageSendingService.swift index 3856ab129..22341a3ec 100644 --- a/Nynja/Services/MessageSendingService/MessageSendingService.swift +++ b/Nynja/Services/MessageSendingService/MessageSendingService.swift @@ -9,14 +9,16 @@ import Foundation protocol MessageSendingServiceProtocol: class { - func isCanSendMessageTo(contact: Contact?) -> Bool - func sendMessage(_ message: Message) func sendReplayedMessage(_ repliedMessage: Message, message: Message) func sendTyping(_ type: TypingModelType, contact: Contact?, room: Room?) + func canSendMessage(to contact: Contact) -> Bool } final class MessageSendingService: MessageSendingServiceProtocol, InitializeInjectable { + + // MARK: - Dependencies + private let mqttService: MQTTService private let storageService: StorageService private let processingManager: MessageProcessingManagerInterface @@ -27,6 +29,9 @@ final class MessageSendingService: MessageSendingServiceProtocol, InitializeInje let processingManager: MessageProcessingManagerInterface } + + // MARK: - Init + init(dependencies: Dependencies) { mqttService = dependencies.mqttService storageService = dependencies.storageService @@ -35,22 +40,14 @@ final class MessageSendingService: MessageSendingServiceProtocol, InitializeInje // MARK: - MessageSendingServiceProtocol - - func isCanSendMessageTo(contact: Contact?) -> Bool { - guard let contact = contact else { - return true - } - return !contact.isBan - } func sendMessage(_ message: Message) { try? storageService.perform(action: .save, with: message) - processingManager.uploadMessage(message) } func sendReplayedMessage(_ repliedMessage: Message, message: Message) { - message.link = repliedMessage.id + message.linkedId = repliedMessage.id message.types = ["reply"] sendMessage(message) @@ -71,4 +68,8 @@ final class MessageSendingService: MessageSendingServiceProtocol, InitializeInje mqttService.sendTyping(typing: typing) } + + func canSendMessage(to contact: Contact) -> Bool { + return !contact.isBan + } } diff --git a/Nynja/Services/NynjaCommunicatorService.swift b/Nynja/Services/NynjaCalls/NynjaCommunicatorService.swift similarity index 91% rename from Nynja/Services/NynjaCommunicatorService.swift rename to Nynja/Services/NynjaCalls/NynjaCommunicatorService.swift index 5b9207ec0..e9d86ad74 100644 --- a/Nynja/Services/NynjaCommunicatorService.swift +++ b/Nynja/Services/NynjaCalls/NynjaCommunicatorService.swift @@ -14,6 +14,7 @@ protocol NynjaCommunicatorServiceDelegate: class { func dialing(call: NYNCall) func creatingGroupCall(name: String, call: NYNCall) func incomingCallRinging(call: NYNCall) + func incomingCallAccepted(call: NYNCall) func callEnded(call: NYNCall, isError: Bool) func didAllocateConference(requestId: String, call: NYNCall?, error: Error?) func didEndConference(requestId: String, error: Error?) @@ -23,6 +24,7 @@ extension NynjaCommunicatorServiceDelegate { func dialing(call: NYNCall) {} func creatingGroupCall(name: String, call: NYNCall){} func incomingCallRinging(call: NYNCall){} + func incomingCallAccepted(call: NYNCall){} func callEnded(call: NYNCall, isError: Bool){} func didAllocateConference(requestId: String, call: NYNCall?, error: Error?){} func didEndConference(requestId: String, error: Error?){} @@ -67,6 +69,8 @@ class NynjaCommunicatorService: NSObject, NynjaCommunicatorDelegate, NYNCallDele weak var messageInteractorCallProtocol: MessageInteractorCallProtocol? + private let nynjaRingingService = NynjaRingingService() + override init() { var logDir:String? let searchPaths = NSSearchPathForDirectoriesInDomains(.applicationSupportDirectory, .userDomainMask, true) @@ -104,7 +108,7 @@ class NynjaCommunicatorService: NSObject, NynjaCommunicatorDelegate, NYNCallDele let server = Bundle.main.confServerAddress let port = Bundle.main.confServerPort - let secure = false + let secure = Bundle.main.confServerSecure self.nynComm.setServerAddress(server, andPort: Int32(port), secure: secure) self.nynComm.setDeviceId(self.myDeviceId) @@ -141,7 +145,7 @@ class NynjaCommunicatorService: NSObject, NynjaCommunicatorDelegate, NYNCallDele isCallInProgress = true let roomName = (room != nil && room?.name != nil) ? room?.name! : "unnamed" - let members = makeMembers(contacts: contacts) + let members = makeMembers(contacts: contacts, room: room) let roomId = (room != nil && room?.id != nil) ? room?.id! : "" let creator = CallCreatorMediator(createId: UUID().uuidString, members: members, @@ -169,7 +173,7 @@ class NynjaCommunicatorService: NSObject, NynjaCommunicatorDelegate, NYNCallDele isCallInProgress = true let roomName = room?.name ?? "unnamed" - let members = makeMembers(contacts: contacts) + let members = makeMembers(contacts: contacts, room: room) let roomId = room?.id ?? "" let creator = CallCreatorMediator(createId: UUID().uuidString, members: members, @@ -189,6 +193,10 @@ class NynjaCommunicatorService: NSObject, NynjaCommunicatorDelegate, NYNCallDele withSubject: subject) } + func removeConference(byId conferenceId: String) { + self.nynComm.getCallManager().removeConference(byId: conferenceId) + } + func endConference(requestId:String, conferenceId: String) { self.nynComm.getCallManager().endConference(withRequestId: requestId, @@ -221,27 +229,16 @@ class NynjaCommunicatorService: NSObject, NynjaCommunicatorDelegate, NYNCallDele } func acceptConference(call: NYNCall) { - if (self.call == call) { - if (self.call?.isConference())! { - let joinId = UUID().uuidString - let rmId = UUID().uuidString - SoundService.sharedInstance.stopIncomingCallPlayer() - - self.nynComm.getCallManager().requestMembers(withRequestId: rmId, withConferenceId: call.callId) - self.nynComm.getCallManager().joinConference(withRequestId: joinId, - withConferenceId: call.callId, - withMemberId: call.memberId, - withDeviceId: self.myDeviceId) - } else { - self.call?.accept() - } + if self.call == call { + nynjaRingingService.stopIncomingCallPlayer() + self.nynComm.getCallManager().answer(call) } } func rejectConference(call: NYNCall) { if (self.call == call) { - SoundService.sharedInstance.stopIncomingCallPlayer() + nynjaRingingService.stopIncomingCallPlayer() if (self.call?.isConference())! { let joinId = UUID().uuidString @@ -312,11 +309,16 @@ class NynjaCommunicatorService: NSObject, NynjaCommunicatorDelegate, NYNCallDele withConferenceId: call.callId, withMemberId: call.memberId, withDeviceId: self.myDeviceId) - SoundService.sharedInstance.voipCallStarted() + SystemSoundManager.sharedInstance.voipCallStarted() } } } + func currentMembersCountForCallWithRoom(_ roomId: String) -> UInt { + guard let call = self.nynComm.getCallManager().getCallForRunningCall(withRoom: roomId) else { return 0 } + return call.membersCount + } + func didChangeConferenceState(_ state: NYNCallState) { if let c = self.call { self.callDelegate?.stateDidChange(call: c, state: state) @@ -341,7 +343,7 @@ class NynjaCommunicatorService: NSObject, NynjaCommunicatorDelegate, NYNCallDele self.delegates.invokeDelegates { delegate in delegate.dialing(call: nc) } - SoundService.sharedInstance.voipCallStarted() + SystemSoundManager.sharedInstance.voipCallStarted() } }; } @@ -383,13 +385,12 @@ class NynjaCommunicatorService: NSObject, NynjaCommunicatorDelegate, NYNCallDele return roster.myContact } - func makeMembers(contacts: [Contact]) -> [Member] { - var members = [Member]() - for i in contacts { - members.append(Member(contact: i)) - } - - if let mySelf = getMySelf() { + func makeMembers(contacts: [Contact], room: Room?) -> [Member] { + var members = contacts.map() { Member(contact: $0) } + + if let selfMember = room?.selfMember { + members.append(selfMember) + } else if let mySelf = getMySelf() { let myMember = Member(contact: mySelf) members.append(myMember) } @@ -475,8 +476,8 @@ class NynjaCommunicatorService: NSObject, NynjaCommunicatorDelegate, NYNCallDele self.call?.setDelegate(nil) self.call = nil - SoundService.sharedInstance.voipCallStopped() - SoundService.sharedInstance.stopRingbackPlayer() + SystemSoundManager.sharedInstance.voipCallStopped() + nynjaRingingService.stopRingbackPlayer() } } @@ -487,14 +488,14 @@ class NynjaCommunicatorService: NSObject, NynjaCommunicatorDelegate, NYNCallDele self.callDelegate?.stateDidChange(call: c, state: state) if c.callState == .ringing { - SoundService.sharedInstance.playRingbackSound() + nynjaRingingService.playRingbackSound() } else if c.callState == .connected { - SoundService.sharedInstance.stopRingbackPlayer() - if c.recvVideo { - AudioManager.sharedInstance.speaker = .loud - } + nynjaRingingService.stopRingbackPlayer() + + let speaker: AudioSessionManager.Speaker = c.recvVideo ? .loud : .soft + AudioSessionManager.shared.speaker = speaker } else { - SoundService.sharedInstance.stopRingbackPlayer() + nynjaRingingService.stopRingbackPlayer() } } } @@ -558,7 +559,7 @@ class NynjaCommunicatorService: NSObject, NynjaCommunicatorDelegate, NYNCallDele delegate.creatingGroupCall(name: cr.name, call: nc) } - SoundService.sharedInstance.voipCallStarted() + SystemSoundManager.sharedInstance.voipCallStarted() for m in cr.members { let request = UUID().uuidString @@ -570,7 +571,7 @@ class NynjaCommunicatorService: NSObject, NynjaCommunicatorDelegate, NYNCallDele isMe = true } - let name = "\(m.names ?? "") \(m.surnames ?? "")" + let name = m.alias ?? m.fullName ?? "" self.nynComm.getCallManager().addConferenceMember(withRequestId: request, withConferenceId: conferenceId, withAccountId: m.phone_id!, @@ -675,8 +676,8 @@ class NynjaCommunicatorService: NSObject, NynjaCommunicatorDelegate, NYNCallDele self.call?.setDelegate(nil) self.call = nil isCallInProgress = false - SoundService.sharedInstance.voipCallStopped() - SoundService.sharedInstance.stopRingbackPlayer() + SystemSoundManager.sharedInstance.voipCallStopped() + nynjaRingingService.stopRingbackPlayer() } } } @@ -704,8 +705,8 @@ class NynjaCommunicatorService: NSObject, NynjaCommunicatorDelegate, NYNCallDele self.call?.setDelegate(nil) self.call = nil isCallInProgress = false - SoundService.sharedInstance.voipCallStopped() - SoundService.sharedInstance.stopRingbackPlayer() + SystemSoundManager.sharedInstance.voipCallStopped() + nynjaRingingService.stopRingbackPlayer() } } } @@ -753,8 +754,8 @@ class NynjaCommunicatorService: NSObject, NynjaCommunicatorDelegate, NYNCallDele delegate.incomingCallRinging(call: call) } - SoundService.sharedInstance.voipCallStarted() - SoundService.sharedInstance.playCallSound() + SystemSoundManager.sharedInstance.voipCallStarted() + nynjaRingingService.playCallSound() } } @@ -766,9 +767,9 @@ class NynjaCommunicatorService: NSObject, NynjaCommunicatorDelegate, NYNCallDele self.call?.setDelegate(nil) self.call = nil isCallInProgress = false - SoundService.sharedInstance.voipCallStopped() - SoundService.sharedInstance.stopIncomingCallPlayer() - SoundService.sharedInstance.stopRingbackPlayer() + SystemSoundManager.sharedInstance.voipCallStopped() + nynjaRingingService.stopIncomingCallPlayer() + nynjaRingingService.stopRingbackPlayer() } } @@ -777,10 +778,10 @@ class NynjaCommunicatorService: NSObject, NynjaCommunicatorDelegate, NYNCallDele self.call = call self.call?.setDelegate(self) delegates.invokeDelegates { delegate in - delegate.incomingCallRinging(call: call) + delegate.incomingCallAccepted(call: call) } - SoundService.sharedInstance.voipCallStarted() + SystemSoundManager.sharedInstance.voipCallStarted() } } @@ -792,8 +793,8 @@ class NynjaCommunicatorService: NSObject, NynjaCommunicatorDelegate, NYNCallDele self.call?.setDelegate(nil) self.call = nil isCallInProgress = false - SoundService.sharedInstance.voipCallStopped() - SoundService.sharedInstance.stopIncomingCallPlayer() + SystemSoundManager.sharedInstance.voipCallStopped() + nynjaRingingService.stopIncomingCallPlayer() } } diff --git a/Nynja/Services/NynjaCalls/NynjaRingingService.swift b/Nynja/Services/NynjaCalls/NynjaRingingService.swift new file mode 100644 index 000000000..c21d000b4 --- /dev/null +++ b/Nynja/Services/NynjaCalls/NynjaRingingService.swift @@ -0,0 +1,59 @@ +// +// RingingService.swift +// Nynja +// +// Created by Volodymyr Hryhoriev on 9/4/18. +// Copyright © 2018 TecSynt Solutions. All rights reserved. +// + +final class NynjaRingingService { + + private let audioManager = AudioManager.sharedInstance + private let systemSoundManager = SystemSoundManager.sharedInstance + + private let soundBundle = SoundBundle.shared + + // MARK: - Calls + + func playCallSound() { + guard let soundUrl = soundBundle.defaultCall.url else { + return + } + + do { + try audioManager.play(with: soundUrl, options: [.isInfinite, .vibrateAtStart]) + } catch let error { + LogService.log(topic: .audioSystem) { return error.localizedDescription } + } + } + + func stopIncomingCallPlayer() { + guard let soundUrl = soundBundle.defaultCall.url else { + return + } + audioManager.stop(with: soundUrl, shouldDeactivate: false) + } + + func playRingbackSound() { + guard let soundUrl = soundBundle.defaultRingback.url else { + LogService.log(topic: .audioSystem) { return "ringback sound not found" } + return + } + + do { + try audioManager.play(with: soundUrl, options: [.isInfinite, .usingSoftSpeaker]) + LogService.log(topic: .audioSystem) { return "play ringback" } + } catch let error { + LogService.log(topic: .audioSystem) { return "Error init ringback player \(error.localizedDescription)" } + } + } + + func stopRingbackPlayer() { + LogService.log(topic: .audioSystem) { return "stop ringback" } + guard let soundUrl = soundBundle.defaultRingback.url else { + return + } + audioManager.stop(with: soundUrl, shouldDeactivate: false) + } + +} diff --git a/Nynja/Services/PushService.swift b/Nynja/Services/PushService.swift index ff6185ada..02f235655 100644 --- a/Nynja/Services/PushService.swift +++ b/Nynja/Services/PushService.swift @@ -64,12 +64,186 @@ final class PushService: NSObject, PKPushRegistryDelegate, UserSettingsRespondab handleNynjaNotifications(with: aps, data: payload.dictionaryPayload) } + private func getSyncType(_ aps: Aps) -> SyncPushType { + let array = aps.nynja.title.split(separator: ":").map() { String($0) } + guard array.first == "SyncPush" else { return SyncPushType.undefined } + let type = array.last ?? "" + return SyncPushType(rawValue: type) ?? .undefined + } + + enum SyncPushType: String { + case historyDelete = "history_delete" + case updateHistory = "history_update" + case messageDelete = "message_delete" + case messageEdit = "message_edit" + case undefined = "" + } // MARK: - Handle Notifications private func handleNynjaNotifications(with aps: Aps, data: [AnyHashable: Any]) { + let syncType = getSyncType(aps) + switch syncType { + case .historyDelete: + guard let message = getMessage(aps) else { return } + ChatService.clearHistory(message) + removePushesFromChat(message) + case .updateHistory: + if let room = aps.room { + guard let reader = room.selfReader, + let phoneId = room.selfMember?.phone_id, + let roomId = room.id else { + return + } + + MemberDAO.updateReader(reader, roomId: roomId, phoneId: phoneId) + RoomDAO.updateColumns([.unread], room: room) + readPushes(feed: room, reader: reader) + } + if let contact = aps.contact { + guard let reader = contact.selfReader else { + return + } + ContactDAO.updateColumns([.unread, .reader], contact: contact) + readPushes(feed: contact, reader: reader) + } + case .messageDelete: + guard let message = getMessage(aps) else { return } + try? saveIntoDatabase(message: message) + if MessageDAO.shouldMarkMessageAsDeleted(message) { + ChatService.removeMessage(message) + removePush(message) + } + case .messageEdit: + guard let message = getMessage(aps) else { return } + try? saveIntoDatabase(message: message) + + guard let link = message.linkedId else { + return + } + + if let oldMessage = MessageDAO.fetchMessage(serverId: link) { + oldMessage.edit(by: message) + + do { + try saveIntoDatabase(message: oldMessage) + ChatService.updateLastMessage(oldMessage) { $0 == link } + } catch { + LogService.log(topic: .db) { return "EditMessage update error: \(error.localizedDescription)" } + } + + let messageEditService: MessageEditServiceProtocol = { + let dependencies = MessageEditService.Dependencies(storageService: StorageService.sharedInstance) + let messageEditService = MessageEditService(dependencies: dependencies) + return messageEditService + }() + + try? messageEditService.deleteEditActionForMessage(oldMessage) + } + updatePush(message, text: aps.text) + case .undefined: handleNynjaNotification(with: aps, data: data) + } + } + + private func removePushesFromChat(_ message: Message) { + let predicate = basePredicate(message: message) + getNotificationIds(predicate: predicate) { ids in + UNUserNotificationCenter.current().removeDeliveredNotifications(withIdentifiers: ids) + } + } + + private func basePredicate(message: Message) -> String { + var predicate: String = "" + if let id = message.p2pFeed?.opponentId { + predicate = "Message_p2p;\(id);" + } + if let id = message.mucFeed?.name { + predicate = "Message_muc;\(id);" + } + return predicate + } + + private func readPushes(feed: BaseChatModel, reader: Int64) { + var predicate: String = "" + if let _p2p = feed as? p2p { + predicate = "Message_p2p;\(_p2p.opponentId);\(reader)" + } + if let _muc = feed as? muc { + predicate = "Message_muc;\(_muc.name);\(reader)" + } + getNotificationIds(predicate: predicate) { ids in + UNUserNotificationCenter.current().removeDeliveredNotifications(withIdentifiers: ids) + } + } + + private func removePush(_ message: Message) { + guard let link = message.link as? Int64 else { return } + let predicate = basePredicate(message: message) + "\(link)" + getNotificationIds(predicate: predicate) { ids in + UNUserNotificationCenter.current().removeDeliveredNotifications(withIdentifiers: ids) + } + } + + private func updatePush(_ message: Message, text: String) { + guard let link = message.link as? Int64 else { return } + let predicate = basePredicate(message: message) + "\(link)" + UNUserNotificationCenter.current().getDeliveredNotifications { (notifications) in + let find = notifications.filter({ (notif) -> Bool in + notif.request.identifier.range(of: predicate) != nil + }) + let request = find.first?.request + let data = request?.content.body.split(separator: ":") + let sender = String(data?.first ?? "") + let newBody = text + + let content = UNMutableNotificationContent() + content.body = newBody + if let userInfo = request?.content.userInfo { + content.userInfo = userInfo + } + if let sound = request?.content.sound { + content.sound = sound + } + let trigger = UNTimeIntervalNotificationTrigger(timeInterval: 1, + repeats: false) + var identifier = "" + if let id = request?.identifier { + identifier = id + } + let new_request = UNNotificationRequest(identifier: identifier, + content: content, trigger: trigger) + let center = UNUserNotificationCenter.current() + center.removeDeliveredNotifications(withIdentifiers: [identifier]) + center.add(new_request, withCompletionHandler: nil) + } + } + + private func getNotificationIds(predicate: String, result: (([String]) -> Void)?) { + UNUserNotificationCenter.current().getDeliveredNotifications { (notifications) in + let ids = notifications.map() { $0.request.identifier } + let find = ids.filter({ (str) -> Bool in + str.range(of: predicate) != nil + }) + result?(find) + } + } + + private func saveIntoDatabase(message: Message) throws { + if let repliedMessage = message.repliedMessage { + repliedMessage.localStatus = try MessageDAO.localStatusForRepliedMessage(repliedMessage) + } + try MessageDAO.trustIfNextMessageExists(before: message) + try StorageService.sharedInstance.perform(action: .save, with: message) + } + + private func getMessage(_ aps: Aps) -> Message? { + if let msg = aps.contact?.last_msg { return msg } + if let msg = aps.room?.last_msg { return msg } + return nil + } + + private func handleNynjaNotification(with aps: Aps, data: [AnyHashable: Any]) { var alreadyReceived = false - if let contact = aps.contact { alreadyReceived = self.alreadyReceived(type: aps.nynja.type, msg: contact.lastMessage) save(contact) @@ -95,7 +269,7 @@ final class PushService: NSObject, PKPushRegistryDelegate, UserSettingsRespondab private func save(_ contact: Contact) { do { if let lastMessage = contact.last_msg { - try MessageDAO.trustIfNextMessageExists(before: lastMessage) + try prepareForSave(message: lastMessage) } try StorageService.sharedInstance.perform(action: .save, with: contact) } catch { @@ -106,7 +280,7 @@ final class PushService: NSObject, PKPushRegistryDelegate, UserSettingsRespondab private func save(_ room: Room) { do { if let lastMessage = room.last_msg { - try MessageDAO.trustIfNextMessageExists(before: lastMessage) + try prepareForSave(message: lastMessage) } try StorageService.sharedInstance.perform(action: .save, with: room) } catch { @@ -114,6 +288,16 @@ final class PushService: NSObject, PKPushRegistryDelegate, UserSettingsRespondab } } + private func prepareForSave(message: Message) throws { + try MessageDAO.trustIfNextMessageExists(before: message) + if let repliedMessage = message.repliedMessage { + repliedMessage.localStatus = try MessageDAO.localStatusForRepliedMessage(repliedMessage) + } + if message.messageStatus == .clear { + ChatService.clearMessages(before: message) + } + } + private func sendNewNotification(title: String, sound: String?, id: String, data: [AnyHashable: Any]) { let content = UNMutableNotificationContent() content.body = title diff --git a/Nynja/Services/REST/TranscribeNetworkService/Response/TranscribeLongOperationResponseData.swift b/Nynja/Services/REST/TranscribeNetworkService/Response/TranscribeLongOperationResponseData.swift index 83f4ae513..2ed74635d 100644 --- a/Nynja/Services/REST/TranscribeNetworkService/Response/TranscribeLongOperationResponseData.swift +++ b/Nynja/Services/REST/TranscribeNetworkService/Response/TranscribeLongOperationResponseData.swift @@ -13,15 +13,11 @@ struct TranscribeLongOperationResponseData: Codable { var name: String? var metadata: Metadata? var done: Bool? - var response: Response? + var response: TranscribeResponseData? struct Metadata: Codable { var progressPercent: Int? var startTime: String? var lastUpdateTime: String? } - - struct Response: Codable { - var results: [TranscribeShortResponseData.TranscribeResult]? - } } diff --git a/Nynja/Services/REST/TranscribeNetworkService/Response/TranscribeResponseData.swift b/Nynja/Services/REST/TranscribeNetworkService/Response/TranscribeResponseData.swift new file mode 100644 index 000000000..85d13a0b7 --- /dev/null +++ b/Nynja/Services/REST/TranscribeNetworkService/Response/TranscribeResponseData.swift @@ -0,0 +1,41 @@ +// +// TranscribeResponseData.swift +// Nynja +// +// Created by Andrey Reznik on 04.07.2018. +// Copyright © 2018 TecSynt Solutions. All rights reserved. +// + +import Foundation + +struct TranscribeResponseData: Codable { + var results: [TranscribeResult]? + + struct TranscribeResult: Codable { + var alternatives: [TranscribeAlternative]? + + struct TranscribeAlternative: Codable { + var transcript: String? + var confidence: Double? + } + } +} + +extension TranscribeResponseData { + + var fullTranscription: String? { + guard let results = results, !results.isEmpty else { + return nil + } + return results.compactMap { result -> String? in + return result.alternatives?.max { + guard let lhs = $0.confidence, + let rhs = $1.confidence else { + return false + } + return lhs < rhs + }?.transcript + }.joined(separator: Constants.spaceSeparator) + } +} + diff --git a/Nynja/Services/REST/TranscribeNetworkService/Response/TranscribeShortResponseData.swift b/Nynja/Services/REST/TranscribeNetworkService/Response/TranscribeShortResponseData.swift deleted file mode 100644 index 14263e594..000000000 --- a/Nynja/Services/REST/TranscribeNetworkService/Response/TranscribeShortResponseData.swift +++ /dev/null @@ -1,24 +0,0 @@ -// -// TranscribeResponseData.swift -// Nynja -// -// Created by Andrey Reznik on 04.07.2018. -// Copyright © 2018 TecSynt Solutions. All rights reserved. -// - -import Foundation - -struct TranscribeShortResponseData: Codable { - var results: [TranscribeResult]? - - struct TranscribeResult: Codable { - var alternatives: [TranscribeAlternative]? - - struct TranscribeAlternative: Codable { - var transcript: String? - var confidence: Double? - } - } -} - - diff --git a/Nynja/Services/REST/TranscribeNetworkService/TranscribeNetworkService.swift b/Nynja/Services/REST/TranscribeNetworkService/TranscribeNetworkService.swift index 71f7c672b..dcb640966 100644 --- a/Nynja/Services/REST/TranscribeNetworkService/TranscribeNetworkService.swift +++ b/Nynja/Services/REST/TranscribeNetworkService/TranscribeNetworkService.swift @@ -11,7 +11,7 @@ import Foundation protocol TranscribeNetworkServiceProtocol: NetworkService { func transcribeShortAudio(input: TranscribeShortRequestData, - completion: ((HTTPResponseResult) -> Void)?) -> URLSessionTask + completion: ((HTTPResponseResult) -> Void)?) -> URLSessionTask func transcribeLongAudio(input: TranscribeLongRequestData, completion: ((HTTPResponseResult) -> Void)?) -> URLSessionTask @@ -29,7 +29,7 @@ final class TranscribeNetworkService: TranscribeNetworkServiceProtocol { } @discardableResult - func transcribeShortAudio(input: TranscribeShortRequestData, completion: ((HTTPResponseResult) -> Void)?) -> URLSessionTask { + func transcribeShortAudio(input: TranscribeShortRequestData, completion: ((HTTPResponseResult) -> Void)?) -> URLSessionTask { guard let params: HTTPParameters = input.dictionary else { assertionFailure("Something went wrong") @@ -40,7 +40,7 @@ final class TranscribeNetworkService: TranscribeNetworkServiceProtocol { let target = TranscribeNetworkRouter.speechRecognize(params: params, queryItems: queryItems) - return client.request(to: target) { (result: HTTPResponseResult) in + return client.request(to: target) { (result: HTTPResponseResult) in completion?(result) } } diff --git a/Nynja/Services/ReachabilityService.swift b/Nynja/Services/ReachabilityService.swift index 637ce84f1..954555c5b 100644 --- a/Nynja/Services/ReachabilityService.swift +++ b/Nynja/Services/ReachabilityService.swift @@ -8,19 +8,8 @@ import Foundation -protocol ReachabilityServiceObserver: class { - func reachabilityStatusChanged(isReachable: Bool) - func reachabilityStatusChanged(from: Network.Status?, to: Network.Status?) -} - -extension ReachabilityServiceObserver { - func reachabilityStatusChanged(from: Network.Status?, to: Network.Status?) {} -} - class ReachabilityService: NSObject { - private var observers = [WeakRef]() - var isReachable: Bool { return Network.reachability?.isReachable ?? false } @@ -48,40 +37,24 @@ class ReachabilityService: NSObject { } NotificationCenter.default.addObserver(self, selector: #selector(statusManager), name: .flagsChanged, object: Network.reachability) } - - - // MARK: - Add, remove observer - - func addRechabilityObserver(_ observer: ReachabilityServiceObserver) { - let ref = WeakRef(value: observer as AnyObject) - objc_sync_enter(observers) - observers.append(ref) - objc_sync_exit(observers) - } - - func removeRechabilityObserver(_ observer: ReachabilityServiceObserver) { - if let index = observers.index(where: { $0.value === observer }) { - objc_sync_enter(observers) - observers.remove(at: index) - objc_sync_exit(observers) - } - } - // MARK: - Status changed - var status: Network.Status? = Network.reachability?.status { + private var status: Network.Status? = Network.reachability?.status { didSet { - notify { $0.reachabilityStatusChanged(from: oldValue, to: status) } + if (oldValue == .wifi && status == .wwan) || (status == .wifi && oldValue == .wwan) { + let state: ConnectionService.ConnectionServiceState = .switched + ConnectionService.shared.updateCallServiceState(.networking, state: state) + } } } - @objc func statusManager(_ notification: NSNotification) { + @objc private func statusManager(_ notification: NSNotification) { status = Network.reachability?.status guard let reachability = Network.reachability else { return } self.isWWAN = reachability.isWWAN - - notify { $0.reachabilityStatusChanged(isReachable: reachability.isReachable) } + let state: ConnectionService.ConnectionServiceState = reachability.isReachable ? .connected : .disconnected + ConnectionService.shared.updateCallServiceState(.networking, state: state) } @@ -96,22 +69,5 @@ class ReachabilityService: NSObject { } } #endif - - - // MARK: - Notify - - private func notify(_ closure: (ReachabilityServiceObserver) -> Void) { - objc_sync_enter(observers) - observers = observers.filter({ (ref) -> Bool in - return ref.value != nil - }) - - for ref in observers { - if let value = ref.value as? ReachabilityServiceObserver { - closure(value) - } - } - objc_sync_exit(observers) - } - + } diff --git a/Nynja/Services/ServiceFactory/ServiceFactory.swift b/Nynja/Services/ServiceFactory/ServiceFactory.swift index 0e604f265..a790935d3 100644 --- a/Nynja/Services/ServiceFactory/ServiceFactory.swift +++ b/Nynja/Services/ServiceFactory/ServiceFactory.swift @@ -8,7 +8,7 @@ import Foundation -protocol ServiceFactoryProtocol { +protocol ServiceFactoryProtocol: class { func makeMessageSendingService() -> MessageSendingServiceProtocol func makeResourceManager() -> ResourceManagerProtocol func makeMessageFactory() -> MessageFactoryProtocol @@ -21,6 +21,8 @@ protocol ServiceFactoryProtocol { func makeMQTTService() -> MQTTService func makeStorageService() -> StorageService func makeMesageProcessingManager() -> MessageProcessingManagerInterface + + func makeHistoryRequestFactory() -> HistoryRequestModelFactoryProtocol func makeContactsProvider() -> ContactsProviding func makeConversationsProvider() -> ConversationsProviding @@ -36,7 +38,7 @@ protocol ServiceFactoryProtocol { func makeMuteChatService() -> MuteChatServiceProtocol - func makeReachabilityService() -> ReachabilityService + func makeConnectionService() -> ConnectionService func makeAlertManager() -> AlertManager @@ -44,6 +46,8 @@ protocol ServiceFactoryProtocol { func makeChatScreenAlertFactory() -> ChatScreenAlertFactoryProtocol func makeUseCaseValidationServise() -> UseCaseValidationServiceProtocol + + func makeAudioSessionManager() -> AudioSessionManager } final class ServiceFactory: ServiceFactoryProtocol { @@ -93,6 +97,10 @@ final class ServiceFactory: ServiceFactoryProtocol { func makeMesageProcessingManager() -> MessageProcessingManagerInterface { return DefaultMessagesProcessingManager.shared } + + func makeHistoryRequestFactory() -> HistoryRequestModelFactoryProtocol { + return HistoryRequestModelFactory() + } func makeContactsProvider() -> ContactsProviding { return ContactsProvider() @@ -134,8 +142,8 @@ final class ServiceFactory: ServiceFactoryProtocol { return MuteChatService(dependencies: dependencies) } - func makeReachabilityService() -> ReachabilityService { - return ReachabilityService.sharedInstance + func makeConnectionService() -> ConnectionService { + return ConnectionService.shared } func makeAlertManager() -> AlertManager { @@ -153,4 +161,8 @@ final class ServiceFactory: ServiceFactoryProtocol { func makeUseCaseValidationServise() -> UseCaseValidationServiceProtocol { return UseCaseValidationService() } + + func makeAudioSessionManager() -> AudioSessionManager { + return AudioSessionManager.shared + } } diff --git a/Nynja/Services/TranscribeService/Helpers/Array+Operation.swift b/Nynja/Services/TranscribeService/Helpers/Array+Operation.swift new file mode 100644 index 000000000..5e3ad9533 --- /dev/null +++ b/Nynja/Services/TranscribeService/Helpers/Array+Operation.swift @@ -0,0 +1,25 @@ +// +// Array+Operation.swift +// Nynja +// +// Created by Andrey Reznik on 26.09.2018. +// Copyright © 2018 TecSynt Solutions. All rights reserved. +// + +import Foundation + +extension Array where Element: Operation { + enum Direction { + case forward + case backward + } + + func setupDependencies(_ direction: Direction) { + let transformIndex = direction == .forward ? { $0 + 1 } : { $0 - 1 } + for (index, operation) in self.enumerated() { + if let dependency = self[safe: transformIndex(index)] { + operation.addDependency(dependency) + } + } + } +} diff --git a/Nynja/Services/TranscribeService/Operations /AudioLongTranscribeOperation.swift b/Nynja/Services/TranscribeService/Operations /AudioLongTranscribeOperation.swift index 9e8a05ddd..143800a96 100644 --- a/Nynja/Services/TranscribeService/Operations /AudioLongTranscribeOperation.swift +++ b/Nynja/Services/TranscribeService/Operations /AudioLongTranscribeOperation.swift @@ -1,3 +1,4 @@ + // // AudioLongTranscribeOperation.swift // Nynja diff --git a/Nynja/Services/TranscribeService/Operations /AudioLongTranscribeProccessingOperation.swift b/Nynja/Services/TranscribeService/Operations /AudioLongTranscribeProccessingOperation.swift index d833a14d2..cd8b26224 100644 --- a/Nynja/Services/TranscribeService/Operations /AudioLongTranscribeProccessingOperation.swift +++ b/Nynja/Services/TranscribeService/Operations /AudioLongTranscribeProccessingOperation.swift @@ -56,7 +56,10 @@ final class AudioLongTranscribeProccessingOperation: TranscribeOperation, Initia } private func transcribeLongOperation(with name: String) { - let processingTask = networkService.loadTranscriptionProcessingResult(name: name){ result in + let processingTask = networkService.loadTranscriptionProcessingResult(name: name) { [weak self] result in + guard let `self` = self else { + return + } switch result { case .failure(let error): self.completion?(.failure(.networkClient(error))) @@ -68,7 +71,7 @@ final class AudioLongTranscribeProccessingOperation: TranscribeOperation, Initia self.operationGroup?.leave() self.state = .finished } - guard let transcription = response.response?.results?.first?.alternatives?.first?.transcript else { + guard let transcription = response.response?.fullTranscription else { self.dataWrapper.processingResult = nil self.completion?(.failure(.emptyResponse(self.language))) return diff --git a/Nynja/Services/TranscribeService/Operations /AudioShortTranscribeOperation.swift b/Nynja/Services/TranscribeService/Operations /AudioShortTranscribeOperation.swift index 45f918882..8a416a249 100644 --- a/Nynja/Services/TranscribeService/Operations /AudioShortTranscribeOperation.swift +++ b/Nynja/Services/TranscribeService/Operations /AudioShortTranscribeOperation.swift @@ -52,11 +52,12 @@ final class AudioShortTranscribeOperation: TranscribeOperation, InitializeInject case .failure(let error): self.completion?(.failure(.networkClient(error))) case .success(let response): - guard let transcription = response.results?.first?.alternatives?.first?.transcript else { + guard let transcription = response.fullTranscription else { self.completion?(.failure(.emptyResponse(self.language))) self.state = .finished return } + self.dataWrapper.processingResult = transcription self.completion?(.success(transcription)) } self.state = .finished @@ -64,6 +65,6 @@ final class AudioShortTranscribeOperation: TranscribeOperation, InitializeInject task = transcribeTask - completion?(.updateProccess(self, .transcribing)) + completion?(.updateProccess(self, .transcribingShort)) } } diff --git a/Nynja/Services/TranscribeService/TranscribeService.swift b/Nynja/Services/TranscribeService/TranscribeService.swift index 5e7fc53fa..61ecca93b 100644 --- a/Nynja/Services/TranscribeService/TranscribeService.swift +++ b/Nynja/Services/TranscribeService/TranscribeService.swift @@ -7,6 +7,7 @@ // import Foundation +import AVFoundation enum TranscribeServiceState { case updateProccess(Cancelable, DBConvertMessage.Process) @@ -61,7 +62,6 @@ struct TranscribeServiceInput { protocol TranscribeServiceProtocol { - var transcribeSubscribers: [AnyWeakSubscriber] { get set } func observeState(_ subscriber: AnyObject, handler: @escaping TranscribeProcessingOuterHandler) func removeObserver(_ object: AnyObject) @@ -106,13 +106,15 @@ final class TranscribeService: InitializeInjectable { return TranscribeService(dependencies: dependencies) }() + private var transcribeSubscribers: [AnyWeakSubscriber] = [] + private let transcribeNetworkService: TranscribeNetworkServiceProtocol private let processingManager: DefaultMessagesProcessingManager private let messageFactory: MessageFactoryProtocol private let storageService: StorageService - var transcribeSubscribers: [AnyWeakSubscriber] = [] + private let maxShortTranscribeDuration = 60 private let queue: OperationQueue = { let queue = OperationQueue() @@ -149,26 +151,28 @@ extension TranscribeService: TranscribeServiceProtocol { self.handle(with: processing.id, state: state) } + var operations: [Operation] = [] + let convertion = AudioConvertOperation( dependencies: .init(dataWrapper: dataWrapper, audioUrl: input.localUrl, completion: completion)) - let upload = AudioUploadOperation( - dependencies: .init(dataWrapper: dataWrapper, - completion: completion)) - upload.addDependency(convertion) - let transcription = AudioLongTranscribeOperation( - dependencies: .init(dataWrapper: dataWrapper, - networkService: transcribeNetworkService, - language: input.language, - completion: completion)) - transcription.addDependency(upload) - let proccessing = AudioLongTranscribeProccessingOperation( - dependencies: .init(dataWrapper: dataWrapper, - networkService: transcribeNetworkService, - language: input.language, - completion: completion)) - proccessing.addDependency(transcription) + + operations.append(convertion) + + let audioDuration = Int64(AVURLAsset.duration(from: input.localUrl)) + let chain: [TranscribeOperation] + if audioDuration < maxShortTranscribeDuration { + chain = makeShortTranscribeOperations(with: input, + dataWrapper: dataWrapper, + completion: completion) + } else { + chain = makeLongTranscribeOperations(with: input, + dataWrapper: dataWrapper, + completion: completion) + } + + operations.append(contentsOf: chain) let send = AudioTranscribeSendOperation( dependencies: .init(dataWrapper: dataWrapper, @@ -177,9 +181,9 @@ extension TranscribeService: TranscribeServiceProtocol { message: input.message, language: input.language, completion: completion)) - send.addDependency(proccessing) + operations.append(send) - let operations = [convertion, upload, transcription, proccessing, send] + operations.setupDependencies(.backward) operations.forEach { $0.name = processing.id } queue.addOperations(operations, waitUntilFinished: false) } @@ -191,6 +195,7 @@ extension TranscribeService: TranscribeServiceProtocol { var convert = convert let dataWrapper = TranscribeServiceDataWrapper() + dataWrapper.processingURL = URL(string: convert.value) dataWrapper.processingResult = convert.value let completion: TranscribeProcessingInnerHandler = { [weak self] state in @@ -201,67 +206,18 @@ extension TranscribeService: TranscribeServiceProtocol { self.handle(with: convert.messageId, state: state) } - guard let process = DBConvertMessage.Process(rawValue: convert.process) else { - return - } - var operations: [Operation] = [] - let setupDependency: ([Operation], Operation) -> () = { dependencies, operation in - guard let dependency = dependencies.last else { - return - } - operation.addDependency(dependency) - } - switch process { - case .convert: - guard let localUrl = URL(string: convert.value) else { - return - } - let convertion = AudioConvertOperation( - dependencies: .init(dataWrapper: dataWrapper, - audioUrl: localUrl, - completion: completion)) - setupDependency(operations, convertion) - operations.append(convertion) - fallthrough - case .upload: - let upload = AudioUploadOperation( - dependencies: .init(dataWrapper: dataWrapper, - completion: completion)) - setupDependency(operations, upload) - operations.append(upload) - fallthrough - case .transcribing: - let transcription = AudioLongTranscribeOperation( - dependencies: .init(dataWrapper: dataWrapper, - networkService: transcribeNetworkService, - language: convert.language, - completion: completion)) - setupDependency(operations, transcription) - operations.append(transcription) - fallthrough - case .transcribeProcessing: - let proccessing = AudioLongTranscribeProccessingOperation( - dependencies: .init(dataWrapper: dataWrapper, - networkService: transcribeNetworkService, - language: convert.language, - completion: completion)) - setupDependency(operations, proccessing) - operations.append(proccessing) - fallthrough - case .send: - let send = AudioTranscribeSendOperation( - dependencies: .init(dataWrapper: dataWrapper, - processingManager: processingManager, - messageFactory: messageFactory, - message: message, - language: convert.language, - completion: completion)) - setupDependency(operations, send) - operations.append(send) + let processChain = restoreProcessChain(from: convert) + let operations: [Operation] = processChain.compactMap { + return makeOperation(for: $0, + message: message, + convert: convert, + dataWrapper: dataWrapper, + completion: completion) } + operations.setupDependencies(.backward) operations.forEach { $0.name = convert.messageId } queue.addOperations(operations, waitUntilFinished: false) } @@ -307,7 +263,7 @@ private extension TranscribeService { switch state { case .updateProccess(_, let type): process.process = type.rawValue - process.value = data.processingResult ?? "" + process.value = (data.processingResult ?? data.processingURL?.absoluteString) ?? "" case .success: break case .failure(let error): @@ -331,4 +287,115 @@ private extension TranscribeService { private func handle(with id: String, state: TranscribeServiceState) { transcribeSubscribers.forEach { $0.handler(id, state) } } + + private func makeLongTranscribeOperations(with input: TranscribeServiceInput, dataWrapper: TranscribeServiceDataWrapper, completion: @escaping TranscribeProcessingInnerHandler) -> [TranscribeOperation] { + let upload = AudioUploadOperation( + dependencies: .init(dataWrapper: dataWrapper, + completion: completion)) + let transcription = AudioLongTranscribeOperation( + dependencies: .init(dataWrapper: dataWrapper, + networkService: transcribeNetworkService, + language: input.language, + completion: completion)) + let proccessing = AudioLongTranscribeProccessingOperation( + dependencies: .init(dataWrapper: dataWrapper, + networkService: transcribeNetworkService, + language: input.language, + completion: completion)) + + return [upload, transcription, proccessing] + } + + private func makeShortTranscribeOperations(with input: TranscribeServiceInput, dataWrapper: TranscribeServiceDataWrapper, completion: @escaping TranscribeProcessingInnerHandler) -> [TranscribeOperation] { + + let transcription = AudioShortTranscribeOperation( + dependencies: .init(dataWrapper: dataWrapper, + networkService: transcribeNetworkService, + language: input.language, + completion: completion)) + + return [transcription] + } + + private func restoreProcessChain(from convert: DBConvertMessage) -> [DBConvertMessage.Process] { + guard var process = DBConvertMessage.Process(rawValue: convert.process) else { + return [] + } + + var chain: [DBConvertMessage.Process] = [process] + + if case .convert = process { + guard let localUrl = URL(string: convert.value) else { + return [] + } + let audioDuration = Int64(AVURLAsset.duration(from: localUrl)) + if audioDuration < maxShortTranscribeDuration { + process = .transcribingShort + } else { + process = .upload + } + chain.append(process) + } + while let nextProcess = process.nextProcess { + chain.append(nextProcess) + process = nextProcess + } + + return chain + } + + private func makeOperation(for process: DBConvertMessage.Process, + message: Message, + convert: DBConvertMessage, + dataWrapper: TranscribeServiceDataWrapper, + completion: @escaping TranscribeProcessingInnerHandler) -> Operation? { + switch process { + case .convert: + guard let localUrl = URL(string: convert.value) else { + return nil + } + let convertion = AudioConvertOperation( + dependencies: .init(dataWrapper: dataWrapper, + audioUrl: localUrl, + completion: completion)) + + return convertion + case .upload: + let upload = AudioUploadOperation( + dependencies: .init(dataWrapper: dataWrapper, + completion: completion)) + return upload + case .transcribing: + let transcription = AudioLongTranscribeOperation( + dependencies: .init(dataWrapper: dataWrapper, + networkService: transcribeNetworkService, + language: convert.language, + completion: completion)) + return transcription + case .transcribeProcessing: + let proccessing = AudioLongTranscribeProccessingOperation( + dependencies: .init(dataWrapper: dataWrapper, + networkService: transcribeNetworkService, + language: convert.language, + completion: completion)) + return proccessing + case .transcribingShort: + let transcription = AudioShortTranscribeOperation( + dependencies: .init(dataWrapper: dataWrapper, + networkService: transcribeNetworkService, + language: convert.language, + completion: completion)) + return transcription + case .send: + let send = AudioTranscribeSendOperation( + dependencies: .init(dataWrapper: dataWrapper, + processingManager: processingManager, + messageFactory: messageFactory, + message: message, + language: convert.language, + completion: completion)) + return send + } + + } } diff --git a/Nynja/Services/WheelContainer/Manager/WCDataManager.swift b/Nynja/Services/WheelContainer/Manager/WCDataManager.swift index 1983432df..db2cf033b 100644 --- a/Nynja/Services/WheelContainer/Manager/WCDataManager.swift +++ b/Nynja/Services/WheelContainer/Manager/WCDataManager.swift @@ -196,51 +196,44 @@ class WCDataManager: WCDataManagerProtocol, UserSettingsRespondable { wheelContainer?.deselectItem(at: indexPath) } - func updateActionsState(_ isDisabled: Bool) { - let index: Int = 5 + func setActionsState(disable isDisabled: Bool) { + guard let indexOfAction = items.first?.index(where: { + return ($0 as? ImageWheelItemModel)?.navItem == .actions + }) else { + return + } + let state: WheelItemState = isDisabled ? .disabled : .selected let secondLevelItems: ItemModels = isDisabled ? [] : _factory.secondLevelItems - if var _dsItems = wheelContainerDS?.items, !_dsItems.isEmpty { - _dsItems.first?[index].state = state + + let transform: ([ItemModels]) -> [ItemModels] = { items in + guard !items.isEmpty else { + return items + } + var transformedItems = items + transformedItems.first?[indexOfAction].state = state - if secondLevelItems.isEmpty { - if _dsItems.count > 1 { - _dsItems = [_dsItems[0]] + if isDisabled { + if transformedItems.count > 1 { + transformedItems = [transformedItems[0]] } } else { - if _dsItems.count == 1 { - _dsItems.append(secondLevelItems) + if transformedItems.count == 1 { + transformedItems.append(secondLevelItems) } } - - wheelContainerDS?.items = _dsItems + return transformedItems } - if var _prevItems = prevItems, !_prevItems.isEmpty { - _prevItems.first?[index].state = state - - if secondLevelItems.isEmpty { - if _prevItems.count > 1 { - _prevItems = [_prevItems[0]] - } - } else { - if _prevItems.count == 1 { - _prevItems.append(secondLevelItems) - } - } - prevItems = _prevItems + if let items = wheelContainerDS?.items { + wheelContainerDS?.items = transform(items) } - items.first?[index].state = state - if secondLevelItems.isEmpty { - if items.count > 1 { - items = [items[0]] - } - } else { - if items.count == 1 { - items.append(secondLevelItems) - } + if let items = prevItems { + prevItems = transform(items) } + + items = transform(items) } diff --git a/Nynja/TypingStatusCache.swift b/Nynja/TypingStatusCache.swift new file mode 100644 index 000000000..bc5657ff3 --- /dev/null +++ b/Nynja/TypingStatusCache.swift @@ -0,0 +1,48 @@ +// +// TypingStatusCache.swift +// Nynja +// +// Created by Ash on 9/12/18. +// Copyright © 2018 TecSynt Solutions. All rights reserved. +// + +import Foundation + + +protocol ITypingStatusCache { + func isCacheValueOutdated(for messageId: String, at date: Date) -> Bool + func renewCacheValue(for messageId: String, at date: Date) + func clearDeprecatedCaсhe(at date: Date) +} + +final class TypingStatusCache: ITypingStatusCache { + struct Config { + let cacheValueMinimalRenewTime: TimeInterval + } + + private var cache: [String: Date] + private let config: Config + + init(config: Config) { + cache = [String: Date]() + self.config = config + } + + func isCacheValueOutdated(for messageId: String, at date: Date) -> Bool { + guard let value = cache[messageId] else { + return true + } + + return value.addingTimeInterval(config.cacheValueMinimalRenewTime) < date + } + + func renewCacheValue(for messageId: String, at date: Date) { + cache[messageId] = date + } + + func clearDeprecatedCaсhe(at date: Date) { + cache = cache.filter { + $1.addingTimeInterval(config.cacheValueMinimalRenewTime) >= date + } + } +} diff --git a/Nynja/Viper/BaseModule/Wireframe/WireframeProtocol.swift b/Nynja/Viper/BaseModule/Wireframe/WireframeProtocol.swift index c024cea8c..a109004a1 100644 --- a/Nynja/Viper/BaseModule/Wireframe/WireframeProtocol.swift +++ b/Nynja/Viper/BaseModule/Wireframe/WireframeProtocol.swift @@ -16,3 +16,19 @@ protocol WireframeProtocol: class { func prepareModule(parameters: Parameters, dependencies: Dependencies) -> UIViewController } + +protocol NavigableWireframeProtocol: class { + var navigation: UINavigationController? { get } + + func tryToPresentModule(with closure: (UINavigationController) -> Void) +} + +extension NavigableWireframeProtocol { + func tryToPresentModule(with closure: (UINavigationController) -> Void) { + guard let navigation = navigation else { + return + } + + closure(navigation) + } +} diff --git a/Nynja/WCBaseItemsFactory.swift b/Nynja/WCBaseItemsFactory.swift index 58b219d27..c211d63e6 100644 --- a/Nynja/WCBaseItemsFactory.swift +++ b/Nynja/WCBaseItemsFactory.swift @@ -93,8 +93,9 @@ class WCBaseItemsFactory: WCItemsFactory { } var channels: ImageActionItemModel { - return ImageActionItemModel(nameImage: "ic_channel_inactive", navItem: .channels) { [weak navigateDelegate] (item, indexPath) in - navigateDelegate?.showChannels(indexPath: indexPath) + return ImageActionItemModel(nameImage: "ic_channel_inactive", navItem: .channels, isSelectable: false) { [weak navigateDelegate] (item, indexPath) in + // navigateDelegate?.showChannels(indexPath: indexPath) + navigateDelegate?.unavailableFunctionality() } } diff --git a/Podfile b/Podfile index 7591f30d6..af9cccf5e 100644 --- a/Podfile +++ b/Podfile @@ -8,7 +8,7 @@ project 'Nynja' project 'Frameworks/NynjaUIKit/NynjaUIKit' plugin 'cocoapods-art', :sources => [ - 'cocoapods-local' +'cocoapods-local' ] def installationSettings @@ -17,67 +17,69 @@ def installationSettings end def commonPodsForNynja - pod 'TestFairy', '~> 1.13.1' - pod 'SnapKit', '~> 4.0.0' - pod 'Fabric', '~> 1.6.13' - pod 'Crashlytics', '~> 3.8.6' - pod 'CocoaMQTT' - pod 'libPhoneNumber-iOS', '~> 0.8' - pod 'QRCode' + pod 'TestFairy', '= 1.13.4' + pod 'SnapKit', '= 4.0.0' + pod 'Fabric', '= 1.6.13' + pod 'Crashlytics', '= 3.8.6' + pod 'CocoaMQTT', '= 1.1.2' + pod 'libPhoneNumber-iOS', '= 0.9.13' + pod 'QRCode', '= 2.0' pod 'CocoaLumberjack', :git => 'https://github.com/CocoaLumberjack/CocoaLumberjack', :commit => '12948ff' - pod 'AWSS3', '~> 2.6.1' - pod 'SDWebImage', '~> 4.0' - - pod 'GoogleMaps' - pod 'GooglePlaces' + pod 'AWSS3', '= 2.6.20' + pod 'SDWebImage', '= 4.4.2' + + pod 'GoogleMaps', '= 2.7.0' + pod 'GooglePlaces', '= 2.7.0' pod 'Firebase/Storage' pod 'Firebase/Auth' - pod 'GRDBCipher', '~> 2.10.0' - pod 'SwiftyJSON', '~> 4.0.0' - - pod 'AutoScrollLabel' - pod 'MaterialComponents/FlexibleHeader' - pod 'JTAppleCalendar', '~> 7.0' - - pod 'NynjaSDK', '= 1.5.5' - - pod 'CryptoSwift' + pod 'GRDBCipher', '= 2.10.0' + pod 'SwiftyJSON', '= 4.0.0' + + pod 'AutoScrollLabel', '= 0.4.3' + pod 'MaterialComponents/FlexibleHeader', '= 55.3.0' + pod 'JTAppleCalendar', '= 7.1.5' + + pod 'NynjaSDK', '= 1.5.6' + + pod 'CryptoSwift', '= 0.10.0' - pod 'MulticastDelegateSwift', '~> 2.1.1' + pod 'MulticastDelegateSwift', '= 2.1.1' + + pod 'Intercom', '= 5.1.6' end def commonPodsForNynjaTests - pod 'SnapKit', '~> 4.0.0' - pod 'Fabric', '~> 1.6.13' - pod 'Crashlytics', '~> 3.8.6' - pod 'CocoaMQTT' - pod 'libPhoneNumber-iOS', '~> 0.8' - pod 'QRCode' + pod 'SnapKit', '= 4.0.0' + pod 'Fabric', '= 1.6.13' + pod 'Crashlytics', '= 3.8.6' + pod 'CocoaMQTT', '= 1.1.2' + pod 'libPhoneNumber-iOS', '= 0.9.13' + pod 'QRCode', '= 2.0' pod 'CocoaLumberjack', :git => 'https://github.com/CocoaLumberjack/CocoaLumberjack', :commit => '12948ff' - pod 'AWSS3', '~> 2.6.1' - pod 'SDWebImage', '~> 4.0' - - pod 'GoogleMaps' - pod 'GooglePlaces' - pod 'GRDBCipher', '~> 2.10.0' - - pod 'AutoScrollLabel' - pod 'MaterialComponents/FlexibleHeader' - pod 'JTAppleCalendar', '~> 7.0' + pod 'AWSS3', '= 2.6.20' + pod 'SDWebImage', '= 4.4.2' + + pod 'GoogleMaps', '= 2.7.0' + pod 'GooglePlaces', '= 2.7.0' + pod 'GRDBCipher', '= 2.10.0' + + pod 'AutoScrollLabel', '= 0.4.3' + pod 'MaterialComponents/FlexibleHeader', '= 55.3.0' + pod 'JTAppleCalendar', '= 7.1.5' end def commonPodsForNynjaShare - pod 'libPhoneNumber-iOS', '~> 0.8' - pod 'AWSS3', '~> 2.6.1' - pod 'CocoaMQTT' - pod 'SnapKit', '~> 4.0.0' - pod 'SDWebImage', '~> 4.0' - pod 'GRDBCipher', '~> 2.10.0' - pod 'CryptoSwift' + pod 'libPhoneNumber-iOS', '= 0.9.13' + pod 'AWSS3', '= 2.6.20' + pod 'CocoaMQTT', '= 1.1.2' + pod 'SnapKit', '= 4.0.0' + pod 'SDWebImage', '= 4.4.2' + pod 'GRDBCipher', '= 2.10.0' + pod 'CryptoSwift', '= 0.10.0' end def commonPodsForNynjaUIKit - pod 'SnapKit', '~> 4.0.0' + pod 'SnapKit', '= 4.0.0' end target 'Nynja' do @@ -110,7 +112,7 @@ post_install do |installer| config.build_settings['SWIFT_VERSION'] = '4.0' end end - + installer.pods_project.build_configurations.each do |config| config.build_settings['PROVISIONING_PROFILE_SPECIFIER'] = '' end diff --git a/Podfile.lock b/Podfile.lock new file mode 100644 index 000000000..872ef199d --- /dev/null +++ b/Podfile.lock @@ -0,0 +1,182 @@ +PODS: + - AutoScrollLabel (0.4.3) + - AWSCore (2.6.20) + - AWSS3 (2.6.20): + - AWSCore (= 2.6.20) + - CocoaAsyncSocket (7.6.3) + - CocoaLumberjack (3.2.1): + - CocoaLumberjack/Default (= 3.2.1) + - CocoaLumberjack/Extensions (= 3.2.1) + - CocoaLumberjack/Default (3.2.1) + - CocoaLumberjack/Extensions (3.2.1): + - CocoaLumberjack/Default + - CocoaMQTT (1.1.2): + - CocoaAsyncSocket (~> 7.6.1) + - SwiftyTimer (~> 2.0.0) + - Crashlytics (3.8.6): + - Fabric (~> 1.6.3) + - CryptoSwift (0.10.0) + - Fabric (1.6.13) + - Firebase/Auth (5.1.0): + - Firebase/CoreOnly + - FirebaseAuth (= 5.0.0) + - Firebase/CoreOnly (5.1.0): + - FirebaseCore (= 5.0.2) + - Firebase/Storage (5.1.0): + - Firebase/CoreOnly + - FirebaseStorage (= 3.0.0) + - FirebaseAuth (5.0.0): + - FirebaseCore (~> 5.0) + - GTMSessionFetcher/Core (~> 1.1) + - FirebaseCore (5.0.2): + - "GoogleToolboxForMac/NSData+zlib (~> 2.1)" + - FirebaseStorage (3.0.0): + - FirebaseCore (~> 5.0) + - GTMSessionFetcher/Core (~> 1.1) + - GoogleMaps (2.7.0): + - GoogleMaps/Maps (= 2.7.0) + - GoogleMaps/Base (2.7.0) + - GoogleMaps/Maps (2.7.0): + - GoogleMaps/Base + - GooglePlaces (2.7.0): + - GoogleMaps/Base (= 2.7.0) + - GoogleToolboxForMac/Defines (2.1.4) + - "GoogleToolboxForMac/NSData+zlib (2.1.4)": + - GoogleToolboxForMac/Defines (= 2.1.4) + - GRDBCipher (2.10.0): + - SQLCipher (~> 3.4.1) + - GTMSessionFetcher/Core (1.1.15) + - Intercom (5.1.6) + - JTAppleCalendar (7.1.5) + - libPhoneNumber-iOS (0.9.13) + - MaterialComponents/FlexibleHeader (55.3.0): + - MaterialComponents/private/Application + - MaterialComponents/private/UIMetrics + - MDFTextAccessibility + - MaterialComponents/private/Application (55.3.0) + - MaterialComponents/private/UIMetrics (55.3.0): + - MaterialComponents/private/Application + - MDFTextAccessibility (1.2.0) + - MulticastDelegateSwift (2.1.1) + - NynjaSDK (1.5.6) + - QRCode (2.0) + - SDWebImage (4.4.2): + - SDWebImage/Core (= 4.4.2) + - SDWebImage/Core (4.4.2) + - SnapKit (4.0.0) + - SQLCipher (3.4.2): + - SQLCipher/standard (= 3.4.2) + - SQLCipher/common (3.4.2) + - SQLCipher/standard (3.4.2): + - SQLCipher/common + - SwiftyJSON (4.0.0) + - SwiftyTimer (2.0.0) + - TestFairy (1.13.4) + +DEPENDENCIES: + - AutoScrollLabel (= 0.4.3) + - AWSS3 (= 2.6.20) + - CocoaLumberjack (from `https://github.com/CocoaLumberjack/CocoaLumberjack`, commit `12948ff`) + - CocoaMQTT (= 1.1.2) + - Crashlytics (= 3.8.6) + - CryptoSwift (= 0.10.0) + - Fabric (= 1.6.13) + - Firebase/Auth + - Firebase/Storage + - GoogleMaps (= 2.7.0) + - GooglePlaces (= 2.7.0) + - GRDBCipher (= 2.10.0) + - Intercom (= 5.1.6) + - JTAppleCalendar (= 7.1.5) + - libPhoneNumber-iOS (= 0.9.13) + - MaterialComponents/FlexibleHeader (= 55.3.0) + - MulticastDelegateSwift (= 2.1.1) + - NynjaSDK (= 1.5.6) + - QRCode (= 2.0) + - SDWebImage (= 4.4.2) + - SnapKit (= 4.0.0) + - SwiftyJSON (= 4.0.0) + - TestFairy (= 1.13.4) + +SPEC REPOS: + https://github.com/cocoapods/specs.git: + - AutoScrollLabel + - AWSCore + - AWSS3 + - CocoaAsyncSocket + - CocoaMQTT + - Crashlytics + - CryptoSwift + - Fabric + - Firebase + - FirebaseAuth + - FirebaseCore + - FirebaseStorage + - GoogleMaps + - GooglePlaces + - GoogleToolboxForMac + - GRDBCipher + - GTMSessionFetcher + - Intercom + - JTAppleCalendar + - libPhoneNumber-iOS + - MaterialComponents + - MDFTextAccessibility + - MulticastDelegateSwift + - QRCode + - SDWebImage + - SnapKit + - SQLCipher + - SwiftyJSON + - SwiftyTimer + - TestFairy + https://nynjagroup.jfrog.io/nynjagroup/api/pods/cocoapods-local: + - NynjaSDK + +EXTERNAL SOURCES: + CocoaLumberjack: + :commit: 12948ff + :git: https://github.com/CocoaLumberjack/CocoaLumberjack + +CHECKOUT OPTIONS: + CocoaLumberjack: + :commit: 12948ff + :git: https://github.com/CocoaLumberjack/CocoaLumberjack + +SPEC CHECKSUMS: + AutoScrollLabel: bee53a1d4532569b49cbecaf9b80ca003bed3767 + AWSCore: dd4d3d4fcafdce68a42025c3e9e45b026cf1f33b + AWSS3: 69589dec1883cdca872b4ddd1ea2ef814b0846fe + CocoaAsyncSocket: eafaa68a7e0ec99ead0a7b35015e0bf25d2c8987 + CocoaLumberjack: 520616f8e72226ca2c729b43981b66bc483745ce + CocoaMQTT: d33ab3cd4e329f9f1cfbb62e25306a318a606616 + Crashlytics: 95d05f4e4c19a771250c4bd9ce344d996de32bbf + CryptoSwift: 6c778d69282bed3b4e975ff97a79d074f20bb011 + Fabric: 2fb5676bc811af011a04513451f463dac6803206 + Firebase: e08fb0795f35707aeb1d8a715c731c45bdf6fd56 + FirebaseAuth: acbeef02fe7c3a26624e309849f3fe30c84115af + FirebaseCore: b81044df1044c0857a0737c6324678b72d4f7f00 + FirebaseStorage: 7ca4bb7b58a25fa647b04f524033fc7cb7eb272b + GoogleMaps: f79af95cb24d869457b1f961c93d3ce8b2f3b848 + GooglePlaces: 3d06e6c99654545b4738ce49648745779c25f2ef + GoogleToolboxForMac: 91c824d21e85b31c2aae9bb011c5027c9b4e738f + GRDBCipher: eef21d242c727a21e0f87ad44f8ea2df03edd252 + GTMSessionFetcher: 5fa5b80fd20e439ef5f545fb2cb3ca6c6714caa2 + Intercom: 083a05bf222811b0b5e0a0b24c863544123397f0 + JTAppleCalendar: 2d4f974f9f3c8b4964d51ca1f6e004883c031fbe + libPhoneNumber-iOS: e444379ac18bbfbdefad571da735b2cd7e096caa + MaterialComponents: 915f4e844400a35db3ea4c710a9af40aa8bcb093 + MDFTextAccessibility: 94098925e0853551c5a311ce7c1ecefbe297cdb6 + MulticastDelegateSwift: 93eb077c24f50574b3f8a3f23bf71be6de6e3b41 + NynjaSDK: 83e97b19149b19ffd5219bca3c5ef4a9906b1b0f + QRCode: f98a1886c8f37523704a7512a4c0cd45b34c18a4 + SDWebImage: 624d6e296c69b244bcede364c72ae0430ac14681 + SnapKit: a42d492c16e80209130a3379f73596c3454b7694 + SQLCipher: f9fcf29b2e59ced7defc2a2bdd0ebe79b40d4990 + SwiftyJSON: 070dabdcb1beb81b247c65ffa3a79dbbfb3b48aa + SwiftyTimer: 2efd74b060d69ad4f1496baf5bbedbe132125fcf + TestFairy: 842f8ddc45477b208eb85326b0418047b40f7137 + +PODFILE CHECKSUM: 95f05f4ee5cc99d269461cdd9ca4184f0c839a2e + +COCOAPODS: 1.5.3 diff --git a/Shared/Library/Extensions/Models/Contact/Contact+BaseChatModel.swift b/Shared/Library/Extensions/Models/Contact/Contact+BaseChatModel.swift index 9646e7079..f548d3562 100644 --- a/Shared/Library/Extensions/Models/Contact/Contact+BaseChatModel.swift +++ b/Shared/Library/Extensions/Models/Contact/Contact+BaseChatModel.swift @@ -11,11 +11,24 @@ import Foundation extension Contact: BaseChatModel { var otherReader: Int64? { - get { return reader?.first as? Int64 } + get { return reader?[safe: 0] as? Int64 } + set { update(reader: newValue, kind: .other) } } var selfReader: Int64? { - get { return reader?.last as? Int64 } + get { return reader?[safe: 1] as? Int64 } + set { update(reader: newValue, kind: .own) } + } + + private func update(reader newValue: Int64?, kind: ReaderKind) { + guard let newValue = newValue else { + return + } + + var temp = (reader as? [Int64]) ?? [] + temp.complete(to: 2, with: 0) + temp[kind.rawValue] = newValue + reader = temp as [AnyObject] } var id: String? { diff --git a/Shared/Library/Extensions/Models/Contact/ContactExtension.swift b/Shared/Library/Extensions/Models/Contact/ContactExtension.swift index 7e71bbc1a..0cc738fe8 100644 --- a/Shared/Library/Extensions/Models/Contact/ContactExtension.swift +++ b/Shared/Library/Extensions/Models/Contact/ContactExtension.swift @@ -26,8 +26,8 @@ extension Contact { /// If contact doesn't have name and surname, it will return nil var fullName: String? { if let name = self.names, let surname = self.surnames { - if surname != "" { - return name + " " + surname + if !surname.isEmpty { + return "\(name) \(surname)" } else { return name } @@ -41,18 +41,7 @@ extension Contact { } var alias: String? { - if let name = self.names, let surname = self.surnames { - if surname != "" { - return name + "_" + surname - } else { - return name - } - } else if names != nil && surnames == nil { - return names - } else if names == nil && surnames == nil { - return surnames - } - return nil + return fullName } /// Returns the first letter of name. If name doesn't exist, it will return the first letter of surname. @@ -93,8 +82,19 @@ extension Contact { } var originalStatus: Status? { - guard let statuString = StringAtom.string(status), let status = Status(rawValue: statuString) else { return nil } - return status + return StringAtom.string(status).flatMap { Status(rawValue: $0) } + } + + func hasPendingIncomingRequest() -> Bool { + guard let status = originalStatus else { + return false + } + switch status { + case .authorization, .ignore: + return true + case .get, .request, .internal, .friend, .lastMessage, .ban, .banned, .deleted: + return false + } } /// Returns phone number in form '+'. diff --git a/Shared/Library/Extensions/Models/Message/Message+Factory.swift b/Shared/Library/Extensions/Models/Message/Message+Factory.swift index 0513e042a..197a44394 100644 --- a/Shared/Library/Extensions/Models/Message/Message+Factory.swift +++ b/Shared/Library/Extensions/Models/Message/Message+Factory.swift @@ -39,7 +39,7 @@ extension Message { message.types = ["forward"] - message.link = link + message.linkedId = link message.status = nil message.files = message.files?.filter({ $0.mime != "translate" @@ -67,7 +67,7 @@ extension Message { func cloned() -> Message { let msg = Message(message: self) - msg.created = Date.currentTimestamp as AnyObject + msg.created = Date.currentTimestamp let builder = IdBuilder(format: .defaultId) diff --git a/Shared/Library/Extensions/Models/Message/Message+Files.swift b/Shared/Library/Extensions/Models/Message/Message+Files.swift index 3ea70a95d..235692049 100644 --- a/Shared/Library/Extensions/Models/Message/Message+Files.swift +++ b/Shared/Library/Extensions/Models/Message/Message+Files.swift @@ -44,7 +44,7 @@ extension Message { return _files.first { !types.contains($0.mime ?? "") - } + } ?? _files.first } var mainUrl: URL? { diff --git a/Shared/Library/Extensions/Models/Message/MessageExtension.swift b/Shared/Library/Extensions/Models/Message/MessageExtension.swift index 10dfdcae5..5e1fbf946 100644 --- a/Shared/Library/Extensions/Models/Message/MessageExtension.swift +++ b/Shared/Library/Extensions/Models/Message/MessageExtension.swift @@ -8,6 +8,8 @@ import UIKit +// MARK: - Hashable + extension Message: Hashable { var hashValue: Int { @@ -22,6 +24,8 @@ extension Message: Hashable { } } +// MARK: - Init + extension Message { convenience init(message: Message) { @@ -48,7 +52,8 @@ extension Message { self.seenby = message.seenby self.repliedby = message.repliedby self.mentioned = message.mentioned - self.status = StringAtom(any: message.status) + self.messageStatus = message.messageStatus + self.localStatus = message.localStatus self.feedName = message.feedName self.senderName = message.senderName @@ -56,6 +61,11 @@ extension Message { self.isTrusted = message.isTrusted } +} + +// MARK: - Convenience Access + +extension Message { var isDelivered: Bool { return id != nil @@ -70,15 +80,14 @@ extension Message { } var createdInt: Int64 { - return (created as? Int64) ?? 0 + return created ?? 0 } var createdDate: Date? { - if let timestamp = created as? Int64 { + if let timestamp = created { let formattedTimestamp = Double(timestamp) / 1000 return Date(timeIntervalSince1970: formattedTimestamp) } - return nil } @@ -86,10 +95,6 @@ extension Message { return files?.first?.payload } - var statusString: String? { - return (status as? StringAtom)?.string - } - var types: Set { get { let arr = (type as? [StringAtom])?.compactMap { $0.string } ?? [] @@ -99,6 +104,35 @@ extension Message { type = newValue.compactMap { StringAtom(string: $0) } } } + + var linkedId: MessageServerId? { + get { + if let link = link as? MessageServerId { + return link + } + return (link as? Message)?.id + } + set { link = newValue as AnyObject? } + } + + var repliedMessage: Message? { + guard isReply, let repliedMessage = link as? Message else { + return nil + } + return repliedMessage + } +} + +// MARK: - Actions + +extension Message { + + func edit(by message: Message) { + files = message.files + mentioned = message.mentioned + markAsEdited() + messageStatus = nil + } } // MARK: - Feed @@ -125,7 +159,7 @@ extension Message { } -// MARK: - Types +// MARK: - Type extension Message { @@ -162,15 +196,50 @@ extension Message { } } -// MARK: - Statuses +// MARK: - Statuse extension Message { - var isStatusClear: Bool { - return statusString == "clear" + enum Status: String { + case edit + case delete + case update + case clear + + func isIn(_ statuses: [Status]) -> Bool { + return statuses.contains(self) + } + } + + struct LocalStatus: OptionSet { + + var rawValue: Int64 + + init(rawValue: Int64) { + self.rawValue = rawValue + } + + static let deleted = LocalStatus(rawValue: 1 << 0) + static let replied = LocalStatus(rawValue: 1 << 1) + } + + var statusString: String? { + return StringAtom.string(status) ?? (status as? String) + } + + var messageStatus: Status? { + get { + return statusString.flatMap { Status(rawValue: $0) } + } + set { + status = newValue.flatMap { StringAtom(string: $0.rawValue) } + } } - var isStatusDelete: Bool { - return statusString == "delete" + var isDeleted: Bool { + guard let localStatus = localStatus else { + return false + } + return localStatus.contains(.deleted) } } diff --git a/Shared/Library/Extensions/Models/Room/Room+BaseChatModel.swift b/Shared/Library/Extensions/Models/Room/Room+BaseChatModel.swift index 850657c31..e750f4c63 100644 --- a/Shared/Library/Extensions/Models/Room/Room+BaseChatModel.swift +++ b/Shared/Library/Extensions/Models/Room/Room+BaseChatModel.swift @@ -9,16 +9,24 @@ import Foundation extension Room: BaseChatModel { + var otherReader: Int64? { - get { - return readers?.first as? Int64 + get { return readers?[safe: 0] as? Int64 } + set { + guard let newValue = newValue else { + return + } + + var temp = (readers as? [Int64]) ?? [] + temp.complete(to: 1, with: 0) + temp[0] = newValue + readers = temp as [AnyObject] } } var selfReader: Int64? { - get { - return allMembers?.filter { $0.phone_id == StorageService.sharedInstance.phoneId}.first?.reader - } + get { return selfMember?.reader } + set { selfMember?.reader = newValue } } enum Status: String { @@ -40,16 +48,13 @@ extension Room: BaseChatModel { case video } - var reader: Int64? { - get { return readers?.first as? Int64 } - set { readers = [reader as AnyObject] } - } - var originalStatus: Status? { - guard let status = self.status, let stringStatus = StringAtom.string(status) else { - return nil + get { + return StringAtom.string(status).flatMap { Status(rawValue: $0) } + } + set(newStatus) { + self.status = newStatus.flatMap { StringAtom(string: $0.rawValue) } } - return Status(rawValue: stringStatus) } var avatarUrl: URL? { diff --git a/Shared/Library/Models/BaseChatModel.swift b/Shared/Library/Models/BaseChatModel.swift index 4e3a09331..33c297955 100644 --- a/Shared/Library/Models/BaseChatModel.swift +++ b/Shared/Library/Models/BaseChatModel.swift @@ -8,13 +8,18 @@ import Foundation +enum ReaderKind: Int { + case other = 0 + case own +} + protocol BaseChatModel: class { var id: String? { get set } var unread: Int64? { get set } - var selfReader: Int64? { get } - var otherReader: Int64? { get } + var selfReader: Int64? { get set } + var otherReader: Int64? { get set } var last_msg: Message? { get set } - var lastMessageId: Int64? { get set } + var lastMessageId: String? { get set } var name: String? { get } var avatarUrl: URL? { get } diff --git a/Shared/Services/Handlers/IoHandler.swift b/Shared/Services/Handlers/IoHandler.swift index bb2397a01..6e5f5ccb9 100644 --- a/Shared/Services/Handlers/IoHandler.swift +++ b/Shared/Services/Handlers/IoHandler.swift @@ -11,12 +11,10 @@ import Foundation protocol IoHandlerDelegate: class { func smsSent() func logined() - func smsNotSent() - func notVerified() func wrongCode() func mismatchUserData() func sessionNotFound() - func attempts_expired() + func attemptsExpired() func notAuthorized() func added() func invalidData() @@ -41,12 +39,10 @@ protocol IoHandlerDelegate: class { extension IoHandlerDelegate { func smsSent() {} func logined() {} - func smsNotSent() {} - func notVerified() {} func wrongCode() {} func mismatchUserData() {} func sessionNotFound() {} - func attempts_expired() {} + func attemptsExpired() {} func notAuthorized() {} func added() {} func invalidData() {} @@ -127,12 +123,10 @@ class IoHandler:BaseHandler { StorageService.sharedInstance.wasLogined = true case "mismatch_user_data": self.delegate?.mismatchUserData() - case "invalid_jwt_code": - self.delegate?.wrongCode() case "invalid_sms_code": self.delegate?.wrongCode() case "attempts_expired": - self.delegate?.attempts_expired() + self.delegate?.attemptsExpired() case "number_not_allowed": self.delegate?.numberNotAllowed() case "logout": -- GitLab