diff --git a/Frameworks/NynjaUIKit/NynjaUIKit.xcodeproj/project.pbxproj b/Frameworks/NynjaUIKit/NynjaUIKit.xcodeproj/project.pbxproj index d227aa9667c4a83d7a6505aa14378e2c445a08ab..f648336203b0564e26ca49d665df15f7f8e6b946 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/Nynja-Share/Resources/Info.plist b/Nynja-Share/Resources/Info.plist index c672c023bda40a35702dfc0a3e8f1f8e6874230a..d0d60f446db6d5b4b9405b3684f87838a5ae3fad 100644 --- a/Nynja-Share/Resources/Info.plist +++ b/Nynja-Share/Resources/Info.plist @@ -21,7 +21,7 @@ CFBundleShortVersionString 1.0 CFBundleVersion - 0.5.3.RC + 0.5.3 Config $(Config) ModelsVersion diff --git a/Nynja.xcodeproj/project.pbxproj b/Nynja.xcodeproj/project.pbxproj index 86da160bb8ad05ad9f2a514f12d007d23a787c6b..ce85e09a634f03b52f201563cd89cb51a03f10a5 100644 --- a/Nynja.xcodeproj/project.pbxproj +++ b/Nynja.xcodeproj/project.pbxproj @@ -250,6 +250,7 @@ 266AE8C42034971A0096A12C /* AsyncOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 266AE8C2203496B60096A12C /* AsyncOperation.swift */; }; 266F04CB2015050400B97A83 /* DBStarMessage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 266F04CA2015050400B97A83 /* DBStarMessage.swift */; }; 266F04CF201541BC00B97A83 /* StarExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 266F04CE201541BC00B97A83 /* StarExtension.swift */; }; + 2676D032217F72B80058838F /* ThirdPartyServices.swift in Sources */ = {isa = PBXBuildFile; fileRef = F1313B0120888FE600E04092 /* ThirdPartyServices.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 */; }; @@ -2050,7 +2051,6 @@ F11786E020A9F11D007A9A1B /* UploadOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = F11786DF20A9F11D007A9A1B /* UploadOperation.swift */; }; F11786E320AACEBF007A9A1B /* DescInfo.swift in Sources */ = {isa = PBXBuildFile; fileRef = F11786E220AACEBF007A9A1B /* DescInfo.swift */; }; F11786E820AC3562007A9A1B /* Bundle+Keys.swift in Sources */ = {isa = PBXBuildFile; fileRef = 85788C4920442887003600C9 /* Bundle+Keys.swift */; }; - F11786E920AC36A3007A9A1B /* ThirdPartyServices.swift in Sources */ = {isa = PBXBuildFile; fileRef = F1313B0120888FE600E04092 /* ThirdPartyServices.swift */; }; F11786EB20AC3778007A9A1B /* Configurable.swift in Sources */ = {isa = PBXBuildFile; fileRef = F1D4A49F20762A1D00F31089 /* Configurable.swift */; }; F11786ED20AC383D007A9A1B /* CollapsedView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F11786EC20AC383D007A9A1B /* CollapsedView.swift */; }; F11786EE20AC39E9007A9A1B /* Job_Spec.swift in Sources */ = {isa = PBXBuildFile; fileRef = A42CE51020692EDA000889CC /* Job_Spec.swift */; }; @@ -14488,7 +14488,6 @@ A415132320DBD5C100C2C01F /* Link_Spec.swift in Sources */, A42CE5C620692EDB000889CC /* reader_Spec.swift in Sources */, 359EB2831F9A2E6A00147437 /* ProfileHandler.swift in Sources */, - F11786E920AC36A3007A9A1B /* ThirdPartyServices.swift in Sources */, A42CE5A820692EDB000889CC /* Star.swift in Sources */, A42CE5C020692EDB000889CC /* ok_Spec.swift in Sources */, A42CE60E20692EDB000889CC /* messageEvent_Spec.swift in Sources */, @@ -14528,6 +14527,7 @@ A42CE61420692EDB000889CC /* io_Spec.swift in Sources */, 4B5A0B7B216E3C7D002C4160 /* AttachmentProviding.swift in Sources */, A42CE59020692EDB000889CC /* Task.swift in Sources */, + 2676D032217F72B80058838F /* ThirdPartyServices.swift in Sources */, A42CE58A20692EDB000889CC /* boundaryEvent.swift in Sources */, 85458CDA212D6FFE00BA8814 /* String+Split.swift in Sources */, A42CE5A420692EDB000889CC /* Search.swift in Sources */, @@ -16892,8 +16892,8 @@ LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; PRODUCT_BUNDLE_IDENTIFIER = "$(BundleIdentifier)"; PRODUCT_NAME = "$(TARGET_NAME)"; - PROVISIONING_PROFILE = "452734fb-95bb-4d88-8842-c3dcb9f232fb"; - PROVISIONING_PROFILE_SPECIFIER = ProductionBundle_Dev; + PROVISIONING_PROFILE = "1971b115-2b2c-48b6-a20f-3686249e0a88"; + PROVISIONING_PROFILE_SPECIFIER = ProductionBundle_Appstore; SWIFT_ACTIVE_COMPILATION_CONDITIONS = RELEASE; SWIFT_COMPILATION_MODE = wholemodule; SWIFT_OBJC_BRIDGING_HEADER = "Nynja-Bridging-Header.h"; diff --git a/Nynja/AudioManager.swift b/Nynja/AudioManager.swift new file mode 100644 index 0000000000000000000000000000000000000000..1fedb65bdd09f1598c2877b50a577c59279b9f25 --- /dev/null +++ b/Nynja/AudioManager.swift @@ -0,0 +1,280 @@ +// +// AudioManager.swift +// Nynja +// +// Created by Volodymyr Hryhoriev on 10/17/17. +// Copyright © 2017 TecSynt Solutions. All rights reserved. +// + +import AVFoundation + +enum Speaker { + case unknown + case soft + case loud +} + +protocol SpeakerDelegate: class { + func speakerUpdated(state: Speaker) +} + +extension SpeakerDelegate { + func speakerUpdated(state: Speaker) {} +} + +final class AudioManager: NSObject, AVAudioPlayerDelegate { + + static let sharedInstance: AudioManager = { + let instance = AudioManager() + return instance + }() + + weak var delegate: AudioManagerDelegate? + weak var speakerDelegate: SpeakerDelegate? + + private var audioPlayer: AVAudioPlayer? { + didSet { + audioPlayer?.delegate = self + audioPlayer?.volume = 1.0 + speaker = .loud + audioPlayer?.prepareToPlay() + } + } + + private var progressTimer: Timer? + + private var audioSession: AVAudioSession! + + private let audioSessionManager = AudioSessionManager.shared + + private var needAdjust : Bool = true + + /// Determines type of speaker + /// Default: .unknown + var speaker: Speaker = .unknown { + didSet { + if (needAdjust && oldValue != speaker) { + if adjustSpeaker() { + speakerDelegate?.speakerUpdated(state: speaker) + } else { + speakerDelegate?.speakerUpdated(state: oldValue) + } + } + } + } + + var currentUrl: URL? { + return audioPlayer?.url + } + + var currentDuration: TimeInterval { + return audioPlayer?.duration ?? 0 + } + + var isPlaying: Bool { + return audioPlayer?.isPlaying ?? false + } + + + // MARK: - Init + + override init() { + super.init() + configureAudioSession() + NotificationCenter.default.addObserver(self, + selector: #selector(audioSessionRouteChange(notification:)), + name: .AVAudioSessionRouteChange, + object: nil) + } + + private func configureAudioSession() { + audioSession = AVAudioSession.sharedInstance() + var isSpeaker: Bool = false + for output in audioSession.currentRoute.outputs where output.portType == AVAudioSessionPortBuiltInSpeaker { + isSpeaker = true + break + } + + speaker = isSpeaker ? .loud : .soft + } + + + // MARK: - Playing + + func play(with url: URL) throws { + try setup(for: url) + _play() + } + + func pause() { + if let player = audioPlayer, player.isPlaying { + player.pause() + invalidateProgressTimer() + } + audioSessionManager.stopPlayback() + } + + func stop() { + // Check that audio is playing + if let player = audioPlayer, player.isPlaying { + player.stop() + audioPlayer?.delegate = nil + audioPlayer = nil + invalidateProgressTimer() + } + audioSessionManager.stopPlayback() + } + + func resume() { + _play() + } + + private func setup(for url: URL) throws { + if let currentUrl = currentUrl, url == currentUrl { + return + } + stop() + audioPlayer = try AVAudioPlayer(contentsOf: url) + } + + private func _play() { + guard let player = audioPlayer, !player.isPlaying else { + return + } + adjustSpeaker() + player.play() + audioSessionManager.startPlayback() + startProgressTimer() + } + + + // MARK: - Progress Timer + + private func startProgressTimer() { + progressTimer = Timer.scheduledTimer(timeInterval: 0.01, + target:self, + selector:#selector(updateProgress(_:)), + userInfo:nil, + repeats:true) + } + + private func invalidateProgressTimer() { + progressTimer?.invalidate() + progressTimer = nil + } + + @objc private func updateProgress(_ timer: Timer) { + // Check that player is playing something + if let player = audioPlayer, player.isPlaying { + self.delegate?.didChangedCurrentTime(self, currentTime: player.currentTime) + } + } + + func changedProgress(_ progress: Double, for url: URL) { + try? setup(for: url) + guard let player = audioPlayer else { + return + } + player.currentTime = player.duration * progress / 100.0 + } + + + // MARK: - Utils + + @discardableResult + private func adjustSpeaker() -> Bool { + switch speaker { + case .soft: + do { + guard try AudioSessionManager.shared.request(category: .playAndRecord) else { + return false + } + try audioSession.overrideOutputAudioPort(.none) + return true + } catch { + LogService.log(topic: .audioSystem) { return error.localizedDescription } + } + case .loud: + do { + guard try AudioSessionManager.shared.request(category: .playAndRecord) else { + return false + } + try audioSession.overrideOutputAudioPort(.speaker) + return true + } catch { + LogService.log(topic: .audioSystem) { return error.localizedDescription } + } + case .unknown: + LogService.log(topic: .audioSystem) { return "unexpected case of speaker" } + } + return false + } + + + // MARK: - AVAudioPlayerDelegate + + func audioPlayerDidFinishPlaying(_ player: AVAudioPlayer, successfully flag: Bool) { + audioSessionManager.stopPlayback() + + invalidateProgressTimer() + player.stop() + player.currentTime = 0.0 + + + 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 + } + } + + + // disable adjust speaker on set + needAdjust = false + speaker = isSpeaker ? .loud : .soft + // enable adjust speaker on set + needAdjust = true + } +} diff --git a/Nynja/ConnectionSubscriberService.swift b/Nynja/ConnectionSubscriberService.swift new file mode 100644 index 0000000000000000000000000000000000000000..9aa73f9d36641b432f792a645c4b3971d92d1d2f --- /dev/null +++ b/Nynja/ConnectionSubscriberService.swift @@ -0,0 +1,30 @@ +// +// ConnectionSubscriberService.swift +// Nynja +// +// Created by Anton Poltoratskyi on 16.02.2018. +// Copyright © 2018 TecSynt Solutions. All rights reserved. +// + +import Foundation + +final class ConnectionSubscriberService { + + private let taskHandlers: [BackgroundTaskHandler] = [] + + func subscribe() { + MQTTService.sharedInstance.addSubscriber(self) + } + + func unsubscribe() { + MQTTService.sharedInstance.removeSubscriber(self) + } +} + +// MARK: - MQTTServiceDelegate +extension ConnectionSubscriberService: MQTTServiceDelegate { + + func mqttServiceDidConnect(_ mqttService: MQTTService) { + taskHandlers.forEach { $0.performTask() } + } +} diff --git a/Nynja/Extensions/Models/Feature/FeatureExtension.swift b/Nynja/Extensions/Models/Feature/FeatureExtension.swift index 99ce7663b6f9ae818013855b2b338b5c75dccb32..11bb869466566ad179be247cf7af11f500a156ee 100644 --- a/Nynja/Extensions/Models/Feature/FeatureExtension.swift +++ b/Nynja/Extensions/Models/Feature/FeatureExtension.swift @@ -270,6 +270,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/HomeItemsFactory.swift b/Nynja/HomeItemsFactory.swift index d7b81ddb747fb6a31fe628f4ed524fc545139833..61da44877b4995aab50ae5ef43349d926d3d85b7 100644 --- a/Nynja/HomeItemsFactory.swift +++ b/Nynja/HomeItemsFactory.swift @@ -31,10 +31,7 @@ class HomeItemsFactory: WCBaseItemsFactory { }) 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, help] } diff --git a/Nynja/Library/UI/TextInput/InputBar/InputBar.swift b/Nynja/Library/UI/TextInput/InputBar/InputBar.swift index eeaee43fcc9b607b44d7f6f5b03587731d4d026c..b1ea9ec47893722c09c713ba3a55f95591405057 100644 --- a/Nynja/Library/UI/TextInput/InputBar/InputBar.swift +++ b/Nynja/Library/UI/TextInput/InputBar/InputBar.swift @@ -19,7 +19,7 @@ protocol InputBarDelegate: class { final class InputBar: UIView { - weak var delegate: (InputBarDelegate & InputTextStorageDelegate)? + weak var delegate: InputBarDelegate? var willSendHandler: NewSendHandler? var newSendHandler: NewSendHandler? @@ -757,10 +757,6 @@ 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 - } } diff --git a/Nynja/LogService/LogService.swift b/Nynja/LogService/LogService.swift new file mode 100644 index 0000000000000000000000000000000000000000..fc96a8b5ab59f151e0de599566561a49f714445e --- /dev/null +++ b/Nynja/LogService/LogService.swift @@ -0,0 +1,73 @@ +// +// 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] = LogService.allValues + + 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" + } + + 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 { + return allValues.reduce("") { (result, topic) -> String in + return "\(result)\(topic.rawValue),\n" + } + } + + 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 0000000000000000000000000000000000000000..1a1453996317c2eddf70009f308f09ba730dcfd6 --- /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/Modules/Auth/Interactor/AuthInteractor.swift b/Nynja/Modules/Auth/Interactor/AuthInteractor.swift new file mode 100644 index 0000000000000000000000000000000000000000..b9c4dc62e98121e25a1d687ac2f49cf16f96404f --- /dev/null +++ b/Nynja/Modules/Auth/Interactor/AuthInteractor.swift @@ -0,0 +1,166 @@ +// +// AuthAuthInteractor.swift +// Nynja +// +// Created by Anton Makarov on 31/05/2017. +// Copyright © 2017 TecSynt Solutions. All rights reserved. +// + +class AuthInteractor: BaseInteractor, AuthInteractorInputProtocol, IoHandlerDelegate, MQTTServiceDelegate { + + override var subscribes: [SubscribeType]? { + return [.roster(nil)] + } + + weak var presenter: AuthInteractorOutputProtocol! + + private let mqtt = MQTTService.sharedInstance + + var phone = "" + + private var enteredCode: String? + + override init() { + super.init() + + IoHandler.delegate = self + let time = DispatchTime(uptimeNanoseconds: 200) + DispatchQueue.global.asyncAfter(deadline: time) { + MQTTService.sharedInstance.addSubscriber(self) + } + } + + func login(phone: String) { + self.phone = phone + mqtt.registration(number: phone) + } + + func checkPassword() { + guard let code = enteredCode else { return } + mqtt.checkSMS(code: code, phone: phone) + } + + func resendSMS(phone: String) { + mqtt.resendSMS(number: phone) + } + + func voiceCall() { + mqtt.voiceCall(phone: phone) + } + + func getSMSCode(result: ((String) -> ())?) { + #if !RELEASE + let path = "http://\(MQTTService.sharedInstance.host.url):8888/sessions?phone=\(phone)" + if let url = URL(string: path) { + var req = URLRequest(url: url) + + req.allHTTPHeaderFields = makeHTTPHeaderFields() + + let session = URLSession.shared + let task = session.dataTask(with: req, completionHandler: { (data, resp, err) in + if data == nil { + return + } + do { + if let todoJSON = try JSONSerialization.jsonObject(with: data!, options: []) as? [[String: Any]] { + for i in todoJSON { + let model = Modelka(input: i) + if model.smsCode != nil && model.devKey == MQTTService.sharedInstance.deviceId { + result?(model.smsCode!) + } + } + } + } catch { + + } + }) + 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)" + ] + } + + // MARK: - IoHandlerDelegate + func sessionNotFound() { + mqtt.removeTokenAndLogin(number: phone) + } + + func smsAlreadySent() { + mqtt.resendSMS(number: phone) + } + + func invalidSMS() { + self.presenter.invalidCode() + } + + func smsSent() { + self.presenter.smsSent() + } + + func smsNotSent() { + AlertManager.sharedInstance.showAlertOk(message: "auth_something_went_wrong".localized) + } + + func notVerified() { + mqtt.resendSMS(number: phone) + } + + func wrongCode() { + AlertManager.sharedInstance.showAlertOk(message: "auth_sms_code_is_wrong".localized) + } + + func attempts_expired() { + AlertManager.sharedInstance.showAlertOk(message: "auth_attempts_expired".localized) + } + + func mismatchUserData() { + mqtt.removeTokenAndLogin(number: phone) + } + + func numberNotAllowed() { + self.presenter.numberNotAllowed() + } + + // MARK: - MQTTServiceDelegate + func mqttServiceDidReceiveWrongServerVersion() { + DispatchQueue.main.async { + AlertManager.sharedInstance.showAlertOk(message: "wrongVersion".localized) + } + } + + // 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 { + presenter.registered(contact: contact) + } else { + presenter.logined() + } + } + } + + func saveEnteredPassword(code: String) { + self.enteredCode = code + } + +} diff --git a/Nynja/Modules/Main/View/NavigateProtocol.swift b/Nynja/Modules/Main/View/NavigateProtocol.swift index 82fe2cca38ebd2d553e8af856bb45715f76f0a97..b00a29af47e2f7e352df9cd0f3020840491f045d 100644 --- a/Nynja/Modules/Main/View/NavigateProtocol.swift +++ b/Nynja/Modules/Main/View/NavigateProtocol.swift @@ -34,7 +34,6 @@ protocol SecondLevelNavigateProtocol: class { func showGroupOptions(indexPath: IndexPath?) func showMessages(indexPath: IndexPath?) - func helpFeedBack(indexPath: IndexPath?) // MARK: - Chat Actions func showLocation(indexPath: IndexPath?) 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 0000000000000000000000000000000000000000..f077dc5c2c18d9f4bf58711e8cd87365a0d84b42 --- /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/SystemCell.swift b/Nynja/Modules/Message/View/Views/TableView/Cells/SystemCell.swift new file mode 100644 index 0000000000000000000000000000000000000000..f40b38b31ac8d0e4fe6b2846e68693ebe88ff524 --- /dev/null +++ b/Nynja/Modules/Message/View/Views/TableView/Cells/SystemCell.swift @@ -0,0 +1,58 @@ +// +// SystemCell.swift +// Nynja +// +// Created by Andrey Reznik on 12.01.18. +// Copyright © 2018 TecSynt Solutions. All rights reserved. +// + +final class SystemCell: UICollectionViewCell { + + // MARK: - Views + + private lazy var systemLabel: UILabel = { + let label = UILabel(height: Constraints.Label.height, + color: Constants.colors.white.getColor(), + fontName: Constants.fonts.regular, + textAlignment: .center) + + label.numberOfLines = 0 + + 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) { + backgroundColor = UIColor.clear + systemLabel.text = message + } +} + + +// MARK: - Constraints + +private extension SystemCell { + + enum Constraints { + enum Label { + static let height = CGFloat(20).adjustedByWidth + static let horizontalInset = 16.0.adjustedByWidth + } + } +} diff --git a/Nynja/Modules/Message/View/Views/TableView/Cells/TimeCell.swift b/Nynja/Modules/Message/View/Views/TableView/Cells/TimeCell.swift new file mode 100644 index 0000000000000000000000000000000000000000..8faac2290d893b4fdc930ac911d0a658bde92b12 --- /dev/null +++ b/Nynja/Modules/Message/View/Views/TableView/Cells/TimeCell.swift @@ -0,0 +1,51 @@ +// +// TimeCell.swift +// Nynja +// +// Created by Anton Makarov on 28.08.2017. +// Copyright © 2017 TecSynt Solutions. All rights reserved. +// + +import Foundation + +final class TimeCell: UICollectionViewCell { + + private static let dateFormatter = DateFormatter() + + + // 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() + 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) { + backgroundColor = UIColor.clear + timeStampLabel.text = getText(fromDate: date) + } + + private func getText(fromDate: Date) -> String { + 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/Views/Message/MessagePaymentView.swift b/Nynja/Modules/Message/View/Views/TableView/Cells/Views/Message/MessagePaymentView.swift new file mode 100644 index 0000000000000000000000000000000000000000..99163870febf0eff2d219c1d3659fac32727c839 --- /dev/null +++ b/Nynja/Modules/Message/View/Views/TableView/Cells/Views/Message/MessagePaymentView.swift @@ -0,0 +1,173 @@ +// +// MessagePaymentView.swift +// Nynja +// +// Created by Aleksandr Pavliuk on 6/24/18. +// Copyright © 2018 TecSynt Solutions. All rights reserved. +// + +class MessagePaymentView: MessageContentView { + + 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] + } + + class var textViewBottomInset: CGFloat { + return CGFloat(30.0.adjustedByWidth) + } + + var textViewEdges: UIEdgeInsets { + return UIEdgeInsets(top: Constraints.textView.topInset, + left: Constraints.textView.letftInset + Constraints.paymentDirectionImage.sideSize, + bottom: MessagePaymentView.textViewBottomInset, + right: Constraints.textView.rightInset) + } + + // MARK: - Constraints + struct Constraints { + + static let width = BaseChatCell.Constraints.bubble.maxWidth + static let height = CGFloat(67.adjustedByWidth) + + struct textView { + static let topInset = CGFloat(4.0.adjustedByWidth) + static let letftInset = CGFloat(8.0.adjustedByWidth) + static let rightInset = CGFloat(20.0.adjustedByWidth) + static let bottomInset = CGFloat(21.0.adjustedByWidth) + + static let labelHeight = CGFloat(34.0.adjustedByWidth) + static let nynLocaleHeight = CGFloat(24.0.adjustedByWidth) + static let minWidth = CGFloat(40) + } + + struct infoView { + static let inset = CGPoint(x: 0, y: 2.0.adjustedByWidth) + } + + struct paymentDirectionImage { + static let sideSize = CGFloat(28.adjustedByWidth) + static let horizontalInset = CGFloat(5.adjustedByWidth) + static let topInset = CGFloat(10.0.adjustedByWidth) + } + } + + // MARK: - Views + lazy var textView: UITextView = { + let v = UITextView() + + v.isUserInteractionEnabled = true + v.isScrollEnabled = false + v.isEditable = false + v.isSelectable = false + + v.backgroundColor = UIColor.clear + v.textContainerInset = .zero + v.textContainer.lineFragmentPadding = 0 + + v.linkTextAttributes = [NSAttributedStringKey.foregroundColor.rawValue:Constants.colors.blue.getColor(), + NSAttributedStringKey.underlineColor.rawValue: Constants.colors.blue.getColor(), + NSAttributedStringKey.underlineStyle.rawValue: NSUnderlineStyle.styleSingle.rawValue + ] as [String : Any] + + self.addSubview(v) + v.snp.makeConstraints { make in + make.edges.equalTo(textViewEdges) + make.width.height.equalTo(0) + } + return v + }() + + lazy var paymentDirectionImageView: UIImageView = { + let imageView = UIImageView() + + imageView.contentMode = .scaleAspectFill + let sideSize = Constraints.paymentDirectionImage.sideSize + let verticalInset = Constraints.paymentDirectionImage.topInset + + self.addSubview(imageView) + imageView.snp.makeConstraints { make in + make.width.height.equalTo(sideSize) + make.centerY.equalTo(self.textView.snp.centerY) + make.left.equalTo(Constraints.paymentDirectionImage.horizontalInset) + } + + return imageView + }() + + // MARK: - BaseView + override func baseSetup() { + textView.textContainerInset = .zero + } + + // MARK: - BubbleInjectible + override func configure(with model: BaseChatCellModel) { + super.configure(with: model) + + guard model.type == .payment else { return } + + textView.textColor = Constants.colors.darkLight.getColor() + textView.attributedText = prepareText(in: model) + textView.accessibilityIdentifier = model.text + + let size = textSize(in: model) + textView.snp.updateConstraints { $0.size.equalTo(size) } + + var paymentDirectionImage: UIImage? { + if model.isOwner { + return UIImage.paymentArrowUp + } + return UIImage.paymentArrowDown + } + paymentDirectionImageView.image = paymentDirectionImage + } + + class func size(for model: BaseChatCellModel) -> CGSize? { + return CGSize(width: Constraints.width, height: Constraints.height) + } + + override var infoInset: CGPoint { + return Constraints.infoView.inset + } + + // MARK: - Setup + + private func prepareText(in model: BaseChatCellModel) -> NSAttributedString { + let amountText = model.text ?? "" + let localeText = NYN.code + let resultText = "\(amountText) \(localeText)" + + let font = MessagePaymentView.textFont + let nynFont = MessagePaymentView.nynLocaleFont + let attributedText = NSMutableAttributedString(string: resultText, attributes: [.font: nynFont]) + + let amountRange = NSMakeRange(0, amountText.count) + attributedText.setAttributes([.font: font], range: amountRange) + + return attributedText + } + + private func textSize(in model: BaseChatCellModel) -> CGSize { + + let insets = Constraints.textView.letftInset + Constraints.textView.rightInset + + let maxWidth = BaseChatCell.Constraints.bubble.maxWidth - insets + var minWidth: CGFloat = InfoView.width(for: model) - Constraints.textView.rightInset + + if let headerWidth = BaseChatCell.calculateSenderHeaderSize(for: model)?.width { + minWidth = headerWidth < maxWidth && headerWidth > minWidth ? headerWidth : minWidth + } + + let text = prepareText(in: model) + var textSize = text.boundingRect(with: CGSize(width: maxWidth, height: 0), + options: .usesLineFragmentOrigin, + context: nil).size + + textSize.width = ceil(min(maxWidth, max(textSize.width, minWidth))) + textSize.height = ceil(textSize.height) + + return textSize + } +} diff --git a/Nynja/MotionManager/MotionManager.swift b/Nynja/MotionManager/MotionManager.swift new file mode 100644 index 0000000000000000000000000000000000000000..b0829e81e9c07449abef90b9175c720a8f1333c7 --- /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/Resources/Info.plist b/Nynja/Resources/Info.plist index 4cc06311b1365018e1a6b7aff8ff23f44b25bba0..0bb800f19a0ed6b5cd353ed47424402de41b33bb 100644 --- a/Nynja/Resources/Info.plist +++ b/Nynja/Resources/Info.plist @@ -21,7 +21,7 @@ CFBundleShortVersionString 1.0 CFBundleVersion - 0.5.3.RC + 0.5.3 ConfServerAddress $(ConfServerAddress) ConfServerPort diff --git a/Nynja/Resources/ThirdPartyServices.swift b/Nynja/Resources/ThirdPartyServices.swift index a789c287465470b240bb3707b0b88d548320f672..1a85a5e7b98eeed6b33f8f7691728e55813f75c0 100644 --- a/Nynja/Resources/ThirdPartyServices.swift +++ b/Nynja/Resources/ThirdPartyServices.swift @@ -127,22 +127,6 @@ struct SupportService: 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 TestFairyService: ThirdPartyService { diff --git a/Nynja/Services/SoundService.swift b/Nynja/Services/SoundService.swift new file mode 100644 index 0000000000000000000000000000000000000000..11f2e376fcf4ce5c27ed4c61724333f1a4134528 --- /dev/null +++ b/Nynja/Services/SoundService.swift @@ -0,0 +1,201 @@ +// +// SoundService.swift +// Nynja +// +// Created by Anton Makarov on 25.05.17. +// Copyright © 2017 TecSynt Solutions. All rights reserved. +// + +import AVFoundation + +final class SoundService { + + private let audioSessionManager = AudioSessionManager.shared + + 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")! + let data = try! Data(contentsOf: dataURL) + return try! JSONDecoder().decode(SoundBundle.self, from: data) + }() + + 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 = 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) + } + + private func canPlayMessageSound() -> Bool { + let isAppActive = UIApplication.shared.applicationState == .active + return !isVoipCallActive && isAppActive + } + + + // MARK: - Calls + // MARK: Incoming + + func playCallSound() { + guard let soundURL = soundBundle.defaultCall.url else { + return + } + do { + guard try audioSessionManager.request(category: .playAndRecord) else { + return + } + let shouldVibrate = userSettingsService.notifications.isInAppVibrateEnabled + incomingCallPlayer = try SoundPlayer(soundURL: soundURL, isInfinite: true, shouldVibrate: shouldVibrate) + incomingCallPlayer?.play() + } catch let error as NSError { + LogService.log(topic: .audioSystem) { return error.localizedDescription } + } + } + + func stopIncomingCallPlayer() { + incomingCallPlayer?.stop() + } + + + // MARK: Ring back tone + + func playRingbackSound() { + guard let soundURL = soundBundle.defaultRingback.url else { + LogService.log(topic: .audioSystem) { return "ringback sound not found" } + return + } + + do { + 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) { return "Error init ringback player \(error.localizedDescription)" } + } + } + + func stopRingbackPlayer() { + LogService.log(topic: .audioSystem) { return "stop ringback" } + ringbackCallPlayer?.stop() + } + +} + + +// MARK: - SoundInfo + +extension SoundService { + + struct SoundInfo { + let soundId: SystemSoundID + let lastTimeSoundPlayed: CFAbsoluteTime? + } + +} diff --git a/Nynja/SoundPlayer.swift b/Nynja/SoundPlayer.swift new file mode 100644 index 0000000000000000000000000000000000000000..49ad0654de6f30869cda287295bed804eb0c38a3 --- /dev/null +++ b/Nynja/SoundPlayer.swift @@ -0,0 +1,71 @@ +// +// SoundPlayer.swift +// Nynja +// +// Created by Anton Poltoratskyi on 12.03.2018. +// Copyright © 2018 TecSynt Solutions. All rights reserved. +// + +import Foundation +import AVFoundation + +final class SoundPlayer: NSObject, AVAudioPlayerDelegate { + + private var player: AVAudioPlayer? + private var isInfinite: Bool + private var shouldVibrate: Bool + + var isPlaying: Bool { + return player?.isPlaying ?? false + } + + + // MARK: - Init + + init(soundURL: URL, isInfinite: Bool = false, shouldVibrate: Bool = false) throws { + /// change fileTypeHint according to the type of your audio file (you can omit this) + self.player = try AVAudioPlayer(contentsOf: soundURL, fileTypeHint: AVFileType.mp3.rawValue) + self.isInfinite = isInfinite + self.shouldVibrate = shouldVibrate + super.init() + player?.delegate = self + } + + deinit { + self.player?.delegate = nil + } + + // MARK: - Actions + + func play() { + do { + if shouldVibrate { + AudioServicesPlayAlertSound(kSystemSoundID_Vibrate) + } + player?.play() + } catch { + LogService.log(topic: .audioSystem) { return "Error while try to play audio: \(error.localizedDescription)" } + } + } + + func stop() { + player?.stop() + } + + func pause() { + player?.pause() + } + + + + // MARK: - AVAudioPlayerDelegate + + func audioPlayerDidFinishPlaying(_ player: AVAudioPlayer, successfully flag: Bool) { + guard isInfinite, flag else { return } + + player.play() + if shouldVibrate { + AudioServicesPlaySystemSoundWithCompletion(kSystemSoundID_Vibrate, nil) + } + } +} diff --git a/Shared/Library/Extensions/Models/Message/Message+Factory.swift b/Shared/Library/Extensions/Models/Message/Message+Factory.swift new file mode 100644 index 0000000000000000000000000000000000000000..197a44394fd058eabef3375e9ed4837ddcc5ce1e --- /dev/null +++ b/Shared/Library/Extensions/Models/Message/Message+Factory.swift @@ -0,0 +1,116 @@ +// +// Message+Factory.swift +// Nynja +// +// Created by Anton Poltoratskyi on 22.08.2018. +// Copyright © 2018 TecSynt Solutions. All rights reserved. +// + +import Foundation + +extension Message { + + 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 + } + + 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.linkedId = 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 + + 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/MessageExtension.swift b/Shared/Library/Extensions/Models/Message/MessageExtension.swift index 8f0b97346ebff0eaa93d8965f595248d9e163944..eea958357f9df4548e6375e5e8eb83c2c2bc0d07 100644 --- a/Shared/Library/Extensions/Models/Message/MessageExtension.swift +++ b/Shared/Library/Extensions/Models/Message/MessageExtension.swift @@ -69,18 +69,6 @@ extension Message { } } -// MARK: - Actions - -extension Message { - - func edit(by message: Message) { - files = message.files - mentioned = message.mentioned - markAsEdited() - messageStatus = nil - } -} - // MARK: - Feed extension Message {