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 {