diff --git a/Frameworks/NynjaUIKit/NynjaUIKit.xcodeproj/project.pbxproj b/Frameworks/NynjaUIKit/NynjaUIKit.xcodeproj/project.pbxproj index f648336203b0564e26ca49d665df15f7f8e6b946..8c3037efe185974eea36c9309023ff1ce2513c51 100644 --- a/Frameworks/NynjaUIKit/NynjaUIKit.xcodeproj/project.pbxproj +++ b/Frameworks/NynjaUIKit/NynjaUIKit.xcodeproj/project.pbxproj @@ -32,6 +32,13 @@ 8514D51C20EE41E90002378A /* UIWindowExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8514D51B20EE41E90002378A /* UIWindowExtensions.swift */; }; 8514D51E20EE43880002378A /* UIWindow+HitTestDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8514D51D20EE43880002378A /* UIWindow+HitTestDelegate.swift */; }; 851CFD3D20F8A1CF00DBF743 /* NynjaContextMenuUserInfo.swift in Sources */ = {isa = PBXBuildFile; fileRef = 851CFD3C20F8A1CF00DBF743 /* NynjaContextMenuUserInfo.swift */; }; + 85409FFF2181C8C8003A010F /* AvatarStatusView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 85409FFE2181C8C8003A010F /* AvatarStatusView.swift */; }; + 8540A0082181EA2F003A010F /* TypingIndicatorView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8540A0072181EA2F003A010F /* TypingIndicatorView.swift */; }; + 8540A00A2181EB87003A010F /* TypingView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8540A0092181EB87003A010F /* TypingView.swift */; }; + 8540A00C2181EBD2003A010F /* BaseView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8540A00B2181EBD2003A010F /* BaseView.swift */; }; + 8540A00F2181ED2E003A010F /* CALayer+Animation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8540A00E2181ED2E003A010F /* CALayer+Animation.swift */; }; + 8540A019218213E2003A010F /* RoundView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8540A018218213E2003A010F /* RoundView.swift */; }; + 85EB37F621832D41003A2D6F /* TypingBoldIndicatorView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 85EB37F521832D41003A2D6F /* TypingBoldIndicatorView.swift */; }; /* End PBXBuildFile section */ /* Begin PBXFileReference section */ @@ -63,6 +70,13 @@ 8514D51B20EE41E90002378A /* UIWindowExtensions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UIWindowExtensions.swift; sourceTree = ""; }; 8514D51D20EE43880002378A /* UIWindow+HitTestDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIWindow+HitTestDelegate.swift"; sourceTree = ""; }; 851CFD3C20F8A1CF00DBF743 /* NynjaContextMenuUserInfo.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NynjaContextMenuUserInfo.swift; sourceTree = ""; }; + 85409FFE2181C8C8003A010F /* AvatarStatusView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AvatarStatusView.swift; sourceTree = ""; }; + 8540A0072181EA2F003A010F /* TypingIndicatorView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TypingIndicatorView.swift; sourceTree = ""; }; + 8540A0092181EB87003A010F /* TypingView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TypingView.swift; sourceTree = ""; }; + 8540A00B2181EBD2003A010F /* BaseView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BaseView.swift; sourceTree = ""; }; + 8540A00E2181ED2E003A010F /* CALayer+Animation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "CALayer+Animation.swift"; sourceTree = ""; }; + 8540A018218213E2003A010F /* RoundView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoundView.swift; sourceTree = ""; }; + 85EB37F521832D41003A2D6F /* TypingBoldIndicatorView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TypingBoldIndicatorView.swift; sourceTree = ""; }; B90E6396110C47D18FB00838 /* Pods-NynjaUIKit.dev.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-NynjaUIKit.dev.xcconfig"; path = "../../Pods/Target Support Files/Pods-NynjaUIKit/Pods-NynjaUIKit.dev.xcconfig"; sourceTree = ""; }; C6C80841C9BA48F16147BAAE /* Pods_NynjaUIKit.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_NynjaUIKit.framework; sourceTree = BUILT_PRODUCTS_DIR; }; C90742AD8E6E2E817F7DB1E9 /* Pods-NynjaUIKit.channels.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-NynjaUIKit.channels.xcconfig"; path = "../../Pods/Target Support Files/Pods-NynjaUIKit/Pods-NynjaUIKit.channels.xcconfig"; sourceTree = ""; }; @@ -265,6 +279,7 @@ 8514D51920EE41AC0002378A /* Extensions */ = { isa = PBXGroup; children = ( + 8540A00D2181ED10003A010F /* CoreAnimation */, 8514D51F20EE47350002378A /* UIWindow */, ); path = Extensions; @@ -273,6 +288,10 @@ 8514D51A20EE41BA0002378A /* Views */ = { isa = PBXGroup; children = ( + 8540A00B2181EBD2003A010F /* BaseView.swift */, + 8540A01A218213E8003A010F /* Utils */, + 85409FFD2181C8AF003A010F /* Avatar */, + 8540A0062181EA0D003A010F /* Typing */, 8514D50120EE40530002378A /* ContextMenu */, ); path = Views; @@ -287,6 +306,40 @@ path = UIWindow; sourceTree = ""; }; + 85409FFD2181C8AF003A010F /* Avatar */ = { + isa = PBXGroup; + children = ( + 85409FFE2181C8C8003A010F /* AvatarStatusView.swift */, + ); + path = Avatar; + sourceTree = ""; + }; + 8540A0062181EA0D003A010F /* Typing */ = { + isa = PBXGroup; + children = ( + 8540A0072181EA2F003A010F /* TypingIndicatorView.swift */, + 85EB37F521832D41003A2D6F /* TypingBoldIndicatorView.swift */, + 8540A0092181EB87003A010F /* TypingView.swift */, + ); + path = Typing; + sourceTree = ""; + }; + 8540A00D2181ED10003A010F /* CoreAnimation */ = { + isa = PBXGroup; + children = ( + 8540A00E2181ED2E003A010F /* CALayer+Animation.swift */, + ); + path = CoreAnimation; + sourceTree = ""; + }; + 8540A01A218213E8003A010F /* Utils */ = { + isa = PBXGroup; + children = ( + 8540A018218213E2003A010F /* RoundView.swift */, + ); + path = Utils; + sourceTree = ""; + }; 85C65C7C20EE6D9C00C468B2 /* Core */ = { isa = PBXGroup; children = ( @@ -411,12 +464,16 @@ 8514D4E820EE2D970002378A /* UITableView+ViewModels.swift in Sources */, 8514D51E20EE43880002378A /* UIWindow+HitTestDelegate.swift in Sources */, 8514D4E020EE2D970002378A /* LayoutRepresentableCellViewModel.swift in Sources */, + 8540A00C2181EBD2003A010F /* BaseView.swift in Sources */, 8514D51320EE40540002378A /* NynjaContextMenuLayout.swift in Sources */, + 85409FFF2181C8C8003A010F /* AvatarStatusView.swift in Sources */, + 8540A0082181EA2F003A010F /* TypingIndicatorView.swift in Sources */, 8514D4E420EE2D970002378A /* AccessiblityDisplayOptions.swift in Sources */, 8514D4EA20EE2D970002378A /* LayoutAdjustment.swift in Sources */, 8514D4E220EE2D970002378A /* CellViewModel.swift in Sources */, 8514D51420EE40540002378A /* NynjaContextMenuItemCellModel.swift in Sources */, 8514D51220EE40540002378A /* ContextMenuRow.swift in Sources */, + 8540A019218213E2003A010F /* RoundView.swift in Sources */, 8514D51620EE40540002378A /* NynjaContextMenuArrowView.swift in Sources */, 8514D4E620EE2D970002378A /* Reusable.swift in Sources */, 8514D4E320EE2D970002378A /* SupplementaryViewModel.swift in Sources */, @@ -426,7 +483,10 @@ 8514D4E920EE2D970002378A /* UICollectionView+ViewModel.swift in Sources */, 8514D51120EE40540002378A /* ContextMenuItem.swift in Sources */, 8514D51820EE40540002378A /* NynjaContextMenuItemsFactory.swift in Sources */, + 8540A00A2181EB87003A010F /* TypingView.swift in Sources */, + 85EB37F621832D41003A2D6F /* TypingBoldIndicatorView.swift in Sources */, 8514D51520EE40540002378A /* NynjaContextMenuItemCollectionViewCell.swift in Sources */, + 8540A00F2181ED2E003A010F /* CALayer+Animation.swift in Sources */, 8514D4E120EE2D970002378A /* SelectableCellViewModel.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; diff --git a/Frameworks/NynjaUIKit/NynjaUIKit/Core/Extensions/CoreAnimation/CALayer+Animation.swift b/Frameworks/NynjaUIKit/NynjaUIKit/Core/Extensions/CoreAnimation/CALayer+Animation.swift new file mode 100644 index 0000000000000000000000000000000000000000..df70d9566c817f5aafe48a81d6a54d51c3bd887a --- /dev/null +++ b/Frameworks/NynjaUIKit/NynjaUIKit/Core/Extensions/CoreAnimation/CALayer+Animation.swift @@ -0,0 +1,16 @@ +// +// CAAnimationExtensions.swift +// NynjaUIKit +// +// Created by Anton Poltoratskyi on 25.10.2018. +// Copyright © 2018 TecSynt Solutions. All rights reserved. +// + +import UIKit + +extension CALayer { + + public func hasAnimation(forKey key: String) -> Bool { + return animation(forKey: key) != nil + } +} diff --git a/Frameworks/NynjaUIKit/NynjaUIKit/Views/Avatar/AvatarStatusView.swift b/Frameworks/NynjaUIKit/NynjaUIKit/Views/Avatar/AvatarStatusView.swift new file mode 100644 index 0000000000000000000000000000000000000000..3706b9a210a19d91ebb0686e612b22402721bda8 --- /dev/null +++ b/Frameworks/NynjaUIKit/NynjaUIKit/Views/Avatar/AvatarStatusView.swift @@ -0,0 +1,143 @@ +// +// AvatarStatusView.swift +// NynjaUIKit +// +// Created by Anton Poltoratskyi on 25.10.2018. +// Copyright © 2018 TecSynt Solutions. All rights reserved. +// + +import UIKit + +public final class AvatarStatusView: BaseView { + + public var angle: CGFloat = .pi / 4 { + didSet { + setNeedsLayout() + } + } + + public var statusIconSize: CGFloat = 8 { + didSet { + setNeedsLayout() + } + } + + public var statusIconPadding: CGFloat = 2 { + didSet { + setNeedsLayout() + } + } + + public var imageRadius: CGFloat { + return bounds.height / 2 + } + + + // MARK: - Views + + public let imageView = UIImageView() + + private let statusView = UIImageView() + + private let maskLayer = CAShapeLayer() + + private var isMaskActive: Bool { + return !statusView.isHidden + } + + + // MARK: - Setup + + public override func setup() { + super.setup() + + backgroundColor = .clear + + imageView.layer.masksToBounds = true + addSubview(imageView) + + statusView.layer.masksToBounds = true + addSubview(statusView) + + update(.none) + } + + + // MARK: - Layout + + public override func layoutSubviews() { + super.layoutSubviews() + + imageView.frame = bounds + imageView.layer.cornerRadius = bounds.height / 2 + + if isMaskActive { + let clipCircleCenter = statusPosition(in: bounds) + let clipCircleSize = statusIconSize + statusIconPadding * 2 + let clipCircleFrame = CGRect( + x: clipCircleCenter.x - clipCircleSize / 2, + y: clipCircleCenter.y - clipCircleSize / 2, + width: clipCircleSize, + height: clipCircleSize + ) + updateClipMask(with: clipCircleFrame) + updateStatusView(with: clipCircleFrame) + } else { + imageView.layer.mask = nil + } + } + + private func updateClipMask(with frame: CGRect) { + let statusCirclePath = UIBezierPath(ovalIn: frame) + + let path = UIBezierPath(rect: bounds) + path.append(statusCirclePath.reversing()) + + maskLayer.path = path.cgPath + + imageView.layer.mask = maskLayer + } + + private func updateStatusView(with frame: CGRect) { + let size = frame.width - statusIconPadding * 2 + + statusView.frame = CGRect(x: frame.minX + statusIconPadding, + y: frame.minY + statusIconPadding, + width: size, + height: size) + + statusView.layer.cornerRadius = size / 2 + } + + private func statusPosition(in bounds: CGRect) -> CGPoint { + return CGPoint(x: imageRadius * cos(angle) + bounds.width / 2, + y: imageRadius * sin(angle) + bounds.height / 2) + } +} + +extension AvatarStatusView { + + public enum StatusAppearance { + case color(UIColor) + case image(UIImage) + case none + } + + public func update(_ statusAppearance: StatusAppearance) { + switch statusAppearance { + case let .color(color): + statusView.isHidden = false + statusView.backgroundColor = color + statusView.image = nil + case let .image(image): + statusView.isHidden = false + statusView.backgroundColor = nil + statusView.image = image + case .none: + statusView.isHidden = true + statusView.backgroundColor = nil + statusView.image = nil + } + setNeedsLayout() + } +} diff --git a/Frameworks/NynjaUIKit/NynjaUIKit/Views/BaseView.swift b/Frameworks/NynjaUIKit/NynjaUIKit/Views/BaseView.swift new file mode 100644 index 0000000000000000000000000000000000000000..dc6523cbbe9b57b916c875f011e64be33e68e6b0 --- /dev/null +++ b/Frameworks/NynjaUIKit/NynjaUIKit/Views/BaseView.swift @@ -0,0 +1,32 @@ +// +// BaseView.swift +// NynjaUIKit +// +// Created by Anton Poltoratskyi on 25.10.2018. +// Copyright © 2018 TecSynt Solutions. All rights reserved. +// + +import UIKit + +public class BaseView: UIView { + + // MARK: - Init + + public override init(frame: CGRect) { + super.init(frame: frame) + setup() + } + + public required init?(coder aDecoder: NSCoder) { + super.init(coder: aDecoder) + setup() + } + + + // MARK: - Setup + + public func setup() { + // should be implemented in childs + } +} + diff --git a/Frameworks/NynjaUIKit/NynjaUIKit/Views/ContextMenu/View/NynjaContextMenu.swift b/Frameworks/NynjaUIKit/NynjaUIKit/Views/ContextMenu/View/NynjaContextMenu.swift index ecea64cfb62c2404b92bac2a963a6ba790876020..23e4a01d3f97f84dcdc2c73e12214592d2015347 100644 --- a/Frameworks/NynjaUIKit/NynjaUIKit/Views/ContextMenu/View/NynjaContextMenu.swift +++ b/Frameworks/NynjaUIKit/NynjaUIKit/Views/ContextMenu/View/NynjaContextMenu.swift @@ -18,7 +18,7 @@ public protocol NynjaContextMenuDelegate: class { userInfo: NynjaContextMenuUserInfo?) } -public final class NynjaContextMenu: UIView { +public final class NynjaContextMenu: BaseView { // MARK: - Properties @@ -99,22 +99,10 @@ public final class NynjaContextMenu: UIView { }() - // MARK: - Init - - public override init(frame: CGRect) { - super.init(frame: frame) - setup() - } - - public required init?(coder aDecoder: NSCoder) { - super.init(coder: aDecoder) - setup() - } - - // MARK: - Setup - private func setup() { + public override func setup() { + super.setup() clipsToBounds = true contentView.layer.cornerRadius = cornerRadius contentView.clipsToBounds = true diff --git a/Frameworks/NynjaUIKit/NynjaUIKit/Views/Typing/TypingBoldIndicatorView.swift b/Frameworks/NynjaUIKit/NynjaUIKit/Views/Typing/TypingBoldIndicatorView.swift new file mode 100644 index 0000000000000000000000000000000000000000..7316394e6061e8428a9246fbd05cc7da9df62c63 --- /dev/null +++ b/Frameworks/NynjaUIKit/NynjaUIKit/Views/Typing/TypingBoldIndicatorView.swift @@ -0,0 +1,58 @@ +// +// TypingBoldIndicatorView.swift +// NynjaUIKit +// +// Created by Anton Poltoratskyi on 26.10.2018. +// Copyright © 2018 TecSynt Solutions. All rights reserved. +// + +import Foundation + +public final class TypingBoldIndicatorView: BaseView { + + public var horizontalInset: CGFloat = 4 { + didSet { + indicatorView.snp.updateConstraints { maker in + maker.left.equalToSuperview().offset(horizontalInset) + maker.right.equalToSuperview().inset(horizontalInset) + } + } + } + + public var circleSize: CGFloat = 8 { + didSet { + indicatorView.snp.updateConstraints { maker in + maker.width.height.equalTo(circleSize) + } + } + } + + public var circleColor: UIColor = .lightGray { + didSet { + indicatorView.backgroundColor = circleColor + } + } + + + // MARK: - Views + + private let indicatorView = RoundView() + + + // MARK: - Setup + + public override func setup() { + super.setup() + + backgroundColor = .clear + indicatorView.backgroundColor = backgroundColor + + addSubview(indicatorView) + indicatorView.snp.makeConstraints { maker in + maker.centerY.equalToSuperview() + maker.left.equalToSuperview().offset(horizontalInset) + maker.right.equalToSuperview().inset(horizontalInset) + maker.width.height.equalTo(circleSize) + } + } +} diff --git a/Frameworks/NynjaUIKit/NynjaUIKit/Views/Typing/TypingIndicatorView.swift b/Frameworks/NynjaUIKit/NynjaUIKit/Views/Typing/TypingIndicatorView.swift new file mode 100644 index 0000000000000000000000000000000000000000..a3946fc2d2e01056eff6871bfd500d4456260f2a --- /dev/null +++ b/Frameworks/NynjaUIKit/NynjaUIKit/Views/Typing/TypingIndicatorView.swift @@ -0,0 +1,112 @@ +// +// TypingIndicatorView.swift +// NynjaUIKit +// +// Created by Anton Poltoratskyi on 25.10.2018. +// Copyright © 2018 TecSynt Solutions. All rights reserved. +// + +import UIKit + +public final class TypingIndicatorView: BaseView { + + public var itemsCount: Int = 3 { + didSet { + invalidateIntrinsicContentSize() + setNeedsLayout() + } + } + + public var itemPadding: CGFloat = 4 { + didSet { + invalidateIntrinsicContentSize() + setNeedsLayout() + } + } + + public var itemSize: CGFloat = 4 { + didSet { + invalidateIntrinsicContentSize() + setNeedsLayout() + } + } + + public var itemColor: UIColor = .lightGray { + didSet { + setupColor() + } + } + + public override var intrinsicContentSize: CGSize { + let width = itemSize * CGFloat(itemsCount) + itemPadding * CGFloat(itemsCount - 1) + return CGSize(width: width, height: itemSize) + } + + + // MARK: - Layers + + public override class var layerClass: AnyClass { + return CAReplicatorLayer.self + } + + private var animationLayer: CAReplicatorLayer { + return layer as! CAReplicatorLayer + } + + private let itemLayer = CAShapeLayer() + + + // MARK: - Setup + + public override func setup() { + super.setup() + animationLayer.addSublayer(itemLayer) + animationLayer.masksToBounds = true + setupColor() + } + + private func setupColor() { + itemLayer.backgroundColor = itemColor.cgColor + } + + + // MARK: - Layout + + public override func layoutSubviews() { + super.layoutSubviews() + + itemLayer.frame.size = CGSize(width: itemSize, height: itemSize) + itemLayer.cornerRadius = itemSize / 2 + + animationLayer.instanceCount = itemsCount + animationLayer.instanceTransform = CATransform3DMakeTranslation(itemSize + itemPadding, 0, 0) + animationLayer.instanceAlphaOffset = Float(Animation.toValue - Animation.fromValue) / Float(itemsCount - 1) + animationLayer.instanceDelay = Animation.duration / Double(itemsCount) + + addAnimation() + } + + + // MARK: - Animation + + private func addAnimation() { + guard !itemLayer.hasAnimation(forKey: Animation.key) else { + return + } + let animation = CABasicAnimation(keyPath: "opacity") + animation.fromValue = Animation.fromValue + animation.toValue = Animation.toValue + animation.duration = Animation.duration + animation.autoreverses = true + animation.repeatCount = .infinity + + itemLayer.add(animation, forKey: Animation.key) + } + + private enum Animation { + static let key = "typing" + static let duration = 0.5 + static let fromValue = 0.5 + static let toValue = 1.0 + } +} diff --git a/Frameworks/NynjaUIKit/NynjaUIKit/Views/Typing/TypingView.swift b/Frameworks/NynjaUIKit/NynjaUIKit/Views/Typing/TypingView.swift new file mode 100644 index 0000000000000000000000000000000000000000..9f001bf3207e4ce69ff6d3a9a6e3a13d2aab5fa6 --- /dev/null +++ b/Frameworks/NynjaUIKit/NynjaUIKit/Views/Typing/TypingView.swift @@ -0,0 +1,149 @@ +// +// TypingView.swift +// NynjaUIKit +// +// Created by Anton Poltoratskyi on 25.10.2018. +// Copyright © 2018 TecSynt Solutions. All rights reserved. +// + +import UIKit +import SnapKit + +public final class TypingView: BaseView { + + // MARK: - Appearance + + public struct Appearance { + public enum Indicator { + case dots(UIColor) + case circle(UIColor) + } + public let indicator: Indicator + public let textColor: UIColor + public let textFont: UIFont + public let senderInfo: String? + public let typingInfo: String + + public init(indicator: Indicator, + textColor: UIColor, + textFont: UIFont, + senderInfo: String?, + typingInfo: String) { + self.indicator = indicator + self.textColor = textColor + self.textFont = textFont + self.senderInfo = senderInfo + self.typingInfo = typingInfo + } + } + + + // MARK: - Views + + private lazy var indicatorContainer: UIView = { + let view = UIView() + view.setContentHuggingPriority(.required, for: .horizontal) + addSubview(view) + return view + }() + + private lazy var senderInfoLabel: UILabel = { + let label = UILabel() + addSubview(label) + return label + }() + + + // MARK: - Setup + + public override func setup() { + super.setup() + + indicatorContainer.snp.makeConstraints { maker in + maker.top.bottom.left.equalToSuperview() + } + + senderInfoLabel.snp.makeConstraints { maker in + maker.top.bottom.right.equalToSuperview() + maker.left.equalTo(indicatorContainer.snp.right).offset(Constraints.senderInfo.leftOffset.adjustedByWidth) + } + } + + + // MARK: - Layout + + public func update(_ appearance: Appearance) { + + + switch appearance.indicator { + case let .dots(color): + setupDotsIndicator(color: color) + case let .circle(color): + setupCircleIndicator(color: color) + } + + senderInfoLabel.font = appearance.textFont + senderInfoLabel.textColor = appearance.textColor + senderInfoLabel.text = appearance.senderInfo + .flatMap { "\($0) \(appearance.typingInfo)" } ?? appearance.typingInfo + } + + private func setupDotsIndicator(color: UIColor) { + guard !indicatorContainer.subviews.contains(where: { $0 is TypingIndicatorView }) else { return } + + indicatorContainer.subviews.forEach { $0.removeFromSuperview() } + + let indicatorView = TypingIndicatorView() + indicatorView.itemColor = color + indicatorView.itemSize = Constraints.indicator.dotsSize.adjustedByWidth + indicatorView.itemPadding = Constraints.indicator.dotsPadding.adjustedByWidth + + indicatorView.setContentCompressionResistancePriority(.required, for: .horizontal) + indicatorView.setContentHuggingPriority(.required, for: .horizontal) + + indicatorContainer.addSubview(indicatorView) + + indicatorView.snp.makeConstraints(makeIndicatorViewConstraints()) + } + + private func setupCircleIndicator(color: UIColor) { + guard !indicatorContainer.subviews.contains(where: { $0 is TypingBoldIndicatorView }) else { return } + + indicatorContainer.subviews.forEach { $0.removeFromSuperview() } + + let indicatorView = TypingBoldIndicatorView() + indicatorContainer.addSubview(indicatorView) + + indicatorView.circleColor = color + indicatorView.circleSize = Constraints.indicator.circleSize.adjustedByWidth + indicatorView.horizontalInset = Constraints.indicator.horizontalInset.adjustedByWidth + + indicatorView.snp.makeConstraints(makeIndicatorViewConstraints()) + } + + private func makeIndicatorViewConstraints() -> (ConstraintMaker) -> Void { + return { maker in + maker.centerY.equalToSuperview().offset(Constraints.indicator.centerVerticalOffset.adjustedByWidth) + maker.left.right.equalToSuperview() + } + } + + + // MARK: - Constraints + + private enum Constraints { + + enum indicator { + static let circleSize: CGFloat = 8 + static let dotsSize: CGFloat = 3 + static let dotsPadding: CGFloat = 4 + + static let horizontalInset: CGFloat = 4 + static let centerVerticalOffset: CGFloat = 1 + } + + enum senderInfo { + static let leftOffset: CGFloat = 4 + } + } +} diff --git a/Frameworks/NynjaUIKit/NynjaUIKit/Views/Utils/RoundView.swift b/Frameworks/NynjaUIKit/NynjaUIKit/Views/Utils/RoundView.swift new file mode 100644 index 0000000000000000000000000000000000000000..9e8c2b633000b443b8bf20ab16712fb0c586ac13 --- /dev/null +++ b/Frameworks/NynjaUIKit/NynjaUIKit/Views/Utils/RoundView.swift @@ -0,0 +1,22 @@ +// +// RoundView.swift +// NynjaUIKit +// +// Created by Anton Poltoratskyi on 25.10.2018. +// Copyright © 2018 TecSynt Solutions. All rights reserved. +// + +import UIKit + +class RoundView: BaseView { + + override func setup() { + super.setup() + layer.masksToBounds = true + } + + override func layoutSubviews() { + super.layoutSubviews() + layer.cornerRadius = min(bounds.width, bounds.height) / 2 + } +} diff --git a/Nynja-Share/Services/Handlers/Base/HandlerFactory.swift b/Nynja-Share/Services/Handlers/Base/HandlerFactory.swift index 200518760cc06c4cefaee8a3f04e2fc412a3440e..f4a7d5195a5884ce8f8ff34bb63264ecfed1c6d0 100644 --- a/Nynja-Share/Services/Handlers/Base/HandlerFactory.swift +++ b/Nynja-Share/Services/Handlers/Base/HandlerFactory.swift @@ -8,21 +8,20 @@ final class HandlerFactory { - static func handler(for handlerType: Handlers) -> BaseHandler.Type { + static func handler(for handlerType: Handlers) -> BaseHandler { switch handlerType { case .profile: - return ProfileHandler.self + return ProfileHandler.shared case .contact: - return ContactHandler.self + return ContactHandler.shared case .message: - return MessageHandler.self + return MessageHandler.shared case .io: - return IoHandler.self + return IoHandler.shared case .errors: - return ErrorsHandler.self + return ErrorsHandler.shared case .auth: - return AuthHandler.self + return AuthHandler.shared } } - } diff --git a/Nynja-Share/Services/Handlers/ContactHandler.swift b/Nynja-Share/Services/Handlers/ContactHandler.swift index a2041fabf5a966ec2a9b7060bf89021a504b9b97..bdf7650af20bdbd611ef38f081938f85fe08aba6 100644 --- a/Nynja-Share/Services/Handlers/ContactHandler.swift +++ b/Nynja-Share/Services/Handlers/ContactHandler.swift @@ -16,11 +16,15 @@ extension ContactHandlerDelegate { } // TODO: need to think about this. It is share extension. -class ContactHandler: BaseHandler { +final class ContactHandler: BaseHandler { - static weak var delegate: ContactHandlerDelegate? + static let shared = ContactHandler() - static func executeHandle(data: BertTuple) { + private init() {} + + weak var delegate: ContactHandlerDelegate? + + func executeHandle(data: BertTuple) { guard let contact = get_Contact().parse(bert: data) as? Contact, contact.originalStatus != nil else { return } diff --git a/Nynja-Share/Services/Handlers/MessageHandler.swift b/Nynja-Share/Services/Handlers/MessageHandler.swift index b0daa836e26b469d689d1165b99f1c503d7a9884..3c2b31d72a086b865cad146c16a6ddac16e75055 100644 --- a/Nynja-Share/Services/Handlers/MessageHandler.swift +++ b/Nynja-Share/Services/Handlers/MessageHandler.swift @@ -16,10 +16,15 @@ extension MessageHandlerDelegate { func getMessageSuccess(message: Message) {} } -class MessageHandler:BaseHandler { - static weak var delegate : MessageHandlerDelegate? +final class MessageHandler: BaseHandler { - static func executeHandle(data: BertTuple) { + static let shared = MessageHandler() + + private init() {} + + weak var delegate: MessageHandlerDelegate? + + func executeHandle(data: BertTuple) { if let message = get_Message().parse(bert: data) as? Message { delegate?.getMessageSuccess(message: message) } diff --git a/Nynja-Share/Services/Handlers/ProfileHandler.swift b/Nynja-Share/Services/Handlers/ProfileHandler.swift index 2000956368165f8817ba5d530754e0585f79cdbf..fbda22977eb82d6e4661117438f96f3d1b16ca37 100644 --- a/Nynja-Share/Services/Handlers/ProfileHandler.swift +++ b/Nynja-Share/Services/Handlers/ProfileHandler.swift @@ -18,10 +18,15 @@ extension ProfileHandlerDelegate { func removeProfileSuccess() {} } -class ProfileHandler:BaseHandler { - static weak var delegate :ProfileHandlerDelegate? +final class ProfileHandler: BaseHandler { - static func executeHandle(data: BertTuple) { + static let shared = ProfileHandler() + + private init() {} + + weak var delegate: ProfileHandlerDelegate? + + func executeHandle(data: BertTuple) { if let profile = get_Profile().parse(bert: data) as? Profile { if let status = profile.status?.string { switch status { @@ -38,5 +43,4 @@ class ProfileHandler:BaseHandler { } } } - } diff --git a/Nynja-Share/UI/ForwardSelector/Interactor/ForwardSelectorInteractor.swift b/Nynja-Share/UI/ForwardSelector/Interactor/ForwardSelectorInteractor.swift index 7acf73a6eab73649e44176aec01d0832447bfd16..9c45854f10ece9c48d4d3116b18312328499b4c5 100644 --- a/Nynja-Share/UI/ForwardSelector/Interactor/ForwardSelectorInteractor.swift +++ b/Nynja-Share/UI/ForwardSelector/Interactor/ForwardSelectorInteractor.swift @@ -64,10 +64,10 @@ final class ForwardSelectorInteractor: ForwardSelectorInteractorInputProtocol, P } private func initialize() { - ProfileHandler.delegate = self - MessageHandler.delegate = self - ContactHandler.delegate = self - IoHandler.delegate = self + ProfileHandler.shared.delegate = self + MessageHandler.shared.delegate = self + ContactHandler.shared.delegate = self + IoHandler.shared.delegate = self amazonInitializer.initialize() setupImageCache() notifyHostApplication() diff --git a/Nynja.xcodeproj/project.pbxproj b/Nynja.xcodeproj/project.pbxproj index c86e8d39c6ea9916c1a005fb360eeeed09857cc3..4232a343ddcd86964678f63014ea577e7983091d 100644 --- a/Nynja.xcodeproj/project.pbxproj +++ b/Nynja.xcodeproj/project.pbxproj @@ -868,6 +868,8 @@ 85150C2620BE9EA3005D311A /* StickerDetailsPreviewView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 85150C2520BE9EA3005D311A /* StickerDetailsPreviewView.swift */; }; 851872BF20CD457F007CD6CA /* StickersProviding.swift in Sources */ = {isa = PBXBuildFile; fileRef = 851872BE20CD457F007CD6CA /* StickersProviding.swift */; }; 851872C120CD45B3007CD6CA /* StickersProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 851872C020CD45B3007CD6CA /* StickersProvider.swift */; }; + 851C6A52218B55AC0062B148 /* ServiceFactoryProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 851C6A51218B55AC0062B148 /* ServiceFactoryProtocol.swift */; }; + 851C6A54218B560B0062B148 /* MQTTHandlerFactoryProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 851C6A53218B560B0062B148 /* MQTTHandlerFactoryProtocol.swift */; }; 851EBD7F20B418890065C644 /* StickersInputView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 851EBD7E20B418890065C644 /* StickersInputView.swift */; }; 852003F620D4194A007C0036 /* DBRecentSticker.swift in Sources */ = {isa = PBXBuildFile; fileRef = 852003F520D4194A007C0036 /* DBRecentSticker.swift */; }; 852003F820D419E9007C0036 /* RecentStickerTable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 852003F720D419E9007C0036 /* RecentStickerTable.swift */; }; @@ -933,6 +935,7 @@ 8540A333211B35A4007F65AF /* MessageCollectionViewDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8540A332211B35A4007F65AF /* MessageCollectionViewDelegate.swift */; }; 8541BD68206CE0220093EF1E /* ImagePlaceholderWheelItemModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8541BD67206CE0220093EF1E /* ImagePlaceholderWheelItemModel.swift */; }; 8541BD6B206CE3A40093EF1E /* ChatPlaceholderWheelItemModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8541BD6A206CE3A40093EF1E /* ChatPlaceholderWheelItemModel.swift */; }; + 8542B812218879B100A286E5 /* TypingDisplayModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8542B811218879B100A286E5 /* TypingDisplayModel.swift */; }; 85433F22204D596D00B373A7 /* WebFullScreenPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 85433F1D204D596D00B373A7 /* WebFullScreenPresenter.swift */; }; 85433F23204D596D00B373A7 /* WebFullScreenViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 85433F1E204D596D00B373A7 /* WebFullScreenViewController.swift */; }; 85433F24204D596D00B373A7 /* WebFullScreenProtocols.swift in Sources */ = {isa = PBXBuildFile; fileRef = 85433F1F204D596D00B373A7 /* WebFullScreenProtocols.swift */; }; @@ -971,6 +974,9 @@ 85482848204EA56600DCBEC8 /* PrivacyListDataSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = 85482847204EA56600DCBEC8 /* PrivacyListDataSource.swift */; }; 8548284F204EDD5900DCBEC8 /* FastScrollable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8548284E204EDD5900DCBEC8 /* FastScrollable.swift */; }; 8548340E207769E800604051 /* DocumentInteractionInput.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8548340D207769E800604051 /* DocumentInteractionInput.swift */; }; + 854834182186FADB002064E1 /* TypingProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 854834172186FADB002064E1 /* TypingProvider.swift */; }; + 8548341B2187449F002064E1 /* ObservableContainer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8548341A2187449F002064E1 /* ObservableContainer.swift */; }; + 8548341D218744AC002064E1 /* Observable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8548341C218744AC002064E1 /* Observable.swift */; }; 854A4B2C2080D68200759152 /* CellWithArrowTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 854A4B2A2080D68200759152 /* CellWithArrowTableViewCell.swift */; }; 854A4B2D2080D68200759152 /* CellWithArrowCellModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 854A4B2B2080D68200759152 /* CellWithArrowCellModel.swift */; }; 854A4B302080D6C400759152 /* CellWithImageTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 854A4B2E2080D6C400759152 /* CellWithImageTableViewCell.swift */; }; @@ -1002,6 +1008,8 @@ 855EF421202CC6F800541BE3 /* GetExtendedStarsModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 855EF420202CC6F800541BE3 /* GetExtendedStarsModel.swift */; }; 855EF423202CC85300541BE3 /* MQTTServiceStars.swift in Sources */ = {isa = PBXBuildFile; fileRef = 855EF422202CC85300541BE3 /* MQTTServiceStars.swift */; }; 855EF425202CCADB00541BE3 /* ExtendedStarHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 855EF424202CCADB00541BE3 /* ExtendedStarHandler.swift */; }; + 8560C4C6218997DD006635AE /* ChatStatus.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8560C4C5218997DD006635AE /* ChatStatus.swift */; }; + 8560C4C8218999E3006635AE /* ChatStatusDisplayInfo.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8560C4C7218999E3006635AE /* ChatStatusDisplayInfo.swift */; }; 8562853220D140FC000C9739 /* InputBar+ButtonType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8562853120D140FC000C9739 /* InputBar+ButtonType.swift */; }; 8562853420D16242000C9739 /* StickerPreviewing.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8562853320D16242000C9739 /* StickerPreviewing.swift */; }; 8562853620D164B5000C9739 /* ScaleAnimatableGrid.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8562853520D164B5000C9739 /* ScaleAnimatableGrid.swift */; }; @@ -1037,7 +1045,6 @@ 8580BACA20BD983400239D9D /* MentionTransitionProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8580BAC520BD983400239D9D /* MentionTransitionProtocol.swift */; }; 8580BACC20BD984500239D9D /* MessageEditInfo.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8580BACB20BD984400239D9D /* MessageEditInfo.swift */; }; 8580BACE20BD98CF00239D9D /* UpdateResult.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8580BACD20BD98CF00239D9D /* UpdateResult.swift */; }; - 8580BAD720BD98E700239D9D /* ChatListMessageAccessoryView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8580BAD120BD98E600239D9D /* ChatListMessageAccessoryView.swift */; }; 8580BAD820BD98E700239D9D /* CounterView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8580BAD220BD98E600239D9D /* CounterView.swift */; }; 8580BAD920BD98E700239D9D /* ChatListMessageTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8580BAD320BD98E600239D9D /* ChatListMessageTableViewCell.swift */; }; 8580BADA20BD98E700239D9D /* ChatListMessageContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8580BAD420BD98E600239D9D /* ChatListMessageContentView.swift */; }; @@ -1096,6 +1103,8 @@ 85CB25DC20D723D300D5E565 /* StickerPackDAOProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 85CB25DB20D723D300D5E565 /* StickerPackDAOProtocol.swift */; }; 85CB25DF20D7325500D5E565 /* StickerPackExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 85CB25DE20D7325500D5E565 /* StickerPackExtension.swift */; }; 85CE26D820C5593600553FE7 /* HapticSelectionFeedbackGenerator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 85CE26D720C5593600553FE7 /* HapticSelectionFeedbackGenerator.swift */; }; + 85CEFBC0218C5D9500760F9E /* TypingObservable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 85CEFBBF218C5D9500760F9E /* TypingObservable.swift */; }; + 85CEFBC5218CAD8F00760F9E /* ChatListMessageIndicatorsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 85CEFBC4218CAD8F00760F9E /* ChatListMessageIndicatorsView.swift */; }; 85D669E420BD956000FBD803 /* Int+AnyObject.swift in Sources */ = {isa = PBXBuildFile; fileRef = 85D669E120BD955F00FBD803 /* Int+AnyObject.swift */; }; 85D669E520BD956000FBD803 /* UIButtonExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 85D669E220BD955F00FBD803 /* UIButtonExtensions.swift */; }; 85D669E620BD956000FBD803 /* UIView+Shadow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 85D669E320BD956000FBD803 /* UIView+Shadow.swift */; }; @@ -1126,6 +1135,9 @@ 85E1DD2520BEBE17008AD211 /* MessageVC+StickerInputModuleDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 85E1DD2420BEBE17008AD211 /* MessageVC+StickerInputModuleDelegate.swift */; }; 85E1DD2720BEE961008AD211 /* ScalableCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 85E1DD2620BEE961008AD211 /* ScalableCell.swift */; }; 85E3AB3D21218A57005FC49A /* SeparatorView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8580BAE620BD9A5600239D9D /* SeparatorView.swift */; }; + 85EB37F321831094003A2D6F /* ChatListMessageTextView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 85EB37F221831094003A2D6F /* ChatListMessageTextView.swift */; }; + 85EB37FB21837235003A2D6F /* KeyedObservable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 85EB37FA21837235003A2D6F /* KeyedObservable.swift */; }; + 85EB37FD21837253003A2D6F /* KeyedObservableContainer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 85EB37FC21837253003A2D6F /* KeyedObservableContainer.swift */; }; 85EBBE052056E8B2009BB269 /* outcoming_message.mp3 in Resources */ = {isa = PBXBuildFile; fileRef = 85EBBE042056E8B2009BB269 /* outcoming_message.mp3 */; }; 85F0866220D6412300A7762E /* RemoteStorageDestination.swift in Sources */ = {isa = PBXBuildFile; fileRef = 85F0866120D6412300A7762E /* RemoteStorageDestination.swift */; }; 85F0866320D6551500A7762E /* RemoteStorageDestination.swift in Sources */ = {isa = PBXBuildFile; fileRef = 85F0866120D6412300A7762E /* RemoteStorageDestination.swift */; }; @@ -3117,6 +3129,8 @@ 85150C2520BE9EA3005D311A /* StickerDetailsPreviewView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StickerDetailsPreviewView.swift; sourceTree = ""; }; 851872BE20CD457F007CD6CA /* StickersProviding.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = StickersProviding.swift; path = Services/StickersProvider/StickersProviding.swift; sourceTree = ""; }; 851872C020CD45B3007CD6CA /* StickersProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = StickersProvider.swift; path = Services/StickersProvider/StickersProvider.swift; sourceTree = ""; }; + 851C6A51218B55AC0062B148 /* ServiceFactoryProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ServiceFactoryProtocol.swift; sourceTree = ""; }; + 851C6A53218B560B0062B148 /* MQTTHandlerFactoryProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MQTTHandlerFactoryProtocol.swift; sourceTree = ""; }; 851EBD7E20B418890065C644 /* StickersInputView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StickersInputView.swift; sourceTree = ""; }; 852003F520D4194A007C0036 /* DBRecentSticker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DBRecentSticker.swift; sourceTree = ""; }; 852003F720D419E9007C0036 /* RecentStickerTable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RecentStickerTable.swift; sourceTree = ""; }; @@ -3171,6 +3185,7 @@ 8540A332211B35A4007F65AF /* MessageCollectionViewDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageCollectionViewDelegate.swift; sourceTree = ""; }; 8541BD67206CE0220093EF1E /* ImagePlaceholderWheelItemModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImagePlaceholderWheelItemModel.swift; sourceTree = ""; }; 8541BD6A206CE3A40093EF1E /* ChatPlaceholderWheelItemModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatPlaceholderWheelItemModel.swift; sourceTree = ""; }; + 8542B811218879B100A286E5 /* TypingDisplayModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TypingDisplayModel.swift; sourceTree = ""; }; 85433F1D204D596D00B373A7 /* WebFullScreenPresenter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WebFullScreenPresenter.swift; sourceTree = ""; }; 85433F1E204D596D00B373A7 /* WebFullScreenViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WebFullScreenViewController.swift; sourceTree = ""; }; 85433F1F204D596D00B373A7 /* WebFullScreenProtocols.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WebFullScreenProtocols.swift; sourceTree = ""; }; @@ -3190,6 +3205,9 @@ 85482847204EA56600DCBEC8 /* PrivacyListDataSource.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PrivacyListDataSource.swift; sourceTree = ""; }; 8548284E204EDD5900DCBEC8 /* FastScrollable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FastScrollable.swift; sourceTree = ""; }; 8548340D207769E800604051 /* DocumentInteractionInput.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DocumentInteractionInput.swift; sourceTree = ""; }; + 854834172186FADB002064E1 /* TypingProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TypingProvider.swift; sourceTree = ""; }; + 8548341A2187449F002064E1 /* ObservableContainer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ObservableContainer.swift; sourceTree = ""; }; + 8548341C218744AC002064E1 /* Observable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Observable.swift; sourceTree = ""; }; 854A4B2A2080D68200759152 /* CellWithArrowTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CellWithArrowTableViewCell.swift; sourceTree = ""; }; 854A4B2B2080D68200759152 /* CellWithArrowCellModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CellWithArrowCellModel.swift; sourceTree = ""; }; 854A4B2E2080D6C400759152 /* CellWithImageTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CellWithImageTableViewCell.swift; sourceTree = ""; }; @@ -3218,6 +3236,8 @@ 855EF420202CC6F800541BE3 /* GetExtendedStarsModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GetExtendedStarsModel.swift; sourceTree = ""; }; 855EF422202CC85300541BE3 /* MQTTServiceStars.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MQTTServiceStars.swift; sourceTree = ""; }; 855EF424202CCADB00541BE3 /* ExtendedStarHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ExtendedStarHandler.swift; sourceTree = ""; }; + 8560C4C5218997DD006635AE /* ChatStatus.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatStatus.swift; sourceTree = ""; }; + 8560C4C7218999E3006635AE /* ChatStatusDisplayInfo.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatStatusDisplayInfo.swift; sourceTree = ""; }; 8562853120D140FC000C9739 /* InputBar+ButtonType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "InputBar+ButtonType.swift"; sourceTree = ""; }; 8562853320D16242000C9739 /* StickerPreviewing.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StickerPreviewing.swift; sourceTree = ""; }; 8562853520D164B5000C9739 /* ScaleAnimatableGrid.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ScaleAnimatableGrid.swift; sourceTree = ""; }; @@ -3251,7 +3271,6 @@ 8580BAC520BD983400239D9D /* MentionTransitionProtocol.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MentionTransitionProtocol.swift; sourceTree = ""; }; 8580BACB20BD984400239D9D /* MessageEditInfo.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MessageEditInfo.swift; sourceTree = ""; }; 8580BACD20BD98CF00239D9D /* UpdateResult.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UpdateResult.swift; sourceTree = ""; }; - 8580BAD120BD98E600239D9D /* ChatListMessageAccessoryView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ChatListMessageAccessoryView.swift; sourceTree = ""; }; 8580BAD220BD98E600239D9D /* CounterView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CounterView.swift; sourceTree = ""; }; 8580BAD320BD98E600239D9D /* ChatListMessageTableViewCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ChatListMessageTableViewCell.swift; sourceTree = ""; }; 8580BAD420BD98E600239D9D /* ChatListMessageContentView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ChatListMessageContentView.swift; sourceTree = ""; }; @@ -3307,6 +3326,8 @@ 85CB25DB20D723D300D5E565 /* StickerPackDAOProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StickerPackDAOProtocol.swift; sourceTree = ""; }; 85CB25DE20D7325500D5E565 /* StickerPackExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StickerPackExtension.swift; sourceTree = ""; }; 85CE26D720C5593600553FE7 /* HapticSelectionFeedbackGenerator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HapticSelectionFeedbackGenerator.swift; sourceTree = ""; }; + 85CEFBBF218C5D9500760F9E /* TypingObservable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TypingObservable.swift; sourceTree = ""; }; + 85CEFBC4218CAD8F00760F9E /* ChatListMessageIndicatorsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatListMessageIndicatorsView.swift; sourceTree = ""; }; 85D669E120BD955F00FBD803 /* Int+AnyObject.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "Int+AnyObject.swift"; sourceTree = ""; }; 85D669E220BD955F00FBD803 /* UIButtonExtensions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UIButtonExtensions.swift; sourceTree = ""; }; 85D669E320BD956000FBD803 /* UIView+Shadow.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "UIView+Shadow.swift"; sourceTree = ""; }; @@ -3334,6 +3355,9 @@ 85D77806211D9B980044E72F /* ScrollPosition.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ScrollPosition.swift; sourceTree = ""; }; 85E1DD2420BEBE17008AD211 /* MessageVC+StickerInputModuleDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "MessageVC+StickerInputModuleDelegate.swift"; sourceTree = ""; }; 85E1DD2620BEE961008AD211 /* ScalableCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ScalableCell.swift; sourceTree = ""; }; + 85EB37F221831094003A2D6F /* ChatListMessageTextView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatListMessageTextView.swift; sourceTree = ""; }; + 85EB37FA21837235003A2D6F /* KeyedObservable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeyedObservable.swift; sourceTree = ""; }; + 85EB37FC21837253003A2D6F /* KeyedObservableContainer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeyedObservableContainer.swift; sourceTree = ""; }; 85EBBE042056E8B2009BB269 /* outcoming_message.mp3 */ = {isa = PBXFileReference; lastKnownFileType = audio.mp3; path = outcoming_message.mp3; sourceTree = ""; }; 85F0866120D6412300A7762E /* RemoteStorageDestination.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RemoteStorageDestination.swift; sourceTree = ""; }; 85F3DD43203F410D00F210C0 /* TimerHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimerHandler.swift; sourceTree = ""; }; @@ -6023,6 +6047,8 @@ 8509AC61206A54420089089B /* ResponseResult.swift */, B7F4C2AA211995A500E48A98 /* Validation */, A42C44E220F340DA00BC3CBB /* StatusCodeManager.swift */, + 8548341921874434002064E1 /* Observable */, + 85EB37F9218365A6003A2D6F /* Statuses */, ); name = Services; sourceTree = ""; @@ -8460,6 +8486,17 @@ path = Documents; sourceTree = ""; }; + 8548341921874434002064E1 /* Observable */ = { + isa = PBXGroup; + children = ( + 8548341C218744AC002064E1 /* Observable.swift */, + 8548341A2187449F002064E1 /* ObservableContainer.swift */, + 85EB37FA21837235003A2D6F /* KeyedObservable.swift */, + 85EB37FC21837253003A2D6F /* KeyedObservableContainer.swift */, + ); + path = Observable; + sourceTree = ""; + }; 854A4B392080E5D500759152 /* TableView */ = { isa = PBXGroup; children = ( @@ -8612,6 +8649,32 @@ path = CollectionView; sourceTree = ""; }; + 8560C4C221899793006635AE /* Typing */ = { + isa = PBXGroup; + children = ( + A45F10BE20B4218D00F45004 /* ActionStatus.swift */, + A45F10BC20B4218D00F45004 /* RecordingStatus.swift */, + A45F10BF20B4218D00F45004 /* SendingStatus.swift */, + ); + path = Typing; + sourceTree = ""; + }; + 8560C4C32189979B006635AE /* Internet */ = { + isa = PBXGroup; + children = ( + A45F10BD20B4218D00F45004 /* InternetStatus.swift */, + ); + path = Internet; + sourceTree = ""; + }; + 8560C4C4218997A4006635AE /* Presence */ = { + isa = PBXGroup; + children = ( + A45F10BB20B4218D00F45004 /* PresenceStatus.swift */, + ); + path = Presence; + sourceTree = ""; + }; 8562853720D164BE000C9739 /* ScaleAnimation */ = { isa = PBXGroup; children = ( @@ -8740,9 +8803,10 @@ 8580BAD020BD98E600239D9D /* Cell */ = { isa = PBXGroup; children = ( - 8580BAD120BD98E600239D9D /* ChatListMessageAccessoryView.swift */, 8580BAD320BD98E600239D9D /* ChatListMessageTableViewCell.swift */, 8580BAD420BD98E600239D9D /* ChatListMessageContentView.swift */, + 85EB37F221831094003A2D6F /* ChatListMessageTextView.swift */, + 85CEFBC4218CAD8F00760F9E /* ChatListMessageIndicatorsView.swift */, 8580BAD220BD98E600239D9D /* CounterView.swift */, ); path = Cell; @@ -8751,8 +8815,8 @@ 8580BAD520BD98E600239D9D /* Model */ = { isa = PBXGroup; children = ( - 850C0B5320E0369E003341D0 /* ChatListMessageCellModelDelegate.swift */, 8580BAD620BD98E600239D9D /* ChatListMessageCellModel.swift */, + 850C0B5320E0369E003341D0 /* ChatListMessageCellModelDelegate.swift */, ); path = Model; sourceTree = ""; @@ -9172,6 +9236,16 @@ path = BBCode; sourceTree = ""; }; + 85EB37F9218365A6003A2D6F /* Statuses */ = { + isa = PBXGroup; + children = ( + 854834172186FADB002064E1 /* TypingProvider.swift */, + 85CEFBBF218C5D9500760F9E /* TypingObservable.swift */, + 8542B811218879B100A286E5 /* TypingDisplayModel.swift */, + ); + path = Statuses; + sourceTree = ""; + }; 85EF7C342090DEFF0090C418 /* Models */ = { isa = PBXGroup; children = ( @@ -10318,11 +10392,11 @@ A45F10BA20B4218D00F45004 /* Statuses */ = { isa = PBXGroup; children = ( - A45F10BB20B4218D00F45004 /* PresenceStatus.swift */, - A45F10BC20B4218D00F45004 /* RecordingStatus.swift */, - A45F10BD20B4218D00F45004 /* InternetStatus.swift */, - A45F10BE20B4218D00F45004 /* ActionStatus.swift */, - A45F10BF20B4218D00F45004 /* SendingStatus.swift */, + 8560C4C5218997DD006635AE /* ChatStatus.swift */, + 8560C4C7218999E3006635AE /* ChatStatusDisplayInfo.swift */, + 8560C4C4218997A4006635AE /* Presence */, + 8560C4C32189979B006635AE /* Internet */, + 8560C4C221899793006635AE /* Typing */, ); path = Statuses; sourceTree = ""; @@ -12833,6 +12907,8 @@ F11786EF20AC5474007A9A1B /* ServiceFactory */ = { isa = PBXGroup; children = ( + 851C6A51218B55AC0062B148 /* ServiceFactoryProtocol.swift */, + 851C6A53218B560B0062B148 /* MQTTHandlerFactoryProtocol.swift */, F11786F020AC5482007A9A1B /* ServiceFactory.swift */, ); name = ServiceFactory; @@ -14817,6 +14893,7 @@ F119E66E20D24BBF0043A532 /* MultiplePreviewWireframe.swift in Sources */, 2648C40F2069B52100863614 /* ChangeNumberStep3Presenter.swift in Sources */, A42D51A0206A361400EEB952 /* reader.swift in Sources */, + 8560C4C8218999E3006635AE /* ChatStatusDisplayInfo.swift in Sources */, A42D52BB206A53AA00EEB952 /* Vox_Spec.swift in Sources */, A418DA3420ED0D1300FE780B /* CountView.swift in Sources */, 4B8FC3082163ABC300602D6B /* Desc+Sticker.swift in Sources */, @@ -14873,6 +14950,7 @@ A45F112920B4218D00F45004 /* MessageContentAppearance.swift in Sources */, C9C694F9201FA4AB00A57297 /* SlideAnimatedTransitioning.swift in Sources */, FE2D7CCD211C71AE00520D78 /* WalletService.swift in Sources */, + 85CEFBC5218CAD8F00760F9E /* ChatListMessageIndicatorsView.swift in Sources */, 4BE2C5E22142EB0F00A73DD9 /* AudioManager.swift in Sources */, E7598F5B1FA1D5D90082FBE7 /* ProfileActionCellLayout.swift in Sources */, 85082DDD2045A873000AE4B2 /* UserSettingsService.swift in Sources */, @@ -15059,6 +15137,7 @@ 26342CA920ECBAEF00D2196B /* TranscribeNetworkClient.swift in Sources */, 852003F620D4194A007C0036 /* DBRecentSticker.swift in Sources */, 267BE2831FDE905D00C47E18 /* SettingsProtocols.swift in Sources */, + 851C6A52218B55AC0062B148 /* ServiceFactoryProtocol.swift in Sources */, 264638231FFFE269002590E6 /* RepliesHeaderView.swift in Sources */, 263D66331FE8D95100A509F8 /* TypingHandler.swift in Sources */, 4B8996F5204EF75500DCB183 /* FeedDAOProtocol.swift in Sources */, @@ -15181,7 +15260,6 @@ A415132220DBD59B00C2C01F /* Link_Spec.swift in Sources */, 85433F25204D596D00B373A7 /* WebFullScreenInteractor.swift in Sources */, 8504DEA920693588006722AC /* MediaFullWheelItemModel.swift in Sources */, - 8580BAD720BD98E700239D9D /* ChatListMessageAccessoryView.swift in Sources */, 6F3F21025258D8071BCF95EF /* LoginWireframe.swift in Sources */, 26DCB2522064BA46001EF0AB /* ContactsInteractor.swift in Sources */, B767F48F215D1E0A00FA9B27 /* ComingSoonExtension.swift in Sources */, @@ -15252,6 +15330,8 @@ A45F112420B4218D00F45004 /* MessageTextView.swift in Sources */, 85D66A0420BD963C00FBD803 /* MessagePayloadBuilder.swift in Sources */, 004581212036073100F8E413 /* JobMessageTable.swift in Sources */, + 85EB37F321831094003A2D6F /* ChatListMessageTextView.swift in Sources */, + 85EB37FD21837253003A2D6F /* KeyedObservableContainer.swift in Sources */, 8572C3B62092315B00E4840C /* CollectionViewDataProxy.swift in Sources */, A45F110520B4218D00F45004 /* DisplayChatConfiguration.swift in Sources */, E7598F681FA1D8B90082FBE7 /* ProfileScheduledMesssageCell.swift in Sources */, @@ -15405,6 +15485,7 @@ E7E06C681F792B0200BFC8FA /* LoginWheelContainerDataSource.swift in Sources */, 854751492093BDD300F8D5F8 /* CollectionViewScrollProxy.swift in Sources */, 850C301B204DA87A00DB26C2 /* PrivacyListPresenter.swift in Sources */, + 8542B812218879B100A286E5 /* TypingDisplayModel.swift in Sources */, 267BE28E1FDE9FCC00C47E18 /* SettingsGroupWireFrame.swift in Sources */, 85BDD2B821465EFA00695DE5 /* ScrollDirection.swift in Sources */, A4679BA620B2DD0F0021FE9C /* SubscribersSelectorWireFrame.swift in Sources */, @@ -15414,6 +15495,7 @@ F117871020ACF018007A9A1B /* CameraQualitySettingsProtocols.swift in Sources */, A44B4D5920CE9BDF00CA700A /* ImageCellViewModel.swift in Sources */, A415132020DBD58900C2C01F /* Link.swift in Sources */, + 85EB37FB21837235003A2D6F /* KeyedObservable.swift in Sources */, 852DF263203720E600A4F8B6 /* FileIcons.swift in Sources */, A43B25DB20AB1EE400FF8107 /* NewChannelInteractor.swift in Sources */, FBCE840F20E525A6003B7558 /* HTTPParameters.swift in Sources */, @@ -15423,6 +15505,7 @@ 8580BADA20BD98E700239D9D /* ChatListMessageContentView.swift in Sources */, E757B53D1FE9225C00467BA2 /* TypingExtension.swift in Sources */, C940514C204C7FAF00D72B04 /* DataAndStorageInteractor.swift in Sources */, + 8560C4C6218997DD006635AE /* ChatStatus.swift in Sources */, F1A9FA3590CC1F834B727955 /* AddContactPresenter.swift in Sources */, 6DD72F601F1547AC008CFF83 /* GCD.swift in Sources */, A49CC1D820E4AB2C00879D41 /* DisplayModeConfigFactory.swift in Sources */, @@ -15624,6 +15707,7 @@ A42D52C4206A53AA00EEB952 /* error2_Spec.swift in Sources */, 85458CD9212D6FED00BA8814 /* String+Split.swift in Sources */, 00E9824E205C2604008BF03D /* SessionItemView.swift in Sources */, + 85CEFBC0218C5D9500760F9E /* TypingObservable.swift in Sources */, 8520040920D4F9B4007C0036 /* MessageStickerRepliedView.swift in Sources */, 00102F40202C8E5300A877A9 /* NynjaCalendarView.swift in Sources */, 85150C2620BE9EA3005D311A /* StickerDetailsPreviewView.swift in Sources */, @@ -15636,6 +15720,7 @@ 850571222050B0AD00EDF794 /* NotificationAlertSoundsViewController.swift in Sources */, 6D6731101F29E1F4003E8F8F /* BottomCallView.swift in Sources */, 26245F40204EF58E00C8D3DD /* BaseViewProtocol.swift in Sources */, + 8548341D218744AC002064E1 /* Observable.swift in Sources */, 4B1D7DFE2029C41C00703228 /* AboutItemsFactory.swift in Sources */, A4688DFC20652DE30013660D /* StorageChange.swift in Sources */, 5683555B8382F7F37FEE1AF5 /* ProfileWireframe.swift in Sources */, @@ -15900,6 +15985,7 @@ E70938371FBEDA2B006CCDC6 /* ProfileTable.swift in Sources */, A416DA602075341C00FBF1BA /* CLLocationCoordinate2D+Payload.swift in Sources */, A4679BAE20B2DD100021FE9C /* SubscribersSelectorInteractor.swift in Sources */, + 8548341B2187449F002064E1 /* ObservableContainer.swift in Sources */, FEA655FD2167777F00B44029 /* TransferDetailsInteractor.swift in Sources */, E70F78B91FD6C64E00385565 /* ChatCheckpointTable.swift in Sources */, 4B06D30620287060003B275B /* WCDataManagerProtocol.swift in Sources */, @@ -16299,6 +16385,7 @@ 850D220020D2E7E20018BBA4 /* SelectionFeedbackInteractive.swift in Sources */, 43711F24FF65C36730467BFF /* EditPhotoViewController.swift in Sources */, A42D519F206A361400EEB952 /* messageEvent.swift in Sources */, + 854834182186FADB002064E1 /* TypingProvider.swift in Sources */, F11DF06520BD96D000F3E005 /* GalleryFilterType.swift in Sources */, FBCE841220E525A6003B7558 /* NetworkClient.swift in Sources */, 7A8FE56A8E5D02256D8BE936 /* EditPhotoPresenter.swift in Sources */, @@ -16485,6 +16572,7 @@ 8505445720627C7C00E0F2B3 /* HistoryCellModel.swift in Sources */, 2F2A5C12A7202E7834F923DC /* GroupRulesWireframe.swift in Sources */, 2625DBF820EFC5DE00E01C05 /* FourCharCode+StringLiteralConvertible.swift in Sources */, + 851C6A54218B560B0062B148 /* MQTTHandlerFactoryProtocol.swift in Sources */, D3A30AF05BD7C46A9A8C1FC1 /* GroupStorageProtocols.swift in Sources */, 8520040720D4F436007C0036 /* StickerPreviewConfig.swift in Sources */, F1607B1D20B20F7800BDF60A /* GridView.swift in Sources */, diff --git a/Nynja/AuthHandler.swift b/Nynja/AuthHandler.swift index c5ec8f5f2faab78ae37eaca7954d6a64fae8cd6f..ca8b88591b57d8b97500ed0db13df23135b7f820 100644 --- a/Nynja/AuthHandler.swift +++ b/Nynja/AuthHandler.swift @@ -18,11 +18,15 @@ extension AuthHandlerDelegate { func processDelete(auth: Auth) {} } -class AuthHandler: BaseHandler { +final class AuthHandler: BaseHandler { - static weak var delegate: AuthHandlerDelegate? + static let shared = AuthHandler() - static func executeHandle(data: BertTuple) { + private init() {} + + weak var delegate: AuthHandlerDelegate? + + func executeHandle(data: BertTuple) { guard let auth = get_Auth().parse(bert: data) as? Auth else {return} guard let type = StringAtom.string(auth.type) else {return} if type == "deleted" { @@ -36,7 +40,7 @@ class AuthHandler: BaseHandler { } } - static func executeHandle(data: BertList) { + func executeHandle(data: BertList) { let auths = data.elements.compactMap { get_Auth().parse(bert: $0) as? Auth } delegate?.processGetAll(auths: auths) } diff --git a/Nynja/ChatService/SenderService.swift b/Nynja/ChatService/SenderService.swift index c82826118553ddb16bc0d37adc1d6e6b427cbece..7c1d019547e824b39cf5693cfa9e18c7bec4c383 100644 --- a/Nynja/ChatService/SenderService.swift +++ b/Nynja/ChatService/SenderService.swift @@ -16,7 +16,7 @@ final class SenderService: InitializeInjectable { init(dependencies: Dependencies) { mqttService = dependencies.mqttService - IoHandler.delegate = self + IoHandler.shared.delegate = self } @@ -35,7 +35,7 @@ final class SenderService: InitializeInjectable { } func updateSubscribes() { - IoHandler.delegate = self + IoHandler.shared.delegate = self } } diff --git a/Nynja/DB/Models/DBMember.swift b/Nynja/DB/Models/DBMember.swift index 2b456cbcb55bab7d3f707ed44e840a73af12b98c..1ef475ada08446711a226c1c08a4a112bc5dbecc 100644 --- a/Nynja/DB/Models/DBMember.swift +++ b/Nynja/DB/Models/DBMember.swift @@ -176,6 +176,10 @@ class DBMember: Record, DBModelProtocol { return member } + static func memberAlias(from db: Database, roomId: String, phoneId: String) throws -> String? { + return try requestAlias(roomId: roomId, phoneId: phoneId).fetchOne(db) + } + private func construct(_ db: Database) throws { let memberId = "\(self.id)" self.features = (try? DBMember.requestFeature(targetId: memberId).fetchAll(db)) ?? [] @@ -202,16 +206,35 @@ class DBMember: Record, DBModelProtocol { } // MARK: - Requests + static private func requestFeature(targetId: String) -> QueryInterfaceRequest { return DBFeature.request(targetId: targetId, targetType: DBFeature.TargetType.member) } + static private func requestAlias(roomId: String, phoneId: String) -> AnyTypedRequest { + let sql = sqlMember(roomId: roomId, phoneId: phoneId, selection: MemberTable.Column.alias.title) + return SQLRequest(sql).asRequest(of: String.self) + } + static private func requestMember(roomId: String, phoneId: String) -> AnyTypedRequest { + let sql = sqlMember(roomId: roomId, phoneId: phoneId) + return SQLRequest(sql).asRequest(of: DBMember.self) + } + + static private func requestMember(roomId: String, isAdmin: Bool) -> AnyTypedRequest { + let sql = sqlAdmin(roomId: roomId, isAdmin: isAdmin) + return SQLRequest(sql).asRequest(of: DBMember.self) + } + + + // MARK: SQL + + static private func sqlMember(roomId: String, phoneId: String, selection: String = "*") -> String { let memberTable = MemberTable.name let roomMemberTable = RoomMemberTable.name let sql = """ - SELECT \(memberTable).* + SELECT \(memberTable).\(selection) FROM \(roomMemberTable) LEFT JOIN \(memberTable) ON \(roomMemberTable).\(RoomMemberTable.Column.memberId.title) = \(memberTable).\(MemberTable.Column.id.title) @@ -219,23 +242,22 @@ class DBMember: Record, DBModelProtocol { AND \(MemberTable.Column.phoneId.title) = '\(phoneId)' """ - return SQLRequest(sql).asRequest(of: DBMember.self) + return sql } - static private func requestMember(roomId: String, isAdmin: Bool) -> AnyTypedRequest { + static private func sqlAdmin(roomId: String, isAdmin: Bool) -> String { let memberTable = MemberTable.name let roomMemberTable = RoomMemberTable.name let sql = """ - SELECT \(memberTable).* - FROM \(roomMemberTable) - LEFT JOIN \(memberTable) ON \(roomMemberTable).\(RoomMemberTable.Column.memberId.title) = - \(memberTable).\(MemberTable.Column.id.title) - WHERE \(RoomMemberTable.Column.roomId.title) = '\(roomId)' - AND \(RoomMemberTable.Column.isAdmin.title) = \(isAdmin ? 1 : 0) - """ + SELECT \(memberTable).* + FROM \(roomMemberTable) + LEFT JOIN \(memberTable) ON \(roomMemberTable).\(RoomMemberTable.Column.memberId.title) = + \(memberTable).\(MemberTable.Column.id.title) + WHERE \(RoomMemberTable.Column.roomId.title) = '\(roomId)' + AND \(RoomMemberTable.Column.isAdmin.title) = \(isAdmin ? 1 : 0) + """ - return SQLRequest(sql).asRequest(of: DBMember.self) + return sql } - } diff --git a/Nynja/ExtendedStarHandler.swift b/Nynja/ExtendedStarHandler.swift index 0322062f2e031322ad7f8afdbbb6ff2e5b64bf09..f0dc2beb4cebac87220cf0b692c72d04f8c48f72 100644 --- a/Nynja/ExtendedStarHandler.swift +++ b/Nynja/ExtendedStarHandler.swift @@ -10,7 +10,16 @@ import Foundation final class ExtendedStarHandler: BaseHandler { - static func executeHandle(data: BertList) { + // MARK: - Singleton + + static let shared = ExtendedStarHandler() + + private init() {} + + + // MARK: - Handler + + func executeHandle(data: BertList) { let extendedStars = data.elements.compactMap { get_ExtendedStar().parse(bert: $0) as? ExtendedStar } let stars = extendedStars.compactMap { extendedStar -> DBStar? in @@ -24,5 +33,4 @@ final class ExtendedStarHandler: BaseHandler { try? StorageService.sharedInstance.perform(action: .save, with: stars) } - } diff --git a/Nynja/Extensions/Models/StarExtension.swift b/Nynja/Extensions/Models/StarExtension.swift index 95cbaf53a88b93451596b8e51355680792934dbf..450e052fcbe5856abb5810e61a9ff66d3db45054 100644 --- a/Nynja/Extensions/Models/StarExtension.swift +++ b/Nynja/Extensions/Models/StarExtension.swift @@ -12,6 +12,10 @@ extension Star: DialogCellModel { private static let deletedAccountTitle = MessageSender.deleted.fullname + var feedId: String! { + return message?.chatId + } + var title: String! { if let feedName = self.message?.feedName { let sender = self.message?.senderName ?? Star.deletedAccountTitle.localized diff --git a/Nynja/Generated/LocalizableConstants.swift b/Nynja/Generated/LocalizableConstants.swift index 361ff5bfb02bf8b4558fb996008433d36e663ddc..056de23857f46e2fecab79849c897863f8466f48 100644 --- a/Nynja/Generated/LocalizableConstants.swift +++ b/Nynja/Generated/LocalizableConstants.swift @@ -678,12 +678,18 @@ internal extension String { static var messageDelay: String { return localizable.tr("Localizable", "message_delay") } /// New messages static var messageNewMessages: String { return localizable.tr("Localizable", "message_new_messages") } - /// ...sending a + /// sending static var messageSending: String { return localizable.tr("Localizable", "message_sending") } /// edited static var messageStatusEdited: String { return localizable.tr("Localizable", "message_status_edited") } - /// ...typing + /// typing static var messageStatusTyping: String { return localizable.tr("Localizable", "message_status_typing") } + /// %@ people + static func messageTypingStatusPeople(_ p1: String) -> String { + return localizable.tr("Localizable", "message_typing_status_people", p1) + } + /// ... + static var messageTypingStatusUndefined: String { return localizable.tr("Localizable", "message_typing_status_undefined") } /// meters static var meters: String { return localizable.tr("Localizable", "meters") } /// microphone @@ -864,7 +870,7 @@ internal extension String { static var questionEndCall: String { return localizable.tr("Localizable", "question_end_call") } /// Are you sure you want to leave the call? static var questionEndCallP2p: String { return localizable.tr("Localizable", "question_end_call_p2p") } - /// ...recording a + /// recording static var recording: String { return localizable.tr("Localizable", "recording") } /// Remove static var remove: String { return localizable.tr("Localizable", "remove") } diff --git a/Nynja/HandlerFactory.swift b/Nynja/HandlerFactory.swift index c94fbcbee74bc48dd6bffb23471d432beba6e071..4f855ddb8466aa5e1dce6458fbbc46bf7cf60774 100644 --- a/Nynja/HandlerFactory.swift +++ b/Nynja/HandlerFactory.swift @@ -8,40 +8,40 @@ final class HandlerFactory { - static func handler(for handlerType: Handlers) -> BaseHandler.Type { + static func handler(for handlerType: Handlers) -> BaseHandler { switch handlerType { case .io: - return IoHandler.self + return IoHandler.shared case .profile: - return ProfileHandler.self + return ProfileHandler.shared case .roster: - return RosterHandler.self + return RosterHandler.shared case .contact: - return ContactHandler.self + return ContactHandler.shared case .history: - return HistoryHandler.self + return HistoryHandler.shared case .message: - return MessageHandler.self + return MessageHandler.shared case .search: - return SearchHandler.self + return SearchHandler.shared case .room: - return RoomHandler.self + return RoomHandler.shared case .member: - return MemberHandler.self + return MemberHandler.shared case .typing: - return TypingHandler.self + return TypingHandler.shared case .star: - return StarHandler.self + return StarHandler.shared case .job: - return JobHandler.self + return JobHandler.shared case .extendedStar: - return ExtendedStarHandler.self + return ExtendedStarHandler.shared case .auth: - return AuthHandler.self + return AuthHandler.shared case .link: - return LinkHandler.self + return LinkHandler.shared case .errors: - return ErrorsHandler.self + return ErrorsHandler.shared } } diff --git a/Nynja/JobHandler.swift b/Nynja/JobHandler.swift index cbf72bb791a2cebc289622668adfc32f874e154b..c9b40eaf4c1f01753b0524045238ad2e7552a372 100644 --- a/Nynja/JobHandler.swift +++ b/Nynja/JobHandler.swift @@ -6,9 +6,18 @@ // Copyright © 2018 TecSynt Solutions. All rights reserved. // -class JobHandler: BaseHandler { +final class JobHandler: BaseHandler { - static func executeHandle(data: BertTuple) { + // MARK: - Singleton + + static let shared = JobHandler() + + private init() {} + + + // MARK: - Handler + + func executeHandle(data: BertTuple) { guard let job = get_Job().parse(bert: data) as? Job, let status = StringAtom.string(job.status) else { return @@ -23,6 +32,5 @@ class JobHandler: BaseHandler { break } } - } diff --git a/Nynja/Library/UI/Lists/TableView/Cells/ChatListMessageCell/Cell/ChatListMessageContentView.swift b/Nynja/Library/UI/Lists/TableView/Cells/ChatListMessageCell/Cell/ChatListMessageContentView.swift index c0c44cc226b785112da28533472b7ef70c3a50e5..afbdb2a0aa24981d92db926eb1d551bafa983024 100644 --- a/Nynja/Library/UI/Lists/TableView/Cells/ChatListMessageCell/Cell/ChatListMessageContentView.swift +++ b/Nynja/Library/UI/Lists/TableView/Cells/ChatListMessageCell/Cell/ChatListMessageContentView.swift @@ -8,19 +8,14 @@ import UIKit import SnapKit +import NynjaUIKit final class ChatListMessageContentView: BaseView { - private static let contentFont = UIFont.makeFont(with: FontFamily.NotoSans.regular.name, - height: Constraints.contentLabel.height.adjustedByWidth)! + private static let dateFormatter = DialogDateConverter() - private static let contentBoldFont = UIFont.makeFont(with: FontFamily.NotoSans.bold.name, - height: Constraints.contentLabel.height.adjustedByWidth)! - - // MARK: - Views - private var contentLeftSuperviewConstraint: Constraint? - private var contentLeftContentTypeConstraint: Constraint? + // MARK: - Views private(set) lazy var titleLabel: UILabel = { let height = Constraints.titleLabel.height.adjustedByWidth @@ -28,50 +23,78 @@ final class ChatListMessageContentView: BaseView { let label = UILabel(height: height, color: color, fontName: FontFamily.NotoSans.medium.name) label.accessibilityIdentifier = "chat_name" + addSubview(label) label.snp.makeConstraints { maker in - maker.top.left.right.equalToSuperview() + maker.top.left.equalToSuperview() maker.height.equalTo(height) } return label }() - private(set) lazy var contentLabel: AlignableLabel = { - let height = Constraints.contentLabel.height.adjustedByWidth - let contentIconInset = Constraints.contentLabel.contentTypeIconInset.adjustedByWidth + private(set) lazy var timeLabel: UILabel = { + let leftInfset = Constraints.timeLabel.leftInset.adjustedByWidth + + let height = Constraints.timeLabel.height.adjustedByWidth let color = UIColor.nynja.manatee - let label = AlignableLabel(height: height, color: color, fontName: FontFamily.NotoSans.regular.name) - label.verticalAlignement = .top - label.numberOfLines = 1 + let label = UILabel(height: height, color: color, fontName: FontFamily.NotoSans.regular.name, textAlignment: .right) + label.setContentCompressionResistancePriority(.required, for: .horizontal) + label.setContentHuggingPriority(.required, for: .horizontal) addSubview(label) label.snp.makeConstraints { maker in - maker.top.equalTo(titleLabel.snp.bottom) - contentLeftSuperviewConstraint = maker.left.equalToSuperview() - .constraint - contentLeftContentTypeConstraint = maker.left.equalTo(contentTypeImageView.snp.right).offset(contentIconInset) - .constraint - maker.bottom.right.equalToSuperview() + maker.top.right.equalToSuperview() + maker.left.equalTo(titleLabel.snp.right).offset(leftInfset) maker.height.equalTo(height) } return label }() + + private(set) lazy var textView: ChatListMessageTextView = { + let textView = ChatListMessageTextView() + + addSubview(textView) + textView.snp.makeConstraints { maker in + maker.top.equalTo(titleLabel.snp.bottom) + maker.bottom.left.equalToSuperview() + } + + return textView + }() - private(set) lazy var contentTypeImageView: UIImageView = { - let size = Constraints.contentTypeImageView.size.adjustedByWidth - let imageView = UIImageView() + private(set) lazy var typingView: TypingView = { + let height = Constraints.typingView.height.adjustedByWidth - addSubview(imageView) - imageView.snp.makeConstraints { maker in - maker.left.equalToSuperview() + let typingView = TypingView() + + addSubview(typingView) + typingView.snp.makeConstraints { maker in maker.top.equalTo(titleLabel.snp.bottom) - maker.width.height.equalTo(size) + maker.left.equalToSuperview() + maker.height.equalTo(height) } - return imageView + return typingView + }() + + private(set) lazy var indicatorsView: ChatListMessageIndicatorsView = { + let leftInfset = Constraints.indicatorsView.leftInset.adjustedByWidth + let topInset = Constraints.indicatorsView.topInset.adjustedByWidth + + let indicatorsView = ChatListMessageIndicatorsView() + + addSubview(indicatorsView) + indicatorsView.snp.makeConstraints { maker in + maker.top.equalTo(timeLabel.snp.bottom).offset(topInset) + maker.right.equalToSuperview() + maker.left.equalTo(textView.snp.right).offset(leftInfset) + maker.left.equalTo(typingView.snp.right).offset(leftInfset) + } + + return indicatorsView }() @@ -79,20 +102,7 @@ final class ChatListMessageContentView: BaseView { override func baseSetup() { super.baseSetup() - contentLabel.isHidden = false - hideImageView() - } - - func showImageView() { - contentTypeImageView.isHidden = false - contentLeftSuperviewConstraint?.deactivate() - contentLeftContentTypeConstraint?.activate() - } - - func hideImageView() { - contentTypeImageView.isHidden = true - contentLeftSuperviewConstraint?.activate() - contentLeftContentTypeConstraint?.deactivate() + indicatorsView.isHidden = false } func setupTitle(_ title: String?) { @@ -100,60 +110,53 @@ final class ChatListMessageContentView: BaseView { titleLabel.accessibilityValue = title } - func setupContentTypeImage(_ image: UIImage?) { - contentTypeImageView.image = image + func setup(sender: String?, image: UIImage?, text: String?) { + textView.setup(sender: sender, image: image, text: text) } - func setupContent(_ text: String?) { - contentLabel.text = text + func setup(date: Date) { + timeLabel.text = type(of: self).dateFormatter.toString(date) } - func setupContent(sender: String, text: String) { - let defaultAttributes: [NSAttributedStringKey: Any] = [ - .foregroundColor: UIColor.nynja.manatee, - .font: type(of: self).contentFont - ] - let boldAttributes: [NSAttributedStringKey: Any] = [ - .foregroundColor: UIColor.nynja.manatee, - .font: type(of: self).contentBoldFont - ] - - let boldText = "\(sender):" - let resultText = "\(boldText) \(text)" - - let attributedText = NSMutableAttributedString(string: resultText, attributes: defaultAttributes) - - let boldRange = (boldText.startIndex.. BertObject { let topic = BertAtom(fromString: "Typing") - let _phone_id = Bert.getBin(self.phone_id) - var _comments: BertObject = BertNil() - if let com = self.comments as? String { - _comments = Bert.getBin(com) + + let feedId = Bert.getBin(self.feed_id) + let senderId = Bert.getBin(self.sender_id) + let senderAlias = Bert.getBin(self.sender_alias) + + let comments: BertObject + if let _comments = self.comments as? String { + comments = Bert.getBin(_comments) + } else { + comments = BertNil() } - return BertTuple(fromElements: [topic, _phone_id, _comments]) + return BertTuple(fromElements: [topic, feedId, senderId, senderAlias, comments]) } } - diff --git a/Nynja/MemberHandler.swift b/Nynja/MemberHandler.swift index 361d24a8bb912bcf30904ab72ba52ca8e1b1c7b1..f81c1b922293f8b409441c15f5835223c6259239 100644 --- a/Nynja/MemberHandler.swift +++ b/Nynja/MemberHandler.swift @@ -6,9 +6,18 @@ // Copyright © 2017 TecSynt Solutions. All rights reserved. // -class MemberHandler: BaseHandler { +final class MemberHandler: BaseHandler { - static func executeHandle(data: BertTuple) { + // MARK: - Singleton + + static let shared = MemberHandler() + + private init() {} + + + // MARK: - Handler + + func executeHandle(data: BertTuple) { guard let member = get_Member().parse(bert: data) as? Member, let status = (member.status as? StringAtom)?.string else { return @@ -24,8 +33,6 @@ class MemberHandler: BaseHandler { default: return } - } - } diff --git a/Nynja/Modules/AddContact/Interactor/AddContactInteractor.swift b/Nynja/Modules/AddContact/Interactor/AddContactInteractor.swift index 170fa07f9578d6d48b227e34a879ff9450e928e8..f21673f79e970bae7a1c789e52b7ef745ee32b1e 100644 --- a/Nynja/Modules/AddContact/Interactor/AddContactInteractor.swift +++ b/Nynja/Modules/AddContact/Interactor/AddContactInteractor.swift @@ -11,7 +11,7 @@ class AddContactInteractor: AddContactInteractorInputProtocol, IoHandlerDelegate weak var presenter: AddContactInteractorOutputProtocol! init() { - IoHandler.delegate = self + IoHandler.shared.delegate = self } func addContact(contact: Contact) { diff --git a/Nynja/Modules/AddContactByUsername/Interactor/AddContactByUsernameInteractor.swift b/Nynja/Modules/AddContactByUsername/Interactor/AddContactByUsernameInteractor.swift index 21d47a38e4c9670ccbf18447c72b5fbb0104b731..2639da68284eb0e5f38fd392077352cd0a872cdb 100644 --- a/Nynja/Modules/AddContactByUsername/Interactor/AddContactByUsernameInteractor.swift +++ b/Nynja/Modules/AddContactByUsername/Interactor/AddContactByUsernameInteractor.swift @@ -20,7 +20,7 @@ final class AddContactByUsernameInteractor: AddContactByUsernameInteractorInputP init() { mqttService = MQTTService.sharedInstance mqttService.addSubscriber(self) - IoHandler.delegate = self + IoHandler.shared.delegate = self } deinit { diff --git a/Nynja/Modules/AddContactViaPhone/Interactor/AddContactViaPhoneInteractor.swift b/Nynja/Modules/AddContactViaPhone/Interactor/AddContactViaPhoneInteractor.swift index b3e89801c91bff6e6a5b8818808218e91989a17f..4a9c81ff854b8b0a9bda782708784f604db70425 100644 --- a/Nynja/Modules/AddContactViaPhone/Interactor/AddContactViaPhoneInteractor.swift +++ b/Nynja/Modules/AddContactViaPhone/Interactor/AddContactViaPhoneInteractor.swift @@ -20,7 +20,7 @@ final class AddContactViaPhoneInteractor: AddContactViaPhoneInteractorInputProto // MARK: - Init init() { - IoHandler.delegate = self + IoHandler.shared.delegate = self } deinit { diff --git a/Nynja/Modules/Auth/Login/Interactor/LoginInteractor.swift b/Nynja/Modules/Auth/Login/Interactor/LoginInteractor.swift index c0e1a555ba35260a1edaf507eec5b3cb84494e65..3c107f5cdfb582267868c31e6942c72e4b3da213 100644 --- a/Nynja/Modules/Auth/Login/Interactor/LoginInteractor.swift +++ b/Nynja/Modules/Auth/Login/Interactor/LoginInteractor.swift @@ -22,7 +22,7 @@ class LoginInteractor: BaseInteractor, LoginInteractorInputProtocol, IoHandlerDe // MARK: - Configure func configure() { - IoHandler.delegate = self + IoHandler.shared.delegate = self mqttService.addSubscriber(self) mqttService.tryReconnect() } diff --git a/Nynja/Modules/Auth/VerifyNumber/Interactor/VerifyNumberInteractor.swift b/Nynja/Modules/Auth/VerifyNumber/Interactor/VerifyNumberInteractor.swift index c525eddceaeb5054e4b5b97831a66489fafe8be4..a3d25180d923aa89d1563a664d1bfc0b4c995126 100644 --- a/Nynja/Modules/Auth/VerifyNumber/Interactor/VerifyNumberInteractor.swift +++ b/Nynja/Modules/Auth/VerifyNumber/Interactor/VerifyNumberInteractor.swift @@ -37,7 +37,7 @@ final class VerifyNumberInteractor: BaseInteractor, VerifyNumberInteractorInputP // MARK: - Config func configure() { - IoHandler.delegate = self + IoHandler.shared.delegate = self mqttService.addSubscriber(self) setupObservers() } diff --git a/Nynja/Modules/Channel/NewChannel/Interactor/NewChannelInteractor.swift b/Nynja/Modules/Channel/NewChannel/Interactor/NewChannelInteractor.swift index 94352519123695ea4f76a8ef624ef4af030daf2a..c00bf609c69400085aa7b06f0b00d5755f5e5524 100644 --- a/Nynja/Modules/Channel/NewChannel/Interactor/NewChannelInteractor.swift +++ b/Nynja/Modules/Channel/NewChannel/Interactor/NewChannelInteractor.swift @@ -33,7 +33,7 @@ final class NewChannelInteractor: BaseInteractor, NewChannelInteractorInputProto override init() { super.init() - LinkHandler.delegate = self + LinkHandler.shared.delegate = self } deinit { diff --git a/Nynja/Modules/ChatsList/ChatsListProtocols.swift b/Nynja/Modules/ChatsList/ChatsListProtocols.swift index c5aafbdb5f878ebd48702a72ae76ec78f9e495c3..e9ee87810c69f9aefa230b6442d161b9da9ba0eb 100644 --- a/Nynja/Modules/ChatsList/ChatsListProtocols.swift +++ b/Nynja/Modules/ChatsList/ChatsListProtocols.swift @@ -28,7 +28,7 @@ protocol ChatsListViewProtocol: class { func setup(with state: CollectionState<[Contact]>, displayMode: CollectionDisplayMode) } -protocol ChatsListPresenterProtocol: BasePresenterProtocol { +protocol ChatsListPresenterProtocol: BasePresenterProtocol, TypingObservable { var view: ChatsListViewProtocol! { get set } var interactor: ChatsListInteractorInputProtocol! { get set } @@ -50,7 +50,7 @@ protocol ChatsListInteractorOutputProtocol: class { func didFilter(chats: [Contact]) } -protocol ChatsListInteractorInputProtocol: BaseInteractorProtocol { +protocol ChatsListInteractorInputProtocol: BaseInteractorProtocol, TypingObservable { var presenter: ChatsListInteractorOutputProtocol! { get set } diff --git a/Nynja/Modules/ChatsList/Interactor/ChatsListInteractor.swift b/Nynja/Modules/ChatsList/Interactor/ChatsListInteractor.swift index 02aa173111d987dda1e4f5fa6d1a242e6bee4f26..1dc1e96a3137922bffd4b1127d386842d9ddbd38 100644 --- a/Nynja/Modules/ChatsList/Interactor/ChatsListInteractor.swift +++ b/Nynja/Modules/ChatsList/Interactor/ChatsListInteractor.swift @@ -12,21 +12,32 @@ class ChatsListInteractor: BaseInteractor, ChatsListInteractorInputProtocol, Ini private let storageService: StorageService private let conversationsProvider: ConversationsProviding + private let typingProvider: TypingProvider private var chats: [Contact] = [] private var searchText: String = "" + private var typingHandlers: [FeedId: (TypingDisplayModel) -> ()] = [:] + // MARK: - InitializeInjectable struct Dependencies { let storageService: StorageService let conversationsProvider: ConversationsProviding + let typingProvider: TypingProvider } required init(dependencies: Dependencies) { storageService = dependencies.storageService conversationsProvider = dependencies.conversationsProvider + typingProvider = dependencies.typingProvider + + super.init() + } + + deinit { + typingProvider.removeObserver(self) } @@ -38,6 +49,10 @@ class ChatsListInteractor: BaseInteractor, ChatsListInteractorInputProtocol, Ini override func loadData() { super.loadData() + + typingProvider.addObserver(self) { [weak self] feedId, typing in + self?.typingHandlers[feedId]?(typing) + } fetchChats() } @@ -49,6 +64,15 @@ class ChatsListInteractor: BaseInteractor, ChatsListInteractorInputProtocol, Ini applyFilter(with: searchText) } + func observeChanges(for feedId: FeedId, handler: @escaping (TypingDisplayModel) -> ()) { + typingHandlers[feedId] = handler + typingProvider.typingStatus(for: feedId).flatMap { handler($0) } + } + + func removeObserver(for feedId: FeedId) { + typingHandlers[feedId] = nil + } + // MARK: - StorageSubscriber diff --git a/Nynja/Modules/ChatsList/Presenter/ChatsListPresenter.swift b/Nynja/Modules/ChatsList/Presenter/ChatsListPresenter.swift index 6456463ebd7fe386e9047eda0385b601e0f07b05..2db4c11d7c668fe9fc3cf33157c434f6aa80b22b 100644 --- a/Nynja/Modules/ChatsList/Presenter/ChatsListPresenter.swift +++ b/Nynja/Modules/ChatsList/Presenter/ChatsListPresenter.swift @@ -33,6 +33,14 @@ class ChatsListPresenter: BasePresenter, ChatsListPresenterProtocol, ChatsListIn wireFrame.showChatWith(contact: contact) } + func observeChanges(for feedId: FeedId, handler: @escaping (TypingDisplayModel) -> ()) { + interactor.observeChanges(for: feedId, handler: handler) + } + + func removeObserver(for feedId: FeedId) { + interactor.removeObserver(for: feedId) + } + // MARK: - ChatsListInteractorOutputProtocol diff --git a/Nynja/Modules/ChatsList/View/ChatListTableDS.swift b/Nynja/Modules/ChatsList/View/ChatListTableDS.swift index 1744972294bf83d00dd33afb6facb07e20f90c84..1a8c94bc17d96b09608f3524f7dd019592ab83bc 100644 --- a/Nynja/Modules/ChatsList/View/ChatListTableDS.swift +++ b/Nynja/Modules/ChatsList/View/ChatListTableDS.swift @@ -14,10 +14,14 @@ final class ChatListTableDS: NSObject, UITableViewDataSource { var chatList = [Contact]() private let payloadParser: MessagePayloadParserInput + + private let typingObservable: TypingObservable + private weak var delegate: ChatListMessageCellModelDelegate? - init(payloadParser: MessagePayloadParserInput, delegate: ChatListMessageCellModelDelegate) { + init(payloadParser: MessagePayloadParserInput, typingObservable: TypingObservable, delegate: ChatListMessageCellModelDelegate) { self.payloadParser = payloadParser + self.typingObservable = typingObservable self.delegate = delegate } @@ -30,6 +34,9 @@ final class ChatListTableDS: NSObject, UITableViewDataSource { } private func itemModel(at indexPath: IndexPath) -> AnyCellViewModel { - return ChatListMessageCellModel(model: chatList[indexPath.row], payloadParser: payloadParser, delegate: delegate) + return ChatListMessageCellModel(model: chatList[indexPath.row], + observable: typingObservable, + payloadParser: payloadParser, + delegate: delegate) } } diff --git a/Nynja/Modules/ChatsList/View/ChatsListViewController.swift b/Nynja/Modules/ChatsList/View/ChatsListViewController.swift index 7f512269ceacaf2deee96ea1c522660f2c16808d..15702f53ddb9a418ca7ab8e75227df4b0829d5d7 100644 --- a/Nynja/Modules/ChatsList/View/ChatsListViewController.swift +++ b/Nynja/Modules/ChatsList/View/ChatsListViewController.swift @@ -78,6 +78,12 @@ final class ChatsListViewController: BaseVC, ChatsListViewProtocol, BackSwipable setupUI() } + override func viewWillAppear(_ animated: Bool) { + super.viewWillAppear(animated) + // FIXME: update visile cells in other way + tableView.reloadData() + } + override func prepareForDissappear() { super.prepareForDissappear() if !swipeBackHelper.isSwipeActive { @@ -120,7 +126,7 @@ final class ChatsListViewController: BaseVC, ChatsListViewProtocol, BackSwipable tableView.rowHeight = ChatListMessageCellModel.Cell.Constraints.height tableView.estimatedRowHeight = tableView.rowHeight - dataSource = ChatListTableDS(payloadParser: MessagePayloadParser(), delegate: self) + dataSource = ChatListTableDS(payloadParser: MessagePayloadParser(), typingObservable: presenter, delegate: self) emptyStateDS = EmptyStateTableViewDS(dataSource: dataSource) tableView.dataSource = emptyStateDS diff --git a/Nynja/Modules/ChatsList/WireFrame/ChatsListWireframe.swift b/Nynja/Modules/ChatsList/WireFrame/ChatsListWireframe.swift index 608c9433cb5997956536ebe4d43078ece267b25c..a1bbd89b0f9681d398d3caf7b56de5800335c6aa 100644 --- a/Nynja/Modules/ChatsList/WireFrame/ChatsListWireframe.swift +++ b/Nynja/Modules/ChatsList/WireFrame/ChatsListWireframe.swift @@ -14,13 +14,17 @@ class ChatsListWireFrame: ChatsListWireFrameProtocol { func presentChatsList(navigation: UINavigationController, main: MainWireFrame?, animated: Bool) { + let serviceFactory = ServiceFactory() + // Componenets let view = ChatsListViewController() let presenter = ChatsListPresenter() let interactor = ChatsListInteractor( - dependencies: .init(storageService: StorageService.sharedInstance, - conversationsProvider: ConversationsProvider())) + dependencies: .init(storageService: serviceFactory.makeStorageService(), + conversationsProvider: serviceFactory.makeConversationsProvider(), + typingProvider: serviceFactory.makeTypingProvider()) + ) self.main = main diff --git a/Nynja/Modules/Contacts/Interactor/ContactsInteractor.swift b/Nynja/Modules/Contacts/Interactor/ContactsInteractor.swift index 5b0a313da3c5f5d86a112a58d55a93e5dbbf2214..f6839b61862e932ed27b3d49d0d1d250eb5a2e31 100644 --- a/Nynja/Modules/Contacts/Interactor/ContactsInteractor.swift +++ b/Nynja/Modules/Contacts/Interactor/ContactsInteractor.swift @@ -19,7 +19,7 @@ class ContactsInteractor: BaseInteractor, ContactsInteractorInputProtocol, IoHan init(mode: ContactViewMode) { contactViewMode = mode super.init() - IoHandler.delegate = self + IoHandler.shared.delegate = self } //MARK: - BaseInteractor diff --git a/Nynja/Modules/EditUsername/Interactor/EditUsernameInteractor.swift b/Nynja/Modules/EditUsername/Interactor/EditUsernameInteractor.swift index cb9d7bd2566bcc8ca12ac5b3d51122cb4d78271e..f7319c2ccd56edec806617abe20f189ed881f1c4 100644 --- a/Nynja/Modules/EditUsername/Interactor/EditUsernameInteractor.swift +++ b/Nynja/Modules/EditUsername/Interactor/EditUsernameInteractor.swift @@ -25,7 +25,7 @@ class EditUsernameInteractor: BaseInteractor, EditUsernameInteractorInputProtoco override init() { super.init() - IoHandler.delegate = self + IoHandler.shared.delegate = self } func save(username: String) { diff --git a/Nynja/Modules/GroupsList/GroupsListProtocols.swift b/Nynja/Modules/GroupsList/GroupsListProtocols.swift index 6f973d06534f6e384d568f2e5da4bb991337f171..f10ba5bd2d0163a3d41bebaca0cf2d9507690a5e 100644 --- a/Nynja/Modules/GroupsList/GroupsListProtocols.swift +++ b/Nynja/Modules/GroupsList/GroupsListProtocols.swift @@ -30,7 +30,7 @@ protocol GroupsListViewProtocol: class { func setup(with state: CollectionState<[Room]>, displayMode: CollectionDisplayMode) } -protocol GroupsListPresenterProtocol: BasePresenterProtocol { +protocol GroupsListPresenterProtocol: BasePresenterProtocol, TypingObservable { var view: GroupsListViewProtocol! { get set } var wireFrame: GroupsListWireFrameProtocol! { get set } @@ -55,7 +55,7 @@ protocol GroupsListInteractorOutputProtocol: class { func didFilter(groups: [Room]) } -protocol GroupsListInteractorInputProtocol: BaseInteractorProtocol { +protocol GroupsListInteractorInputProtocol: BaseInteractorProtocol, TypingObservable { var presenter: GroupsListInteractorOutputProtocol! { get set } diff --git a/Nynja/Modules/GroupsList/Interactor/GroupsListInteractor.swift b/Nynja/Modules/GroupsList/Interactor/GroupsListInteractor.swift index c16ee18b592dc3cf72bec06869f27034189d6902..030bc26d89ad0f403a040f9db4bf17151987be5a 100644 --- a/Nynja/Modules/GroupsList/Interactor/GroupsListInteractor.swift +++ b/Nynja/Modules/GroupsList/Interactor/GroupsListInteractor.swift @@ -12,23 +12,35 @@ class GroupsListInteractor: BaseInteractor, GroupsListInteractorInputProtocol, I private let storageService: StorageService private let conversationsProvider: ConversationsProviding + private let typingProvider: TypingProvider private var chats: [Room] = [] private var searchText: String = "" + private var typingHandlers: [FeedId: (TypingDisplayModel) -> ()] = [:] + // MARK: - InitializeInjectable struct Dependencies { let storageService: StorageService let conversationsProvider: ConversationsProviding + let typingProvider: TypingProvider } required init(dependencies: Dependencies) { storageService = dependencies.storageService conversationsProvider = dependencies.conversationsProvider + typingProvider = dependencies.typingProvider + + super.init() + } + + deinit { + typingProvider.removeObserver(self) } + // MARK: - BaseInteractor override var subscribes: [SubscribeType]? { @@ -41,6 +53,10 @@ class GroupsListInteractor: BaseInteractor, GroupsListInteractorInputProtocol, I override func loadData() { super.loadData() + + typingProvider.addObserver(self) { [weak self] feedId, typing in + self?.typingHandlers[feedId]?(typing) + } fetchGroups() } @@ -52,6 +68,15 @@ class GroupsListInteractor: BaseInteractor, GroupsListInteractorInputProtocol, I applyFilter(with: searchText) } + func observeChanges(for feedId: FeedId, handler: @escaping (TypingDisplayModel) -> ()) { + typingHandlers[feedId] = handler + typingProvider.typingStatus(for: feedId).flatMap { handler($0) } + } + + func removeObserver(for feedId: FeedId) { + typingHandlers[feedId] = nil + } + // MARK: - StorageSubscriber diff --git a/Nynja/Modules/GroupsList/Presenter/GroupsListPresenter.swift b/Nynja/Modules/GroupsList/Presenter/GroupsListPresenter.swift index 4cd181f2e7f20fdc369cff1379529dbd428de83b..8600c93aeca2a43e631a22c70b9b1e8edde24ac6 100644 --- a/Nynja/Modules/GroupsList/Presenter/GroupsListPresenter.swift +++ b/Nynja/Modules/GroupsList/Presenter/GroupsListPresenter.swift @@ -43,6 +43,14 @@ class GroupsListPresenter: BasePresenter, GroupsListPresenterProtocol, GroupsLis self.interactor.filter(with: text) } + func observeChanges(for feedId: FeedId, handler: @escaping (TypingDisplayModel) -> ()) { + interactor.observeChanges(for: feedId, handler: handler) + } + + func removeObserver(for feedId: FeedId) { + interactor.removeObserver(for: feedId) + } + // MARK: - GroupsListInteractorOutputProtocol diff --git a/Nynja/Modules/GroupsList/View/GroupsListTableDS.swift b/Nynja/Modules/GroupsList/View/GroupsListTableDS.swift index c7a9dbc10d80184dd2741e665c44be015c07d305..49470a986166bbe9c2901de3ee5653e7803c0665 100644 --- a/Nynja/Modules/GroupsList/View/GroupsListTableDS.swift +++ b/Nynja/Modules/GroupsList/View/GroupsListTableDS.swift @@ -14,10 +14,17 @@ class GroupsListTableDS: NSObject, UITableViewDataSource { var rooms = [Room]() private let payloadParser: MessagePayloadParserInput + + private let typingObservable: TypingObservable? + private weak var delegate: ChatListMessageCellModelDelegate? - init(payloadParser: MessagePayloadParserInput, delegate: ChatListMessageCellModelDelegate? = nil) { + init(payloadParser: MessagePayloadParserInput, + typingObservable: TypingObservable? = nil, + delegate: ChatListMessageCellModelDelegate? = nil) { + self.payloadParser = payloadParser + self.typingObservable = typingObservable self.delegate = delegate } @@ -30,7 +37,9 @@ class GroupsListTableDS: NSObject, UITableViewDataSource { } private func itemModel(at indexPath: IndexPath) -> AnyCellViewModel { - return ChatListMessageCellModel(model: rooms[indexPath.row], payloadParser: payloadParser, delegate: delegate) + return ChatListMessageCellModel(model: rooms[indexPath.row], + observable: typingObservable, + payloadParser: payloadParser, + delegate: delegate) } - } diff --git a/Nynja/Modules/GroupsList/View/GroupsListViewController.swift b/Nynja/Modules/GroupsList/View/GroupsListViewController.swift index 780533112f8007ae20b227a218f2230e6caf7f26..1c779d577314297db56004c0db66c7c35215cbbf 100644 --- a/Nynja/Modules/GroupsList/View/GroupsListViewController.swift +++ b/Nynja/Modules/GroupsList/View/GroupsListViewController.swift @@ -88,6 +88,8 @@ final class GroupsListViewController: BaseVC, GroupsListViewProtocol, BackSwipab override func viewWillAppear(_ animated: Bool) { super.viewWillAppear(animated) presenter.viewWillAppear() + // FIXME: update visile cells in other way + tableView.reloadData() } @@ -107,7 +109,7 @@ final class GroupsListViewController: BaseVC, GroupsListViewProtocol, BackSwipab tableView.register(viewModel: ChatListMessageCellModel.self) tableView.rowHeight = ChatListMessageCellModel.Cell.Constraints.height - dataSource = GroupsListTableDS(payloadParser: MessagePayloadParser(), delegate: self) + dataSource = GroupsListTableDS(payloadParser: MessagePayloadParser(), typingObservable: presenter, delegate: self) emptyStateDS = EmptyStateTableViewDS(dataSource: dataSource) tableView.dataSource = emptyStateDS diff --git a/Nynja/Modules/GroupsList/WireFrame/GroupsListWireframe.swift b/Nynja/Modules/GroupsList/WireFrame/GroupsListWireframe.swift index 3db6d4df5599742459d5f5299f85c2822cd85f30..bb52986b2ae72e7344a7a920fdf490fcbb16f6c3 100644 --- a/Nynja/Modules/GroupsList/WireFrame/GroupsListWireframe.swift +++ b/Nynja/Modules/GroupsList/WireFrame/GroupsListWireframe.swift @@ -18,15 +18,16 @@ class GroupsListWireFrame: GroupsListWireFrameProtocol { self.main = main // Dependencies - let conversationsProvider = ConversationsProvider() - + let serviceFactory = ServiceFactory() // Compomentes let view = GroupsListViewController() let presenter = GroupsListPresenter() let interactor = GroupsListInteractor( - dependencies: .init(storageService: StorageService.sharedInstance, - conversationsProvider: ConversationsProvider())) + dependencies: .init(storageService: serviceFactory.makeStorageService(), + conversationsProvider: serviceFactory.makeConversationsProvider(), + typingProvider: serviceFactory.makeTypingProvider()) + ) // Connecting view.presenter = presenter diff --git a/Nynja/Modules/InviteFriends/Interactor/InviteFriendsInteractor.swift b/Nynja/Modules/InviteFriends/Interactor/InviteFriendsInteractor.swift index 0c3cf1677c85ac39567cdd09ffc95595a64cfc76..6608c5d169a6b32b3707cbf69908846bae3acce7 100644 --- a/Nynja/Modules/InviteFriends/Interactor/InviteFriendsInteractor.swift +++ b/Nynja/Modules/InviteFriends/Interactor/InviteFriendsInteractor.swift @@ -24,7 +24,7 @@ class InviteFriendsInteractor: BaseInteractor, InviteFriendsInteractorInputProto override init() { super.init() - IoHandler.delegate = self + IoHandler.shared.delegate = self } //MARK: - BaseInteractor diff --git a/Nynja/Modules/Message/Interactor/MessageInteractor.swift b/Nynja/Modules/Message/Interactor/MessageInteractor.swift index 2b624dc6e02a3492aa89d919d3ffc70a26185754..431beb1579b0f0a36342869284690bf16992062f 100644 --- a/Nynja/Modules/Message/Interactor/MessageInteractor.swift +++ b/Nynja/Modules/Message/Interactor/MessageInteractor.swift @@ -10,7 +10,7 @@ import UIKit import CoreLocation -final class MessageInteractor: BaseInteractor, MessageInteractorInputProtocol, HistoryHandlerDelegate, TypingHandlerDelegate, ConnectionServiceDelegate, MQTTServiceDelegate, MessageProcessingDelegate, MessageHandlerSubscriber, MessageInteractorCallProtocol { +final class MessageInteractor: BaseInteractor, MessageInteractorInputProtocol, HistoryHandlerDelegate, ConnectionServiceDelegate, MQTTServiceDelegate, MessageProcessingDelegate, MessageHandlerSubscriber, MessageInteractorCallProtocol { private var callService = NynjaCommunicatorService.sharedInstance @@ -98,6 +98,8 @@ final class MessageInteractor: BaseInteractor, MessageInteractorInputProtocol, H let stickersProvider: StickersProviding private var presenceProvider: PresenceStatusProvider! + + private let typingProvider: TypingProvider private let historyRequestFactory: HistoryRequestModelFactoryProtocol = HistoryRequestModelFactory() @@ -176,6 +178,7 @@ final class MessageInteractor: BaseInteractor, MessageInteractorInputProtocol, H payloadParser = MessagePayloadParser() payloadBuilder = MessagePayloadBuilder() stickersProvider = StickersProvider(dependencies: .init(storage: StorageService.sharedInstance)) + typingProvider = TypingProviderImpl.shared super.init() @@ -184,20 +187,26 @@ final class MessageInteractor: BaseInteractor, MessageInteractorInputProtocol, H } mqttService.addSubscriber(self) + + if let chatId = chat.id { + typingProvider.addObserver(self, for: chatId) { [weak self] chatId, typingInfo in + self?.presenter?.didReceiveTyping(typingInfo) + } + } ConnectionService.shared.addSubscriber(self) - MessageHandler.addSubscriber(self) - HistoryHandler.addSubscriber(self) + MessageHandler.shared.addSubscriber(self) + HistoryHandler.shared.addSubscriber(self) NynjaCommunicatorService.sharedInstance.messageInteractorCallProtocol = self subscribeToTranscribeProcessing() } - - + deinit { callService.messageInteractorCallProtocol = nil mqttService.removeSubscriber(self) - MessageHandler.removeSubscriber(self) - HistoryHandler.removeSubscriber(self) + typingProvider.removeObserver(self) + MessageHandler.shared.removeSubscriber(self) + HistoryHandler.shared.removeSubscriber(self) ConnectionService.shared.removeSubscriber(self) unsubscribeFromTranscribeProcessing() } @@ -222,8 +231,6 @@ final class MessageInteractor: BaseInteractor, MessageInteractorInputProtocol, H processingManager.delegate = self - TypingHandler.delegate = self - isAfterConnectionAppeared = false prepareInitialValues() @@ -436,6 +443,13 @@ final class MessageInteractor: BaseInteractor, MessageInteractorInputProtocol, H presenter?.internetStatusChanged(.waiting) } } + + func askForTypingStatus() { + guard let feedId = chat.id, let typing = typingProvider.typingStatus(for: feedId) else { + return + } + presenter?.didReceiveTyping(typing) + } // MARK: - Send Message func sendMessage(_ message: InputTextMessage) { @@ -940,21 +954,6 @@ final class MessageInteractor: BaseInteractor, MessageInteractorInputProtocol, H self.performAutoConversion(for: self.configuration.messages) } } - - // MARK: - TypingHandlerDelegate - func getTyping(typing: Typing) { - guard self.contact?.phone_id == typing.phone_id, let typingModelType = typing.type else { - return - } - - let actionStatus = ActionStatus(typingModelType: typingModelType) - presenter?.actionStatusChanged(actionStatus) - if typingModelType != .done { - dispatchAsyncMainThrotlle(key: "remove_typing_status", seconds: 10.0) { [weak self] in - self?.presenter?.restoreStatus() - } - } - } func connectionStatusChanged(_ sender: ConnectionService, service: ConnectionService.Service, oldValue: ConnectionService.ConnectionServiceState) { if service == .networking { @@ -982,6 +981,7 @@ final class MessageInteractor: BaseInteractor, MessageInteractorInputProtocol, H private func chatUpdated(from oldChat: ChatModel, to newChat: ChatModel) { presenter?.chatUpdated(newChat) if let status = presenceProvider.presence(for: newChat) { + presenceProvider.refreshTimer(for: newChat) presenter?.presenceStatusChanged(status) } diff --git a/Nynja/Modules/Message/Interactor/PresenceStatusProvider.swift b/Nynja/Modules/Message/Interactor/PresenceStatusProvider.swift index c53f3c85666260e79809f68a61e48b9da0d6efb3..1c207c69c6a8aa883e4735ac6d82bab3eb257adc 100644 --- a/Nynja/Modules/Message/Interactor/PresenceStatusProvider.swift +++ b/Nynja/Modules/Message/Interactor/PresenceStatusProvider.swift @@ -46,42 +46,23 @@ class PresenceStatusProvider { } private func presence(contact: Contact) -> PresenceStatus { - invalidatePresenceTimer() - guard contact.presenceStatus != "online" else { return .active } - let minutesDiff = minutesAfterOffline(contact.updated) - if minutesDiff >= minutes { - return .inactive - } else { - let interval = timeInterval - minutesDiff * 60 - presenceTimer = TimerHandler(interval: interval, repeats: false) { [weak self] _ in - self?.didContactBecomeInactive() - } - - return .active - } + return minutesDiff >= minutes ? .inactive : .active } private func presence(group: Room) -> PresenceStatus { - invalidatePresenceTimer() - let allMembers = group.allMembers ?? [] - let activeCount = allMembers.reduce(0) { (temp, member) in + let activeCount = allMembers.reduce(0) { count, member in let minutesDiff = minutesAfterOffline(member.updated) if StringAtom.string(member.presence) == "online" || minutesDiff < minutes { - return temp + 1 + return count + 1 } - - return temp - } - - presenceTimer = TimerHandler(interval: timeInterval, repeats: false) { [weak self] _ in - self?.checkCountOfActiveMembers() + return count } return .room(allMembers.count, activeCount) @@ -102,15 +83,38 @@ class PresenceStatusProvider { // MARK: - Timer - @objc private func didContactBecomeInactive() { + func refreshTimer(for chat: ChatModel) { invalidatePresenceTimer() - handler(.inactive) + + switch chat { + case let chat as Contact: + let minutesDiff = minutesAfterOffline(chat.updated) + if minutesDiff < minutes { + let interval = timeInterval - minutesDiff * 60 + presenceTimer = TimerHandler(interval: interval, repeats: false) { [weak self] _ in + self?.didContactBecomeInactive() + } + } + case let chat as Room where chat.kind == .group: + presenceTimer = TimerHandler(interval: timeInterval, repeats: false) { [weak self] _ in + self?.checkCountOfActiveMembers() + } + default: + break + } } - @objc private func checkCountOfActiveMembers() { + private func didContactBecomeInactive() { invalidatePresenceTimer() + handler(.inactive) + } + + private func checkCountOfActiveMembers() { if let chat = self.chat, let status = presence(for: chat) { + refreshTimer(for: chat) handler(status) + } else { + invalidatePresenceTimer() } } @@ -118,5 +122,4 @@ class PresenceStatusProvider { presenceTimer?.invalidate() presenceTimer = nil } - } diff --git a/Nynja/Modules/Message/Models/Statuses/ChatStatus.swift b/Nynja/Modules/Message/Models/Statuses/ChatStatus.swift new file mode 100644 index 0000000000000000000000000000000000000000..1cedf26b2931fc81fcc70fc975128c82712b7c5f --- /dev/null +++ b/Nynja/Modules/Message/Models/Statuses/ChatStatus.swift @@ -0,0 +1,13 @@ +// +// ChatStatus.swift +// Nynja +// +// Created by Anton Poltoratskyi on 31.10.2018. +// Copyright © 2018 TecSynt Solutions. All rights reserved. +// + +struct ChatStatus { + var presence: PresenceStatus? + var internet: InternetStatus + var typing: TypingDisplayModel +} diff --git a/Nynja/Modules/Message/Models/Statuses/ChatStatusDisplayInfo.swift b/Nynja/Modules/Message/Models/Statuses/ChatStatusDisplayInfo.swift new file mode 100644 index 0000000000000000000000000000000000000000..dfd9c1d6f789120c16ed5d90e94c80dfa497f77a --- /dev/null +++ b/Nynja/Modules/Message/Models/Statuses/ChatStatusDisplayInfo.swift @@ -0,0 +1,12 @@ +// +// ChatStatusDisplayInfo.swift +// Nynja +// +// Created by Anton Poltoratskyi on 31.10.2018. +// Copyright © 2018 TecSynt Solutions. All rights reserved. +// + +enum ChatStatusDisplayInfo { + case text(String) + case typing(TypingDisplayModel.SenderInfo?, TypingDisplayModel.Status) +} diff --git a/Nynja/Modules/Message/Models/Statuses/InternetStatus.swift b/Nynja/Modules/Message/Models/Statuses/Internet/InternetStatus.swift similarity index 100% rename from Nynja/Modules/Message/Models/Statuses/InternetStatus.swift rename to Nynja/Modules/Message/Models/Statuses/Internet/InternetStatus.swift diff --git a/Nynja/Modules/Message/Models/Statuses/PresenceStatus.swift b/Nynja/Modules/Message/Models/Statuses/Presence/PresenceStatus.swift similarity index 100% rename from Nynja/Modules/Message/Models/Statuses/PresenceStatus.swift rename to Nynja/Modules/Message/Models/Statuses/Presence/PresenceStatus.swift diff --git a/Nynja/Modules/Message/Models/Statuses/ActionStatus.swift b/Nynja/Modules/Message/Models/Statuses/Typing/ActionStatus.swift similarity index 69% rename from Nynja/Modules/Message/Models/Statuses/ActionStatus.swift rename to Nynja/Modules/Message/Models/Statuses/Typing/ActionStatus.swift index 5c3c25c454490bbb3e2fc649e15aae7f93279073..13c65fdba7a3877289daf7a326272a79790337d8 100644 --- a/Nynja/Modules/Message/Models/Statuses/ActionStatus.swift +++ b/Nynja/Modules/Message/Models/Statuses/Typing/ActionStatus.swift @@ -6,12 +6,40 @@ // Copyright © 2017 TecSynt Solutions. All rights reserved. // -enum ActionStatus { +enum ActionStatus: Equatable { case done case typing case sending(SendingStatus) case recording(RecordingStatus) + var isDone: Bool { + if case .done = self { + return true + } + return false + } + + var isTyping: Bool { + if case .typing = self { + return true + } + return false + } + + var isSendingFile: Bool { + if case .sending = self { + return true + } + return false + } + + var isRecording: Bool { + if case .recording = self { + return true + } + return false + } + var title: String { switch self { case .done: @@ -47,5 +75,4 @@ enum ActionStatus { self = .recording(.voice) } } - } diff --git a/Nynja/Modules/Message/Models/Statuses/RecordingStatus.swift b/Nynja/Modules/Message/Models/Statuses/Typing/RecordingStatus.swift similarity index 89% rename from Nynja/Modules/Message/Models/Statuses/RecordingStatus.swift rename to Nynja/Modules/Message/Models/Statuses/Typing/RecordingStatus.swift index bfb75367e4bee7d1789358c94646fe30759b3ceb..a65d33387b05a36a4c3930d9f6c56301edf67a8c 100644 --- a/Nynja/Modules/Message/Models/Statuses/RecordingStatus.swift +++ b/Nynja/Modules/Message/Models/Statuses/Typing/RecordingStatus.swift @@ -6,7 +6,7 @@ // Copyright © 2017 TecSynt Solutions. All rights reserved. // -enum RecordingStatus: String { +enum RecordingStatus: String, Equatable { case video = "video" case voice = "voice_message" diff --git a/Nynja/Modules/Message/Models/Statuses/SendingStatus.swift b/Nynja/Modules/Message/Models/Statuses/Typing/SendingStatus.swift similarity index 91% rename from Nynja/Modules/Message/Models/Statuses/SendingStatus.swift rename to Nynja/Modules/Message/Models/Statuses/Typing/SendingStatus.swift index 7ab43601746749ae72151073726623b202fda2ac..3a5ad85bc44168b699da906507aed058d0f707fa 100644 --- a/Nynja/Modules/Message/Models/Statuses/SendingStatus.swift +++ b/Nynja/Modules/Message/Models/Statuses/Typing/SendingStatus.swift @@ -6,7 +6,7 @@ // Copyright © 2017 TecSynt Solutions. All rights reserved. // -enum SendingStatus: String { +enum SendingStatus: String, Equatable { case file = "file" case video = "video" case voice = "voice_message" diff --git a/Nynja/Modules/Message/Presenter/MessagePresenter.swift b/Nynja/Modules/Message/Presenter/MessagePresenter.swift index d2fa97a0d36704ee0e5bd2c9643a5a04a7fd3d3b..d7f0d6d4fc304b7988f0372a0b7194985f1ac993 100644 --- a/Nynja/Modules/Message/Presenter/MessagePresenter.swift +++ b/Nynja/Modules/Message/Presenter/MessagePresenter.swift @@ -10,23 +10,19 @@ import UIKit import CoreLocation.CLLocation -class MessagePresenter: BasePresenter, MessagePresenterProtocol, MessageInteractorOutputProtocol, ForwardSelectorDelegate { +final class MessagePresenter: BasePresenter, MessagePresenterProtocol, MessageInteractorOutputProtocol, ForwardSelectorDelegate { + + // MARK: - Properties - //MARK: - Properties var isMyselfChat: Bool { return interactor.isMyselfChat } - private var lastStatus: String = "" { - didSet { updateStatus(lastStatus) } - } - - private var internetStatus: InternetStatus = .connected private var isActionsEnabled: Bool = true private var wasViewDisappeared: Bool = false private let chatScreenAlertFactory: ChatScreenAlertFactoryProtocol = ChatScreenAlertFactory() - // -- unread mention counter + // MARK: Unread mention counter var uniqueUnreadMentionIds: Set = [] var unreadMentionIds: [MessageServerId] = [] @@ -39,10 +35,44 @@ class MessagePresenter: BasePresenter, MessagePresenterProtocol, MessageInteract } } } + + // MARK: Header Status + + private var headerStatus = ChatStatus(presence: nil, internet: .connected, typing: .none) { + didSet { + let internetStatus = headerStatus.internet + let presenceStatus = headerStatus.presence + let typing = headerStatus.typing + + switch internetStatus { + case .waiting, .connecting: + // Always show internet status when not connected + view?.updateHeaderStatus(.text(internetStatus.rawValue.localized)) + + case .connected: + // Show typing if exists, otherwise - show presence + + switch typing { + case let .typing(sender, status): + if status.isDone { + fallthrough + } + view.updateHeaderStatus(.typing(sender, status)) + + case .none: + if let presence = presenceStatus { + view?.updateHeaderStatus(.text(presence.title)) + } else { + view.updateHeaderStatus(.text(internetStatus.rawValue.localized)) + } + } + } + } + } + - // -- - - //MARK: - BasePresenter + // MARK: - BasePresenter + override var itemsFactory: WCItemsFactory? { if isMyselfChat { return MySelfItemsFactory(isActionsEnabled: true) @@ -73,11 +103,13 @@ class MessagePresenter: BasePresenter, MessagePresenterProtocol, MessageInteract super.screenLoaded() interactor.askForInternetStatus() + interactor.askForTypingStatus() interactor.room.map { fetchMentionedMessages(in: $0) } } - //MARK: - MessagePresenterProtocol + // MARK: - MessagePresenterProtocol + weak var view: MessageViewProtocol! var wireFrame: MessageWireframeProtocol! var interactor: MessageInteractorInputProtocol! { @@ -951,29 +983,15 @@ class MessagePresenter: BasePresenter, MessagePresenterProtocol, MessageInteract } func internetStatusChanged(_ status: InternetStatus) { - internetStatus = status - switch status { - case .waiting, .connecting: - view?.updateHeaderStatus(status.rawValue.localized) - case .connected: - restoreStatus() - } + headerStatus.internet = status } func presenceStatusChanged(_ status: PresenceStatus) { - lastStatus = status.title - } - - func actionStatusChanged(_ status: ActionStatus) { - if case .done = status { - restoreStatus() - } else { - view.updateHeaderStatus(status.title) - } + headerStatus.presence = status } - func restoreStatus() { - view.updateHeaderStatus(lastStatus) + func didReceiveTyping(_ typing: TypingDisplayModel) { + headerStatus.typing = typing } func messageSent(_ localId: String) { @@ -1015,12 +1033,9 @@ class MessagePresenter: BasePresenter, MessagePresenterProtocol, MessageInteract self?.openProfileScreen(contact: contact) } } + + // MARK: - Utils - private func updateStatus(_ status: String) { - if internetStatus == .connected { - view.updateHeaderStatus(status) - } - } func getPreviousMessages(id: MessageServerId) { self.interactor.fetchMessages(from: id, isNew: true) diff --git a/Nynja/Modules/Message/Protocols/MessageProtocols.swift b/Nynja/Modules/Message/Protocols/MessageProtocols.swift index a63aa0a4a6ca530b03949c1ebcdcc01c648dd635..d8999361dc9c882d9d2d3c2cf339f655c201f500 100644 --- a/Nynja/Modules/Message/Protocols/MessageProtocols.swift +++ b/Nynja/Modules/Message/Protocols/MessageProtocols.swift @@ -157,8 +157,7 @@ protocol MessageInteractorOutputProtocol: class, MentionFetchOutputProtocol, Mes func internetStatusChanged(_ status: InternetStatus) func presenceStatusChanged(_ status: PresenceStatus) - func actionStatusChanged(_ status: ActionStatus) - func restoreStatus() + func didReceiveTyping(_ typing: TypingDisplayModel) func messageSent(_ localId: MessageLocalId) func messageRead(_ localId: MessageLocalId) @@ -251,6 +250,7 @@ protocol MessageInteractorInputProtocol: BaseInteractorProtocol, MentionFetchInp func sender(for message: Message) -> MessageSender func askForInternetStatus() + func askForTypingStatus() func editMessage(_ message: InputTextMessage) func clearEditMessageObject() @@ -302,7 +302,7 @@ protocol MessageViewProtocol: class { func scrollToBottomIfNeeded() func scrollToBottom() - func updateHeaderStatus(_ status: String) + func updateHeaderStatus(_ status: ChatStatusDisplayInfo) func updateDeliveryStatus(_ status: DeliveryStatus, messageId: String) func removeMessage(_ messageId: MessageLocalId, isForAllUsers: Bool) diff --git a/Nynja/Modules/Message/View/MessageVC.swift b/Nynja/Modules/Message/View/MessageVC.swift index c04f374aa4372177bc4e473479dd1a6b282949d8..e43591926e17938ffa268a745a1b4dc2853328e1 100644 --- a/Nynja/Modules/Message/View/MessageVC.swift +++ b/Nynja/Modules/Message/View/MessageVC.swift @@ -88,12 +88,10 @@ final class MessageVC: BaseVC, MessageViewProtocol, ReplyPreviewDelegate, BackSw av.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(avatarPressed))) self.view.addSubview(av) - av.snp.makeConstraints({ (make) in - make.height.equalTo(Constraints.avatarView.height.adjustedByWidth) - - self.adjustVerticalInset(.top, make: make, offset: Constraints.avatarView.topInset.adjustedByWidth) + av.snp.makeConstraints { make in + adjustVerticalInset(.top, make: make) make.left.right.equalToSuperview() - }) + } return av }() @@ -1162,7 +1160,7 @@ final class MessageVC: BaseVC, MessageViewProtocol, ReplyPreviewDelegate, BackSw avatarView.setup(with: viewModel) } - func updateHeaderStatus(_ status: String) { + func updateHeaderStatus(_ status: ChatStatusDisplayInfo) { avatarView.status = status } diff --git a/Nynja/Modules/Message/View/MessageVCLayout.swift b/Nynja/Modules/Message/View/MessageVCLayout.swift index b39e1d1f860a61f80e637c36631317bd8dd3ef55..fae8c725233505da6bd34d394fddc07ec88a3b15 100644 --- a/Nynja/Modules/Message/View/MessageVCLayout.swift +++ b/Nynja/Modules/Message/View/MessageVCLayout.swift @@ -10,11 +10,6 @@ extension MessageVC { enum Constraints { - enum avatarView { - static let topInset = 8.0 - static let height = 48.0 - } - enum tableView { static let defaultVerticalInset = CGFloat(4.adjustedByWidth) static let bottomInset = CGFloat(gradientView.height) diff --git a/Nynja/Modules/Message/View/Views/AvatarView/AvatarView.swift b/Nynja/Modules/Message/View/Views/AvatarView/AvatarView.swift index c8c2d2ce67581d08da39f147af08b18c5ca854f4..44212f7e316536bcd12f85bf0291a4efb7a5233e 100644 --- a/Nynja/Modules/Message/View/Views/AvatarView/AvatarView.swift +++ b/Nynja/Modules/Message/View/Views/AvatarView/AvatarView.swift @@ -7,49 +7,87 @@ // import SnapKit +import NynjaUIKit -class AvatarView: BaseView { +final class AvatarView: BaseView { override var activatedViews: [UIView] { return [statusLabel, separatorView] } - var status: String? { - didSet { statusLabel.text = status - statusLabel.accessibilityValue = status + var status: ChatStatusDisplayInfo? { + didSet { + guard let status = status else { + return + } + + switch status { + case let .text(statusString): + statusLabel.text = statusString + statusLabel.accessibilityValue = statusString + + hideTyping() + + case let .typing(sender, status): + let indicator: TypingView.Appearance.Indicator + switch status.action { + case .recording?: + indicator = .circle(UIColor.nynja.white) + default: + indicator = .dots(UIColor.nynja.white) + } + + let appearance = TypingView.Appearance(indicator: indicator, + textColor: titleLabel.textColor, + textFont: statusLabel.font, + senderInfo: sender?.displayName, + typingInfo: status.title + ) + typingView.update(appearance) + + showTyping() + } } } private var muteWidthConstraint: Constraint? private var muteLeftConstraint: Constraint? + // MARK: - Views - private lazy var imageView: UIImageView = { - let img = UIImageView() - img.contentMode = .scaleAspectFill + + private lazy var avatarView: AvatarStatusView = { + let statusIconPadding = Constraints.imageView.statusIconPadding.adjustedByWidth + let statusIconSize = Constraints.imageView.statusIconSize.adjustedByWidth + + let avatarView = AvatarStatusView() + avatarView.imageView.contentMode = .scaleAspectFill + avatarView.statusIconPadding = statusIconPadding + avatarView.statusIconSize = statusIconSize let width = Constraints.imageView.width.adjustedByWidth - img.roundCorners(radius: width / 2) + let verticalInset = Constraints.imageView.vertivalInset.adjustedByWidth - self.addSubview(img) - img.snp.makeConstraints({ (make) in + addSubview(avatarView) + avatarView.snp.makeConstraints { make in make.width.height.equalTo(width) + make.top.equalToSuperview().offset(verticalInset) + make.bottom.equalToSuperview().offset(-verticalInset) make.left.equalTo(Constraints.imageView.leftInset.adjustedByWidth) - make.centerY.equalToSuperview() - }) + } - return img + return avatarView }() - private lazy var labelsView: UIView = { + private lazy var titleContainerView: UIView = { let view = UIView() - let horizontalInset = Constraints.labelsView.horizontalInset.adjustedByWidth + let horizontalInset = Constraints.titleContainerView.horizontalInset.adjustedByWidth - self.addSubview(view) - view.snp.makeConstraints { (make) in + addSubview(view) + view.snp.makeConstraints { make in make.centerY.equalToSuperview() - make.left.equalTo(imageView.snp.right).offset(horizontalInset) + make.left.equalTo(avatarView.snp.right).offset(horizontalInset) make.right.equalTo(-horizontalInset) } @@ -58,15 +96,17 @@ class AvatarView: BaseView { private lazy var titleLabel: UILabel = { let height = Constraints.titleLabel.height.adjustedByWidth - let us = UILabel(height: height, color: UIColor.nynja.white, fontName: FontFamily.NotoSans.medium.name) - us.accessibilityIdentifier = "chat_title" - self.labelsView.addSubview(us) - us.snp.makeConstraints({ (make) in + + let label = UILabel(height: height, color: UIColor.nynja.white, fontName: FontFamily.NotoSans.medium.name) + label.accessibilityIdentifier = "chat_title" + + titleContainerView.addSubview(label) + label.snp.makeConstraints { make in make.height.equalTo(height) make.top.left.equalToSuperview() - }) + } - return us + return label }() private lazy var muteImageView: UIImageView = { @@ -74,7 +114,7 @@ class AvatarView: BaseView { let side = Constraints.muteImageView.side.adjustedByWidth - self.labelsView.addSubview(imageView) + titleContainerView.addSubview(imageView) imageView.snp.makeConstraints { make in make.height.equalTo(side) muteWidthConstraint = make.width.equalTo(0).constraint @@ -89,28 +129,39 @@ class AvatarView: BaseView { private lazy var statusLabel: UILabel = { let height = Constraints.statusLabel.height.adjustedByWidth - let sl = UILabel(height: height, color: UIColor.nynja.manatee, fontName: FontFamily.NotoSans.regular.name) - sl.accessibilityIdentifier = "chat_status" - self.labelsView.addSubview(sl) - sl.snp.makeConstraints({ (make) in + + let label = UILabel(height: height, color: UIColor.nynja.manatee, fontName: FontFamily.NotoSans.regular.name) + label.accessibilityIdentifier = "chat_status" + + titleContainerView.addSubview(label) + label.snp.makeConstraints { make in make.height.equalTo(height) - make.top.equalTo(muteImageView.snp.bottom) + make.top.equalTo(titleLabel.snp.bottom) make.left.right.bottom.equalToSuperview() - }) + } - return sl + return label }() - private lazy var separatorView: UIView = { - let view = UIView() + private lazy var typingView: TypingView = { + let typingView = TypingView() + + titleContainerView.addSubview(typingView) + typingView.snp.makeConstraints { maker in + maker.top.bottom.equalTo(statusLabel) + maker.left.right.equalToSuperview() + } - view.backgroundColor = UIColor.nynja.backgroundGray + return typingView + }() + + private lazy var separatorView: SeparatorView = { + let view = SeparatorView() - self.addSubview(view) - view.snp.makeConstraints({ (make) in - make.height.equalTo(Constraints.separatorView.height) + addSubview(view) + view.snp.makeConstraints { make in make.left.right.bottom.equalToSuperview() - }) + } return view }() @@ -121,6 +172,8 @@ class AvatarView: BaseView { func setup(with viewModel: AvatarViewModel) { titleLabel.text = viewModel.title titleLabel.accessibilityValue = viewModel.title + + let imageView = avatarView.imageView let placeholder = imageView.image ?? UIImage.nynja.Contacts.avaPlaceholder.image imageView.setImage(url: viewModel.avatarUrl, placeHolder: placeholder, options: .delayPlaceholder) @@ -137,4 +190,16 @@ class AvatarView: BaseView { } } + + // MARK: - Layout + + private func showTyping() { + statusLabel.isHidden = true + typingView.isHidden = false + } + + private func hideTyping() { + statusLabel.isHidden = false + typingView.isHidden = true + } } diff --git a/Nynja/Modules/Message/View/Views/AvatarView/AvatarViewLayout.swift b/Nynja/Modules/Message/View/Views/AvatarView/AvatarViewLayout.swift index 7ee65abf2316487906980ad3aab67b58e0e06203..7a5ca994f7a23a358db75795d4ea7ab4198f9460 100644 --- a/Nynja/Modules/Message/View/Views/AvatarView/AvatarViewLayout.swift +++ b/Nynja/Modules/Message/View/Views/AvatarView/AvatarViewLayout.swift @@ -8,35 +8,32 @@ extension AvatarView { - struct Constraints { + enum Constraints { - struct imageView { - static let width: CGFloat = 32.0 - + enum imageView { + static let width: CGFloat = 40.0 + static let vertivalInset: CGFloat = 8.0 static let leftInset = 16.0 + + static let statusIconPadding: CGFloat = 2.0 + static let statusIconSize: CGFloat = 8.0 } - struct labelsView { + enum titleContainerView { static let horizontalInset = 16.0 } - struct titleLabel { + enum titleLabel { static let height: CGFloat = 22.0 } - struct muteImageView { + enum muteImageView { static let side = 12.0 static let leftInset = 8.0 } - struct statusLabel { + enum statusLabel { static let height: CGFloat = 20.0 } - - struct separatorView { - static let height = 1.0 - } - } - } diff --git a/Nynja/Modules/Participants/Interactor/ParticipantsInteractor.swift b/Nynja/Modules/Participants/Interactor/ParticipantsInteractor.swift index babd3b0c4482bd136a25a35d649b297d32a50d29..0097dde47970ac2cf515ac1c09de21cdb624883c 100644 --- a/Nynja/Modules/Participants/Interactor/ParticipantsInteractor.swift +++ b/Nynja/Modules/Participants/Interactor/ParticipantsInteractor.swift @@ -17,7 +17,7 @@ class ParticipantsInteractor: BaseInteractor, ParticipantsInteractorInputProtoco override init() { super.init() - IoHandler.delegate = self + IoHandler.shared.delegate = self } diff --git a/Nynja/Modules/Profile/Interactor/ProfileInteractor.swift b/Nynja/Modules/Profile/Interactor/ProfileInteractor.swift index 0b73d2bbf27c8549af0f788996db16b08311efc1..aa441303dbb564d0de5365453697374e2ca69b7d 100644 --- a/Nynja/Modules/Profile/Interactor/ProfileInteractor.swift +++ b/Nynja/Modules/Profile/Interactor/ProfileInteractor.swift @@ -20,17 +20,30 @@ class ProfileInteractor: BaseInteractor, ProfileInteractorInputProtocol { var starred: CellModels = [] var scheduled: CellModels = [] + private var typingHandlers: [FeedId: (TypingDisplayModel) -> ()] = [:] + private var contactsProvider: ContactsProviding! private var conversationsProvider: ConversationsProviding! + private var typingProvider: TypingProvider! private var mqttService: MQTTService! - //MARK: - BaseInteractor + deinit { + typingProvider.removeObserver(self) + } + + + // MARK: - BaseInteractor + override var subscribes: [SubscribeType]? { return [.contact(StorageService.sharedInstance.phoneId!), .contact(nil), .room(nil), .star(nil), .job(nil), .profile] } override func loadData() { super.loadData() + + typingProvider.addObserver(self) { [weak self] feedId, typing in + self?.typingHandlers[feedId]?(typing) + } fetchContact() fetchRooms() fetchChats() @@ -40,6 +53,9 @@ class ProfileInteractor: BaseInteractor, ProfileInteractorInputProtocol { fetchLastEvents() } + + // MARK: - ProfileInteractorInputProtocol + func acceptContact(with phoneId: String) { mqttService.confirmFriend(friendPhoneId: phoneId) @@ -64,6 +80,16 @@ class ProfileInteractor: BaseInteractor, ProfileInteractorInputProtocol { } } + func observeChanges(for feedId: FeedId, handler: @escaping (TypingDisplayModel) -> ()) { + typingHandlers[feedId] = handler + typingProvider.typingStatus(for: feedId).flatMap { handler($0) } + } + + func removeObserver(for feedId: FeedId) { + typingHandlers[feedId] = nil + } + + // MARK: - StorageSubscriber override func update(with changes: [StorageChange], type: SubscribeType) { @@ -88,8 +114,10 @@ class ProfileInteractor: BaseInteractor, ProfileInteractorInputProtocol { } } -//MARK: Private methods -fileprivate extension ProfileInteractor { + +// MARK: - Private methods + +private extension ProfileInteractor { func fetchContact() { guard let phoneId = StorageService.sharedInstance.phoneId, @@ -156,11 +184,15 @@ fileprivate extension ProfileInteractor { } + +// MARK: - SetInjectable + extension ProfileInteractor: SetInjectable { func inject(dependencies: ProfileInteractor.Dependencies) { presenter = dependencies.presenter contactsProvider = dependencies.contactsProvider conversationsProvider = dependencies.conversationsProvider + typingProvider = dependencies.typingProvider mqttService = dependencies.mqttService } @@ -168,6 +200,7 @@ extension ProfileInteractor: SetInjectable { let presenter: ProfileInteractorOutputProtocol let contactsProvider: ContactsProviding let conversationsProvider: ConversationsProviding + let typingProvider: TypingProvider let mqttService: MQTTService } } diff --git a/Nynja/Modules/Profile/Presenter/Contact+DialogCellModel.swift b/Nynja/Modules/Profile/Presenter/Contact+DialogCellModel.swift index 1001ca59aeb9e432b1d2f2e98a154dc044ad537a..45a78599e02410546b3b29017a129afabda39966 100644 --- a/Nynja/Modules/Profile/Presenter/Contact+DialogCellModel.swift +++ b/Nynja/Modules/Profile/Presenter/Contact+DialogCellModel.swift @@ -10,7 +10,10 @@ import Foundation extension Contact : DialogCellModel { - // MARK: DialogCellModel methods + var feedId: String! { + return phone_id + } + var title: String! { return fullName } diff --git a/Nynja/Modules/Profile/Presenter/DialogCellModel.swift b/Nynja/Modules/Profile/Presenter/DialogCellModel.swift index c247a31722dd7aa697d48ac9af1eeed5409be46b..38e1392c27a62817af3b479591cb0b3ab9aa4ec5 100644 --- a/Nynja/Modules/Profile/Presenter/DialogCellModel.swift +++ b/Nynja/Modules/Profile/Presenter/DialogCellModel.swift @@ -9,6 +9,7 @@ import Foundation protocol DialogCellModel : CellModel { + var feedId: String! { get } var title: String! { get } var message: Message? { get } var hasMentions: Bool { get } diff --git a/Nynja/Modules/Profile/Presenter/ProfilePresenter.swift b/Nynja/Modules/Profile/Presenter/ProfilePresenter.swift index 4728e19edefba80a0a68ec439717a31d421f0946..806d35cd7d1dc976c6406a8291ac9242669489f8 100644 --- a/Nynja/Modules/Profile/Presenter/ProfilePresenter.swift +++ b/Nynja/Modules/Profile/Presenter/ProfilePresenter.swift @@ -115,6 +115,16 @@ class ProfilePresenter: BasePresenter, ProfilePresenterProtocol, ProfileInteract return nil } } + + func observeChanges(for feedId: FeedId, handler: @escaping (TypingDisplayModel) -> ()) { + interactor.observeChanges(for: feedId, handler: handler) + } + + func removeObserver(for feedId: FeedId) { + interactor.removeObserver(for: feedId) + } + + // MARK: - ProfileInteractorOutputProtocol diff --git a/Nynja/Modules/Profile/Presenter/Room+DialogCellModel.swift b/Nynja/Modules/Profile/Presenter/Room+DialogCellModel.swift index 1e8cd4a007e965ab9f45977c8bf2fc6d98d10e18..b22acc76f2983daaf8452a4f59172b0398322ed8 100644 --- a/Nynja/Modules/Profile/Presenter/Room+DialogCellModel.swift +++ b/Nynja/Modules/Profile/Presenter/Room+DialogCellModel.swift @@ -10,7 +10,10 @@ import Foundation extension Room: DialogCellModel { - // MARK: DialogCellModel methods + var feedId: String! { + return id + } + var title: String! { return name != nil ? name! : "" } diff --git a/Nynja/Modules/Profile/ProfileProtocols.swift b/Nynja/Modules/Profile/ProfileProtocols.swift index 045570511bde444a5aa7d25a45d4ddd110921327..83626aca30517ed7203d1b36fce04fafbf3cb66b 100644 --- a/Nynja/Modules/Profile/ProfileProtocols.swift +++ b/Nynja/Modules/Profile/ProfileProtocols.swift @@ -28,7 +28,7 @@ protocol ProfileWireFrameProtocol: class { func showFavorites() } -protocol ProfilePresenterProtocol: BasePresenterProtocol { +protocol ProfilePresenterProtocol: BasePresenterProtocol, TypingObservable { func showImagePreview(from imageView: UIImageView) func showQRGenerator() @@ -66,7 +66,7 @@ protocol ProfileInteractorOutputProtocol: class { func showWallet(with balance: NYNMoney?) } -protocol ProfileInteractorInputProtocol: BaseInteractorProtocol { +protocol ProfileInteractorInputProtocol: BaseInteractorProtocol, TypingObservable { var myContact: Contact! { get set } diff --git a/Nynja/Modules/Profile/View/DetailsView/ProfileDetailsView.swift b/Nynja/Modules/Profile/View/DetailsView/ProfileDetailsView.swift index f956cc96b181b53618acd3435235a77c2345ae3d..3a44d47a931f3eecf77f5e2e0379ae87addd0bf8 100644 --- a/Nynja/Modules/Profile/View/DetailsView/ProfileDetailsView.swift +++ b/Nynja/Modules/Profile/View/DetailsView/ProfileDetailsView.swift @@ -6,25 +6,27 @@ // Copyright © 2017 TecSynt Solutions. All rights reserved. // +import NynjaUIKit + class ProfileDetailsView: UIView { // MARK: Views - lazy var avatarImageView: UIImageView = { + lazy var avatarImageView: AvatarStatusView = { let width = Constraints.avatarImageView.width.adjustedByWidth - let imageView = UIImageView(frame: CGRect(x: 0, y:0, width: width, height: width)) - - imageView.contentMode = .scaleAspectFill - imageView.roundImageView() - - self.addSubview(imageView) - imageView.snp.makeConstraints({ (make) in + let avatarView = AvatarStatusView() + avatarView.imageView.contentMode = .scaleAspectFill + avatarView.statusIconSize = CGFloat(Constraints.avatarImageView.statusIconSize.adjustedByWidth) + avatarView.statusIconPadding = CGFloat(Constraints.avatarImageView.statusIconPadding.adjustedByWidth) + + addSubview(avatarView) + avatarView.snp.makeConstraints { make in make.width.height.equalTo(width) make.top.equalToSuperview().offset(Constraints.avatarImageView.topInset.adjustedByHeight) make.left.equalToSuperview().offset(Constraints.avatarImageView.leftInset.adjustedByWidth) - }) + } - return imageView + return avatarView }() lazy var infoView: UIView = { diff --git a/Nynja/Modules/Profile/View/DetailsView/ProfileDetailsViewLayout.swift b/Nynja/Modules/Profile/View/DetailsView/ProfileDetailsViewLayout.swift index 38591f0c88b835cc3a0b3b47a4c1b57ca2ef03d5..33c9b7617f05ffbf17743b3af6680b76c4e3ebec 100644 --- a/Nynja/Modules/Profile/View/DetailsView/ProfileDetailsViewLayout.swift +++ b/Nynja/Modules/Profile/View/DetailsView/ProfileDetailsViewLayout.swift @@ -8,21 +8,23 @@ extension ProfileDetailsView { - struct Constraints { + enum Constraints { - struct avatarImageView { + enum avatarImageView { static let width = 95.0 + static let statusIconSize = 16.0 + static let statusIconPadding = 4.0 static let topInset = 30.0 static let leftInset = 16.0 } // MARK: Info View - struct infoView { + enum infoView { static let leftInset = 16.0 } - struct nameLabel { + enum nameLabel { static let width = 220.0 static let height = 33.0 @@ -32,7 +34,7 @@ extension ProfileDetailsView { static let heightProportion = height / width } - struct surnameLabel { + enum surnameLabel { static let width = 197.0 static let height = 33.0 @@ -41,14 +43,14 @@ extension ProfileDetailsView { static let heightProportion = height / width } - struct usernameLabel { + enum usernameLabel { static let width = 223.0 static let height = 22.0 static let heightProportion = height / width } - struct phoneLabel { + enum phoneLabel { static let width = 223.0 static let height = 22.0 @@ -57,14 +59,14 @@ extension ProfileDetailsView { static let heightProportion = height / width } - struct balanceLabel { + enum balanceLabel { static let width = 44.0 static let height = 17.0 static let topInset = 4.0 static let heightProportion = height / width } - struct fundsLabel { + enum fundsLabel { static let width = 66.0 static let height = 17.0 @@ -72,13 +74,13 @@ extension ProfileDetailsView { } // MARK: QR Button - struct qrButton { + enum qrButton { static let width: CGFloat = 40.0 static let rightInset = 16.0 } - struct addWalletButton { + enum addWalletButton { static let width = 50 static let height = 36 } diff --git a/Nynja/Modules/Profile/View/ProfileViewController.swift b/Nynja/Modules/Profile/View/ProfileViewController.swift index d5459dd4df20c979d9483f6a6e43574a0842cbdf..81be85f01a06986a99548d131d1ea14ce929d791 100644 --- a/Nynja/Modules/Profile/View/ProfileViewController.swift +++ b/Nynja/Modules/Profile/View/ProfileViewController.swift @@ -58,6 +58,12 @@ class ProfileViewController: BaseVC, ProfileViewProtocol { super.initialize() setupUI() } + + override func viewWillAppear(_ animated: Bool) { + super.viewWillAppear(animated) + // FIXME: update visile cells in other way + tableView.reloadData() + } override func viewDidLayoutSubviews() { super.viewDidLayoutSubviews() @@ -85,7 +91,10 @@ class ProfileViewController: BaseVC, ProfileViewProtocol { tableView.register(StarMessageCell.self, forCellReuseIdentifier: StarMessageCell.cellId) tableViewDelegate = ProfileTableViewDelegate(sectionDelegate: self) - tableViewDS = ProfileTablewViewDS(payloadParser: payloadParser, contactCellDelegate: self, chatCellDelegate: self) + tableViewDS = ProfileTablewViewDS(payloadParser: payloadParser, + typingObservable: presenter, + contactCellDelegate: self, + chatCellDelegate: self) tableView.delegate = tableViewDelegate tableView.dataSource = tableViewDS @@ -108,9 +117,9 @@ class ProfileViewController: BaseVC, ProfileViewProtocol { //MARK: - ProfileViewProtocol func setup(contact: Contact) { - detailsView.avatarImageView.setImage(url: contact.avatarUrl, - placeHolder: UIImage.nynja.Contacts.avaPlaceholder.image, - accessibilityPrefix: "profile_details_view") + detailsView.avatarImageView.imageView.setImage(url: contact.avatarUrl, + placeHolder: UIImage.nynja.Contacts.avaPlaceholder.image, + accessibilityPrefix: "profile_details_view") detailsView.nameLabel.text = contact.names detailsView.surnameLabel.text = contact.surnames @@ -150,7 +159,7 @@ class ProfileViewController: BaseVC, ProfileViewProtocol { } @objc private func avatarTapped() { - presenter.showImagePreview(from: detailsView.avatarImageView) + presenter.showImagePreview(from: detailsView.avatarImageView.imageView) } @objc private func walletButtonAction() { diff --git a/Nynja/Modules/Profile/View/TableView/ProfileTablewViewDS.swift b/Nynja/Modules/Profile/View/TableView/ProfileTablewViewDS.swift index f5b2e40fad90b69dc2a2f5eec217d916f23ce76d..1c7cf6a6b032c1d08dc47318442c742f51c10f0e 100644 --- a/Nynja/Modules/Profile/View/TableView/ProfileTablewViewDS.swift +++ b/Nynja/Modules/Profile/View/TableView/ProfileTablewViewDS.swift @@ -21,11 +21,15 @@ class ProfileTablewViewDS: NSObject, UITableViewDataSource { private let payloadParser: MessagePayloadParserInput + private let typingObservable: TypingObservable + init(payloadParser: MessagePayloadParserInput, + typingObservable: TypingObservable, contactCellDelegate: ProfileContactCellDelegate, chatCellDelegate: ChatListMessageCellModelDelegate) { self.payloadParser = payloadParser + self.typingObservable = typingObservable self.contactCellDelegate = contactCellDelegate self.chatCellDelegate = chatCellDelegate } @@ -68,6 +72,7 @@ class ProfileTablewViewDS: NSObject, UITableViewDataSource { if let cellModel = cellModel as? DialogCellModel { let model = ChatListMessageCellModel( model: cellModel, + observable: typingObservable, payloadParser: payloadParser, delegate: chatCellDelegate ) diff --git a/Nynja/Modules/Profile/WireFrame/ProfileWireframe.swift b/Nynja/Modules/Profile/WireFrame/ProfileWireframe.swift index 119210b333d4166f8356782538a7e9dff47d225b..e64262d1e54d2006f490bc29bb49e55bfb528e9f 100644 --- a/Nynja/Modules/Profile/WireFrame/ProfileWireframe.swift +++ b/Nynja/Modules/Profile/WireFrame/ProfileWireframe.swift @@ -16,9 +16,7 @@ class ProfileWireFrame: ProfileWireFrameProtocol { main: MainWireFrame?) { // Dependencies - let contactsProvider = ContactsProvider() - let conversationsProvider = ConversationsProvider() - let mqttService = MQTTService.sharedInstance + let serviceFactory = ServiceFactory() // Components let view = ProfileViewController() @@ -26,9 +24,10 @@ class ProfileWireFrame: ProfileWireFrameProtocol { let interactor = ProfileInteractor() let interactorDependencies = ProfileInteractor.Dependencies(presenter: presenter, - contactsProvider: contactsProvider, - conversationsProvider: conversationsProvider, - mqttService: mqttService) + contactsProvider: serviceFactory.makeContactsProvider(), + conversationsProvider: serviceFactory.makeConversationsProvider(), + typingProvider: serviceFactory.makeTypingProvider(), + mqttService: serviceFactory.makeMQTTService()) let presenterDependencies = ProfilePresenter.Dependencies(view: view, wireFrame: self, interactor: interactor) let viewDependencies = ProfileViewController.Dependencies(presenter: presenter) diff --git a/Nynja/Modules/QRCodeReader/Interactor/QRCodeReaderInteractor.swift b/Nynja/Modules/QRCodeReader/Interactor/QRCodeReaderInteractor.swift index 60cf171cd21e43a42267ef7b9f62a73bf58541e6..b5fb466d43b222b0e4c67eb2cd1ab4d8e113080f 100644 --- a/Nynja/Modules/QRCodeReader/Interactor/QRCodeReaderInteractor.swift +++ b/Nynja/Modules/QRCodeReader/Interactor/QRCodeReaderInteractor.swift @@ -14,7 +14,7 @@ class QRCodeReaderInteractor: QRCodeReaderInteractorInputProtocol, IoHandlerDele var status = "" init() { - IoHandler.delegate = self + IoHandler.shared.delegate = self } func getContactByPhone(number: String) { diff --git a/Nynja/Modules/ScheduleMessage/Interactor/ScheduleMessageInteractor.swift b/Nynja/Modules/ScheduleMessage/Interactor/ScheduleMessageInteractor.swift index dec068752168e6dbf8ddf503faa8ba341d79b067..788fc8f48e606eed2f834bf6ef1a3c4887137d0c 100644 --- a/Nynja/Modules/ScheduleMessage/Interactor/ScheduleMessageInteractor.swift +++ b/Nynja/Modules/ScheduleMessage/Interactor/ScheduleMessageInteractor.swift @@ -58,7 +58,7 @@ final class ScheduleMessageInteractor: BaseInteractor, ScheduleMessageInteractor required init(mode: ScheduledMessageMode) { self.mode = mode super.init() - IoHandler.delegate = self + IoHandler.shared.delegate = self } func fetchInfo() { diff --git a/Nynja/Modules/Settings/Security/Interactor/SecurityInteractor.swift b/Nynja/Modules/Settings/Security/Interactor/SecurityInteractor.swift index a1b34aa39d156bc36fd69e466c5fff409f7c04d1..99602f66925088650c29da8873e6a6fe58dc3136 100644 --- a/Nynja/Modules/Settings/Security/Interactor/SecurityInteractor.swift +++ b/Nynja/Modules/Settings/Security/Interactor/SecurityInteractor.swift @@ -15,8 +15,8 @@ class SecurityInteractor: SecurityInteractorInputProtocol, AuthHandlerDelegate, private var timer : Timer? init() { - AuthHandler.delegate = self - IoHandler.delegate = self + AuthHandler.shared.delegate = self + IoHandler.shared.delegate = self } //MARK: - SecurityInteractorInputProtocol diff --git a/Nynja/Observable/KeyedObservable.swift b/Nynja/Observable/KeyedObservable.swift new file mode 100644 index 0000000000000000000000000000000000000000..3ade428dc853fbdbec356624aec02f98d3f432d4 --- /dev/null +++ b/Nynja/Observable/KeyedObservable.swift @@ -0,0 +1,44 @@ +// +// KeyedObservable.swift +// Nynja +// +// Created by Anton Poltoratskyi on 26.10.2018. +// Copyright © 2018 TecSynt Solutions. All rights reserved. +// + +protocol KeyedObservable: class { + associatedtype Key: Hashable + associatedtype Value + typealias Callback = (Key, Value) -> Void + + var observable: KeyedObservableContainer { get } + + func addObserver(_ observer: AnyObject, callback: @escaping Callback) + func addObserver(_ observer: AnyObject, for key: Key, callback: @escaping Callback) + func removeObserver(_ observer: AnyObject) + func removeObserver(_ observer: AnyObject, for key: Key) + func notify(_ key: Key, with value: Value) +} + +extension KeyedObservable { + + func addObserver(_ observer: AnyObject, callback: @escaping Callback) { + observable.addObserver(observer, callback: callback) + } + + func addObserver(_ observer: AnyObject, for key: Key, callback: @escaping Callback) { + observable.addObserver(observer, for: key, callback: callback) + } + + func removeObserver(_ observer: AnyObject) { + observable.removeObserver(observer) + } + + func removeObserver(_ observer: AnyObject, for key: Key) { + observable.removeObserver(observer, for: key) + } + + func notify(_ key: Key, with value: Value) { + observable.notify(key, with: value) + } +} diff --git a/Nynja/Observable/KeyedObservableContainer.swift b/Nynja/Observable/KeyedObservableContainer.swift new file mode 100644 index 0000000000000000000000000000000000000000..f728dfe2e7c35c96673ead03adb016a2bb5c3223 --- /dev/null +++ b/Nynja/Observable/KeyedObservableContainer.swift @@ -0,0 +1,72 @@ +// +// KeyedObservableContainer.swift +// Nynja +// +// Created by Anton Poltoratskyi on 26.10.2018. +// Copyright © 2018 TecSynt Solutions. All rights reserved. +// + +import Foundation + +final class KeyedObservableContainer { + + private typealias Observers = [AnyWeakSubscriber] + + private struct Handler { + var callback: (Key, Value) -> Void + } + + private let lock = NSLock() + + private var allObservers: Observers = [] + + private var observers: [Key: Observers] = [:] + + func addObserver(_ observer: AnyObject, callback: @escaping (Key, Value) -> Void) { + let handler = Handler(callback: callback) + let container = AnyWeakSubscriber(object: observer, handler: handler) + + lock.lock() + + allObservers.append(container) + + lock.unlock() + } + + func addObserver(_ observer: AnyObject, for key: Key, callback: @escaping (Key, Value) -> Void) { + let handler = Handler(callback: callback) + let container = AnyWeakSubscriber(object: observer, handler: handler) + + lock.lock() + + var newObservers = observers[key] ?? [] + newObservers.append(container) + observers[key] = newObservers + + lock.unlock() + } + + func removeObserver(_ observer: AnyObject) { + lock.lock() + allObservers.removeAll { $0.object.value === observer || $0.object.value == nil } + for (key, _) in observers { + observers[key]?.removeAll { $0.object.value === observer || $0.object.value == nil } + } + lock.unlock() + } + + func removeObserver(_ observer: AnyObject, for key: Key) { + lock.lock() + observers[key]?.removeAll { $0.object.value === observer || $0.object.value == nil } + lock.unlock() + } + + func notify(_ key: Key, with value: Value) { + lock.lock() + for observer in allObservers { + observer.handler.callback(key, value) + } + observers[key]?.forEach { $0.handler.callback(key, value) } + lock.unlock() + } +} diff --git a/Nynja/Observable/Observable.swift b/Nynja/Observable/Observable.swift new file mode 100644 index 0000000000000000000000000000000000000000..0f648eb7dd42ab1b6fd5e2f5ff2a6b0b662db7a6 --- /dev/null +++ b/Nynja/Observable/Observable.swift @@ -0,0 +1,31 @@ +// +// Observable.swift +// Nynja +// +// Created by Anton Poltoratskyi on 29.10.2018. +// Copyright © 2018 TecSynt Solutions. All rights reserved. +// + +protocol Observable: class { + associatedtype Observer + var observable: ObservableContainer { get } + + func addObserver(_ observer: Observer) + func removeObserver(_ observer: Observer) + func notify(_ block: (Observer) -> Void) +} + +extension Observable { + + func addObserver(_ observer: Observer) { + observable.addObserver(observer) + } + + func removeObserver(_ observer: Observer) { + observable.removeObserver(observer) + } + + func notify(_ block: (Observer) -> Void) { + observable.notify(block) + } +} diff --git a/Nynja/Observable/ObservableContainer.swift b/Nynja/Observable/ObservableContainer.swift new file mode 100644 index 0000000000000000000000000000000000000000..183ee0dd5f5e998cdb58d8c88530012233e8ecc9 --- /dev/null +++ b/Nynja/Observable/ObservableContainer.swift @@ -0,0 +1,41 @@ +// +// ObservableContainer.swift +// Nynja +// +// Created by Anton Poltoratskyi on 29.10.2018. +// Copyright © 2018 TecSynt Solutions. All rights reserved. +// + +import Foundation + +final class ObservableContainer { + + private typealias Observers = [WeakRef] + + private let lock = NSLock() + + private var observers: Observers = [] + + func addObserver(_ observer: T) { + let container = WeakRef(value: observer as AnyObject) + + lock.lock() + + observers.append(container) + + lock.unlock() + } + + func removeObserver(_ observer: T) { + let observer = observer as AnyObject + lock.lock() + observers.removeAll { $0.value === observer || $0.value == nil } + lock.unlock() + } + + func notify(_ block: (T) -> Void) { + lock.lock() + observers.forEach { ($0.value as? T).flatMap(block) } + lock.unlock() + } +} diff --git a/Nynja/Resources/en.lproj/Localizable.strings b/Nynja/Resources/en.lproj/Localizable.strings index 081cc02f6dae9ee025a2257052291d3caedd0531..f0a3d16be68419f082ebc8bf3420b9e89e40af0e 100644 --- a/Nynja/Resources/en.lproj/Localizable.strings +++ b/Nynja/Resources/en.lproj/Localizable.strings @@ -492,9 +492,11 @@ "deleted_message_replied_preview" = "Deleted message"; // MARK: Message -"message_status_typing"="...typing"; +"message_status_typing"="typing"; "message_new_messages"="New messages"; -"message_sending"="...sending a"; +"message_sending"="sending"; +"message_typing_status_people"="%@ people"; +"message_typing_status_undefined"="..."; // MARK: Sending Status "file"="file"; @@ -502,7 +504,7 @@ // MARK: Recording Status "video"="video"; -"recording"="...recording a"; +"recording"="recording"; // MARK: Presence status "active"="active"; diff --git a/Nynja/ServerModel/Model/Typing.swift b/Nynja/ServerModel/Model/Typing.swift index f37b1826744e18814b877285cb2192b3f194f42f..e807bc87c674d730598f78c0e0dc87e1b840f8c1 100644 --- a/Nynja/ServerModel/Model/Typing.swift +++ b/Nynja/ServerModel/Model/Typing.swift @@ -1,5 +1,7 @@ class Typing { - var phone_id: String? + var feed_id: String? + var sender_id: String? + var sender_alias: String? var comments: AnyObject? -} \ No newline at end of file +} diff --git a/Nynja/ServerModel/Source/Decoder.swift b/Nynja/ServerModel/Source/Decoder.swift index 0af94781c52a8b09bfadef3a11e479ca94bbbb6f..d6116cd2e85fb14e05b9c406a6a8f011c987beca 100644 --- a/Nynja/ServerModel/Source/Decoder.swift +++ b/Nynja/ServerModel/Source/Decoder.swift @@ -304,10 +304,12 @@ func parseObject(name: String, body:[Model], tuple: BertTuple) -> AnyObject? a_History.status = body[5].parse(bert: tuple.elements[6]) as? AnyObject return a_History case "Typing": - if body.count != 2 { return nil } + if body.count != 4 { return nil } let a_Typing = Typing() - a_Typing.phone_id = body[0].parse(bert: tuple.elements[1]) as? String - a_Typing.comments = body[1].parse(bert: tuple.elements[2]) as? AnyObject + a_Typing.feed_id = body[0].parse(bert: tuple.elements[1]) as? String + a_Typing.sender_id = body[1].parse(bert: tuple.elements[2]) as? String + a_Typing.sender_alias = body[2].parse(bert: tuple.elements[3]) as? String + a_Typing.comments = body[3].parse(bert: tuple.elements[4]) as? AnyObject return a_Typing case "Contact": if body.count != 14 || tuple.elements.count != 15 { return nil } @@ -540,3 +542,4 @@ func parseObject(name: String, body:[Model], tuple: BertTuple) -> AnyObject? default: return nil } } + diff --git a/Nynja/ServerModel/Spec/Typing_Spec.swift b/Nynja/ServerModel/Spec/Typing_Spec.swift index 41ceda8a5b1b019907e27ad81475ccf598b5e188..bdc43d54c3686453112917f6a1e008d29f500b0d 100644 --- a/Nynja/ServerModel/Spec/Typing_Spec.swift +++ b/Nynja/ServerModel/Spec/Typing_Spec.swift @@ -1,4 +1,10 @@ func get_Typing() -> Model { return Model(value:Tuple(name:"Typing",body:[ Model(value:Binary()), + Model(value:Chain(types:[ + Model(value:List(constant:"")), + Model(value:Binary())])), + Model(value:Chain(types:[ + Model(value:List(constant:"")), + Model(value:Binary())])), Model(value:Chain(types:[Model(value:Tuple()),Model(value:Atom()),Model(value:Binary()),Model(value:Number()),Model(value:List(constant:""))]))]))} diff --git a/Nynja/Services/HandleServices/ContactHandler.swift b/Nynja/Services/HandleServices/ContactHandler.swift index 8a6aa1cfee40e159df9bea6d0fac0f7605583454..74b1f09b467afb1002c6457ad2f583808c3020f1 100644 --- a/Nynja/Services/HandleServices/ContactHandler.swift +++ b/Nynja/Services/HandleServices/ContactHandler.swift @@ -8,18 +8,25 @@ import Foundation -class ContactHandler: BaseHandler { +final class ContactHandler: BaseHandler { + + // MARK: - Singleton + + static let shared = ContactHandler() + + private init() {} + // MARK: - Dependencies - static var storageService: StorageService { + var storageService: StorageService { return .sharedInstance } // MARK: - Handler - static func executeHandle(data: BertTuple) { + func executeHandle(data: BertTuple) { guard let contact = get_Contact().parse(bert: data) as? Contact, let status = contact.originalStatus else { return @@ -46,11 +53,11 @@ class ContactHandler: BaseHandler { // MARK: - Statuses - private static func handleDeleted(_ contact: Contact) { + private func handleDeleted(_ contact: Contact) { try? storageService.perform(action: .delete, with: contact) } - private static func handleInternal(_ contact: Contact) { + private func handleInternal(_ contact: Contact) { var columns: Set = [.presence] if contact.updated != 0 { columns.insert(.update) @@ -58,11 +65,11 @@ class ContactHandler: BaseHandler { ContactDAO.updateColumns(columns, contact: contact) } - private static func handleLastMessage(_ contact: Contact) { + private func handleLastMessage(_ contact: Contact) { ContactDAO.updateColumns([.unread], contact: contact) } - private static func handleFriend(_ contact: Contact, data: BertTuple) { + private func handleFriend(_ contact: Contact, data: BertTuple) { guard let phoneId = contact.phone_id, let prevContact = ContactDAO.findContactBy(phoneId: phoneId), @@ -87,7 +94,7 @@ class ContactHandler: BaseHandler { } } - private static func handleAuthorization(_ contact: Contact, data: BertTuple) { + private func handleAuthorization(_ contact: Contact, data: BertTuple) { do { try storageService.perform(action: .save, with: contact) NotificationManager.shared.handle(bert: data, type: .request) @@ -96,7 +103,7 @@ class ContactHandler: BaseHandler { } } - private static func handleBan(_ contact: Contact) { + private func handleBan(_ contact: Contact) { guard let phoneId = contact.phone_id, let prevContact = ContactDAO.findContactBy(phoneId: phoneId) else { diff --git a/Nynja/Services/HandleServices/HistoryHandler.swift b/Nynja/Services/HandleServices/HistoryHandler.swift index 11d6bf8246d6aeeb6d5450abd9f793e983d53fb8..9a405174ec6ba7219be676a4b9be756b16951bc0 100644 --- a/Nynja/Services/HandleServices/HistoryHandler.swift +++ b/Nynja/Services/HandleServices/HistoryHandler.swift @@ -22,19 +22,26 @@ extension HistoryHandlerDelegate { final class HistoryHandler: BaseHandler { + // MARK: - Singleton + + static let shared = HistoryHandler() + + private init() {} + + // MARK: - Subscribers - private static let subscribersLock = NSLock() + private let subscribersLock = NSLock() - private static var subscribers = [WeakRef]() + private var subscribers = [WeakRef]() - private static func notify(block: (HistoryHandlerDelegate) -> Void) { + private func notify(block: (HistoryHandlerDelegate) -> Void) { subscribersLock.lock() subscribers.forEach { ($0.value as? HistoryHandlerDelegate).map { block($0) } } subscribersLock.unlock() } - static func addSubscriber(_ subscriber: HistoryHandlerDelegate) { + func addSubscriber(_ subscriber: HistoryHandlerDelegate) { subscribersLock.lock() defer { subscribersLock.unlock() } @@ -45,7 +52,7 @@ final class HistoryHandler: BaseHandler { subscribers.append(ref) } - static func removeSubscriber(_ subscriber: HistoryHandlerDelegate) { + func removeSubscriber(_ subscriber: HistoryHandlerDelegate) { subscribersLock.lock() subscribers = subscribers.filter { $0.value != nil && $0.value !== subscriber } subscribersLock.unlock() @@ -54,22 +61,22 @@ final class HistoryHandler: BaseHandler { // MARK: - Dependencies - static var storageService: StorageService { + var storageService: StorageService { return StorageService.sharedInstance } - static var messageEditService: MessageEditServiceProtocol { + var messageEditService: MessageEditServiceProtocol { return MessageEditService(dependencies: .init(storageService: storageService)) } - static let stickersDownloadingService: StickersDownloadingService = { + let stickersDownloadingService: StickersDownloadingService = { return StickersDownloadingService() }() // MARK: - Handler - static func executeHandle(data: BertTuple) { + func executeHandle(data: BertTuple) { guard let history = get_History().parse(bert: data) as? History else { return } @@ -102,7 +109,7 @@ final class HistoryHandler: BaseHandler { // MARK: -- Messages /// The first message is new, the last is old. - private static func updateMessageHistory(_ messages: [Message]) { + private func updateMessageHistory(_ messages: [Message]) { var stackForSave = [Message](reserveCapacity: messages.count) var stackForDelete = [Message]() @@ -178,7 +185,7 @@ final class HistoryHandler: BaseHandler { systemClearMessage: systemClearMessage) } - private static func saveMessageHistory(stackForSave: [Message], + private func saveMessageHistory(stackForSave: [Message], stackForDelete: [Message], repliedMessages: [MessageServerId: [Message]], visibleRepliedMessages: Set, @@ -213,7 +220,7 @@ final class HistoryHandler: BaseHandler { try? MessageActionDAO.delete(deletedActions) } - private static func fetchType(from feed: AnyObject?) -> FetchType? { + private func fetchType(from feed: AnyObject?) -> FetchType? { switch feed { case let feed as muc: guard let name = feed.name else { @@ -231,14 +238,14 @@ final class HistoryHandler: BaseHandler { } /// Mark messages with 'serverId' <= id as trusted - private static func markHistoryAsTrusted(before id: MessageServerId, in fetchType: FetchType) { + private func markHistoryAsTrusted(before id: MessageServerId, in fetchType: FetchType) { try? MessageDAO.trustMessages(before: id, in: fetchType) } // MARK: -- Jobs - private static func updateJobsHistory(_ jobs: [Job]) { + private func updateJobsHistory(_ jobs: [Job]) { let stackForSave = jobs.filter { StringAtom.string($0.status) == "pending" } let deleteStatuses = ["delete", "complete"] @@ -253,7 +260,7 @@ final class HistoryHandler: BaseHandler { // MARK: -- Stickers - private static func updateStickerPacks(_ stickerPacks: [StickerPack]) { + private func updateStickerPacks(_ stickerPacks: [StickerPack]) { for package in stickerPacks { try? storageService.perform(action: .save, with: package) } diff --git a/Nynja/Services/HandleServices/MessageHandler.swift b/Nynja/Services/HandleServices/MessageHandler.swift index 7fbeebcc33f0c6eb5d591636f5913811cf44f0db..45bff6b4a884660a5a0fd765cdcec67d3b0ea79b 100644 --- a/Nynja/Services/HandleServices/MessageHandler.swift +++ b/Nynja/Services/HandleServices/MessageHandler.swift @@ -10,26 +10,33 @@ import Foundation final class MessageHandler: BaseHandler { + // MARK: - Singleton + + static let shared = MessageHandler() + + private init() {} + + // MARK: - Dependencies - private static var storageService: StorageService { + private var storageService: StorageService { return StorageService.sharedInstance } - private static var notificationManager: NotificationManager { + private var notificationManager: NotificationManager { return NotificationManager.shared } - private static var systemSoundManager: SystemSoundManager { + private var systemSoundManager: SystemSoundManager { return SystemSoundManager.sharedInstance } // MARK: - Subscribers - static var subscribers: [MessageHandlerSubscriberReference] = [] + private var subscribers: [MessageHandlerSubscriberReference] = [] - static func addSubscriber(_ subscriber: MessageHandlerSubscriber) { + func addSubscriber(_ subscriber: MessageHandlerSubscriber) { guard !subscribers.contains(where: { $0.subscriber === subscriber }) else { return } @@ -37,14 +44,14 @@ final class MessageHandler: BaseHandler { subscribers.append(ref) } - static func removeSubscriber(_ subscriber: MessageHandlerSubscriber) { + func removeSubscriber(_ subscriber: MessageHandlerSubscriber) { subscribers = subscribers.filter { $0.subscriber != nil && $0.subscriber !== subscriber } } // MARK: - Execute - static func executeHandle(data: BertTuple) { + func executeHandle(data: BertTuple) { guard let message = get_Message().parse(bert: data) as? Message else { return } let types = message.types @@ -66,11 +73,11 @@ final class MessageHandler: BaseHandler { } } - private static func clearHistory(_ message: Message) { + private func clearHistory(_ message: Message) { ChatService.clearHistory(message) } - private static func updateReader(from message: Message) { + private func updateReader(from message: Message) { let shouldUpdateOwnReader = self.shouldUpdateOwnReader(from: message) let shouldUpdateOtherReader = !shouldUpdateOwnReader || message.isInOwnChat @@ -83,11 +90,11 @@ final class MessageHandler: BaseHandler { } } - private static func shouldUpdateOwnReader(from message: Message) -> Bool { + private func shouldUpdateOwnReader(from message: Message) -> Bool { return message.isOwn } - private static func deleteMessage(_ message: Message) { + private func deleteMessage(_ message: Message) { do { try save(message) ChatService.removeMessage(message) @@ -96,7 +103,7 @@ final class MessageHandler: BaseHandler { } } - private static func editMessage(_ message: Message) { + private func editMessage(_ message: Message) { do { try save(message) try ChatService.editMessage(message) @@ -105,7 +112,7 @@ final class MessageHandler: BaseHandler { } } - private static func updateMessage(_ message: Message) { + private func updateMessage(_ message: Message) { do { try save(message) try ChatService.updateMessage(message) @@ -115,7 +122,7 @@ final class MessageHandler: BaseHandler { } } - private static func saveMessage(_ message: Message, data: BertTuple) { + private func saveMessage(_ message: Message, data: BertTuple) { guard !shouldSkipMessage(message) else { return } @@ -159,7 +166,7 @@ final class MessageHandler: BaseHandler { } } - private static func shouldSkipMessage(_ message: Message) -> Bool { + private func shouldSkipMessage(_ message: Message) -> Bool { if let desc = message.files?.first, desc.mime == SendMessageType.audioCall.rawValue, desc.data?.first?.key == FeatureKeys.File.Call.users.rawValue, @@ -172,7 +179,7 @@ final class MessageHandler: BaseHandler { return false } - private static func save(_ message: Message) throws { + private func save(_ message: Message) throws { if let repliedMessage = message.repliedMessage { repliedMessage.localStatus = try MessageDAO.localStatusForRepliedMessage(repliedMessage) } @@ -185,7 +192,7 @@ final class MessageHandler: BaseHandler { /// Play sound for incoming messages if chat isn't muted. /// Play outcoming message only if chat screen is open. - private static func playSoundIfNeeded(for message: Message) { + private func playSoundIfNeeded(for message: Message) { guard message.isDelivered, let currentPhoneId = storageService.phoneId else { return } diff --git a/Nynja/Services/HandleServices/ProfileHandler.swift b/Nynja/Services/HandleServices/ProfileHandler.swift index d2d68a05cd793a71650300a0c934c2a3ae184792..8b99fe60e650747898af00aead986ae4107cad66 100644 --- a/Nynja/Services/HandleServices/ProfileHandler.swift +++ b/Nynja/Services/HandleServices/ProfileHandler.swift @@ -6,34 +6,40 @@ // Copyright © 2017 TecSynt Solutions. All rights reserved. // -class ProfileHandler: BaseHandler { +final class ProfileHandler: BaseHandler { + + // MARK: - Singleton + + static let shared = ProfileHandler() + + private init() {} // MARK: - Dependencies - static var mqttService: MQTTService { + var mqttService: MQTTService { return MQTTService.sharedInstance } - static var historyFactory: HistoryRequestModelFactoryProtocol { + var historyFactory: HistoryRequestModelFactoryProtocol { return HistoryRequestModelFactory() } - static var storageService: StorageService { + var storageService: StorageService { return StorageService.sharedInstance } - static var messageBackgroundTaskHandler: BackgroundTaskHandler { + var messageBackgroundTaskHandler: BackgroundTaskHandler { return MessageBackgroundTaskHandler() } - static var alertManager: AlertManager { + var alertManager: AlertManager { return AlertManager.sharedInstance } // MARK: - Handler - static func executeHandle(data: BertTuple) { + func executeHandle(data: BertTuple) { guard let profile = get_Profile().parse(bert: data) as? Profile, let status = profile.status?.string else { return @@ -60,7 +66,7 @@ class ProfileHandler: BaseHandler { // MARK: Get & Init - private static func handleGetInit(_ profile: Profile) { + private func handleGetInit(_ profile: Profile) { do { guard let roster = (profile.rosters as? [Roster])?.first else { return @@ -82,7 +88,7 @@ class ProfileHandler: BaseHandler { } catch { } } - private static func prepareForReceived(_ newRoster: Roster) { + private func prepareForReceived(_ newRoster: Roster) { let currentRoster = RosterDAO.currentRoster func shouldSave(_ message: Message?) -> Bool { @@ -146,11 +152,11 @@ class ProfileHandler: BaseHandler { } } - private static func configureTestFairy(with roster: Roster) { + private func configureTestFairy(with roster: Roster) { TestFairy.setUserId("\(roster.myContact?.phone_id ?? "")_\(roster.myContact?.fullName ?? "")") } - private static func configureNynjaCommunicatorService(_ profile: Profile) { + private func configureNynjaCommunicatorService(_ profile: Profile) { guard let rosterId = (profile.rosters?.first as? Roster)?.myContact?.phone_id else { return } @@ -158,7 +164,7 @@ class ProfileHandler: BaseHandler { NynjaCommunicatorService.sharedInstance.initialize() } - private static func requestJobs(with phoneId: String) { + private func requestJobs(with phoneId: String) { do { let historyModel = try historyFactory.makeAllJobsRequest(rosterId: phoneId) mqttService.sendHistoryRequest(with: historyModel) @@ -167,7 +173,7 @@ class ProfileHandler: BaseHandler { } } - private static func requestStickerPacks(with rosterId: String) { + private func requestStickerPacks(with rosterId: String) { do { let historyModel = try historyFactory.makeStickerPackagesRequest(rosterId: rosterId) mqttService.sendHistoryRequest(with: historyModel) @@ -179,7 +185,7 @@ class ProfileHandler: BaseHandler { // MARK: Remove - private static func handleRemove(_ profile: Profile) { + private func handleRemove(_ profile: Profile) { try? storageService.perform(action: .delete, with: profile) storageService.phone = nil alertManager.showAlertOk(message: String.localizable.authAttemptsRemoved) diff --git a/Nynja/Services/HandleServices/RoomHandler.swift b/Nynja/Services/HandleServices/RoomHandler.swift index 6193bb93165b4f1981a579924b9920c7061b479d..a1ca2f862c449058f475565a3bd4ce942982a725 100644 --- a/Nynja/Services/HandleServices/RoomHandler.swift +++ b/Nynja/Services/HandleServices/RoomHandler.swift @@ -6,22 +6,29 @@ // Copyright © 2017 TecSynt Solutions. All rights reserved. // -class RoomHandler: BaseHandler { +final class RoomHandler: BaseHandler { + + // MARK: - Singleton + + static let shared = RoomHandler() + + private init() {} + // MARK: - Dependencies - static var storageService: StorageService { + var storageService: StorageService { return .sharedInstance } - static var notificationManager: NotificationManager { + var notificationManager: NotificationManager { return .shared } // MARK: - Handler - static func executeHandle(data: BertTuple, codes: StatusCodes) { + func executeHandle(data: BertTuple, codes: StatusCodes) { guard let room = get_Room().parse(bert: data) as? Room else { return } @@ -32,7 +39,7 @@ class RoomHandler: BaseHandler { } } - private static func handle(room: Room, data: BertTuple) { + private func handle(room: Room, data: BertTuple) { guard let status = room.originalStatus else { return } @@ -62,7 +69,7 @@ class RoomHandler: BaseHandler { } } - private static func handle(codes: StatusCodes, room: Room) { + private func handle(codes: StatusCodes, room: Room) { let statusCodeManager = StatusCodeManager.shared codes.forEach { statusCodeManager.notify(model: room, code: $0) } } @@ -71,7 +78,7 @@ class RoomHandler: BaseHandler { // MARK: - Statuses // MARK: - Add Member - private static func handleAddMember(_ room: Room) { + private func handleAddMember(_ room: Room) { trustLastMessageIfNeeded(for: room) guard let id = room.id, let oldRoom = RoomDAO.findRoom(by: id) else { @@ -91,7 +98,7 @@ class RoomHandler: BaseHandler { // MARK: - Add Member Channel - private static func handleChannelAddMember(_ room: Room, oldRoom: Room) { + private func handleChannelAddMember(_ room: Room, oldRoom: Room) { if let features = room.settings { oldRoom.settings = features } @@ -102,7 +109,7 @@ class RoomHandler: BaseHandler { // MARK: - Add Member Room - private static func handleGroupAddMember(_ room: Room, oldRoom: Room) { + private func handleGroupAddMember(_ room: Room, oldRoom: Room) { addNotExistedMembers(from: room, to: oldRoom) filterAdmins(using: room, in: oldRoom) @@ -117,7 +124,7 @@ class RoomHandler: BaseHandler { try? storageService.perform(action: .save, with: oldRoom) } - private static func addNotExistedMembers(from room: Room, to oldRoom: Room) { + private func addNotExistedMembers(from room: Room, to oldRoom: Room) { var notExistedMembers: [Member] = [] room.members?.forEach { member in @@ -133,7 +140,7 @@ class RoomHandler: BaseHandler { oldRoom.members = members } - private static func filterAdmins(using room: Room, in oldRoom: Room) { + private func filterAdmins(using room: Room, in oldRoom: Room) { oldRoom.admins = oldRoom.admins?.filter { admin in guard let members = room.members else { return true @@ -142,7 +149,7 @@ class RoomHandler: BaseHandler { } } - private static func addNotExistedAdmins(from room: Room, to oldRoom: Room) { + private func addNotExistedAdmins(from room: Room, to oldRoom: Room) { var notExistsAdmins: [Member] = [] room.admins?.forEach { member in @@ -158,7 +165,7 @@ class RoomHandler: BaseHandler { oldRoom.admins = admins } - private static func filterMembers(using room: Room, in oldRoom: Room) { + private func filterMembers(using room: Room, in oldRoom: Room) { oldRoom.members = oldRoom.members?.filter { member in guard let admins = room.admins else { return true @@ -170,7 +177,7 @@ class RoomHandler: BaseHandler { // MARK: - Remove Member - private static func handleRemoveMember(_ room: Room) { + private func handleRemoveMember(_ room: Room) { trustLastMessageIfNeeded(for: room) if let id = room.id, let oldRoom = RoomDAO.findRoom(by: id) { @@ -187,7 +194,7 @@ class RoomHandler: BaseHandler { // MARK: - Leave - private static func handleLeave(_ room: Room) { + private func handleLeave(_ room: Room) { trustLastMessageIfNeeded(for: room) guard let id = room.id, let oldRoom = RoomDAO.findRoom(by: id) else { @@ -213,7 +220,7 @@ class RoomHandler: BaseHandler { // MARK: - Last Message - private static func trustLastMessageIfNeeded(for room: Room) { + private func trustLastMessageIfNeeded(for room: Room) { guard let lastMessage = room.last_msg else { return } @@ -223,10 +230,9 @@ class RoomHandler: BaseHandler { // MARK: - Update - private static func updateReadersUnreadAndStatus(from room: Room, oldRoom: Room) { + private func updateReadersUnreadAndStatus(from room: Room, oldRoom: Room) { oldRoom.unread = room.unread oldRoom.readers = room.readers oldRoom.status = room.status } - } diff --git a/Nynja/Services/HandleServices/RosterHandler.swift b/Nynja/Services/HandleServices/RosterHandler.swift index 419d89e79e2b9b98e44842f63957df0f916714d3..bab717eca8ab577bae747bf843bac630251f11fe 100644 --- a/Nynja/Services/HandleServices/RosterHandler.swift +++ b/Nynja/Services/HandleServices/RosterHandler.swift @@ -8,9 +8,18 @@ import Foundation -class RosterHandler: BaseHandler { +final class RosterHandler: BaseHandler { - static func executeHandle(data: BertTuple) { + // MARK: - Singleton + + static let shared = RosterHandler() + + private init() {} + + + // MARK: - Handler + + func executeHandle(data: BertTuple) { guard let roster = get_Roster().parse(bert: data) as? Roster, let status = (roster.status as? StringAtom)?.string else { return @@ -24,5 +33,4 @@ class RosterHandler: BaseHandler { try? StorageService.sharedInstance.perform(action: .save, with: roster) } } - } diff --git a/Nynja/Services/HandleServices/SearchHandler.swift b/Nynja/Services/HandleServices/SearchHandler.swift index 9d8570b15de66e722bcc946a1585f1b35d51705d..be743eb61d14d7fa9ca4e9f4406d11aa2269ef0a 100644 --- a/Nynja/Services/HandleServices/SearchHandler.swift +++ b/Nynja/Services/HandleServices/SearchHandler.swift @@ -8,9 +8,18 @@ import Foundation -class SearchHandler: BaseHandler { +final class SearchHandler: BaseHandler { - static func executeHandle(data: BertTuple) { + // MARK: - Singleton + + static let shared = SearchHandler() + + private init() {} + + + // MARK: - Handler + + func executeHandle(data: BertTuple) { guard let search = get_Search().parse(bert: data) as? Search, let ref = search.ref, let refType = SearchModelReference(rawValue: ref) else { diff --git a/Nynja/Services/HandleServices/StarHandler.swift b/Nynja/Services/HandleServices/StarHandler.swift index 41665196251603d483d7086d284b8761f1057901..c555a00d50b567b5f56972fb21d236135e3c3441 100644 --- a/Nynja/Services/HandleServices/StarHandler.swift +++ b/Nynja/Services/HandleServices/StarHandler.swift @@ -6,9 +6,18 @@ // Copyright © 2018 TecSynt Solutions. All rights reserved. // -class StarHandler: BaseHandler { +final class StarHandler: BaseHandler { - static func executeHandle(data: BertTuple) { + // MARK: - Singleton + + static let shared = StarHandler() + + private init() {} + + + // MARK: - Handler + + func executeHandle(data: BertTuple) { guard let star = get_Star().parse(bert: data) as? Star, let status = star.starStatus else { return } diff --git a/Nynja/Services/HandleServices/TypingHandler.swift b/Nynja/Services/HandleServices/TypingHandler.swift index e9023cba80c1b03a0e44589a87eb7567a1a43e88..ef56ab9abe82414714647b53df5901e1fe7f3c05 100644 --- a/Nynja/Services/HandleServices/TypingHandler.swift +++ b/Nynja/Services/HandleServices/TypingHandler.swift @@ -9,17 +9,29 @@ import Foundation protocol TypingHandlerDelegate: class { - func getTyping(typing: Typing) + func didReceiveTyping(_ typing: Typing) } -class TypingHandler: BaseHandler { +final class TypingHandler: BaseHandler, Observable { - static weak var delegate: TypingHandlerDelegate? + // MARK: - Singleton - static func executeHandle(data: BertTuple) { - if let typing = get_Typing().parse(bert: data) as? Typing { - delegate?.getTyping(typing: typing) + static let shared = TypingHandler() + + private init() {} + + + // MARK: - ObservableContainer + + let observable = ObservableContainer() + + + // MARK: - Handler + + func executeHandle(data: BertTuple) { + guard let typing = get_Typing().parse(bert: data) as? Typing else { + return } + notify { $0.didReceiveTyping(typing) } } - } diff --git a/Nynja/Services/MQTT/MQTTService.swift b/Nynja/Services/MQTT/MQTTService.swift index b3639a0f79a4ecd7bc13b08fd60e49cf29d70cc6..8794b9d0b04e5a742481172b0946d53d0bd4dc5a 100644 --- a/Nynja/Services/MQTT/MQTTService.swift +++ b/Nynja/Services/MQTT/MQTTService.swift @@ -172,7 +172,7 @@ final class MQTTService: NSObject, CocoaMQTTDelegate, ConnectionServiceDelegate self.state = .notAuthenticated(isLoggedOutFromServer: true) - IoHandler.delegate?.sessionNotFound() + IoHandler.shared.delegate?.sessionNotFound() notifySubscribers { (delegate) in delegate.mqttServiceDidReceiveAuthenticationFailure(self) } diff --git a/Nynja/Services/Member/MemberDAO.swift b/Nynja/Services/Member/MemberDAO.swift index 9cf7ed62a213c754530a8e8225a72be8f597268f..4375b187ad9e98dda9e3c55a6923a4f589f0aa01 100644 --- a/Nynja/Services/Member/MemberDAO.swift +++ b/Nynja/Services/Member/MemberDAO.swift @@ -52,6 +52,12 @@ class MemberDAO: MemberDAOProtocol { return Member(member: member) } + static func fetchMemberAlias(roomId: String, phoneId: String) -> String? { + return dbManager.fetch { db in + return try DBMember.memberAlias(from: db, roomId: roomId, phoneId: phoneId) + } + } + // MARK: - Update diff --git a/Nynja/Services/Member/MemberDAOProtocol.swift b/Nynja/Services/Member/MemberDAOProtocol.swift index a8d461120e5eafe0a3de125159ba54602ef286a6..bb5b49b1aa46d67fefa6b4bb194abcc5bcf1eb38 100644 --- a/Nynja/Services/Member/MemberDAOProtocol.swift +++ b/Nynja/Services/Member/MemberDAOProtocol.swift @@ -15,10 +15,11 @@ protocol MemberDAOProtocol: DAOProtocol { static func findMemberBy(id: Int64) -> Member? static func findMemberBy(roomId: String, phoneId: String) -> Member? + static func fetchMemberAlias(roomId: String, phoneId: String) -> String? + // MARK: - Update static func updateColumns(_ columns: Set, member: Member) static func updateReader(_ reader: Int64, roomId: String, phoneId: String) - } diff --git a/Nynja/Services/Models/TypingModel.swift b/Nynja/Services/Models/TypingModel.swift index 1306e2159fb3e7d48b9af9d5f98b532a31f4a000..8f74649471c9c6e08f47be6149a7442ec7bced9d 100644 --- a/Nynja/Services/Models/TypingModel.swift +++ b/Nynja/Services/Models/TypingModel.swift @@ -86,7 +86,9 @@ enum TypingModelType: String { final class TypingModel: BaseMQTTModel { enum Topic { - case p2p(phone: String) + /// phone number without '_{roster_id}' + case p2p(phoneNumber: String) + /// room id case room(id: String) fileprivate var path: String { diff --git a/Nynja/Services/ServiceFactory/MQTTHandlerFactoryProtocol.swift b/Nynja/Services/ServiceFactory/MQTTHandlerFactoryProtocol.swift new file mode 100644 index 0000000000000000000000000000000000000000..48fa93797f1458b3ccbd7a3bc4e8eb4569fb5ba3 --- /dev/null +++ b/Nynja/Services/ServiceFactory/MQTTHandlerFactoryProtocol.swift @@ -0,0 +1,13 @@ +// +// MQTTHandlerFactoryProtocol.swift +// Nynja +// +// Created by Anton Poltoratskyi on 01.11.2018. +// Copyright © 2018 TecSynt Solutions. All rights reserved. +// + +import Foundation + +protocol MQTTHandlerFactoryProtocol: class { + func makeTypingHandler() -> TypingHandler +} diff --git a/Nynja/Services/ServiceFactory/ServiceFactory.swift b/Nynja/Services/ServiceFactory/ServiceFactory.swift index f581ec61b326b571744b34cbedbdcf94e48afb35..d4367c0fbf4730d23ce19c02b11ac7c5e1d9f154 100644 --- a/Nynja/Services/ServiceFactory/ServiceFactory.swift +++ b/Nynja/Services/ServiceFactory/ServiceFactory.swift @@ -8,48 +8,7 @@ import Foundation -protocol ServiceFactoryProtocol: SharedServiceFactoryProtocol { - func makeMessageSendingService() -> MessageSendingServiceProtocol - func makeResourceManager() -> ResourceManagerProtocol - func makeMessageFactory() -> MessageFactoryProtocol - - func makeMessagePayloadBuilder() -> MessagePayloadBuilderInput - func makeMessagePayloadParser() -> MessagePayloadParserInput - - func makeCameraSettingsService(with flow: CameraSourceFlow) -> CameraSettingsServiceProtocol - - func makeMesageProcessingManager() -> MessageProcessingManagerInterface - - func makeHistoryRequestFactory() -> HistoryRequestModelFactoryProtocol - - func makeContactsProvider() -> ContactsProviding - func makeConversationsProvider() -> ConversationsProviding - func makeStickersProvider() -> StickersProviding - - func makeTextInputValidationService() -> TextInputValidationServiceProtocol - func makeWalletCreationTextInputValidationService() -> WalletCreationTextInputValidationServiceProtocol - func makeWalletOpeningTextInputValidationService() -> WalletOpeningTextInputValidationServiceProtocol - func makeWalletFundingNetworkService() -> WalletFundingNetworkService - func makePermissionManager() -> PermissionManager - - func makeWalletService() -> WalletService - func makeSyncFileManager() -> SyncFileManager - - func makeMuteChatService() -> MuteChatServiceProtocol - - func makeConnectionService() -> ConnectionService - - func makeAlertManager() -> AlertManager - - func makeStatusCodeManager() -> StatusCodeManager - - func makeChatScreenAlertFactory() -> ChatScreenAlertFactoryProtocol - func makeUseCaseValidationServise() -> UseCaseValidationServiceProtocol - - func makeAudioSessionManager() -> AudioSessionManager -} - -final class ServiceFactory: SharedServiceFactory, ServiceFactoryProtocol { +final class ServiceFactory: SharedServiceFactory, ServiceFactoryProtocol, MQTTHandlerFactoryProtocol { func makeMessageSendingService() -> MessageSendingServiceProtocol { let dependencies = MessageSendingService.Dependencies( @@ -94,6 +53,10 @@ final class ServiceFactory: SharedServiceFactory, ServiceFactoryProtocol { func makeHistoryRequestFactory() -> HistoryRequestModelFactoryProtocol { return HistoryRequestModelFactory() } + + func makeTypingProvider() -> TypingProvider { + return TypingProviderImpl.shared + } func makeContactsProvider() -> ContactsProviding { return ContactsProvider() @@ -170,3 +133,12 @@ final class ServiceFactory: SharedServiceFactory, ServiceFactoryProtocol { return AudioSessionManager.shared } } + +// MARK: - MQTT Handlers + +extension ServiceFactory { + + func makeTypingHandler() -> TypingHandler { + return TypingHandler.shared + } +} diff --git a/Nynja/Services/ServiceFactory/ServiceFactoryProtocol.swift b/Nynja/Services/ServiceFactory/ServiceFactoryProtocol.swift new file mode 100644 index 0000000000000000000000000000000000000000..f0dfad1640130db5d89bb9417e154497471cd971 --- /dev/null +++ b/Nynja/Services/ServiceFactory/ServiceFactoryProtocol.swift @@ -0,0 +1,49 @@ +// +// ServiceFactoryProtocol.swift +// Nynja +// +// Created by Anton Poltoratskyi on 01.11.2018. +// Copyright © 2018 TecSynt Solutions. All rights reserved. +// + +protocol ServiceFactoryProtocol: SharedServiceFactoryProtocol, MQTTHandlerFactoryProtocol { + func makeMessageSendingService() -> MessageSendingServiceProtocol + func makeResourceManager() -> ResourceManagerProtocol + func makeMessageFactory() -> MessageFactoryProtocol + + func makeMessagePayloadBuilder() -> MessagePayloadBuilderInput + func makeMessagePayloadParser() -> MessagePayloadParserInput + + func makeCameraSettingsService(with flow: CameraSourceFlow) -> CameraSettingsServiceProtocol + + func makeMesageProcessingManager() -> MessageProcessingManagerInterface + + func makeHistoryRequestFactory() -> HistoryRequestModelFactoryProtocol + + func makeTypingProvider() -> TypingProvider + func makeContactsProvider() -> ContactsProviding + func makeConversationsProvider() -> ConversationsProviding + func makeStickersProvider() -> StickersProviding + + func makeTextInputValidationService() -> TextInputValidationServiceProtocol + func makeWalletCreationTextInputValidationService() -> WalletCreationTextInputValidationServiceProtocol + func makeWalletOpeningTextInputValidationService() -> WalletOpeningTextInputValidationServiceProtocol + func makeWalletFundingNetworkService() -> WalletFundingNetworkService + func makePermissionManager() -> PermissionManager + + func makeWalletService() -> WalletService + func makeSyncFileManager() -> SyncFileManager + + func makeMuteChatService() -> MuteChatServiceProtocol + + func makeConnectionService() -> ConnectionService + + func makeAlertManager() -> AlertManager + + func makeStatusCodeManager() -> StatusCodeManager + + func makeChatScreenAlertFactory() -> ChatScreenAlertFactoryProtocol + func makeUseCaseValidationServise() -> UseCaseValidationServiceProtocol + + func makeAudioSessionManager() -> AudioSessionManager +} diff --git a/Nynja/Statuses/Observable.swift b/Nynja/Statuses/Observable.swift new file mode 100644 index 0000000000000000000000000000000000000000..00924123ebebb03461840c37d440cd414e827b8d --- /dev/null +++ b/Nynja/Statuses/Observable.swift @@ -0,0 +1,69 @@ +// +// KeyedObservable.swift +// Nynja +// +// Created by Anton Poltoratskyi on 26.10.2018. +// Copyright © 2018 TecSynt Solutions. All rights reserved. +// + +import Foundation + +final class KeyedObservable { + + private typealias Observers = [AnyWeakSubscriber] + + private struct Handler { + var callback: (Key, Value) -> Void + } + + private let lock = NSLock() + + private var allObservers: Observers = [] + + private var observers: [Key: Observers] = [:] + + func addObserver(_ observer: AnyObject, callback: @escaping (Key, Value) -> Void) { + let handler = Handler(callback: callback) + let container = AnyWeakSubscriber(object: observer, handler: handler) + + lock.lock() + + allObservers.append(container) + + lock.unlock() + } + + func addObserver(_ observer: AnyObject, for key: Key, callback: @escaping (Key, Value) -> Void) { + let handler = Handler(callback: callback) + let container = AnyWeakSubscriber(object: observer, handler: handler) + + lock.lock() + + var newObservers = observers[key] ?? [] + newObservers.append(container) + observers[key] = newObservers + + lock.unlock() + } + + func removeObserver(_ observer: AnyObject) { + lock.lock() + allObservers.removeAll { $0.object.value === observer || $0.object.value == nil } + lock.unlock() + } + + func removeObserver(_ observer: AnyObject, for key: Key) { + lock.lock() + observers[key]?.removeAll { $0.object.value === observer || $0.object.value == nil } + lock.unlock() + } + + func notify(_ key: Key, with value: Value) { + lock.lock() + for observer in allObservers { + observer.handler.callback(key, value) + } + observers[key]?.forEach { $0.handler.callback(key, value) } + lock.unlock() + } +} diff --git a/Nynja/Statuses/TypingDisplayModel.swift b/Nynja/Statuses/TypingDisplayModel.swift new file mode 100644 index 0000000000000000000000000000000000000000..99ce063bb12f3ed892c678369c339457e19bb440 --- /dev/null +++ b/Nynja/Statuses/TypingDisplayModel.swift @@ -0,0 +1,66 @@ +// +// TypingDisplayModel.swift +// Nynja +// +// Created by Anton Poltoratskyi on 30.10.2018. +// Copyright © 2018 TecSynt Solutions. All rights reserved. +// + +enum TypingDisplayModel { + case typing(SenderInfo?, Status) + case none + + var displayName: String? { + guard case let .typing(senderInfo, _) = self else { + return nil + } + return senderInfo?.displayName + } + + var status: Status? { + guard case let .typing(_, status) = self else { + return nil + } + return status + } +} + +extension TypingDisplayModel { + + struct SenderInfo { + let senders: [String] + + var displayName: String? { + guard !senders.isEmpty, senders.count > 1 else { + return senders.first + } + return String.localizable.messageTypingStatusPeople(String(senders.count)) + } + } + + enum Status { + case action(ActionStatus) + /// undefined means that there are more then one active typing in chat with different statuses. + case undefined + + var title: String { + switch self { + case let .action(actionStatus): + return actionStatus.title + case .undefined: + return String.localizable.messageTypingStatusUndefined + } + } + + var action: ActionStatus? { + if case let .action(actionStatus) = self { + return actionStatus + } + return nil + } + + var isDone: Bool { + return action?.isDone ?? false + } + } +} diff --git a/Nynja/Statuses/TypingObservable.swift b/Nynja/Statuses/TypingObservable.swift new file mode 100644 index 0000000000000000000000000000000000000000..34a6b75e99cab9d2e8698e56e88155f989613f84 --- /dev/null +++ b/Nynja/Statuses/TypingObservable.swift @@ -0,0 +1,12 @@ +// +// TypingObservable.swift +// Nynja +// +// Created by Anton Poltoratskyi on 02.11.2018. +// Copyright © 2018 TecSynt Solutions. All rights reserved. +// + +protocol TypingObservable: class { + func observeChanges(for feedId: FeedId, handler: @escaping (TypingDisplayModel) -> ()) + func removeObserver(for feedId: FeedId) +} diff --git a/Nynja/Statuses/TypingProvider.swift b/Nynja/Statuses/TypingProvider.swift new file mode 100644 index 0000000000000000000000000000000000000000..ffb9895edea1880ed69e1f94b041ca4dfe25152f --- /dev/null +++ b/Nynja/Statuses/TypingProvider.swift @@ -0,0 +1,227 @@ +// +// TypingProvider.swift +// Nynja +// +// Created by Anton Poltoratskyi on 29.10.2018. +// Copyright © 2018 TecSynt Solutions. All rights reserved. +// + +import Foundation + +typealias FeedId = String + +protocol TypingProvider: class { + typealias Callback = (FeedId, TypingDisplayModel) -> Void + + func addObserver(_ observer: AnyObject, callback: @escaping Callback) + func addObserver(_ observer: AnyObject, for key: FeedId, callback: @escaping Callback) + func removeObserver(_ observer: AnyObject) + func removeObserver(_ observer: AnyObject, for key: FeedId) + + func typingStatus(for feedId: FeedId) -> TypingDisplayModel? +} + +final class TypingProviderImpl: TypingProvider, KeyedObservable, TypingHandlerDelegate { + + let observable = KeyedObservableContainer() + + private var data: [FeedId: TypingData] = [:] + + private var workItems: [FeedId: DispatchWorkItem] = [:] + + private let isolationQueue = DispatchQueue(label: "com.nynja.typing-status-provider", attributes: .concurrent) + + private let processingQueue: DispatchQueue + + private let notifyQueue: DispatchQueue + + private let typingDismissInterval = 10.0 + + + // MARK: - Singleton + + // FIXME: singleton is a temporary solution until we don't have fullly implemented DI using ServiceFactory in the whole application. + static let shared: TypingProvider = TypingProviderImpl( + dependencies: .init(storageService: StorageService.sharedInstance, + typingHandler: TypingHandler.shared) + ) + + + // MARK: - Dependencies + + struct Dependencies { + let storageService: StorageService + let typingHandler: TypingHandler + let processingQueue = DispatchQueue.global(qos: .default) + let notifyQueue = DispatchQueue.main + } + + private let storageService: StorageService + + private let typingHandler: TypingHandler + + + // MARK: - Init + + private init(dependencies: Dependencies) { + storageService = dependencies.storageService + typingHandler = dependencies.typingHandler + processingQueue = dependencies.processingQueue + notifyQueue = dependencies.notifyQueue + typingHandler.addObserver(self) + } + + deinit { + typingHandler.removeObserver(self) + } + + + // MARK: - TypingProvider + + func typingStatus(for feedId: FeedId) -> TypingDisplayModel? { + return typing(for: feedId).flatMap { displayInfo(for: $0) } + } + + + // MARK: - TypingHandlerDelegate + + func didReceiveTyping(_ typing: Typing) { + guard let feedId = typing.feed_id, let typingType = typing.type, shouldHandleTyping(typing) else { + return + } + let status = ActionStatus(typingModelType: typingType) + + let senderInfo = TypingSenderInfo(feedId: feedId, + senderId: typing.sender_id, + senderName: typing.sender_alias, + status: status) + + processingQueue.async { + self.save(senderInfo) + } + } + + private func shouldHandleTyping(_ typing: Typing) -> Bool { + // Ignore typing from myself (in group chat). + if let senderId = typing.sender_id, senderId == storageService.phoneId { + return false + } + return true + } + + private func save(_ typingInfo: TypingSenderInfo) { + let feedId = typingInfo.feedId + + let typing: TypingData + if let oldTypingData = self.typing(for: feedId) { + + var newTypingData = oldTypingData + + newTypingData.senders.removeAll { $0.senderId == typingInfo.senderId } + newTypingData.senders.append(typingInfo) + + typing = newTypingData + + } else { + typing = TypingData(senders: [typingInfo]) + } + + update(typing, for: feedId) + + dismiss(typingInfo, after: typingDismissInterval) + } + + private func update(_ typing: TypingData?, for feedId: FeedId) { + set(typing, for: feedId) + + let displayInfo = self.displayInfo(for: typing) + notifyQueue.async { + self.observable.notify(feedId, with: displayInfo) + } + } + + private func remove(_ typing: TypingSenderInfo) { + let feedId = typing.feedId + guard let currentTypingData = self.typing(for: feedId) else { + return + } + var newData = currentTypingData + newData.senders.removeAll { $0 === typing } + + update(newData, for: feedId) + } + + + // MARK: - Dismiss Timer + + private func dismiss(_ typing: TypingSenderInfo, after delay: TimeInterval) { + let dismissKey = "\(typing.feedId)\(typing.senderId)" + + workItems[dismissKey]?.cancel() + + let workItem = DispatchWorkItem { + self.remove(typing) + } + workItems[dismissKey] = workItem + + processingQueue.asyncAfter(deadline: .now() + delay, execute: workItem) + } + + + // MARK: - Display Model + + private func displayInfo(for typing: TypingData?) -> TypingDisplayModel { + guard let typingInfo = typing?.senders, !typingInfo.isEmpty, let lastStatus = typingInfo.last?.status else { + return .none + } + let senders = typingInfo.compactMap { $0.senderName } + let senderInfo = TypingDisplayModel.SenderInfo(senders: senders) + + let shouldDisplayStatus = !typingInfo.contains { $0.status != lastStatus } + + return .typing(senderInfo, shouldDisplayStatus ? .action(lastStatus) : .undefined) + } + + + // MARK: - Thread-Safe Data Access + + private func set(_ data: TypingData?, for feedId: FeedId) { + isolationQueue.async(flags: .barrier) { + self.data[feedId] = data + } + } + + private func typing(for feedId: FeedId) -> TypingData? { + var typing: TypingData? + + isolationQueue.sync { + typing = data[feedId] + } + + return typing + } +} + + +// MARK: - Inner Types + +private extension TypingProviderImpl { + + struct TypingData { + var senders: [TypingSenderInfo] + } + + final class TypingSenderInfo { + let feedId: FeedId + let senderId: String + let senderName: String? + let status: ActionStatus + + init(feedId: FeedId, senderId: String?, senderName: String?, status: ActionStatus) { + self.feedId = feedId + self.senderId = senderId ?? feedId + self.senderName = senderName + self.status = status + } + } +} diff --git a/Shared/Services/Handlers/Base/BaseHandler.swift b/Shared/Services/Handlers/Base/BaseHandler.swift index cdf0cebeebe97cd2293dba0e59be9aef478cc4a9..3021b29cd732a4e4c7033a867ae2dfcd382ed96f 100644 --- a/Shared/Services/Handlers/Base/BaseHandler.swift +++ b/Shared/Services/Handlers/Base/BaseHandler.swift @@ -6,23 +6,23 @@ // Copyright © 2018 TecSynt Solutions. All rights reserved. // -protocol BaseHandler { - static func executeHandle(data: BertTuple) - static func executeHandle(data: BertList) +protocol BaseHandler: class { + func executeHandle(data: BertTuple) + func executeHandle(data: BertList) - static func executeHandle(data: BertTuple, codes: StatusCodes) - static func executeHandle(data: BertList, codes: StatusCodes) + func executeHandle(data: BertTuple, codes: StatusCodes) + func executeHandle(data: BertList, codes: StatusCodes) } extension BaseHandler { - static func executeHandle(data: BertTuple) { + func executeHandle(data: BertTuple) { executeHandle(data: data, codes: []) } - static func executeHandle(data: BertList) { + func executeHandle(data: BertList) { executeHandle(data: data, codes: []) } - static func executeHandle(data: BertTuple, codes: StatusCodes) {} - static func executeHandle(data: BertList, codes: StatusCodes) {} + func executeHandle(data: BertTuple, codes: StatusCodes) {} + func executeHandle(data: BertList, codes: StatusCodes) {} } diff --git a/Shared/Services/Handlers/ErrorsHandler.swift b/Shared/Services/Handlers/ErrorsHandler.swift index d284a35cb0d3164681243317b280771cd59427e1..d85e43db27d94d3b7f5dc24b23289da0a2115ed3 100644 --- a/Shared/Services/Handlers/ErrorsHandler.swift +++ b/Shared/Services/Handlers/ErrorsHandler.swift @@ -8,7 +8,11 @@ final class ErrorsHandler: BaseHandler { - static func executeHandle(data: BertTuple) { + static let shared = ErrorsHandler() + + private init() {} + + func executeHandle(data: BertTuple) { guard let errors = get_errors().parse(bert: data) as? errors, let dataTuple = data.elements.last as? BertTuple, let handlerKind = dataTuple.handlerKind else { @@ -20,5 +24,4 @@ final class ErrorsHandler: BaseHandler { let handler = HandlerFactory.handler(for: handlerKind) handler.executeHandle(data: dataTuple, codes: Set(codes)) } - } diff --git a/Shared/Services/Handlers/IoHandler.swift b/Shared/Services/Handlers/IoHandler.swift index f6fd64282f4d4fac0dc2b03406c96230694292c5..478cce95e69e0030ebd37a90e154cebf34e10239 100644 --- a/Shared/Services/Handlers/IoHandler.swift +++ b/Shared/Services/Handlers/IoHandler.swift @@ -66,23 +66,27 @@ extension IoHandlerDelegate { } -class IoHandler:BaseHandler { +final class IoHandler: BaseHandler { - static weak var delegate: IoHandlerDelegate? + static let shared = IoHandler() - static var storageService: StorageService { + private init() {} + + weak var delegate: IoHandlerDelegate? + + var storageService: StorageService { return .sharedInstance } - static var mqttService: MQTTService { + var mqttService: MQTTService { return .sharedInstance } - static var keychainService: KeychainService { + var keychainService: KeychainService { return .standard } - static func executeHandle(data: BertTuple) { + func executeHandle(data: BertTuple) { if let IO = get_io().parse(bert: data) as? io { var code: String? = nil if let value = ((IO.code as? ok)?.code as? StringAtom)?.string { diff --git a/Shared/Services/Messaging/TypingSenderService.swift b/Shared/Services/Messaging/TypingSenderService.swift index 5b6ac91d936caf460e36b787da391c082d65d4e3..bc1cf19c7478ba24b16bce4583d714e5031b4898 100644 --- a/Shared/Services/Messaging/TypingSenderService.swift +++ b/Shared/Services/Messaging/TypingSenderService.swift @@ -45,10 +45,10 @@ final class TypingSenderService: TypingSenderServiceProtocol, InitializeInjectab return } if let contact = chat as? Contact, let phone = contact.phoneNumber { - sendTyping(for: .p2p(phone: phone), type: type, ownPhoneId: ownPhoneId) + sendTyping(for: .p2p(phoneNumber: phone), type: type, feedId: ownPhoneId, senderId: nil, senderAlias: nil) - } else if let room = chat as? Room, let roomId = room.id { - sendTyping(for: .room(id: roomId), type: type, ownPhoneId: ownPhoneId) + } else if let room = chat as? Room, let roomId = room.id, let alias = memberAlias(roomId: roomId, phoneId: ownPhoneId) { + sendTyping(for: .room(id: roomId), type: type, feedId: roomId, senderId: ownPhoneId, senderAlias: alias) } } @@ -61,31 +61,36 @@ final class TypingSenderService: TypingSenderServiceProtocol, InitializeInjectab return } if message.feed_id is p2p, let phoneId = message.to, let phone = Contact.phoneNumber(from: phoneId) { - sendTyping(for: .p2p(phone: phone), type: type, ownPhoneId: ownPhoneId) + sendTyping(for: .p2p(phoneNumber: phone), type: type, feedId: ownPhoneId, senderId: nil, senderAlias: nil) - } else if message.feed_id is muc, let roomId = message.to { - sendTyping(for: .room(id: roomId), type: type, ownPhoneId: ownPhoneId) + } else if message.feed_id is muc, let roomId = message.to, let alias = memberAlias(roomId: roomId, phoneId: ownPhoneId) { + sendTyping(for: .room(id: roomId), type: type, feedId: roomId, senderId: ownPhoneId, senderAlias: alias) } } - private func sendTyping(for destination: TypingModel.Topic, type: TypingModelType, ownPhoneId: String) { - let typingModel = self.typingModel(for: destination, type: type, ownPhoneId: ownPhoneId) - mqttService.sendTyping(typingModel) - } - - private func typingModel(for destination: TypingModel.Topic, type: TypingModelType, ownPhoneId: String) -> TypingModel { + private func sendTyping(for topic: TypingModel.Topic, type: TypingModelType, + feedId: String, senderId: String?, senderAlias: String?) { let typing = Typing() - switch destination { - case .p2p: - // Send own phone id for p2p - typing.phone_id = ownPhoneId - case let .room(id): - // Send room id for muc - typing.phone_id = id - } + typing.feed_id = feedId + typing.sender_id = senderId + typing.sender_alias = senderAlias typing.comments = type.rawValue as AnyObject - return TypingModel(typing: typing, to: destination) + let typingModel = TypingModel(typing: typing, to: topic) + + mqttService.sendTyping(typingModel) + } + + + // MARK: - Database + + private func memberAlias(roomId: String, phoneId: String) -> String? { + #if !SHARE_EXTENSION + // FIXME: inject MessageDAO as a dependency + return MemberDAO.fetchMemberAlias(roomId: roomId, phoneId: phoneId) + #else + return nil + #endif } }