From c29827e514d792ac862809dcd69f92021d7f1775 Mon Sep 17 00:00:00 2001 From: Anton Poltoratskyi Date: Thu, 25 Oct 2018 12:51:24 +0300 Subject: [PATCH 01/64] [NY-4699] Implemented AvatarStatusView with clipped round corder for status view. --- .../NynjaUIKit.xcodeproj/project.pbxproj | 48 ++++++- .../AvatarStatusView/AvatarStatusView.swift | 136 ++++++++++++++++++ 2 files changed, 178 insertions(+), 6 deletions(-) create mode 100644 Frameworks/NynjaUIKit/NynjaUIKit/Views/AvatarStatusView/AvatarStatusView.swift diff --git a/Frameworks/NynjaUIKit/NynjaUIKit.xcodeproj/project.pbxproj b/Frameworks/NynjaUIKit/NynjaUIKit.xcodeproj/project.pbxproj index d227aa966..413b47d08 100644 --- a/Frameworks/NynjaUIKit/NynjaUIKit.xcodeproj/project.pbxproj +++ b/Frameworks/NynjaUIKit/NynjaUIKit.xcodeproj/project.pbxproj @@ -32,6 +32,7 @@ 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 */; }; /* End PBXBuildFile section */ /* Begin PBXFileReference section */ @@ -63,6 +64,7 @@ 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 = ""; }; 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 = ""; }; @@ -273,6 +275,7 @@ 8514D51A20EE41BA0002378A /* Views */ = { isa = PBXGroup; children = ( + 85409FFD2181C8AF003A010F /* AvatarStatusView */, 8514D50120EE40530002378A /* ContextMenu */, ); path = Views; @@ -287,6 +290,14 @@ path = UIWindow; sourceTree = ""; }; + 85409FFD2181C8AF003A010F /* AvatarStatusView */ = { + isa = PBXGroup; + children = ( + 85409FFE2181C8C8003A010F /* AvatarStatusView.swift */, + ); + path = AvatarStatusView; + sourceTree = ""; + }; 85C65C7C20EE6D9C00C468B2 /* Core */ = { isa = PBXGroup; children = ( @@ -412,6 +423,7 @@ 8514D51E20EE43880002378A /* UIWindow+HitTestDelegate.swift in Sources */, 8514D4E020EE2D970002378A /* LayoutRepresentableCellViewModel.swift in Sources */, 8514D51320EE40540002378A /* NynjaContextMenuLayout.swift in Sources */, + 85409FFF2181C8C8003A010F /* AvatarStatusView.swift in Sources */, 8514D4E420EE2D970002378A /* AccessiblityDisplayOptions.swift in Sources */, 8514D4EA20EE2D970002378A /* LayoutAdjustment.swift in Sources */, 8514D4E220EE2D970002378A /* CellViewModel.swift in Sources */, @@ -505,7 +517,11 @@ DYLIB_INSTALL_NAME_BASE = "@rpath"; INFOPLIST_FILE = NynjaUIKit/Info.plist; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; - LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@loader_path/Frameworks", + ); PRODUCT_BUNDLE_IDENTIFIER = com.nynja.mobile.communicator.NynjaUIKit; PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; PROVISIONING_PROFILE_SPECIFIER = ""; @@ -586,7 +602,11 @@ DYLIB_INSTALL_NAME_BASE = "@rpath"; INFOPLIST_FILE = NynjaUIKit/Info.plist; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; - LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@loader_path/Frameworks", + ); PRODUCT_BUNDLE_IDENTIFIER = com.nynja.mobile.communicator.NynjaUIKit; PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; PROVISIONING_PROFILE_SPECIFIER = ""; @@ -667,7 +687,11 @@ DYLIB_INSTALL_NAME_BASE = "@rpath"; INFOPLIST_FILE = NynjaUIKit/Info.plist; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; - LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@loader_path/Frameworks", + ); PRODUCT_BUNDLE_IDENTIFIER = com.nynja.mobile.communicator.NynjaUIKit; PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; PROVISIONING_PROFILE_SPECIFIER = ""; @@ -748,7 +772,11 @@ DYLIB_INSTALL_NAME_BASE = "@rpath"; INFOPLIST_FILE = NynjaUIKit/Info.plist; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; - LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@loader_path/Frameworks", + ); PRODUCT_BUNDLE_IDENTIFIER = com.nynja.mobile.communicator.NynjaUIKit; PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; PROVISIONING_PROFILE_SPECIFIER = ""; @@ -829,7 +857,11 @@ DYLIB_INSTALL_NAME_BASE = "@rpath"; INFOPLIST_FILE = NynjaUIKit/Info.plist; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; - LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@loader_path/Frameworks", + ); PRODUCT_BUNDLE_IDENTIFIER = com.nynja.mobile.communicator.NynjaUIKit; PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; PROVISIONING_PROFILE_SPECIFIER = ""; @@ -910,7 +942,11 @@ DYLIB_INSTALL_NAME_BASE = "@rpath"; INFOPLIST_FILE = NynjaUIKit/Info.plist; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; - LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@loader_path/Frameworks", + ); PRODUCT_BUNDLE_IDENTIFIER = com.nynja.mobile.communicator.NynjaUIKit; PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; PROVISIONING_PROFILE_SPECIFIER = ""; diff --git a/Frameworks/NynjaUIKit/NynjaUIKit/Views/AvatarStatusView/AvatarStatusView.swift b/Frameworks/NynjaUIKit/NynjaUIKit/Views/AvatarStatusView/AvatarStatusView.swift new file mode 100644 index 000000000..5f5fe2676 --- /dev/null +++ b/Frameworks/NynjaUIKit/NynjaUIKit/Views/AvatarStatusView/AvatarStatusView.swift @@ -0,0 +1,136 @@ +// +// AvatarStatusView.swift +// NynjaUIKit +// +// Created by Anton Poltoratskyi on 25.10.2018. +// Copyright © 2018 TecSynt Solutions. All rights reserved. +// + +import UIKit + +public final class AvatarStatusView: UIView { + + public var angle: CGFloat = .pi / 4 { + didSet { + setNeedsLayout() + } + } + + public var statusRadius: CGFloat = 24 { + didSet { + setNeedsLayout() + } + } + + public var statusPadding: CGFloat = 8 { + didSet { + setNeedsLayout() + } + } + + public var imageRadius: CGFloat { + return bounds.height / 2 + } + + + // MARK: - Views + + public let imageView = UIImageView() + + private let statusView = UIImageView() + + private let maskLayer = CAShapeLayer() + + + // 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() { + backgroundColor = .clear + + imageView.layer.masksToBounds = true + addSubview(imageView) + + statusView.layer.masksToBounds = true + addSubview(statusView) + } + + + // MARK: - Layout + + public override func layoutSubviews() { + super.layoutSubviews() + + imageView.frame = bounds + imageView.layer.cornerRadius = bounds.height / 2 + + let clipCircleCenter = statusPosition(in: bounds) + let clipCircleSize = statusRadius + statusPadding * 2 + let clipCircleFrame = CGRect( + x: clipCircleCenter.x - clipCircleSize / 2, + y: clipCircleCenter.y - clipCircleSize / 2, + width: clipCircleSize, + height: clipCircleSize + ) + updateClipMask(with: clipCircleFrame) + updateStatusView(with: clipCircleFrame) + } + + 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 - statusPadding * 2 + + statusView.frame = CGRect(x: frame.minX + statusPadding, + y: frame.minY + statusPadding, + 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) + } + + public func update(_ statusAppearance: StatusAppearance) { + switch statusAppearance { + case let .color(color): + statusView.backgroundColor = color + statusView.image = nil + case let .image(image): + statusView.backgroundColor = nil + statusView.image = image + } + } +} -- GitLab From bdfda3853c303285dfa697673750fd300f46301d Mon Sep 17 00:00:00 2001 From: Anton Poltoratskyi Date: Thu, 25 Oct 2018 15:14:30 +0300 Subject: [PATCH 02/64] [NY-4699] Implemented typing animation using CAReplicationLayer. --- .../NynjaUIKit.xcodeproj/project.pbxproj | 18 ++- .../AvatarStatusView.swift | 0 .../Views/Typing/TypingAnimatableView.swift | 124 ++++++++++++++++++ 3 files changed, 139 insertions(+), 3 deletions(-) rename Frameworks/NynjaUIKit/NynjaUIKit/Views/{AvatarStatusView => Avatar}/AvatarStatusView.swift (100%) create mode 100644 Frameworks/NynjaUIKit/NynjaUIKit/Views/Typing/TypingAnimatableView.swift diff --git a/Frameworks/NynjaUIKit/NynjaUIKit.xcodeproj/project.pbxproj b/Frameworks/NynjaUIKit/NynjaUIKit.xcodeproj/project.pbxproj index 413b47d08..5f5185912 100644 --- a/Frameworks/NynjaUIKit/NynjaUIKit.xcodeproj/project.pbxproj +++ b/Frameworks/NynjaUIKit/NynjaUIKit.xcodeproj/project.pbxproj @@ -33,6 +33,7 @@ 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 /* TypingAnimatableView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8540A0072181EA2F003A010F /* TypingAnimatableView.swift */; }; /* End PBXBuildFile section */ /* Begin PBXFileReference section */ @@ -65,6 +66,7 @@ 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 /* TypingAnimatableView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TypingAnimatableView.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 = ""; }; @@ -275,7 +277,8 @@ 8514D51A20EE41BA0002378A /* Views */ = { isa = PBXGroup; children = ( - 85409FFD2181C8AF003A010F /* AvatarStatusView */, + 85409FFD2181C8AF003A010F /* Avatar */, + 8540A0062181EA0D003A010F /* Typing */, 8514D50120EE40530002378A /* ContextMenu */, ); path = Views; @@ -290,12 +293,20 @@ path = UIWindow; sourceTree = ""; }; - 85409FFD2181C8AF003A010F /* AvatarStatusView */ = { + 85409FFD2181C8AF003A010F /* Avatar */ = { isa = PBXGroup; children = ( 85409FFE2181C8C8003A010F /* AvatarStatusView.swift */, ); - path = AvatarStatusView; + path = Avatar; + sourceTree = ""; + }; + 8540A0062181EA0D003A010F /* Typing */ = { + isa = PBXGroup; + children = ( + 8540A0072181EA2F003A010F /* TypingAnimatableView.swift */, + ); + path = Typing; sourceTree = ""; }; 85C65C7C20EE6D9C00C468B2 /* Core */ = { @@ -424,6 +435,7 @@ 8514D4E020EE2D970002378A /* LayoutRepresentableCellViewModel.swift in Sources */, 8514D51320EE40540002378A /* NynjaContextMenuLayout.swift in Sources */, 85409FFF2181C8C8003A010F /* AvatarStatusView.swift in Sources */, + 8540A0082181EA2F003A010F /* TypingAnimatableView.swift in Sources */, 8514D4E420EE2D970002378A /* AccessiblityDisplayOptions.swift in Sources */, 8514D4EA20EE2D970002378A /* LayoutAdjustment.swift in Sources */, 8514D4E220EE2D970002378A /* CellViewModel.swift in Sources */, diff --git a/Frameworks/NynjaUIKit/NynjaUIKit/Views/AvatarStatusView/AvatarStatusView.swift b/Frameworks/NynjaUIKit/NynjaUIKit/Views/Avatar/AvatarStatusView.swift similarity index 100% rename from Frameworks/NynjaUIKit/NynjaUIKit/Views/AvatarStatusView/AvatarStatusView.swift rename to Frameworks/NynjaUIKit/NynjaUIKit/Views/Avatar/AvatarStatusView.swift diff --git a/Frameworks/NynjaUIKit/NynjaUIKit/Views/Typing/TypingAnimatableView.swift b/Frameworks/NynjaUIKit/NynjaUIKit/Views/Typing/TypingAnimatableView.swift new file mode 100644 index 000000000..e99a0607a --- /dev/null +++ b/Frameworks/NynjaUIKit/NynjaUIKit/Views/Typing/TypingAnimatableView.swift @@ -0,0 +1,124 @@ +// +// TypingAnimatableView.swift +// NynjaUIKit +// +// Created by Anton Poltoratskyi on 25.10.2018. +// Copyright © 2018 TecSynt Solutions. All rights reserved. +// + +import UIKit + +public final class TypingAnimatableView: UIView { + + public var itemsCount: Int = 3 { + didSet { + invalidateIntrinsicContentSize() + setNeedsLayout() + } + } + + public var padding: 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) + padding * 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: - 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() { + 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 + padding, 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.animation(forKey: Animation.key) == nil 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 + } +} -- GitLab From 0f9229ab15724526a5ee2b8625bfc3d4783a8a53 Mon Sep 17 00:00:00 2001 From: Anton Poltoratskyi Date: Thu, 25 Oct 2018 15:26:34 +0300 Subject: [PATCH 03/64] [NY-4699] Added base view. --- .../NynjaUIKit.xcodeproj/project.pbxproj | 20 ++++++++++++ .../CoreAnimation/CALayer+Animation.swift | 16 ++++++++++ .../Views/Avatar/AvatarStatusView.swift | 19 +++-------- .../NynjaUIKit/Views/BaseView.swift | 32 +++++++++++++++++++ .../ContextMenu/View/NynjaContextMenu.swift | 18 ++--------- .../Views/Typing/TypingAnimatableView.swift | 20 +++--------- .../NynjaUIKit/Views/Typing/TypingView.swift | 19 +++++++++++ 7 files changed, 98 insertions(+), 46 deletions(-) create mode 100644 Frameworks/NynjaUIKit/NynjaUIKit/Core/Extensions/CoreAnimation/CALayer+Animation.swift create mode 100644 Frameworks/NynjaUIKit/NynjaUIKit/Views/BaseView.swift create mode 100644 Frameworks/NynjaUIKit/NynjaUIKit/Views/Typing/TypingView.swift diff --git a/Frameworks/NynjaUIKit/NynjaUIKit.xcodeproj/project.pbxproj b/Frameworks/NynjaUIKit/NynjaUIKit.xcodeproj/project.pbxproj index 5f5185912..064940d21 100644 --- a/Frameworks/NynjaUIKit/NynjaUIKit.xcodeproj/project.pbxproj +++ b/Frameworks/NynjaUIKit/NynjaUIKit.xcodeproj/project.pbxproj @@ -34,6 +34,9 @@ 851CFD3D20F8A1CF00DBF743 /* NynjaContextMenuUserInfo.swift in Sources */ = {isa = PBXBuildFile; fileRef = 851CFD3C20F8A1CF00DBF743 /* NynjaContextMenuUserInfo.swift */; }; 85409FFF2181C8C8003A010F /* AvatarStatusView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 85409FFE2181C8C8003A010F /* AvatarStatusView.swift */; }; 8540A0082181EA2F003A010F /* TypingAnimatableView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8540A0072181EA2F003A010F /* TypingAnimatableView.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 */; }; /* End PBXBuildFile section */ /* Begin PBXFileReference section */ @@ -67,6 +70,9 @@ 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 /* TypingAnimatableView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TypingAnimatableView.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 = ""; }; 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 = ""; }; @@ -269,6 +275,7 @@ 8514D51920EE41AC0002378A /* Extensions */ = { isa = PBXGroup; children = ( + 8540A00D2181ED10003A010F /* CoreAnimation */, 8514D51F20EE47350002378A /* UIWindow */, ); path = Extensions; @@ -277,6 +284,7 @@ 8514D51A20EE41BA0002378A /* Views */ = { isa = PBXGroup; children = ( + 8540A00B2181EBD2003A010F /* BaseView.swift */, 85409FFD2181C8AF003A010F /* Avatar */, 8540A0062181EA0D003A010F /* Typing */, 8514D50120EE40530002378A /* ContextMenu */, @@ -305,10 +313,19 @@ isa = PBXGroup; children = ( 8540A0072181EA2F003A010F /* TypingAnimatableView.swift */, + 8540A0092181EB87003A010F /* TypingView.swift */, ); path = Typing; sourceTree = ""; }; + 8540A00D2181ED10003A010F /* CoreAnimation */ = { + isa = PBXGroup; + children = ( + 8540A00E2181ED2E003A010F /* CALayer+Animation.swift */, + ); + path = CoreAnimation; + sourceTree = ""; + }; 85C65C7C20EE6D9C00C468B2 /* Core */ = { isa = PBXGroup; children = ( @@ -433,6 +450,7 @@ 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 /* TypingAnimatableView.swift in Sources */, @@ -450,7 +468,9 @@ 8514D4E920EE2D970002378A /* UICollectionView+ViewModel.swift in Sources */, 8514D51120EE40540002378A /* ContextMenuItem.swift in Sources */, 8514D51820EE40540002378A /* NynjaContextMenuItemsFactory.swift in Sources */, + 8540A00A2181EB87003A010F /* TypingView.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 000000000..df70d9566 --- /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 index 5f5fe2676..bad89a549 100644 --- a/Frameworks/NynjaUIKit/NynjaUIKit/Views/Avatar/AvatarStatusView.swift +++ b/Frameworks/NynjaUIKit/NynjaUIKit/Views/Avatar/AvatarStatusView.swift @@ -8,7 +8,7 @@ import UIKit -public final class AvatarStatusView: UIView { +public final class AvatarStatusView: BaseView { public var angle: CGFloat = .pi / 4 { didSet { @@ -42,22 +42,11 @@ public final class AvatarStatusView: UIView { private let maskLayer = CAShapeLayer() - // 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() + backgroundColor = .clear imageView.layer.masksToBounds = true diff --git a/Frameworks/NynjaUIKit/NynjaUIKit/Views/BaseView.swift b/Frameworks/NynjaUIKit/NynjaUIKit/Views/BaseView.swift new file mode 100644 index 000000000..dc6523cbb --- /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 ecea64cfb..23e4a01d3 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/TypingAnimatableView.swift b/Frameworks/NynjaUIKit/NynjaUIKit/Views/Typing/TypingAnimatableView.swift index e99a0607a..c77b7a9de 100644 --- a/Frameworks/NynjaUIKit/NynjaUIKit/Views/Typing/TypingAnimatableView.swift +++ b/Frameworks/NynjaUIKit/NynjaUIKit/Views/Typing/TypingAnimatableView.swift @@ -8,7 +8,7 @@ import UIKit -public final class TypingAnimatableView: UIView { +public final class TypingAnimatableView: BaseView { public var itemsCount: Int = 3 { didSet { @@ -56,22 +56,10 @@ public final class TypingAnimatableView: UIView { private let itemLayer = CAShapeLayer() - // 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() animationLayer.addSublayer(itemLayer) animationLayer.masksToBounds = true setupColor() @@ -102,7 +90,7 @@ public final class TypingAnimatableView: UIView { // MARK: - Animation private func addAnimation() { - guard itemLayer.animation(forKey: Animation.key) == nil else { + guard !itemLayer.hasAnimation(forKey: Animation.key) else { return } let animation = CABasicAnimation(keyPath: "opacity") diff --git a/Frameworks/NynjaUIKit/NynjaUIKit/Views/Typing/TypingView.swift b/Frameworks/NynjaUIKit/NynjaUIKit/Views/Typing/TypingView.swift new file mode 100644 index 000000000..a91a77e08 --- /dev/null +++ b/Frameworks/NynjaUIKit/NynjaUIKit/Views/Typing/TypingView.swift @@ -0,0 +1,19 @@ +// +// 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: - Setup + + public override func setup() { + super.setup() + } +} -- GitLab From 91073edbfc6d1d86d94e886c2884673e60c5ed92 Mon Sep 17 00:00:00 2001 From: Anton Poltoratskyi Date: Thu, 25 Oct 2018 18:05:57 +0300 Subject: [PATCH 04/64] [NY-4699] Implemented typing view. --- .../NynjaUIKit/Views/Typing/TypingView.swift | 102 ++++++++++++++++++ 1 file changed, 102 insertions(+) diff --git a/Frameworks/NynjaUIKit/NynjaUIKit/Views/Typing/TypingView.swift b/Frameworks/NynjaUIKit/NynjaUIKit/Views/Typing/TypingView.swift index a91a77e08..4fc71b153 100644 --- a/Frameworks/NynjaUIKit/NynjaUIKit/Views/Typing/TypingView.swift +++ b/Frameworks/NynjaUIKit/NynjaUIKit/Views/Typing/TypingView.swift @@ -11,9 +11,111 @@ 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 let isTypingInfoPinned: Bool + } + + + // MARK: - Views + + private(set) lazy var indicatorContainer: UIView = { + let view = UIView() + view.setContentHuggingPriority(.required, for: .horizontal) + addSubview(view) + return view + }() + + private(set) 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() + maker.width.equalTo(Constraints.indicator.containerWidth.adjustedByWidth) + } + + 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) { + indicatorContainer.subviews.forEach { $0.removeFromSuperview() } + + 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) \(appearance.typingInfo)" + } + + private func setupDotsIndicator(color: UIColor) { + let indicatorView = TypingAnimatableView() + indicatorView.itemColor = color + + indicatorContainer.addSubview(indicatorView) + + indicatorView.snp.makeConstraints { maker in + maker.centerY.equalToSuperview() + maker.left.equalToSuperview() + } + } + + private func setupCircleIndicator(color: UIColor) { + let indicatorView = UIView() + indicatorView.backgroundColor = color + + indicatorContainer.addSubview(indicatorView) + + let size = Constraints.indicator.circleSize.adjustedByWidth + + indicatorView.layer.cornerRadius = size / 2 + indicatorView.snp.makeConstraints { maker in + maker.center.equalToSuperview() + maker.width.height.equalTo(size) + } + } + + + // MARK: - Constraints + + private enum Constraints { + + enum indicator { + static let containerWidth: CGFloat = 16 + static let circleSize: CGFloat = 8 + } + + enum senderInfo { + static let leftOffset: CGFloat = 8 + } } } -- GitLab From 2ccc203fb95ae5acfb1b587a88342d2378acec2a Mon Sep 17 00:00:00 2001 From: Anton Poltoratskyi Date: Thu, 25 Oct 2018 18:12:27 +0300 Subject: [PATCH 05/64] [NY-4699] Added RoundView. Rename TypingAnimatableView to TypingIndicatorView. --- .../NynjaUIKit.xcodeproj/project.pbxproj | 20 +++++++++++++---- ...leView.swift => TypingIndicatorView.swift} | 4 ++-- .../NynjaUIKit/Views/Typing/TypingView.swift | 5 ++--- .../NynjaUIKit/Views/Utils/RoundView.swift | 22 +++++++++++++++++++ 4 files changed, 42 insertions(+), 9 deletions(-) rename Frameworks/NynjaUIKit/NynjaUIKit/Views/Typing/{TypingAnimatableView.swift => TypingIndicatorView.swift} (97%) create mode 100644 Frameworks/NynjaUIKit/NynjaUIKit/Views/Utils/RoundView.swift diff --git a/Frameworks/NynjaUIKit/NynjaUIKit.xcodeproj/project.pbxproj b/Frameworks/NynjaUIKit/NynjaUIKit.xcodeproj/project.pbxproj index 064940d21..1d07d6b75 100644 --- a/Frameworks/NynjaUIKit/NynjaUIKit.xcodeproj/project.pbxproj +++ b/Frameworks/NynjaUIKit/NynjaUIKit.xcodeproj/project.pbxproj @@ -33,10 +33,11 @@ 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 /* TypingAnimatableView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8540A0072181EA2F003A010F /* TypingAnimatableView.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 */; }; /* End PBXBuildFile section */ /* Begin PBXFileReference section */ @@ -69,10 +70,11 @@ 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 /* TypingAnimatableView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TypingAnimatableView.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 = ""; }; 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 = ""; }; @@ -285,6 +287,7 @@ isa = PBXGroup; children = ( 8540A00B2181EBD2003A010F /* BaseView.swift */, + 8540A01A218213E8003A010F /* Utils */, 85409FFD2181C8AF003A010F /* Avatar */, 8540A0062181EA0D003A010F /* Typing */, 8514D50120EE40530002378A /* ContextMenu */, @@ -312,7 +315,7 @@ 8540A0062181EA0D003A010F /* Typing */ = { isa = PBXGroup; children = ( - 8540A0072181EA2F003A010F /* TypingAnimatableView.swift */, + 8540A0072181EA2F003A010F /* TypingIndicatorView.swift */, 8540A0092181EB87003A010F /* TypingView.swift */, ); path = Typing; @@ -326,6 +329,14 @@ path = CoreAnimation; sourceTree = ""; }; + 8540A01A218213E8003A010F /* Utils */ = { + isa = PBXGroup; + children = ( + 8540A018218213E2003A010F /* RoundView.swift */, + ); + path = Utils; + sourceTree = ""; + }; 85C65C7C20EE6D9C00C468B2 /* Core */ = { isa = PBXGroup; children = ( @@ -453,12 +464,13 @@ 8540A00C2181EBD2003A010F /* BaseView.swift in Sources */, 8514D51320EE40540002378A /* NynjaContextMenuLayout.swift in Sources */, 85409FFF2181C8C8003A010F /* AvatarStatusView.swift in Sources */, - 8540A0082181EA2F003A010F /* TypingAnimatableView.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 */, diff --git a/Frameworks/NynjaUIKit/NynjaUIKit/Views/Typing/TypingAnimatableView.swift b/Frameworks/NynjaUIKit/NynjaUIKit/Views/Typing/TypingIndicatorView.swift similarity index 97% rename from Frameworks/NynjaUIKit/NynjaUIKit/Views/Typing/TypingAnimatableView.swift rename to Frameworks/NynjaUIKit/NynjaUIKit/Views/Typing/TypingIndicatorView.swift index c77b7a9de..5a67c92a5 100644 --- a/Frameworks/NynjaUIKit/NynjaUIKit/Views/Typing/TypingAnimatableView.swift +++ b/Frameworks/NynjaUIKit/NynjaUIKit/Views/Typing/TypingIndicatorView.swift @@ -1,5 +1,5 @@ // -// TypingAnimatableView.swift +// TypingIndicatorView.swift // NynjaUIKit // // Created by Anton Poltoratskyi on 25.10.2018. @@ -8,7 +8,7 @@ import UIKit -public final class TypingAnimatableView: BaseView { +public final class TypingIndicatorView: BaseView { public var itemsCount: Int = 3 { didSet { diff --git a/Frameworks/NynjaUIKit/NynjaUIKit/Views/Typing/TypingView.swift b/Frameworks/NynjaUIKit/NynjaUIKit/Views/Typing/TypingView.swift index 4fc71b153..9e8be914d 100644 --- a/Frameworks/NynjaUIKit/NynjaUIKit/Views/Typing/TypingView.swift +++ b/Frameworks/NynjaUIKit/NynjaUIKit/Views/Typing/TypingView.swift @@ -78,7 +78,7 @@ public final class TypingView: BaseView { } private func setupDotsIndicator(color: UIColor) { - let indicatorView = TypingAnimatableView() + let indicatorView = TypingIndicatorView() indicatorView.itemColor = color indicatorContainer.addSubview(indicatorView) @@ -90,14 +90,13 @@ public final class TypingView: BaseView { } private func setupCircleIndicator(color: UIColor) { - let indicatorView = UIView() + let indicatorView = RoundView() indicatorView.backgroundColor = color indicatorContainer.addSubview(indicatorView) let size = Constraints.indicator.circleSize.adjustedByWidth - indicatorView.layer.cornerRadius = size / 2 indicatorView.snp.makeConstraints { maker in maker.center.equalToSuperview() maker.width.height.equalTo(size) diff --git a/Frameworks/NynjaUIKit/NynjaUIKit/Views/Utils/RoundView.swift b/Frameworks/NynjaUIKit/NynjaUIKit/Views/Utils/RoundView.swift new file mode 100644 index 000000000..9e8c2b633 --- /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 + } +} -- GitLab From a25fd4713f28a147f317ae956cec61242107f473 Mon Sep 17 00:00:00 2001 From: Anton Poltoratskyi Date: Thu, 25 Oct 2018 18:17:12 +0300 Subject: [PATCH 06/64] [NY-4699] Minor refactor. --- .../NynjaUIKit/NynjaUIKit/Views/Typing/TypingView.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Frameworks/NynjaUIKit/NynjaUIKit/Views/Typing/TypingView.swift b/Frameworks/NynjaUIKit/NynjaUIKit/Views/Typing/TypingView.swift index 9e8be914d..33ab72b25 100644 --- a/Frameworks/NynjaUIKit/NynjaUIKit/Views/Typing/TypingView.swift +++ b/Frameworks/NynjaUIKit/NynjaUIKit/Views/Typing/TypingView.swift @@ -29,14 +29,14 @@ public final class TypingView: BaseView { // MARK: - Views - private(set) lazy var indicatorContainer: UIView = { + private lazy var indicatorContainer: UIView = { let view = UIView() view.setContentHuggingPriority(.required, for: .horizontal) addSubview(view) return view }() - private(set) lazy var senderInfoLabel: UILabel = { + private lazy var senderInfoLabel: UILabel = { let label = UILabel() addSubview(label) return label -- GitLab From 3c95ccbc32566d0e391c6286f14a843385b4b633 Mon Sep 17 00:00:00 2001 From: Anton Poltoratskyi Date: Thu, 25 Oct 2018 19:19:32 +0300 Subject: [PATCH 07/64] [NY-4699] Fixed public init --- .../NynjaUIKit/Views/Typing/TypingView.swift | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/Frameworks/NynjaUIKit/NynjaUIKit/Views/Typing/TypingView.swift b/Frameworks/NynjaUIKit/NynjaUIKit/Views/Typing/TypingView.swift index 33ab72b25..c71ecb7c8 100644 --- a/Frameworks/NynjaUIKit/NynjaUIKit/Views/Typing/TypingView.swift +++ b/Frameworks/NynjaUIKit/NynjaUIKit/Views/Typing/TypingView.swift @@ -24,6 +24,20 @@ public final class TypingView: BaseView { public let senderInfo: String public let typingInfo: String public let isTypingInfoPinned: Bool + + public init(indicator: Indicator, + textColor: UIColor, + textFont: UIFont, + senderInfo: String, + typingInfo: String, + isTypingInfoPinned: Bool) { + self.indicator = indicator + self.textColor = textColor + self.textFont = textFont + self.senderInfo = senderInfo + self.typingInfo = typingInfo + self.isTypingInfoPinned = isTypingInfoPinned + } } -- GitLab From fdbb132aea0713fd9173fec98b64ab1afef9edac Mon Sep 17 00:00:00 2001 From: Anton Poltoratskyi Date: Thu, 25 Oct 2018 19:20:46 +0300 Subject: [PATCH 08/64] NY-4699] Minor refactor in AvatarView --- .../View/Views/AvatarView/AvatarView.swift | 46 ++++++++++++------- .../Views/AvatarView/AvatarViewLayout.swift | 18 +++----- 2 files changed, 36 insertions(+), 28 deletions(-) diff --git a/Nynja/Modules/Message/View/Views/AvatarView/AvatarView.swift b/Nynja/Modules/Message/View/Views/AvatarView/AvatarView.swift index e8f8185ea..47fe32217 100644 --- a/Nynja/Modules/Message/View/Views/AvatarView/AvatarView.swift +++ b/Nynja/Modules/Message/View/Views/AvatarView/AvatarView.swift @@ -7,23 +7,27 @@ // 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 + didSet { + statusLabel.text = status + statusLabel.accessibilityValue = status } } private var muteWidthConstraint: Constraint? private var muteLeftConstraint: Constraint? + // MARK: - Views + private lazy var imageView: UIImageView = { let img = UIImageView() img.contentMode = .scaleAspectFill @@ -41,10 +45,10 @@ class AvatarView: BaseView { return img }() - 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 @@ -60,7 +64,8 @@ class AvatarView: BaseView { 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) + + titleContainerView.addSubview(us) us.snp.makeConstraints({ (make) in make.height.equalTo(height) make.top.left.equalToSuperview() @@ -74,7 +79,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 @@ -91,7 +96,8 @@ class AvatarView: BaseView { 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) + + titleContainerView.addSubview(sl) sl.snp.makeConstraints({ (make) in make.height.equalTo(height) make.top.equalTo(muteImageView.snp.bottom) @@ -101,16 +107,25 @@ class AvatarView: BaseView { return sl }() - private lazy var separatorView: UIView = { - let view = UIView() + private lazy var typingView: TypingView = { + let typingView = TypingView() - view.backgroundColor = UIColor.nynja.backgroundGray + titleContainerView.addSubview(typingView) + typingView.snp.makeConstraints { maker in + maker.top.bottom.equalTo(statusLabel) + maker.left.right.equalToSuperview() + } - self.addSubview(view) - view.snp.makeConstraints({ (make) in - make.height.equalTo(Constraints.separatorView.height) + return typingView + }() + + private lazy var separatorView: SeparatorView = { + let view = SeparatorView() + + addSubview(view) + view.snp.makeConstraints { make in make.left.right.bottom.equalToSuperview() - }) + } return view }() @@ -135,5 +150,4 @@ class AvatarView: BaseView { muteLeftConstraint?.update(offset: leftInset) } } - } diff --git a/Nynja/Modules/Message/View/Views/AvatarView/AvatarViewLayout.swift b/Nynja/Modules/Message/View/Views/AvatarView/AvatarViewLayout.swift index 7ee65abf2..bdac3dfd9 100644 --- a/Nynja/Modules/Message/View/Views/AvatarView/AvatarViewLayout.swift +++ b/Nynja/Modules/Message/View/Views/AvatarView/AvatarViewLayout.swift @@ -8,35 +8,29 @@ extension AvatarView { - struct Constraints { + enum Constraints { - struct imageView { + enum imageView { static let width: CGFloat = 32.0 static let leftInset = 16.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 - } - } - } -- GitLab From 9a1f52441f450fac844fc0f7ef2fe8b679fb1de8 Mon Sep 17 00:00:00 2001 From: Anton Poltoratskyi Date: Thu, 25 Oct 2018 19:21:39 +0300 Subject: [PATCH 09/64] [NY-4699] Temp stub for displaying typing --- .../View/Views/AvatarView/AvatarView.swift | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/Nynja/Modules/Message/View/Views/AvatarView/AvatarView.swift b/Nynja/Modules/Message/View/Views/AvatarView/AvatarView.swift index 47fe32217..f78e6cb7a 100644 --- a/Nynja/Modules/Message/View/Views/AvatarView/AvatarView.swift +++ b/Nynja/Modules/Message/View/Views/AvatarView/AvatarView.swift @@ -17,6 +17,25 @@ final class AvatarView: BaseView { var status: String? { didSet { + if let title = status?.replacingOccurrences(of: "...", with: ""), title.contains("typing") { + statusLabel.isHidden = true + typingView.isHidden = false + + let appearance = TypingView.Appearance(indicator: .dots(UIColor.white), + textColor: titleLabel.textColor, + textFont: statusLabel.font, + senderInfo: "Typing", + typingInfo: "", + isTypingInfoPinned: false + ) + typingView.update(appearance) + + } else { + statusLabel.isHidden = false + typingView.isHidden = true + + statusLabel.text = status + } statusLabel.text = status statusLabel.accessibilityValue = status } -- GitLab From 45c26b684f03d7cf11a5fad8b072ab604054c5a4 Mon Sep 17 00:00:00 2001 From: Anton Poltoratskyi Date: Fri, 26 Oct 2018 13:18:54 +0300 Subject: [PATCH 10/64] [NY-4699] Display group member's alias not only for text messages. --- Nynja.xcodeproj/project.pbxproj | 8 +- .../Cell/ChatListMessageAccessoryView.swift | 6 - .../Cell/ChatListMessageContentView.swift | 116 ++---------- .../Cell/ChatListMessageTableViewCell.swift | 7 - .../Cell/ChatListMessageTextView.swift | 178 ++++++++++++++++++ .../Model/ChatListMessageCellModel.swift | 34 +--- 6 files changed, 207 insertions(+), 142 deletions(-) create mode 100644 Nynja/Library/UI/Lists/TableView/Cells/ChatListMessageCell/Cell/ChatListMessageTextView.swift diff --git a/Nynja.xcodeproj/project.pbxproj b/Nynja.xcodeproj/project.pbxproj index fd27189e9..645c418e5 100644 --- a/Nynja.xcodeproj/project.pbxproj +++ b/Nynja.xcodeproj/project.pbxproj @@ -1104,6 +1104,7 @@ 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 */; }; 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 */; }; @@ -3296,6 +3297,7 @@ 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 = ""; }; 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 = ""; }; @@ -8641,9 +8643,10 @@ 8580BAD020BD98E600239D9D /* Cell */ = { isa = PBXGroup; children = ( - 8580BAD120BD98E600239D9D /* ChatListMessageAccessoryView.swift */, 8580BAD320BD98E600239D9D /* ChatListMessageTableViewCell.swift */, 8580BAD420BD98E600239D9D /* ChatListMessageContentView.swift */, + 8580BAD120BD98E600239D9D /* ChatListMessageAccessoryView.swift */, + 85EB37F221831094003A2D6F /* ChatListMessageTextView.swift */, 8580BAD220BD98E600239D9D /* CounterView.swift */, ); path = Cell; @@ -8652,8 +8655,8 @@ 8580BAD520BD98E600239D9D /* Model */ = { isa = PBXGroup; children = ( - 850C0B5320E0369E003341D0 /* ChatListMessageCellModelDelegate.swift */, 8580BAD620BD98E600239D9D /* ChatListMessageCellModel.swift */, + 850C0B5320E0369E003341D0 /* ChatListMessageCellModelDelegate.swift */, ); path = Model; sourceTree = ""; @@ -15141,6 +15144,7 @@ A45F112420B4218D00F45004 /* MessageTextView.swift in Sources */, 85D66A0420BD963C00FBD803 /* MessagePayloadBuilder.swift in Sources */, 004581212036073100F8E413 /* JobMessageTable.swift in Sources */, + 85EB37F321831094003A2D6F /* ChatListMessageTextView.swift in Sources */, 8572C3B62092315B00E4840C /* CollectionViewDataProxy.swift in Sources */, A45F110520B4218D00F45004 /* DisplayChatConfiguration.swift in Sources */, E7598F681FA1D8B90082FBE7 /* ProfileScheduledMesssageCell.swift in Sources */, diff --git a/Nynja/Library/UI/Lists/TableView/Cells/ChatListMessageCell/Cell/ChatListMessageAccessoryView.swift b/Nynja/Library/UI/Lists/TableView/Cells/ChatListMessageCell/Cell/ChatListMessageAccessoryView.swift index 57514705a..0faef603f 100644 --- a/Nynja/Library/UI/Lists/TableView/Cells/ChatListMessageCell/Cell/ChatListMessageAccessoryView.swift +++ b/Nynja/Library/UI/Lists/TableView/Cells/ChatListMessageCell/Cell/ChatListMessageAccessoryView.swift @@ -110,12 +110,6 @@ final class ChatListMessageAccessoryView: BaseView { } } } - - func reset() { - timeLabel.text = nil - mentionIndicatorView.isHidden = true - counterView.isHidden = true - } } // MARK: - Layout 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 c0c44cc22..5117e1d81 100644 --- a/Nynja/Library/UI/Lists/TableView/Cells/ChatListMessageCell/Cell/ChatListMessageContentView.swift +++ b/Nynja/Library/UI/Lists/TableView/Cells/ChatListMessageCell/Cell/ChatListMessageContentView.swift @@ -11,17 +11,8 @@ import SnapKit final class ChatListMessageContentView: BaseView { - private static let contentFont = UIFont.makeFont(with: FontFamily.NotoSans.regular.name, - height: Constraints.contentLabel.height.adjustedByWidth)! - - 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? - private(set) lazy var titleLabel: UILabel = { let height = Constraints.titleLabel.height.adjustedByWidth let color = UIColor.nynja.white @@ -36,42 +27,18 @@ final class ChatListMessageContentView: BaseView { return label }() - - private(set) lazy var contentLabel: AlignableLabel = { - let height = Constraints.contentLabel.height.adjustedByWidth - let contentIconInset = Constraints.contentLabel.contentTypeIconInset.adjustedByWidth - let color = UIColor.nynja.manatee - - let label = AlignableLabel(height: height, color: color, fontName: FontFamily.NotoSans.regular.name) - label.verticalAlignement = .top - label.numberOfLines = 1 - - 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.height.equalTo(height) - } - - return label - }() - - private(set) lazy var contentTypeImageView: UIImageView = { - let size = Constraints.contentTypeImageView.size.adjustedByWidth - let imageView = UIImageView() + + private(set) lazy var textView: ChatListMessageTextView = { + let textView = ChatListMessageTextView() - addSubview(imageView) - imageView.snp.makeConstraints { maker in - maker.left.equalToSuperview() + addSubview(textView) + textView.snp.makeConstraints { maker in maker.top.equalTo(titleLabel.snp.bottom) - maker.width.height.equalTo(size) + maker.bottom.left.right.equalToSuperview() + // FIXME: right constraint must be setuped in parent view } - return imageView + return textView }() @@ -79,20 +46,8 @@ 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() + titleLabel.isHidden = false + textView.isHidden = false } func setupTitle(_ title: String?) { @@ -100,60 +55,17 @@ final class ChatListMessageContentView: BaseView { titleLabel.accessibilityValue = title } - func setupContentTypeImage(_ image: UIImage?) { - contentTypeImageView.image = image - } - - func setupContent(_ text: String?) { - contentLabel.text = text - } - - 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.. Date: Fri, 26 Oct 2018 14:52:28 +0300 Subject: [PATCH 11/64] [NY-4699] Show typing UI on chat list. --- .../NynjaUIKit.xcodeproj/project.pbxproj | 4 ++ .../Typing/TypingBoldIndicatorView.swift | 58 +++++++++++++++++++ .../NynjaUIKit/Views/Typing/TypingView.swift | 26 +++++---- .../Cell/ChatListMessageContentView.swift | 44 +++++++++++++- .../Model/ChatListMessageCellModel.swift | 2 + 5 files changed, 121 insertions(+), 13 deletions(-) create mode 100644 Frameworks/NynjaUIKit/NynjaUIKit/Views/Typing/TypingBoldIndicatorView.swift diff --git a/Frameworks/NynjaUIKit/NynjaUIKit.xcodeproj/project.pbxproj b/Frameworks/NynjaUIKit/NynjaUIKit.xcodeproj/project.pbxproj index 1d07d6b75..8c3037efe 100644 --- a/Frameworks/NynjaUIKit/NynjaUIKit.xcodeproj/project.pbxproj +++ b/Frameworks/NynjaUIKit/NynjaUIKit.xcodeproj/project.pbxproj @@ -38,6 +38,7 @@ 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 */ @@ -75,6 +76,7 @@ 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 = ""; }; @@ -316,6 +318,7 @@ isa = PBXGroup; children = ( 8540A0072181EA2F003A010F /* TypingIndicatorView.swift */, + 85EB37F521832D41003A2D6F /* TypingBoldIndicatorView.swift */, 8540A0092181EB87003A010F /* TypingView.swift */, ); path = Typing; @@ -481,6 +484,7 @@ 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 */, diff --git a/Frameworks/NynjaUIKit/NynjaUIKit/Views/Typing/TypingBoldIndicatorView.swift b/Frameworks/NynjaUIKit/NynjaUIKit/Views/Typing/TypingBoldIndicatorView.swift new file mode 100644 index 000000000..7316394e6 --- /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/TypingView.swift b/Frameworks/NynjaUIKit/NynjaUIKit/Views/Typing/TypingView.swift index c71ecb7c8..45009aa61 100644 --- a/Frameworks/NynjaUIKit/NynjaUIKit/Views/Typing/TypingView.swift +++ b/Frameworks/NynjaUIKit/NynjaUIKit/Views/Typing/TypingView.swift @@ -21,14 +21,14 @@ public final class TypingView: BaseView { public let indicator: Indicator public let textColor: UIColor public let textFont: UIFont - public let senderInfo: String + public let senderInfo: String? public let typingInfo: String public let isTypingInfoPinned: Bool public init(indicator: Indicator, textColor: UIColor, textFont: UIFont, - senderInfo: String, + senderInfo: String?, typingInfo: String, isTypingInfoPinned: Bool) { self.indicator = indicator @@ -52,6 +52,7 @@ public final class TypingView: BaseView { private lazy var senderInfoLabel: UILabel = { let label = UILabel() + label.setContentCompressionResistancePriority(.required, for: .horizontal) addSubview(label) return label }() @@ -64,7 +65,6 @@ public final class TypingView: BaseView { indicatorContainer.snp.makeConstraints { maker in maker.top.bottom.left.equalToSuperview() - maker.width.equalTo(Constraints.indicator.containerWidth.adjustedByWidth) } senderInfoLabel.snp.makeConstraints { maker in @@ -88,32 +88,34 @@ public final class TypingView: BaseView { senderInfoLabel.font = appearance.textFont senderInfoLabel.textColor = appearance.textColor - senderInfoLabel.text = "\(appearance.senderInfo) \(appearance.typingInfo)" + senderInfoLabel.text = appearance.senderInfo + .flatMap { "\($0) \(appearance.typingInfo)" } ?? appearance.typingInfo } private func setupDotsIndicator(color: UIColor) { let indicatorView = TypingIndicatorView() indicatorView.itemColor = color + indicatorView.setContentCompressionResistancePriority(.required, for: .horizontal) + indicatorView.setContentHuggingPriority(.required, for: .horizontal) indicatorContainer.addSubview(indicatorView) indicatorView.snp.makeConstraints { maker in maker.centerY.equalToSuperview() - maker.left.equalToSuperview() + maker.left.right.equalToSuperview() } } private func setupCircleIndicator(color: UIColor) { - let indicatorView = RoundView() - indicatorView.backgroundColor = color - + let indicatorView = TypingBoldIndicatorView() indicatorContainer.addSubview(indicatorView) - let size = Constraints.indicator.circleSize.adjustedByWidth + indicatorView.circleColor = color + indicatorView.circleSize = Constraints.indicator.circleSize.adjustedByWidth + indicatorView.horizontalInset = Constraints.indicator.padding.adjustedByWidth indicatorView.snp.makeConstraints { maker in - maker.center.equalToSuperview() - maker.width.height.equalTo(size) + maker.edges.equalToSuperview() } } @@ -123,8 +125,8 @@ public final class TypingView: BaseView { private enum Constraints { enum indicator { - static let containerWidth: CGFloat = 16 static let circleSize: CGFloat = 8 + static let padding: CGFloat = 4 } enum senderInfo { 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 5117e1d81..4ad6ce2d7 100644 --- a/Nynja/Library/UI/Lists/TableView/Cells/ChatListMessageCell/Cell/ChatListMessageContentView.swift +++ b/Nynja/Library/UI/Lists/TableView/Cells/ChatListMessageCell/Cell/ChatListMessageContentView.swift @@ -8,6 +8,7 @@ import UIKit import SnapKit +import NynjaUIKit final class ChatListMessageContentView: BaseView { @@ -35,12 +36,26 @@ final class ChatListMessageContentView: BaseView { textView.snp.makeConstraints { maker in maker.top.equalTo(titleLabel.snp.bottom) maker.bottom.left.right.equalToSuperview() - // FIXME: right constraint must be setuped in parent view } return textView }() + private(set) lazy var typingView: TypingView = { + let height = Constraints.typingView.height.adjustedByWidth + + let typingView = TypingView() + + addSubview(typingView) + typingView.snp.makeConstraints { maker in + maker.top.equalTo(titleLabel.snp.bottom) + maker.left.right.equalToSuperview() + maker.height.equalTo(height) + } + + return typingView + }() + // MARK: - Setup @@ -48,6 +63,7 @@ final class ChatListMessageContentView: BaseView { super.baseSetup() titleLabel.isHidden = false textView.isHidden = false + typingView.isHidden = false } func setupTitle(_ title: String?) { @@ -59,13 +75,39 @@ final class ChatListMessageContentView: BaseView { textView.setup(sender: sender, image: image, text: text) } + func showTyping(sender: String?) { + typingView.isHidden = false + textView.isHidden = true + + let appearance = TypingView.Appearance(indicator: .dots(UIColor.nynja.white), + textColor: titleLabel.textColor, + textFont: typingFont, + senderInfo: sender, + typingInfo: "typing", + isTypingInfoPinned: false) + + typingView.update(appearance) + } + + func hideTyping() { + typingView.isHidden = true + textView.isHidden = false + } + // MARK: - Layout + private let typingFont = UIFont.makeFont(with: FontFamily.NotoSans.regular.name, + height: Constraints.typingView.height.adjustedByWidth)! + private enum Constraints { enum titleLabel { static let height: CGFloat = 22 } + + enum typingView { + static let height: CGFloat = 20 + } } } diff --git a/Nynja/Library/UI/Lists/TableView/Cells/ChatListMessageCell/Model/ChatListMessageCellModel.swift b/Nynja/Library/UI/Lists/TableView/Cells/ChatListMessageCell/Model/ChatListMessageCellModel.swift index 8af6bacaa..37acf4f1d 100644 --- a/Nynja/Library/UI/Lists/TableView/Cells/ChatListMessageCell/Model/ChatListMessageCellModel.swift +++ b/Nynja/Library/UI/Lists/TableView/Cells/ChatListMessageCell/Model/ChatListMessageCellModel.swift @@ -60,6 +60,8 @@ final class ChatListMessageCellModel: CellViewModel { let type = SendMessageType(rawValue: mime) else { return } + cell.messageContentView.showTyping(sender: sender) + switch type { case .text: setupText(for: message, in: cell) -- GitLab From 753aaa2bfb2e64017b4bf154f243221102eac196 Mon Sep 17 00:00:00 2001 From: Anton Poltoratskyi Date: Fri, 26 Oct 2018 16:33:56 +0300 Subject: [PATCH 12/64] [NY-4699] Temp fix for continue animation of typing after view controller dissapeared. --- Nynja/Modules/ChatsList/View/ChatsListViewController.swift | 6 ++++++ .../Modules/GroupsList/View/GroupsListViewController.swift | 2 ++ Nynja/Modules/Profile/View/ProfileViewController.swift | 6 ++++++ 3 files changed, 14 insertions(+) diff --git a/Nynja/Modules/ChatsList/View/ChatsListViewController.swift b/Nynja/Modules/ChatsList/View/ChatsListViewController.swift index 7f512269c..b5bd6f8a1 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 { diff --git a/Nynja/Modules/GroupsList/View/GroupsListViewController.swift b/Nynja/Modules/GroupsList/View/GroupsListViewController.swift index 780533112..81fac1e09 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() } diff --git a/Nynja/Modules/Profile/View/ProfileViewController.swift b/Nynja/Modules/Profile/View/ProfileViewController.swift index d5459dd4d..43067852b 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() -- GitLab From 71d224345475338356d7ef2c8bbcf1367fe4fe68 Mon Sep 17 00:00:00 2001 From: Anton Poltoratskyi Date: Fri, 26 Oct 2018 17:22:48 +0300 Subject: [PATCH 13/64] [NY-4699] Implemented UI for displaying statuses on p2p chat list. --- .../Views/Avatar/AvatarStatusView.swift | 12 +++--- .../Cell/ChatListMessageTableViewCell.swift | 42 +++++++------------ .../Model/ChatListMessageCellModel.swift | 27 +++++++++--- 3 files changed, 42 insertions(+), 39 deletions(-) diff --git a/Frameworks/NynjaUIKit/NynjaUIKit/Views/Avatar/AvatarStatusView.swift b/Frameworks/NynjaUIKit/NynjaUIKit/Views/Avatar/AvatarStatusView.swift index bad89a549..bcd7ffdc8 100644 --- a/Frameworks/NynjaUIKit/NynjaUIKit/Views/Avatar/AvatarStatusView.swift +++ b/Frameworks/NynjaUIKit/NynjaUIKit/Views/Avatar/AvatarStatusView.swift @@ -16,13 +16,13 @@ public final class AvatarStatusView: BaseView { } } - public var statusRadius: CGFloat = 24 { + public var statusIconSize: CGFloat = 24 { didSet { setNeedsLayout() } } - public var statusPadding: CGFloat = 8 { + public var statusIconPadding: CGFloat = 8 { didSet { setNeedsLayout() } @@ -66,7 +66,7 @@ public final class AvatarStatusView: BaseView { imageView.layer.cornerRadius = bounds.height / 2 let clipCircleCenter = statusPosition(in: bounds) - let clipCircleSize = statusRadius + statusPadding * 2 + let clipCircleSize = statusIconSize + statusIconPadding * 2 let clipCircleFrame = CGRect( x: clipCircleCenter.x - clipCircleSize / 2, y: clipCircleCenter.y - clipCircleSize / 2, @@ -89,10 +89,10 @@ public final class AvatarStatusView: BaseView { } private func updateStatusView(with frame: CGRect) { - let size = frame.width - statusPadding * 2 + let size = frame.width - statusIconPadding * 2 - statusView.frame = CGRect(x: frame.minX + statusPadding, - y: frame.minY + statusPadding, + statusView.frame = CGRect(x: frame.minX + statusIconPadding, + y: frame.minY + statusIconPadding, width: size, height: size) diff --git a/Nynja/Library/UI/Lists/TableView/Cells/ChatListMessageCell/Cell/ChatListMessageTableViewCell.swift b/Nynja/Library/UI/Lists/TableView/Cells/ChatListMessageCell/Cell/ChatListMessageTableViewCell.swift index a76ae6f8a..0230b9458 100644 --- a/Nynja/Library/UI/Lists/TableView/Cells/ChatListMessageCell/Cell/ChatListMessageTableViewCell.swift +++ b/Nynja/Library/UI/Lists/TableView/Cells/ChatListMessageCell/Cell/ChatListMessageTableViewCell.swift @@ -8,26 +8,30 @@ import UIKit import SnapKit +import NynjaUIKit final class ChatListMessageTableViewCell: UITableViewCell { // MARK: - Views - private(set) lazy var avatarImageView: UIImageView = { + private(set) lazy var avatarImageView: AvatarStatusView = { let size = Constraints.avatarImageView.size.adjustedByWidth + let statusIconPadding = Constraints.avatarImageView.statusIconPadding.adjustedByWidth + let statusIconSize = Constraints.avatarImageView.statusIconSize.adjustedByWidth - let imageView = UIImageView() - imageView.contentMode = .scaleAspectFill - applyCorners(to: imageView, radius: size / 2) + let avatarView = AvatarStatusView() + avatarView.imageView.contentMode = .scaleAspectFill + avatarView.statusIconPadding = statusIconPadding + avatarView.statusIconSize = statusIconSize - contentView.addSubview(imageView) - imageView.snp.makeConstraints { maker in + contentView.addSubview(avatarView) + avatarView.snp.makeConstraints { maker in maker.width.height.equalTo(size) maker.left.equalToSuperview().offset(Constraints.avatarImageView.leftInset.adjustedByWidth) maker.centerY.equalToSuperview() } - return imageView + return avatarView }() private(set) lazy var messageContentView: ChatListMessageContentView = { @@ -43,7 +47,6 @@ final class ChatListMessageTableViewCell: UITableViewCell { view.snp.makeConstraints { maker in maker.left.equalTo(avatarImageView.snp.right).offset(leftInset) maker.right.lessThanOrEqualTo(messageAccessoryView.snp.left).offset(-rightInset) - maker.width.equalTo(width).priority(.high) maker.centerY.equalToSuperview() } @@ -71,8 +74,7 @@ final class ChatListMessageTableViewCell: UITableViewCell { private lazy var separatorView: SeparatorView = { let view = SeparatorView() - view.color = UIColor.nynja.backgroundGray - + contentView.addSubview(view) view.snp.makeConstraints { maker in maker.horizontalInset(Constraints.separatorView.horizontalInset.adjustedByWidth) @@ -104,22 +106,6 @@ final class ChatListMessageTableViewCell: UITableViewCell { messageContentView.isHidden = false separatorView.isHidden = false } - - - // MARK: - Life Cycle - - override func layoutSubviews() { - super.layoutSubviews() - applyCorners(to: avatarImageView, radius: Constraints.avatarImageView.cornerRadius.adjustedByWidth) - } - - - // MARK: - Layout - - private func applyCorners(to imageView: UIImageView, radius: CGFloat) { - let borderColor = UIColor.nynja.almostBlack - imageView.roundCornersImage(borderWidth: 2, cornerRadius: radius, borderColor: borderColor) - } } // MARK: - Layout @@ -132,10 +118,12 @@ extension ChatListMessageTableViewCell { enum avatarImageView { static let size: CGFloat = 48.0 - static let cornerRadius: CGFloat = size / 2 static let verticalInset: CGFloat = 8.0 static let leftInset: CGFloat = 16.0 + + static let statusIconPadding: CGFloat = 2.0 + static let statusIconSize: CGFloat = 8.0 } enum messageContentView { diff --git a/Nynja/Library/UI/Lists/TableView/Cells/ChatListMessageCell/Model/ChatListMessageCellModel.swift b/Nynja/Library/UI/Lists/TableView/Cells/ChatListMessageCell/Model/ChatListMessageCellModel.swift index 37acf4f1d..370f62e03 100644 --- a/Nynja/Library/UI/Lists/TableView/Cells/ChatListMessageCell/Model/ChatListMessageCellModel.swift +++ b/Nynja/Library/UI/Lists/TableView/Cells/ChatListMessageCell/Model/ChatListMessageCellModel.swift @@ -34,10 +34,25 @@ final class ChatListMessageCellModel: CellViewModel { } func setup(cell: ChatListMessageTableViewCell) { - // Avatar - cell.avatarImageView.setImage(url: model.photoURL, placeHolder: UIImage.nynja.Contacts.avaPlaceholder.image) + setupAvatar(in: cell) + setupAccessory(in: cell) + setupMessage(model.message, in: cell) + } + + + // MARK: - Avatar + + private func setupAvatar(in cell: Cell) { + cell.avatarImageView.imageView + .setImage(url: model.photoURL, placeHolder: UIImage.nynja.Contacts.avaPlaceholder.image) - // Accessory + cell.avatarImageView.update(.color(UIColor.nynja.callGreen)) + } + + + // MARK: - Accessory + + private func setupAccessory(in cell: Cell) { let unreadCount = min(Int(model.unreadMessagesCount), type(of: self).unreadCounterLimit) let shouldDisplayMention = model.hasMentions @@ -45,11 +60,11 @@ final class ChatListMessageCellModel: CellViewModel { if let createdDate = model.message?.createdDate { cell.messageAccessoryView.setup(date: createdDate) } - - // Message Data - setupMessage(model.message, in: cell) } + + // MARK: - Message + private func setupMessage(_ message: Message?, in cell: Cell) { cell.messageContentView.setupTitle(model.title) -- GitLab From 0a5463e9e7c3d8e3b26a9dfe9c24da4409b0915f Mon Sep 17 00:00:00 2001 From: Anton Poltoratskyi Date: Fri, 26 Oct 2018 17:33:37 +0300 Subject: [PATCH 14/64] [NY-4699] Don't display status in rooms list. --- .../Views/Avatar/AvatarStatusView.swift | 36 +++++++++++++------ .../Model/ChatListMessageCellModel.swift | 2 +- 2 files changed, 27 insertions(+), 11 deletions(-) diff --git a/Frameworks/NynjaUIKit/NynjaUIKit/Views/Avatar/AvatarStatusView.swift b/Frameworks/NynjaUIKit/NynjaUIKit/Views/Avatar/AvatarStatusView.swift index bcd7ffdc8..3e41e2794 100644 --- a/Frameworks/NynjaUIKit/NynjaUIKit/Views/Avatar/AvatarStatusView.swift +++ b/Frameworks/NynjaUIKit/NynjaUIKit/Views/Avatar/AvatarStatusView.swift @@ -41,6 +41,10 @@ public final class AvatarStatusView: BaseView { private let maskLayer = CAShapeLayer() + private var isMaskActive: Bool { + return !statusView.isHidden + } + // MARK: - Setup @@ -65,16 +69,20 @@ public final class AvatarStatusView: BaseView { imageView.frame = bounds imageView.layer.cornerRadius = bounds.height / 2 - 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) + 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) { @@ -110,16 +118,24 @@ 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/Nynja/Library/UI/Lists/TableView/Cells/ChatListMessageCell/Model/ChatListMessageCellModel.swift b/Nynja/Library/UI/Lists/TableView/Cells/ChatListMessageCell/Model/ChatListMessageCellModel.swift index 370f62e03..8d12b5eed 100644 --- a/Nynja/Library/UI/Lists/TableView/Cells/ChatListMessageCell/Model/ChatListMessageCellModel.swift +++ b/Nynja/Library/UI/Lists/TableView/Cells/ChatListMessageCell/Model/ChatListMessageCellModel.swift @@ -46,7 +46,7 @@ final class ChatListMessageCellModel: CellViewModel { cell.avatarImageView.imageView .setImage(url: model.photoURL, placeHolder: UIImage.nynja.Contacts.avaPlaceholder.image) - cell.avatarImageView.update(.color(UIColor.nynja.callGreen)) + cell.avatarImageView.update(model is Contact ? .color(UIColor.nynja.callGreen) : .none) } -- GitLab From 16516ff8daf4fa6094b804f5e39e0d4d51a3cd76 Mon Sep 17 00:00:00 2001 From: Anton Poltoratskyi Date: Fri, 26 Oct 2018 19:23:26 +0300 Subject: [PATCH 15/64] [NY-4699] Implemented base stub for AccountStatusProvider. --- Nynja.xcodeproj/project.pbxproj | 24 ++++++++++ Nynja/Statuses/AccountStatus.swift | 17 +++++++ Nynja/Statuses/AccountStatusProvider.swift | 40 +++++++++++++++++ Nynja/Statuses/Observable.swift | 52 ++++++++++++++++++++++ Nynja/Statuses/ObservableContainer.swift | 44 ++++++++++++++++++ 5 files changed, 177 insertions(+) create mode 100644 Nynja/Statuses/AccountStatus.swift create mode 100644 Nynja/Statuses/AccountStatusProvider.swift create mode 100644 Nynja/Statuses/Observable.swift create mode 100644 Nynja/Statuses/ObservableContainer.swift diff --git a/Nynja.xcodeproj/project.pbxproj b/Nynja.xcodeproj/project.pbxproj index 645c418e5..e84f958e0 100644 --- a/Nynja.xcodeproj/project.pbxproj +++ b/Nynja.xcodeproj/project.pbxproj @@ -1105,6 +1105,10 @@ 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 */; }; + 85EB37F82183659C003A2D6F /* AccountStatusProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 85EB37F72183659C003A2D6F /* AccountStatusProvider.swift */; }; + 85EB37FB21837235003A2D6F /* ObservableContainer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 85EB37FA21837235003A2D6F /* ObservableContainer.swift */; }; + 85EB37FD21837253003A2D6F /* Observable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 85EB37FC21837253003A2D6F /* Observable.swift */; }; + 85EB37FF21837304003A2D6F /* AccountStatus.swift in Sources */ = {isa = PBXBuildFile; fileRef = 85EB37FE21837304003A2D6F /* AccountStatus.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 */; }; @@ -3298,6 +3302,10 @@ 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 = ""; }; + 85EB37F72183659C003A2D6F /* AccountStatusProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountStatusProvider.swift; sourceTree = ""; }; + 85EB37FA21837235003A2D6F /* ObservableContainer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ObservableContainer.swift; sourceTree = ""; }; + 85EB37FC21837253003A2D6F /* Observable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Observable.swift; sourceTree = ""; }; + 85EB37FE21837304003A2D6F /* AccountStatus.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountStatus.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 = ""; }; @@ -5973,6 +5981,7 @@ 8509AC61206A54420089089B /* ResponseResult.swift */, B7F4C2AA211995A500E48A98 /* Validation */, A42C44E220F340DA00BC3CBB /* StatusCodeManager.swift */, + 85EB37F9218365A6003A2D6F /* Statuses */, ); name = Services; sourceTree = ""; @@ -9076,6 +9085,17 @@ path = BBCode; sourceTree = ""; }; + 85EB37F9218365A6003A2D6F /* Statuses */ = { + isa = PBXGroup; + children = ( + 85EB37FE21837304003A2D6F /* AccountStatus.swift */, + 85EB37F72183659C003A2D6F /* AccountStatusProvider.swift */, + 85EB37FA21837235003A2D6F /* ObservableContainer.swift */, + 85EB37FC21837253003A2D6F /* Observable.swift */, + ); + path = Statuses; + sourceTree = ""; + }; 85EF7C342090DEFF0090C418 /* Models */ = { isa = PBXGroup; children = ( @@ -14976,6 +14996,7 @@ 4B4266C1204D917800194BC1 /* ActionsView+Layout.swift in Sources */, AF440BA5CEBE5170D082FF60 /* LoginProtocols.swift in Sources */, 85C16C3520D2520E00EDB77E /* StickersDownloadingService.swift in Sources */, + 85EB37F82183659C003A2D6F /* AccountStatusProvider.swift in Sources */, 6D6234F81F1E158600EF375F /* HistoryCell.swift in Sources */, FEA655F62167777E00B44029 /* PaymentInteractor.swift in Sources */, E74EC9EF1FC2DE23007268E6 /* MemberTable.swift in Sources */, @@ -15117,6 +15138,7 @@ A49B81B320B4BB6400980D36 /* NynjaMTIConfig.swift in Sources */, 8EC2AF6B20053FC300807B20 /* GroupCollectionCell.swift in Sources */, 4B8996D8204EDA7700DCB183 /* JobDAOProtocol.swift in Sources */, + 85EB37FF21837304003A2D6F /* AccountStatus.swift in Sources */, A4CE80C320C95E7F00400713 /* CollectionDisplayMode.swift in Sources */, E74E53951FB45D6800463242 /* ScrollBar.swift in Sources */, FEA655CD2167777E00B44029 /* SeedVerificationWalletProtocols.swift in Sources */, @@ -15145,6 +15167,7 @@ 85D66A0420BD963C00FBD803 /* MessagePayloadBuilder.swift in Sources */, 004581212036073100F8E413 /* JobMessageTable.swift in Sources */, 85EB37F321831094003A2D6F /* ChatListMessageTextView.swift in Sources */, + 85EB37FD21837253003A2D6F /* Observable.swift in Sources */, 8572C3B62092315B00E4840C /* CollectionViewDataProxy.swift in Sources */, A45F110520B4218D00F45004 /* DisplayChatConfiguration.swift in Sources */, E7598F681FA1D8B90082FBE7 /* ProfileScheduledMesssageCell.swift in Sources */, @@ -15308,6 +15331,7 @@ F117871020ACF018007A9A1B /* CameraQualitySettingsProtocols.swift in Sources */, A44B4D5920CE9BDF00CA700A /* ImageCellViewModel.swift in Sources */, A415132020DBD58900C2C01F /* Link.swift in Sources */, + 85EB37FB21837235003A2D6F /* ObservableContainer.swift in Sources */, 852DF263203720E600A4F8B6 /* FileIcons.swift in Sources */, A43B25DB20AB1EE400FF8107 /* NewChannelInteractor.swift in Sources */, FBCE840F20E525A6003B7558 /* HTTPParameters.swift in Sources */, diff --git a/Nynja/Statuses/AccountStatus.swift b/Nynja/Statuses/AccountStatus.swift new file mode 100644 index 000000000..f9f0890d4 --- /dev/null +++ b/Nynja/Statuses/AccountStatus.swift @@ -0,0 +1,17 @@ +// +// AccountStatus.swift +// Nynja +// +// Created by Anton Poltoratskyi on 26.10.2018. +// Copyright © 2018 TecSynt Solutions. All rights reserved. +// + +typealias AccountId = String + +enum AccountStatus { + case active + case inactive + case busy + case offline + case none +} diff --git a/Nynja/Statuses/AccountStatusProvider.swift b/Nynja/Statuses/AccountStatusProvider.swift new file mode 100644 index 000000000..d8996ab5f --- /dev/null +++ b/Nynja/Statuses/AccountStatusProvider.swift @@ -0,0 +1,40 @@ +// +// AccountStatusProvider.swift +// Nynja +// +// Created by Anton Poltoratskyi on 26.10.2018. +// Copyright © 2018 TecSynt Solutions. All rights reserved. +// + +import Foundation + +protocol AccountStatusObservable: class { + typealias Callback = (AccountId, AccountStatus) -> Void + + func addObserver(_ observer: AnyObject, callback: @escaping Callback) + func addObserver(_ observer: AnyObject, for key: AccountId, callback: @escaping Callback) + func removeObserver(_ observer: AnyObject) + func removeObserver(_ observer: AnyObject, for key: AccountId) + func notify(_ key: AccountId, with value: AccountStatus) +} + +protocol AccountStatusProvider: AccountStatusObservable { + func status(for accountId: AccountId) -> AccountStatus + func update(_ status: AccountStatus, for accountId: AccountId) +} + +final class AccountStatusProviderImpl: AccountStatusProvider, ObservableContainer { + + private var data: [AccountId: AccountStatus] = [:] + + private(set) var observable = Observable() + + func status(for accountId: AccountId) -> AccountStatus { + return data[accountId] ?? .none + } + + func update(_ status: AccountStatus, for accountId: AccountId) { + data[accountId] = status + observable.notify(accountId, with: status) + } +} diff --git a/Nynja/Statuses/Observable.swift b/Nynja/Statuses/Observable.swift new file mode 100644 index 000000000..5857ca9f6 --- /dev/null +++ b/Nynja/Statuses/Observable.swift @@ -0,0 +1,52 @@ +// +// Observable.swift +// Nynja +// +// Created by Anton Poltoratskyi on 26.10.2018. +// Copyright © 2018 TecSynt Solutions. All rights reserved. +// + +import Foundation + +final class Observable { + + private typealias Observers = [AnyWeakSubscriber] + + private struct Handler { + var callback: (Key, Value) -> Void + } + + 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) + allObservers.append(container) + } + + func addObserver(_ observer: AnyObject, for key: Key, callback: @escaping (Key, Value) -> Void) { + let handler = Handler(callback: callback) + let container = AnyWeakSubscriber(object: observer, handler: handler) + + var newObservers = observers[key] ?? [] + newObservers.append(container) + observers[key] = newObservers + } + + func removeObserver(_ observer: AnyObject) { + allObservers.removeAll { $0.object.value === observer || $0.object.value == nil } + } + + func removeObserver(_ observer: AnyObject, for key: Key) { + observers[key]?.removeAll { $0.object.value === observer || $0.object.value == nil } + } + + func notify(_ key: Key, with value: Value) { + for observer in allObservers { + observer.handler.callback(key, value) + } + observers[key]?.forEach { $0.handler.callback(key, value) } + } +} diff --git a/Nynja/Statuses/ObservableContainer.swift b/Nynja/Statuses/ObservableContainer.swift new file mode 100644 index 000000000..9b89e430f --- /dev/null +++ b/Nynja/Statuses/ObservableContainer.swift @@ -0,0 +1,44 @@ +// +// ObservableContainer.swift +// Nynja +// +// Created by Anton Poltoratskyi on 26.10.2018. +// Copyright © 2018 TecSynt Solutions. All rights reserved. +// + +protocol ObservableContainer: class { + associatedtype Key: Hashable + associatedtype Value + typealias Callback = (Key, Value) -> Void + + var observable: Observable { 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 ObservableContainer { + + 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) + } +} -- GitLab From 454c2211a6f5db1e312fc5ed4ebdafacbecd1881 Mon Sep 17 00:00:00 2001 From: Anton Poltoratskyi Date: Mon, 29 Oct 2018 10:31:38 +0200 Subject: [PATCH 16/64] [NY-4699] Added TypingStatusProvider. --- Nynja.xcodeproj/project.pbxproj | 8 +++++ Nynja/Statuses/TypingActionStatus.swift | 23 +++++++++++++ Nynja/Statuses/TypingStatusProvider.swift | 40 +++++++++++++++++++++++ 3 files changed, 71 insertions(+) create mode 100644 Nynja/Statuses/TypingActionStatus.swift create mode 100644 Nynja/Statuses/TypingStatusProvider.swift diff --git a/Nynja.xcodeproj/project.pbxproj b/Nynja.xcodeproj/project.pbxproj index e84f958e0..0f640fc47 100644 --- a/Nynja.xcodeproj/project.pbxproj +++ b/Nynja.xcodeproj/project.pbxproj @@ -949,6 +949,7 @@ 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 /* TypingStatusProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 854834172186FADB002064E1 /* TypingStatusProvider.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 */; }; @@ -1109,6 +1110,7 @@ 85EB37FB21837235003A2D6F /* ObservableContainer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 85EB37FA21837235003A2D6F /* ObservableContainer.swift */; }; 85EB37FD21837253003A2D6F /* Observable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 85EB37FC21837253003A2D6F /* Observable.swift */; }; 85EB37FF21837304003A2D6F /* AccountStatus.swift in Sources */ = {isa = PBXBuildFile; fileRef = 85EB37FE21837304003A2D6F /* AccountStatus.swift */; }; + 85EB3801218377E5003A2D6F /* TypingActionStatus.swift in Sources */ = {isa = PBXBuildFile; fileRef = 85EB3800218377E5003A2D6F /* TypingActionStatus.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 */; }; @@ -3157,6 +3159,7 @@ 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 /* TypingStatusProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TypingStatusProvider.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 = ""; }; @@ -3306,6 +3309,7 @@ 85EB37FA21837235003A2D6F /* ObservableContainer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ObservableContainer.swift; sourceTree = ""; }; 85EB37FC21837253003A2D6F /* Observable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Observable.swift; sourceTree = ""; }; 85EB37FE21837304003A2D6F /* AccountStatus.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountStatus.swift; sourceTree = ""; }; + 85EB3800218377E5003A2D6F /* TypingActionStatus.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TypingActionStatus.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 = ""; }; @@ -9089,7 +9093,9 @@ isa = PBXGroup; children = ( 85EB37FE21837304003A2D6F /* AccountStatus.swift */, + 85EB3800218377E5003A2D6F /* TypingActionStatus.swift */, 85EB37F72183659C003A2D6F /* AccountStatusProvider.swift */, + 854834172186FADB002064E1 /* TypingStatusProvider.swift */, 85EB37FA21837235003A2D6F /* ObservableContainer.swift */, 85EB37FC21837253003A2D6F /* Observable.swift */, ); @@ -15057,6 +15063,7 @@ 6D485DDF1F0ACA4700E12FB1 /* UIImageView+Rounded.swift in Sources */, 26CD3FDB2104D19D00597E62 /* AudioShortTranscribeOperation.swift in Sources */, 40C2631343E285717633ADFA /* LoginPresenter.swift in Sources */, + 85EB3801218377E5003A2D6F /* TypingActionStatus.swift in Sources */, A42D51A7206A361400EEB952 /* log.swift in Sources */, DAE89B7EFAB308A6B48AF5EC /* LoginInteractor.swift in Sources */, C940514A204C7FAF00D72B04 /* DataAndStorageViewController.swift in Sources */, @@ -16213,6 +16220,7 @@ 850D220020D2E7E20018BBA4 /* SelectionFeedbackInteractive.swift in Sources */, 43711F24FF65C36730467BFF /* EditPhotoViewController.swift in Sources */, A42D519F206A361400EEB952 /* messageEvent.swift in Sources */, + 854834182186FADB002064E1 /* TypingStatusProvider.swift in Sources */, F11DF06520BD96D000F3E005 /* GalleryFilterType.swift in Sources */, FBCE841220E525A6003B7558 /* NetworkClient.swift in Sources */, 7A8FE56A8E5D02256D8BE936 /* EditPhotoPresenter.swift in Sources */, diff --git a/Nynja/Statuses/TypingActionStatus.swift b/Nynja/Statuses/TypingActionStatus.swift new file mode 100644 index 000000000..7654b102f --- /dev/null +++ b/Nynja/Statuses/TypingActionStatus.swift @@ -0,0 +1,23 @@ +// +// TypingActionStatus.swift +// Nynja +// +// Created by Anton Poltoratskyi on 26.10.2018. +// Copyright © 2018 TecSynt Solutions. All rights reserved. +// + +enum TypingActionStatus { + typealias Sender = String + + enum Record { + case voice, video + } + enum Mime { + case file + case image + } + case typing([Sender]) + case sending([Sender], Mime) + case recording([Sender], Record) + case none +} diff --git a/Nynja/Statuses/TypingStatusProvider.swift b/Nynja/Statuses/TypingStatusProvider.swift new file mode 100644 index 000000000..eaf60ecf3 --- /dev/null +++ b/Nynja/Statuses/TypingStatusProvider.swift @@ -0,0 +1,40 @@ +// +// TypingStatusProvider.swift +// Nynja +// +// Created by Anton Poltoratskyi on 29.10.2018. +// Copyright © 2018 TecSynt Solutions. All rights reserved. +// + +import Foundation + +protocol TypingStatusObservable: class { + typealias Callback = (AccountId, TypingActionStatus) -> Void + + func addObserver(_ observer: AnyObject, callback: @escaping Callback) + func addObserver(_ observer: AnyObject, for key: AccountId, callback: @escaping Callback) + func removeObserver(_ observer: AnyObject) + func removeObserver(_ observer: AnyObject, for key: AccountId) + func notify(_ key: AccountId, with value: TypingActionStatus) +} + +protocol TypingStatusProvider: TypingStatusObservable { + func status(for accountId: AccountId) -> TypingActionStatus + func update(_ status: TypingActionStatus, for accountId: AccountId) +} + +final class TypingStatusProviderImpl: TypingStatusProvider, ObservableContainer { + + private var data: [AccountId: TypingActionStatus] = [:] + + private(set) var observable = Observable() + + func status(for accountId: AccountId) -> TypingActionStatus { + return data[accountId] ?? .none + } + + func update(_ status: TypingActionStatus, for accountId: AccountId) { + data[accountId] = status + observable.notify(accountId, with: status) + } +} -- GitLab From 6d3fc1dd02e4ef8022643bd18603f4bbdfbc74e9 Mon Sep 17 00:00:00 2001 From: Anton Poltoratskyi Date: Mon, 29 Oct 2018 10:54:23 +0200 Subject: [PATCH 17/64] [NY-4699] Added status to AvatarStatusView --- .../Views/Avatar/AvatarStatusView.swift | 4 ++-- .../View/DetailsView/ProfileDetailsView.swift | 24 +++++++++++-------- .../ProfileDetailsViewLayout.swift | 24 ++++++++++--------- .../Profile/View/ProfileViewController.swift | 8 +++---- 4 files changed, 33 insertions(+), 27 deletions(-) diff --git a/Frameworks/NynjaUIKit/NynjaUIKit/Views/Avatar/AvatarStatusView.swift b/Frameworks/NynjaUIKit/NynjaUIKit/Views/Avatar/AvatarStatusView.swift index 3e41e2794..b2bec5db6 100644 --- a/Frameworks/NynjaUIKit/NynjaUIKit/Views/Avatar/AvatarStatusView.swift +++ b/Frameworks/NynjaUIKit/NynjaUIKit/Views/Avatar/AvatarStatusView.swift @@ -16,13 +16,13 @@ public final class AvatarStatusView: BaseView { } } - public var statusIconSize: CGFloat = 24 { + public var statusIconSize: CGFloat = 8 { didSet { setNeedsLayout() } } - public var statusIconPadding: CGFloat = 8 { + public var statusIconPadding: CGFloat = 2 { didSet { setNeedsLayout() } diff --git a/Nynja/Modules/Profile/View/DetailsView/ProfileDetailsView.swift b/Nynja/Modules/Profile/View/DetailsView/ProfileDetailsView.swift index f956cc96b..cb5b5b71f 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 = { @@ -196,6 +198,8 @@ class ProfileDetailsView: UIView { phoneLabel.isHidden = false walletButton.isHidden = false infoView.isHidden = false + + avatarImageView.update(.color(UIColor.nynja.callGreen)) } // MARK: Recognizer diff --git a/Nynja/Modules/Profile/View/DetailsView/ProfileDetailsViewLayout.swift b/Nynja/Modules/Profile/View/DetailsView/ProfileDetailsViewLayout.swift index 38591f0c8..33c9b7617 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 43067852b..33f2fc770 100644 --- a/Nynja/Modules/Profile/View/ProfileViewController.swift +++ b/Nynja/Modules/Profile/View/ProfileViewController.swift @@ -114,9 +114,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 @@ -156,7 +156,7 @@ class ProfileViewController: BaseVC, ProfileViewProtocol { } @objc private func avatarTapped() { - presenter.showImagePreview(from: detailsView.avatarImageView) + presenter.showImagePreview(from: detailsView.avatarImageView.imageView) } @objc private func walletButtonAction() { -- GitLab From 6e007c5e248a981984ce05dd7e787af16f4660c8 Mon Sep 17 00:00:00 2001 From: Anton Poltoratskyi Date: Mon, 29 Oct 2018 11:05:15 +0200 Subject: [PATCH 18/64] [NY-4699] Minor UI fix. --- .../NynjaUIKit/Views/Typing/TypingIndicatorView.swift | 6 +++--- .../NynjaUIKit/NynjaUIKit/Views/Typing/TypingView.swift | 5 +++++ 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/Frameworks/NynjaUIKit/NynjaUIKit/Views/Typing/TypingIndicatorView.swift b/Frameworks/NynjaUIKit/NynjaUIKit/Views/Typing/TypingIndicatorView.swift index 5a67c92a5..a3946fc2d 100644 --- a/Frameworks/NynjaUIKit/NynjaUIKit/Views/Typing/TypingIndicatorView.swift +++ b/Frameworks/NynjaUIKit/NynjaUIKit/Views/Typing/TypingIndicatorView.swift @@ -17,7 +17,7 @@ public final class TypingIndicatorView: BaseView { } } - public var padding: CGFloat = 4 { + public var itemPadding: CGFloat = 4 { didSet { invalidateIntrinsicContentSize() setNeedsLayout() @@ -38,7 +38,7 @@ public final class TypingIndicatorView: BaseView { } public override var intrinsicContentSize: CGSize { - let width = itemSize * CGFloat(itemsCount) + padding * CGFloat(itemsCount - 1) + let width = itemSize * CGFloat(itemsCount) + itemPadding * CGFloat(itemsCount - 1) return CGSize(width: width, height: itemSize) } @@ -79,7 +79,7 @@ public final class TypingIndicatorView: BaseView { itemLayer.cornerRadius = itemSize / 2 animationLayer.instanceCount = itemsCount - animationLayer.instanceTransform = CATransform3DMakeTranslation(itemSize + padding, 0, 0) + animationLayer.instanceTransform = CATransform3DMakeTranslation(itemSize + itemPadding, 0, 0) animationLayer.instanceAlphaOffset = Float(Animation.toValue - Animation.fromValue) / Float(itemsCount - 1) animationLayer.instanceDelay = Animation.duration / Double(itemsCount) diff --git a/Frameworks/NynjaUIKit/NynjaUIKit/Views/Typing/TypingView.swift b/Frameworks/NynjaUIKit/NynjaUIKit/Views/Typing/TypingView.swift index 45009aa61..9258ab028 100644 --- a/Frameworks/NynjaUIKit/NynjaUIKit/Views/Typing/TypingView.swift +++ b/Frameworks/NynjaUIKit/NynjaUIKit/Views/Typing/TypingView.swift @@ -95,6 +95,9 @@ public final class TypingView: BaseView { private func setupDotsIndicator(color: UIColor) { 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) @@ -126,6 +129,8 @@ public final class TypingView: BaseView { enum indicator { static let circleSize: CGFloat = 8 + static let dotsSize: CGFloat = 3 + static let dotsPadding: CGFloat = 4 static let padding: CGFloat = 4 } -- GitLab From 321ff20518eb9d0d99bb423416c76982f7398e43 Mon Sep 17 00:00:00 2001 From: Anton Poltoratskyi Date: Mon, 29 Oct 2018 11:41:42 +0200 Subject: [PATCH 19/64] [NY-4699] Fixed offset in typing view. --- .../NynjaUIKit/Views/Typing/TypingView.swift | 20 +++++++++++-------- .../Cell/ChatListMessageContentView.swift | 2 +- .../View/Views/AvatarView/AvatarView.swift | 2 +- 3 files changed, 14 insertions(+), 10 deletions(-) diff --git a/Frameworks/NynjaUIKit/NynjaUIKit/Views/Typing/TypingView.swift b/Frameworks/NynjaUIKit/NynjaUIKit/Views/Typing/TypingView.swift index 9258ab028..5ffd98f8d 100644 --- a/Frameworks/NynjaUIKit/NynjaUIKit/Views/Typing/TypingView.swift +++ b/Frameworks/NynjaUIKit/NynjaUIKit/Views/Typing/TypingView.swift @@ -103,10 +103,7 @@ public final class TypingView: BaseView { indicatorContainer.addSubview(indicatorView) - indicatorView.snp.makeConstraints { maker in - maker.centerY.equalToSuperview() - maker.left.right.equalToSuperview() - } + indicatorView.snp.makeConstraints(makeIndicatorViewConstraints()) } private func setupCircleIndicator(color: UIColor) { @@ -115,10 +112,15 @@ public final class TypingView: BaseView { indicatorView.circleColor = color indicatorView.circleSize = Constraints.indicator.circleSize.adjustedByWidth - indicatorView.horizontalInset = Constraints.indicator.padding.adjustedByWidth + indicatorView.horizontalInset = Constraints.indicator.horizontalInset.adjustedByWidth - indicatorView.snp.makeConstraints { maker in - maker.edges.equalToSuperview() + indicatorView.snp.makeConstraints(makeIndicatorViewConstraints()) + } + + private func makeIndicatorViewConstraints() -> (ConstraintMaker) -> Void { + return { maker in + maker.centerY.equalToSuperview().offset(Constraints.indicator.centerVerticalOffset.adjustedByWidth) + maker.left.right.equalToSuperview() } } @@ -131,7 +133,9 @@ public final class TypingView: BaseView { static let circleSize: CGFloat = 8 static let dotsSize: CGFloat = 3 static let dotsPadding: CGFloat = 4 - static let padding: CGFloat = 4 + + static let horizontalInset: CGFloat = 4 + static let centerVerticalOffset: CGFloat = 1 } enum senderInfo { 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 4ad6ce2d7..71c5eda1e 100644 --- a/Nynja/Library/UI/Lists/TableView/Cells/ChatListMessageCell/Cell/ChatListMessageContentView.swift +++ b/Nynja/Library/UI/Lists/TableView/Cells/ChatListMessageCell/Cell/ChatListMessageContentView.swift @@ -79,7 +79,7 @@ final class ChatListMessageContentView: BaseView { typingView.isHidden = false textView.isHidden = true - let appearance = TypingView.Appearance(indicator: .dots(UIColor.nynja.white), + let appearance = TypingView.Appearance(indicator: .circle(UIColor.nynja.white), textColor: titleLabel.textColor, textFont: typingFont, senderInfo: sender, diff --git a/Nynja/Modules/Message/View/Views/AvatarView/AvatarView.swift b/Nynja/Modules/Message/View/Views/AvatarView/AvatarView.swift index f78e6cb7a..2d9495146 100644 --- a/Nynja/Modules/Message/View/Views/AvatarView/AvatarView.swift +++ b/Nynja/Modules/Message/View/Views/AvatarView/AvatarView.swift @@ -24,7 +24,7 @@ final class AvatarView: BaseView { let appearance = TypingView.Appearance(indicator: .dots(UIColor.white), textColor: titleLabel.textColor, textFont: statusLabel.font, - senderInfo: "Typing", + senderInfo: "typing", typingInfo: "", isTypingInfoPinned: false ) -- GitLab From 617a97977337098be957d535629a414584022aa8 Mon Sep 17 00:00:00 2001 From: Anton Poltoratskyi Date: Mon, 29 Oct 2018 13:19:09 +0200 Subject: [PATCH 20/64] [NY-4699] Fixed avatar view. --- Nynja/Modules/Message/View/MessageVC.swift | 8 +-- .../Message/View/MessageVCLayout.swift | 5 -- .../View/Views/AvatarView/AvatarView.swift | 62 +++++++++++-------- .../Views/AvatarView/AvatarViewLayout.swift | 7 ++- 4 files changed, 43 insertions(+), 39 deletions(-) diff --git a/Nynja/Modules/Message/View/MessageVC.swift b/Nynja/Modules/Message/View/MessageVC.swift index 515bdd20e..fb54b6343 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 }() diff --git a/Nynja/Modules/Message/View/MessageVCLayout.swift b/Nynja/Modules/Message/View/MessageVCLayout.swift index 0f7309484..008ace7bc 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 2d9495146..bf039f735 100644 --- a/Nynja/Modules/Message/View/Views/AvatarView/AvatarView.swift +++ b/Nynja/Modules/Message/View/Views/AvatarView/AvatarView.swift @@ -29,7 +29,6 @@ final class AvatarView: BaseView { isTypingInfoPinned: false ) typingView.update(appearance) - } else { statusLabel.isHidden = false typingView.isHidden = true @@ -47,21 +46,27 @@ final class AvatarView: BaseView { // 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 titleContainerView: UIView = { @@ -69,10 +74,10 @@ final class AvatarView: BaseView { 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) } @@ -81,16 +86,17 @@ final 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" - titleContainerView.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 = { @@ -113,17 +119,18 @@ final 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" - titleContainerView.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 typingView: TypingView = { @@ -155,7 +162,8 @@ final class AvatarView: BaseView { func setup(with viewModel: AvatarViewModel) { titleLabel.text = viewModel.title titleLabel.accessibilityValue = viewModel.title - imageView.setImage(url: viewModel.avatarUrl, placeHolder: UIImage.nynja.Contacts.avaPlaceholder.image) + avatarView.imageView.setImage(url: viewModel.avatarUrl, + placeHolder: UIImage.nynja.Contacts.avaPlaceholder.image) setupMuteImageView(viewModel.isMuted) } diff --git a/Nynja/Modules/Message/View/Views/AvatarView/AvatarViewLayout.swift b/Nynja/Modules/Message/View/Views/AvatarView/AvatarViewLayout.swift index bdac3dfd9..7a5ca994f 100644 --- a/Nynja/Modules/Message/View/Views/AvatarView/AvatarViewLayout.swift +++ b/Nynja/Modules/Message/View/Views/AvatarView/AvatarViewLayout.swift @@ -11,9 +11,12 @@ extension AvatarView { enum Constraints { enum imageView { - static let width: CGFloat = 32.0 - + 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 } enum titleContainerView { -- GitLab From 0d9a82c436a2389669f272cedf530096f4ebfb8f Mon Sep 17 00:00:00 2001 From: Anton Poltoratskyi Date: Mon, 29 Oct 2018 13:20:52 +0200 Subject: [PATCH 21/64] [NY-4699] No status mask for avatar by default. --- .../NynjaUIKit/NynjaUIKit/Views/Avatar/AvatarStatusView.swift | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Frameworks/NynjaUIKit/NynjaUIKit/Views/Avatar/AvatarStatusView.swift b/Frameworks/NynjaUIKit/NynjaUIKit/Views/Avatar/AvatarStatusView.swift index b2bec5db6..3706b9a21 100644 --- a/Frameworks/NynjaUIKit/NynjaUIKit/Views/Avatar/AvatarStatusView.swift +++ b/Frameworks/NynjaUIKit/NynjaUIKit/Views/Avatar/AvatarStatusView.swift @@ -58,6 +58,8 @@ public final class AvatarStatusView: BaseView { statusView.layer.masksToBounds = true addSubview(statusView) + + update(.none) } -- GitLab From 20d15ae4c5c78419e91d8b2ca8262f4e0fa58b96 Mon Sep 17 00:00:00 2001 From: Anton Poltoratskyi Date: Mon, 29 Oct 2018 14:37:55 +0200 Subject: [PATCH 22/64] [NY-4699] Use ActionStatus instead of TypingActionStatus. --- Nynja.xcodeproj/project.pbxproj | 4 ---- Nynja/Statuses/TypingActionStatus.swift | 23 ----------------------- Nynja/Statuses/TypingStatusProvider.swift | 18 +++++++++--------- 3 files changed, 9 insertions(+), 36 deletions(-) delete mode 100644 Nynja/Statuses/TypingActionStatus.swift diff --git a/Nynja.xcodeproj/project.pbxproj b/Nynja.xcodeproj/project.pbxproj index 0f640fc47..6dca8052b 100644 --- a/Nynja.xcodeproj/project.pbxproj +++ b/Nynja.xcodeproj/project.pbxproj @@ -1110,7 +1110,6 @@ 85EB37FB21837235003A2D6F /* ObservableContainer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 85EB37FA21837235003A2D6F /* ObservableContainer.swift */; }; 85EB37FD21837253003A2D6F /* Observable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 85EB37FC21837253003A2D6F /* Observable.swift */; }; 85EB37FF21837304003A2D6F /* AccountStatus.swift in Sources */ = {isa = PBXBuildFile; fileRef = 85EB37FE21837304003A2D6F /* AccountStatus.swift */; }; - 85EB3801218377E5003A2D6F /* TypingActionStatus.swift in Sources */ = {isa = PBXBuildFile; fileRef = 85EB3800218377E5003A2D6F /* TypingActionStatus.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 */; }; @@ -3309,7 +3308,6 @@ 85EB37FA21837235003A2D6F /* ObservableContainer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ObservableContainer.swift; sourceTree = ""; }; 85EB37FC21837253003A2D6F /* Observable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Observable.swift; sourceTree = ""; }; 85EB37FE21837304003A2D6F /* AccountStatus.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountStatus.swift; sourceTree = ""; }; - 85EB3800218377E5003A2D6F /* TypingActionStatus.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TypingActionStatus.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 = ""; }; @@ -9093,7 +9091,6 @@ isa = PBXGroup; children = ( 85EB37FE21837304003A2D6F /* AccountStatus.swift */, - 85EB3800218377E5003A2D6F /* TypingActionStatus.swift */, 85EB37F72183659C003A2D6F /* AccountStatusProvider.swift */, 854834172186FADB002064E1 /* TypingStatusProvider.swift */, 85EB37FA21837235003A2D6F /* ObservableContainer.swift */, @@ -15063,7 +15060,6 @@ 6D485DDF1F0ACA4700E12FB1 /* UIImageView+Rounded.swift in Sources */, 26CD3FDB2104D19D00597E62 /* AudioShortTranscribeOperation.swift in Sources */, 40C2631343E285717633ADFA /* LoginPresenter.swift in Sources */, - 85EB3801218377E5003A2D6F /* TypingActionStatus.swift in Sources */, A42D51A7206A361400EEB952 /* log.swift in Sources */, DAE89B7EFAB308A6B48AF5EC /* LoginInteractor.swift in Sources */, C940514A204C7FAF00D72B04 /* DataAndStorageViewController.swift in Sources */, diff --git a/Nynja/Statuses/TypingActionStatus.swift b/Nynja/Statuses/TypingActionStatus.swift deleted file mode 100644 index 7654b102f..000000000 --- a/Nynja/Statuses/TypingActionStatus.swift +++ /dev/null @@ -1,23 +0,0 @@ -// -// TypingActionStatus.swift -// Nynja -// -// Created by Anton Poltoratskyi on 26.10.2018. -// Copyright © 2018 TecSynt Solutions. All rights reserved. -// - -enum TypingActionStatus { - typealias Sender = String - - enum Record { - case voice, video - } - enum Mime { - case file - case image - } - case typing([Sender]) - case sending([Sender], Mime) - case recording([Sender], Record) - case none -} diff --git a/Nynja/Statuses/TypingStatusProvider.swift b/Nynja/Statuses/TypingStatusProvider.swift index eaf60ecf3..58b2ed044 100644 --- a/Nynja/Statuses/TypingStatusProvider.swift +++ b/Nynja/Statuses/TypingStatusProvider.swift @@ -9,31 +9,31 @@ import Foundation protocol TypingStatusObservable: class { - typealias Callback = (AccountId, TypingActionStatus) -> Void + typealias Callback = (AccountId, ActionStatus) -> Void func addObserver(_ observer: AnyObject, callback: @escaping Callback) func addObserver(_ observer: AnyObject, for key: AccountId, callback: @escaping Callback) func removeObserver(_ observer: AnyObject) func removeObserver(_ observer: AnyObject, for key: AccountId) - func notify(_ key: AccountId, with value: TypingActionStatus) + func notify(_ key: AccountId, with value: ActionStatus) } protocol TypingStatusProvider: TypingStatusObservable { - func status(for accountId: AccountId) -> TypingActionStatus - func update(_ status: TypingActionStatus, for accountId: AccountId) + func status(for accountId: AccountId) -> ActionStatus + func update(_ status: ActionStatus, for accountId: AccountId) } final class TypingStatusProviderImpl: TypingStatusProvider, ObservableContainer { - private var data: [AccountId: TypingActionStatus] = [:] + private var data: [AccountId: ActionStatus] = [:] - private(set) var observable = Observable() + private(set) var observable = Observable() - func status(for accountId: AccountId) -> TypingActionStatus { - return data[accountId] ?? .none + func status(for accountId: AccountId) -> ActionStatus { + return data[accountId] ?? .done } - func update(_ status: TypingActionStatus, for accountId: AccountId) { + func update(_ status: ActionStatus, for accountId: AccountId) { data[accountId] = status observable.notify(accountId, with: status) } -- GitLab From 0249bf38382122019b9d47855b480c873af816c5 Mon Sep 17 00:00:00 2001 From: Anton Poltoratskyi Date: Mon, 29 Oct 2018 14:44:33 +0200 Subject: [PATCH 23/64] [NY-4699] Make Observable thread-safe --- Nynja/Statuses/Observable.swift | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/Nynja/Statuses/Observable.swift b/Nynja/Statuses/Observable.swift index 5857ca9f6..96da0c491 100644 --- a/Nynja/Statuses/Observable.swift +++ b/Nynja/Statuses/Observable.swift @@ -16,6 +16,8 @@ final class Observable { var callback: (Key, Value) -> Void } + private let lock = NSLock() + private var allObservers: Observers = [] private var observers: [Key: Observers] = [:] @@ -23,30 +25,45 @@ final class Observable { 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() } } -- GitLab From c599ead463c4fe474a42097864139e907ded2390 Mon Sep 17 00:00:00 2001 From: Anton Poltoratskyi Date: Mon, 29 Oct 2018 15:19:29 +0200 Subject: [PATCH 24/64] [NY-4699] Make handlers not static. --- .../Handlers/Base/HandlerFactory.swift | 15 ++++--- .../Services/Handlers/ContactHandler.swift | 10 +++-- .../Services/Handlers/MessageHandler.swift | 11 +++-- .../Services/Handlers/ProfileHandler.swift | 12 ++++-- .../ForwardSelectorInteractor.swift | 8 ++-- Nynja/AuthHandler.swift | 12 ++++-- Nynja/ChatService/SenderService.swift | 4 +- Nynja/ExtendedStarHandler.swift | 12 +++++- Nynja/HandlerFactory.swift | 34 +++++++-------- Nynja/JobHandler.swift | 14 +++++-- Nynja/LinkHandler.swift | 19 ++++++--- Nynja/MemberHandler.swift | 15 +++++-- .../Interactor/AddContactInteractor.swift | 2 +- .../AddContactByUsernameInteractor.swift | 2 +- .../AddContactViaPhoneInteractor.swift | 2 +- .../Login/Interactor/LoginInteractor.swift | 2 +- .../Interactor/VerifyNumberInteractor.swift | 2 +- .../Interactor/NewChannelInteractor.swift | 2 +- .../Interactor/ContactsInteractor.swift | 2 +- .../Interactor/EditUsernameInteractor.swift | 2 +- .../Interactor/InviteFriendsInteractor.swift | 2 +- .../Interactor/MessageInteractor.swift | 10 ++--- .../Interactor/ParticipantsInteractor.swift | 2 +- .../Interactor/QRCodeReaderInteractor.swift | 2 +- .../ScheduleMessageInteractor.swift | 2 +- .../Interactor/SecurityInteractor.swift | 4 +- .../HandleServices/ContactHandler.swift | 25 +++++++---- .../HandleServices/HistoryHandler.swift | 37 +++++++++------- .../HandleServices/MessageHandler.swift | 41 ++++++++++-------- .../HandleServices/ProfileHandler.swift | 34 ++++++++------- .../Services/HandleServices/RoomHandler.swift | 42 +++++++++++-------- .../HandleServices/RosterHandler.swift | 14 +++++-- .../HandleServices/SearchHandler.swift | 13 +++++- .../Services/HandleServices/StarHandler.swift | 13 +++++- .../HandleServices/TypingHandler.swift | 16 +++++-- Nynja/Services/MQTT/MQTTService.swift | 2 +- .../Services/Handlers/Base/BaseHandler.swift | 18 ++++---- Shared/Services/Handlers/ErrorsHandler.swift | 7 +++- Shared/Services/Handlers/IoHandler.swift | 16 ++++--- 39 files changed, 301 insertions(+), 181 deletions(-) diff --git a/Nynja-Share/Services/Handlers/Base/HandlerFactory.swift b/Nynja-Share/Services/Handlers/Base/HandlerFactory.swift index 200518760..f4a7d5195 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 a2041fabf..bdf7650af 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 b0daa836e..3c2b31d72 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 200095636..fbda22977 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 384c34370..a3181e534 100644 --- a/Nynja-Share/UI/ForwardSelector/Interactor/ForwardSelectorInteractor.swift +++ b/Nynja-Share/UI/ForwardSelector/Interactor/ForwardSelectorInteractor.swift @@ -66,10 +66,10 @@ final class ForwardSelectorInteractor: ForwardSelectorInteractorInputProtocol, P dependencies: .init(mqttService: MQTTService.sharedInstance, storageService: StorageService.sharedInstance) ) - 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 setupAmazon() notifyHostApplication() } diff --git a/Nynja/AuthHandler.swift b/Nynja/AuthHandler.swift index c5ec8f5f2..ca8b88591 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 c82826118..7c1d01954 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/ExtendedStarHandler.swift b/Nynja/ExtendedStarHandler.swift index 0322062f2..f0dc2beb4 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/HandlerFactory.swift b/Nynja/HandlerFactory.swift index c94fbcbee..4f855ddb8 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 cbf72bb79..c9b40eaf4 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/LinkHandler.swift b/Nynja/LinkHandler.swift index feebe3ae1..8ca0a5dbc 100644 --- a/Nynja/LinkHandler.swift +++ b/Nynja/LinkHandler.swift @@ -11,12 +11,20 @@ protocol LinkHandlerDelegate: class { func linkIsAvailable(_ link: Link) } - final class LinkHandler: BaseHandler { - static weak var delegate: LinkHandlerDelegate? + // MARK: - Singleton + + static let shared = LinkHandler() + + private init() {} + - static func executeHandle(data: BertTuple, codes: StatusCodes) { + // MARK: - Handler + + weak var delegate: LinkHandlerDelegate? + + func executeHandle(data: BertTuple, codes: StatusCodes) { guard let link = get_Link().parse(bert: data) as? Link else { return } @@ -28,7 +36,7 @@ final class LinkHandler: BaseHandler { } } - private static func handle(link: Link) { + private func handle(link: Link) { guard let status = link.originalStatus else { return } @@ -44,12 +52,11 @@ final class LinkHandler: BaseHandler { } } - private static func handle(codes: StatusCodes, link: Link) { + private func handle(codes: StatusCodes, link: Link) { let statusCodeManager = StatusCodeManager.shared codes .filter { StatusCode.linkCodes.contains($0) } .forEach { statusCodeManager.notify(model: link, code: $0) } } - } diff --git a/Nynja/MemberHandler.swift b/Nynja/MemberHandler.swift index 361d24a8b..f81c1b922 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 170fa07f9..f21673f79 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 21d47a38e..2639da682 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 b3e89801c..4a9c81ff8 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 c0e1a555b..3c107f5cd 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 c525eddce..a3d25180d 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 943525191..c00bf609c 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/Contacts/Interactor/ContactsInteractor.swift b/Nynja/Modules/Contacts/Interactor/ContactsInteractor.swift index 5b0a313da..f6839b618 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 cb9d7bd25..f7319c2cc 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/InviteFriends/Interactor/InviteFriendsInteractor.swift b/Nynja/Modules/InviteFriends/Interactor/InviteFriendsInteractor.swift index c2e141fa1..b7d58124b 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 dcd8bbb7e..7f65539b7 100644 --- a/Nynja/Modules/Message/Interactor/MessageInteractor.swift +++ b/Nynja/Modules/Message/Interactor/MessageInteractor.swift @@ -185,8 +185,8 @@ final class MessageInteractor: BaseInteractor, MessageInteractorInputProtocol, H mqttService.addSubscriber(self) ConnectionService.shared.addSubscriber(self) - MessageHandler.addSubscriber(self) - HistoryHandler.addSubscriber(self) + MessageHandler.shared.addSubscriber(self) + HistoryHandler.shared.addSubscriber(self) NynjaCommunicatorService.sharedInstance.messageInteractorCallProtocol = self subscribeToTranscribeProcessing() @@ -196,8 +196,8 @@ final class MessageInteractor: BaseInteractor, MessageInteractorInputProtocol, H deinit { callService.messageInteractorCallProtocol = nil mqttService.removeSubscriber(self) - MessageHandler.removeSubscriber(self) - HistoryHandler.removeSubscriber(self) + MessageHandler.shared.removeSubscriber(self) + HistoryHandler.shared.removeSubscriber(self) ConnectionService.shared.removeSubscriber(self) unsubscribeFromTranscribeProcessing() } @@ -222,7 +222,7 @@ final class MessageInteractor: BaseInteractor, MessageInteractorInputProtocol, H processingManager.delegate = self - TypingHandler.delegate = self + TypingHandler.shared.delegate = self isAfterConnectionAppeared = false diff --git a/Nynja/Modules/Participants/Interactor/ParticipantsInteractor.swift b/Nynja/Modules/Participants/Interactor/ParticipantsInteractor.swift index babd3b0c4..0097dde47 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/QRCodeReader/Interactor/QRCodeReaderInteractor.swift b/Nynja/Modules/QRCodeReader/Interactor/QRCodeReaderInteractor.swift index 60cf171cd..b5fb466d4 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 dec068752..788fc8f48 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 e497dfc85..f7c596eec 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/Services/HandleServices/ContactHandler.swift b/Nynja/Services/HandleServices/ContactHandler.swift index 8a6aa1cfe..74b1f09b4 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 4b36b4288..f46014cf4 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, @@ -207,7 +214,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 { @@ -225,14 +232,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"] @@ -247,7 +254,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 e9bd99580..fddb6043c 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) } @@ -182,7 +189,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 b1b89d482..af9514889 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 { @@ -130,11 +136,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 } @@ -142,7 +148,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) @@ -151,7 +157,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) @@ -163,7 +169,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 6193bb931..a1ca2f862 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 419d89e79..bab717eca 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 9d8570b15..be743eb61 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 416651962..c555a00d5 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 e9023cba8..e14773beb 100644 --- a/Nynja/Services/HandleServices/TypingHandler.swift +++ b/Nynja/Services/HandleServices/TypingHandler.swift @@ -12,14 +12,22 @@ protocol TypingHandlerDelegate: class { func getTyping(typing: Typing) } -class TypingHandler: BaseHandler { +final class TypingHandler: BaseHandler { - static weak var delegate: TypingHandlerDelegate? + // MARK: - Singleton - static func executeHandle(data: BertTuple) { + static let shared = TypingHandler() + + private init() {} + + + // MARK: - Handler + + weak var delegate: TypingHandlerDelegate? + + func executeHandle(data: BertTuple) { if let typing = get_Typing().parse(bert: data) as? Typing { delegate?.getTyping(typing: typing) } } - } diff --git a/Nynja/Services/MQTT/MQTTService.swift b/Nynja/Services/MQTT/MQTTService.swift index 902a3814a..60445c01d 100644 --- a/Nynja/Services/MQTT/MQTTService.swift +++ b/Nynja/Services/MQTT/MQTTService.swift @@ -168,7 +168,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/Shared/Services/Handlers/Base/BaseHandler.swift b/Shared/Services/Handlers/Base/BaseHandler.swift index cdf0cebee..3021b29cd 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 d284a35cb..d85e43db2 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 f6fd64282..478cce95e 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 { -- GitLab From 9787372cd62cffd36e750f73e17399afe66dc1c4 Mon Sep 17 00:00:00 2001 From: Anton Poltoratskyi Date: Mon, 29 Oct 2018 15:36:55 +0200 Subject: [PATCH 25/64] [NY-4699] Rename Observable to KeyedObservable --- Nynja.xcodeproj/project.pbxproj | 24 ++++--- Nynja/Observable/KeyedObservable.swift | 69 +++++++++++++++++++ .../KeyedObservableContainer.swift} | 8 +-- Nynja/Statuses/AccountStatusProvider.swift | 4 +- Nynja/Statuses/Observable.swift | 4 +- Nynja/Statuses/TypingStatusProvider.swift | 4 +- 6 files changed, 95 insertions(+), 18 deletions(-) create mode 100644 Nynja/Observable/KeyedObservable.swift rename Nynja/{Statuses/ObservableContainer.swift => Observable/KeyedObservableContainer.swift} (87%) diff --git a/Nynja.xcodeproj/project.pbxproj b/Nynja.xcodeproj/project.pbxproj index 6dca8052b..5bc4514ed 100644 --- a/Nynja.xcodeproj/project.pbxproj +++ b/Nynja.xcodeproj/project.pbxproj @@ -1107,8 +1107,8 @@ 85E3AB3D21218A57005FC49A /* SeparatorView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8580BAE620BD9A5600239D9D /* SeparatorView.swift */; }; 85EB37F321831094003A2D6F /* ChatListMessageTextView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 85EB37F221831094003A2D6F /* ChatListMessageTextView.swift */; }; 85EB37F82183659C003A2D6F /* AccountStatusProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 85EB37F72183659C003A2D6F /* AccountStatusProvider.swift */; }; - 85EB37FB21837235003A2D6F /* ObservableContainer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 85EB37FA21837235003A2D6F /* ObservableContainer.swift */; }; - 85EB37FD21837253003A2D6F /* Observable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 85EB37FC21837253003A2D6F /* Observable.swift */; }; + 85EB37FB21837235003A2D6F /* KeyedObservableContainer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 85EB37FA21837235003A2D6F /* KeyedObservableContainer.swift */; }; + 85EB37FD21837253003A2D6F /* KeyedObservable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 85EB37FC21837253003A2D6F /* KeyedObservable.swift */; }; 85EB37FF21837304003A2D6F /* AccountStatus.swift in Sources */ = {isa = PBXBuildFile; fileRef = 85EB37FE21837304003A2D6F /* AccountStatus.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 */; }; @@ -3305,8 +3305,8 @@ 85E1DD2620BEE961008AD211 /* ScalableCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ScalableCell.swift; sourceTree = ""; }; 85EB37F221831094003A2D6F /* ChatListMessageTextView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatListMessageTextView.swift; sourceTree = ""; }; 85EB37F72183659C003A2D6F /* AccountStatusProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountStatusProvider.swift; sourceTree = ""; }; - 85EB37FA21837235003A2D6F /* ObservableContainer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ObservableContainer.swift; sourceTree = ""; }; - 85EB37FC21837253003A2D6F /* Observable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Observable.swift; sourceTree = ""; }; + 85EB37FA21837235003A2D6F /* KeyedObservableContainer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeyedObservableContainer.swift; sourceTree = ""; }; + 85EB37FC21837253003A2D6F /* KeyedObservable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeyedObservable.swift; sourceTree = ""; }; 85EB37FE21837304003A2D6F /* AccountStatus.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountStatus.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 = ""; }; @@ -5983,6 +5983,7 @@ 8509AC61206A54420089089B /* ResponseResult.swift */, B7F4C2AA211995A500E48A98 /* Validation */, A42C44E220F340DA00BC3CBB /* StatusCodeManager.swift */, + 8548341921874434002064E1 /* Observable */, 85EB37F9218365A6003A2D6F /* Statuses */, ); name = Services; @@ -8375,6 +8376,15 @@ path = Documents; sourceTree = ""; }; + 8548341921874434002064E1 /* Observable */ = { + isa = PBXGroup; + children = ( + 85EB37FA21837235003A2D6F /* KeyedObservableContainer.swift */, + 85EB37FC21837253003A2D6F /* KeyedObservable.swift */, + ); + path = Observable; + sourceTree = ""; + }; 854A4B392080E5D500759152 /* TableView */ = { isa = PBXGroup; children = ( @@ -9093,8 +9103,6 @@ 85EB37FE21837304003A2D6F /* AccountStatus.swift */, 85EB37F72183659C003A2D6F /* AccountStatusProvider.swift */, 854834172186FADB002064E1 /* TypingStatusProvider.swift */, - 85EB37FA21837235003A2D6F /* ObservableContainer.swift */, - 85EB37FC21837253003A2D6F /* Observable.swift */, ); path = Statuses; sourceTree = ""; @@ -15170,7 +15178,7 @@ 85D66A0420BD963C00FBD803 /* MessagePayloadBuilder.swift in Sources */, 004581212036073100F8E413 /* JobMessageTable.swift in Sources */, 85EB37F321831094003A2D6F /* ChatListMessageTextView.swift in Sources */, - 85EB37FD21837253003A2D6F /* Observable.swift in Sources */, + 85EB37FD21837253003A2D6F /* KeyedObservable.swift in Sources */, 8572C3B62092315B00E4840C /* CollectionViewDataProxy.swift in Sources */, A45F110520B4218D00F45004 /* DisplayChatConfiguration.swift in Sources */, E7598F681FA1D8B90082FBE7 /* ProfileScheduledMesssageCell.swift in Sources */, @@ -15334,7 +15342,7 @@ F117871020ACF018007A9A1B /* CameraQualitySettingsProtocols.swift in Sources */, A44B4D5920CE9BDF00CA700A /* ImageCellViewModel.swift in Sources */, A415132020DBD58900C2C01F /* Link.swift in Sources */, - 85EB37FB21837235003A2D6F /* ObservableContainer.swift in Sources */, + 85EB37FB21837235003A2D6F /* KeyedObservableContainer.swift in Sources */, 852DF263203720E600A4F8B6 /* FileIcons.swift in Sources */, A43B25DB20AB1EE400FF8107 /* NewChannelInteractor.swift in Sources */, FBCE840F20E525A6003B7558 /* HTTPParameters.swift in Sources */, diff --git a/Nynja/Observable/KeyedObservable.swift b/Nynja/Observable/KeyedObservable.swift new file mode 100644 index 000000000..00924123e --- /dev/null +++ b/Nynja/Observable/KeyedObservable.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/ObservableContainer.swift b/Nynja/Observable/KeyedObservableContainer.swift similarity index 87% rename from Nynja/Statuses/ObservableContainer.swift rename to Nynja/Observable/KeyedObservableContainer.swift index 9b89e430f..ce8dfedc0 100644 --- a/Nynja/Statuses/ObservableContainer.swift +++ b/Nynja/Observable/KeyedObservableContainer.swift @@ -1,17 +1,17 @@ // -// ObservableContainer.swift +// KeyedObservableContainer.swift // Nynja // // Created by Anton Poltoratskyi on 26.10.2018. // Copyright © 2018 TecSynt Solutions. All rights reserved. // -protocol ObservableContainer: class { +protocol KeyedObservableContainer: class { associatedtype Key: Hashable associatedtype Value typealias Callback = (Key, Value) -> Void - var observable: Observable { get } + var observable: KeyedObservable { get } func addObserver(_ observer: AnyObject, callback: @escaping Callback) func addObserver(_ observer: AnyObject, for key: Key, callback: @escaping Callback) @@ -20,7 +20,7 @@ protocol ObservableContainer: class { func notify(_ key: Key, with value: Value) } -extension ObservableContainer { +extension KeyedObservableContainer { func addObserver(_ observer: AnyObject, callback: @escaping Callback) { observable.addObserver(observer, callback: callback) diff --git a/Nynja/Statuses/AccountStatusProvider.swift b/Nynja/Statuses/AccountStatusProvider.swift index d8996ab5f..65d15f633 100644 --- a/Nynja/Statuses/AccountStatusProvider.swift +++ b/Nynja/Statuses/AccountStatusProvider.swift @@ -23,11 +23,11 @@ protocol AccountStatusProvider: AccountStatusObservable { func update(_ status: AccountStatus, for accountId: AccountId) } -final class AccountStatusProviderImpl: AccountStatusProvider, ObservableContainer { +final class AccountStatusProviderImpl: AccountStatusProvider, KeyedObservableContainer { private var data: [AccountId: AccountStatus] = [:] - private(set) var observable = Observable() + private(set) var observable = KeyedObservable() func status(for accountId: AccountId) -> AccountStatus { return data[accountId] ?? .none diff --git a/Nynja/Statuses/Observable.swift b/Nynja/Statuses/Observable.swift index 96da0c491..00924123e 100644 --- a/Nynja/Statuses/Observable.swift +++ b/Nynja/Statuses/Observable.swift @@ -1,5 +1,5 @@ // -// Observable.swift +// KeyedObservable.swift // Nynja // // Created by Anton Poltoratskyi on 26.10.2018. @@ -8,7 +8,7 @@ import Foundation -final class Observable { +final class KeyedObservable { private typealias Observers = [AnyWeakSubscriber] diff --git a/Nynja/Statuses/TypingStatusProvider.swift b/Nynja/Statuses/TypingStatusProvider.swift index 58b2ed044..18145e837 100644 --- a/Nynja/Statuses/TypingStatusProvider.swift +++ b/Nynja/Statuses/TypingStatusProvider.swift @@ -23,11 +23,11 @@ protocol TypingStatusProvider: TypingStatusObservable { func update(_ status: ActionStatus, for accountId: AccountId) } -final class TypingStatusProviderImpl: TypingStatusProvider, ObservableContainer { +final class TypingStatusProviderImpl: TypingStatusProvider, KeyedObservableContainer { private var data: [AccountId: ActionStatus] = [:] - private(set) var observable = Observable() + private(set) var observable = KeyedObservable() func status(for accountId: AccountId) -> ActionStatus { return data[accountId] ?? .done -- GitLab From c767dc59df561030b3cae9b43ce48fa339c0414a Mon Sep 17 00:00:00 2001 From: Anton Poltoratskyi Date: Mon, 29 Oct 2018 16:13:58 +0200 Subject: [PATCH 26/64] [NY-4699] Subscribe to TypingHandler as observer. --- Nynja.xcodeproj/project.pbxproj | 10 ++++- .../Interactor/MessageInteractor.swift | 9 ++-- Nynja/Observable/Observable.swift | 41 +++++++++++++++++++ Nynja/Observable/ObservableContainer.swift | 31 ++++++++++++++ .../HandleServices/TypingHandler.swift | 16 +++++--- 5 files changed, 95 insertions(+), 12 deletions(-) create mode 100644 Nynja/Observable/Observable.swift create mode 100644 Nynja/Observable/ObservableContainer.swift diff --git a/Nynja.xcodeproj/project.pbxproj b/Nynja.xcodeproj/project.pbxproj index 5bc4514ed..6ec362192 100644 --- a/Nynja.xcodeproj/project.pbxproj +++ b/Nynja.xcodeproj/project.pbxproj @@ -950,6 +950,8 @@ 8548284F204EDD5900DCBEC8 /* FastScrollable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8548284E204EDD5900DCBEC8 /* FastScrollable.swift */; }; 8548340E207769E800604051 /* DocumentInteractionInput.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8548340D207769E800604051 /* DocumentInteractionInput.swift */; }; 854834182186FADB002064E1 /* TypingStatusProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 854834172186FADB002064E1 /* TypingStatusProvider.swift */; }; + 8548341B2187449F002064E1 /* Observable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8548341A2187449F002064E1 /* Observable.swift */; }; + 8548341D218744AC002064E1 /* ObservableContainer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8548341C218744AC002064E1 /* ObservableContainer.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 */; }; @@ -3159,6 +3161,8 @@ 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 /* TypingStatusProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TypingStatusProvider.swift; sourceTree = ""; }; + 8548341A2187449F002064E1 /* Observable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Observable.swift; sourceTree = ""; }; + 8548341C218744AC002064E1 /* ObservableContainer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ObservableContainer.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 = ""; }; @@ -8379,8 +8383,10 @@ 8548341921874434002064E1 /* Observable */ = { isa = PBXGroup; children = ( - 85EB37FA21837235003A2D6F /* KeyedObservableContainer.swift */, + 8548341A2187449F002064E1 /* Observable.swift */, + 8548341C218744AC002064E1 /* ObservableContainer.swift */, 85EB37FC21837253003A2D6F /* KeyedObservable.swift */, + 85EB37FA21837235003A2D6F /* KeyedObservableContainer.swift */, ); path = Observable; sourceTree = ""; @@ -15565,6 +15571,7 @@ 850571222050B0AD00EDF794 /* NotificationAlertSoundsViewController.swift in Sources */, 6D6731101F29E1F4003E8F8F /* BottomCallView.swift in Sources */, 26245F40204EF58E00C8D3DD /* BaseViewProtocol.swift in Sources */, + 8548341D218744AC002064E1 /* ObservableContainer.swift in Sources */, 4B1D7DFE2029C41C00703228 /* AboutItemsFactory.swift in Sources */, A4688DFC20652DE30013660D /* StorageChange.swift in Sources */, 5683555B8382F7F37FEE1AF5 /* ProfileWireframe.swift in Sources */, @@ -15828,6 +15835,7 @@ E70938371FBEDA2B006CCDC6 /* ProfileTable.swift in Sources */, A416DA602075341C00FBF1BA /* CLLocationCoordinate2D+Payload.swift in Sources */, A4679BAE20B2DD100021FE9C /* SubscribersSelectorInteractor.swift in Sources */, + 8548341B2187449F002064E1 /* Observable.swift in Sources */, FEA655FD2167777F00B44029 /* TransferDetailsInteractor.swift in Sources */, E70F78B91FD6C64E00385565 /* ChatCheckpointTable.swift in Sources */, 4B06D30620287060003B275B /* WCDataManagerProtocol.swift in Sources */, diff --git a/Nynja/Modules/Message/Interactor/MessageInteractor.swift b/Nynja/Modules/Message/Interactor/MessageInteractor.swift index 7f65539b7..07e5f9f89 100644 --- a/Nynja/Modules/Message/Interactor/MessageInteractor.swift +++ b/Nynja/Modules/Message/Interactor/MessageInteractor.swift @@ -187,17 +187,18 @@ final class MessageInteractor: BaseInteractor, MessageInteractorInputProtocol, H ConnectionService.shared.addSubscriber(self) MessageHandler.shared.addSubscriber(self) HistoryHandler.shared.addSubscriber(self) + TypingHandler.shared.addObserver(self) NynjaCommunicatorService.sharedInstance.messageInteractorCallProtocol = self subscribeToTranscribeProcessing() } - - + deinit { callService.messageInteractorCallProtocol = nil mqttService.removeSubscriber(self) MessageHandler.shared.removeSubscriber(self) HistoryHandler.shared.removeSubscriber(self) + TypingHandler.shared.removeObserver(self) ConnectionService.shared.removeSubscriber(self) unsubscribeFromTranscribeProcessing() } @@ -222,8 +223,6 @@ final class MessageInteractor: BaseInteractor, MessageInteractorInputProtocol, H processingManager.delegate = self - TypingHandler.shared.delegate = self - isAfterConnectionAppeared = false prepareInitialValues() @@ -934,7 +933,7 @@ final class MessageInteractor: BaseInteractor, MessageInteractorInputProtocol, H } // MARK: - TypingHandlerDelegate - func getTyping(typing: Typing) { + func didReceiveTyping(_ typing: Typing) { guard self.contact?.phone_id == typing.phone_id, let typingModelType = typing.type else { return } diff --git a/Nynja/Observable/Observable.swift b/Nynja/Observable/Observable.swift new file mode 100644 index 000000000..fb149c81c --- /dev/null +++ b/Nynja/Observable/Observable.swift @@ -0,0 +1,41 @@ +// +// Observable.swift +// Nynja +// +// Created by Anton Poltoratskyi on 29.10.2018. +// Copyright © 2018 TecSynt Solutions. All rights reserved. +// + +import Foundation + +final class Observable { + + 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/Observable/ObservableContainer.swift b/Nynja/Observable/ObservableContainer.swift new file mode 100644 index 000000000..d995d3c90 --- /dev/null +++ b/Nynja/Observable/ObservableContainer.swift @@ -0,0 +1,31 @@ +// +// ObservableContainer.swift +// Nynja +// +// Created by Anton Poltoratskyi on 29.10.2018. +// Copyright © 2018 TecSynt Solutions. All rights reserved. +// + +protocol ObservableContainer: class { + associatedtype Observer + var observable: Observable { get } + + func addObserver(_ observer: Observer) + func removeObserver(_ observer: Observer) + func notify(_ block: (Observer) -> Void) +} + +extension ObservableContainer { + + 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/Services/HandleServices/TypingHandler.swift b/Nynja/Services/HandleServices/TypingHandler.swift index e14773beb..7774b0c4e 100644 --- a/Nynja/Services/HandleServices/TypingHandler.swift +++ b/Nynja/Services/HandleServices/TypingHandler.swift @@ -9,10 +9,10 @@ import Foundation protocol TypingHandlerDelegate: class { - func getTyping(typing: Typing) + func didReceiveTyping(_ typing: Typing) } -final class TypingHandler: BaseHandler { +final class TypingHandler: BaseHandler, ObservableContainer { // MARK: - Singleton @@ -21,13 +21,17 @@ final class TypingHandler: BaseHandler { private init() {} - // MARK: - Handler + // MARK: - ObservableContainer + + let observable = Observable() - weak var delegate: TypingHandlerDelegate? + + // MARK: - Handler func executeHandle(data: BertTuple) { - if let typing = get_Typing().parse(bert: data) as? Typing { - delegate?.getTyping(typing: typing) + guard let typing = get_Typing().parse(bert: data) as? Typing else { + return } + notify { $0.didReceiveTyping(typing) } } } -- GitLab From 689db962787f13d1450169471d2c02ba6c49cf8a Mon Sep 17 00:00:00 2001 From: Anton Poltoratskyi Date: Mon, 29 Oct 2018 16:42:57 +0200 Subject: [PATCH 27/64] [NY-4699] Use TypingStatusProvider in MessageInteractor. --- .../Interactor/MessageInteractor.swift | 38 ++++++++++--------- Nynja/Observable/KeyedObservable.swift | 3 ++ Nynja/Statuses/TypingStatusProvider.swift | 37 +++++++++++++++++- 3 files changed, 58 insertions(+), 20 deletions(-) diff --git a/Nynja/Modules/Message/Interactor/MessageInteractor.swift b/Nynja/Modules/Message/Interactor/MessageInteractor.swift index 07e5f9f89..a06a64fa3 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 typingStatusProvider: TypingStatusProvider 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)) + typingStatusProvider = TypingStatusProviderImpl(dependencies: TypingHandler.shared) super.init() @@ -184,10 +187,24 @@ final class MessageInteractor: BaseInteractor, MessageInteractorInputProtocol, H } mqttService.addSubscriber(self) + + if let chatId = chat.id { + typingStatusProvider.addObserver(self, for: chatId) { [weak self] chatId, typingStatus in + guard let `self` = self else { return } + + self.presenter?.actionStatusChanged(typingStatus) + + if case .done = typingStatus { + return + } + dispatchAsyncMainThrotlle(key: "remove_typing_status", seconds: 10.0) { [weak self] in + self?.presenter?.restoreStatus() + } + } + } ConnectionService.shared.addSubscriber(self) MessageHandler.shared.addSubscriber(self) HistoryHandler.shared.addSubscriber(self) - TypingHandler.shared.addObserver(self) NynjaCommunicatorService.sharedInstance.messageInteractorCallProtocol = self subscribeToTranscribeProcessing() @@ -196,9 +213,9 @@ final class MessageInteractor: BaseInteractor, MessageInteractorInputProtocol, H deinit { callService.messageInteractorCallProtocol = nil mqttService.removeSubscriber(self) + typingStatusProvider.removeObserver(self) MessageHandler.shared.removeSubscriber(self) HistoryHandler.shared.removeSubscriber(self) - TypingHandler.shared.removeObserver(self) ConnectionService.shared.removeSubscriber(self) unsubscribeFromTranscribeProcessing() } @@ -931,21 +948,6 @@ final class MessageInteractor: BaseInteractor, MessageInteractorInputProtocol, H self.autoTranslateReceiptMessagesIfNeeded() } } - - // MARK: - TypingHandlerDelegate - func didReceiveTyping(_ 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 { diff --git a/Nynja/Observable/KeyedObservable.swift b/Nynja/Observable/KeyedObservable.swift index 00924123e..6c1da682a 100644 --- a/Nynja/Observable/KeyedObservable.swift +++ b/Nynja/Observable/KeyedObservable.swift @@ -49,6 +49,9 @@ final class KeyedObservable { 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() } diff --git a/Nynja/Statuses/TypingStatusProvider.swift b/Nynja/Statuses/TypingStatusProvider.swift index 18145e837..ecf1c9a46 100644 --- a/Nynja/Statuses/TypingStatusProvider.swift +++ b/Nynja/Statuses/TypingStatusProvider.swift @@ -23,12 +23,34 @@ protocol TypingStatusProvider: TypingStatusObservable { func update(_ status: ActionStatus, for accountId: AccountId) } -final class TypingStatusProviderImpl: TypingStatusProvider, KeyedObservableContainer { - +final class TypingStatusProviderImpl: TypingStatusProvider, KeyedObservableContainer, TypingHandlerDelegate, InitializeInjectable { + private var data: [AccountId: ActionStatus] = [:] private(set) var observable = KeyedObservable() + + // MARK: - Dependencies + + typealias Dependencies = TypingHandler + + private let typingHandler: TypingHandler + + + // MARK: - Init + + init(dependencies: Dependencies) { + typingHandler = dependencies + typingHandler.addObserver(self) + } + + deinit { + typingHandler.removeObserver(self) + } + + + // MARK: - TypingStatusProvider + func status(for accountId: AccountId) -> ActionStatus { return data[accountId] ?? .done } @@ -37,4 +59,15 @@ final class TypingStatusProviderImpl: TypingStatusProvider, KeyedObservableConta data[accountId] = status observable.notify(accountId, with: status) } + + + // MARK: - TypingHandlerDelegate + + func didReceiveTyping(_ typing: Typing) { + guard let accountId = typing.phone_id, let typingType = typing.type else { + return + } + let status = ActionStatus(typingModelType: typingType) + notify(accountId, with: status) + } } -- GitLab From f858f2b16da9afb47fd72785a157a00b9e76b320 Mon Sep 17 00:00:00 2001 From: Anton Poltoratskyi Date: Mon, 29 Oct 2018 18:14:41 +0200 Subject: [PATCH 28/64] [NY-4699] Move logic of timer refresh to separate method in PresenceStatusProvider. --- .../Interactor/MessageInteractor.swift | 1 + .../Interactor/PresenceStatusProvider.swift | 57 ++++++++++--------- 2 files changed, 31 insertions(+), 27 deletions(-) diff --git a/Nynja/Modules/Message/Interactor/MessageInteractor.swift b/Nynja/Modules/Message/Interactor/MessageInteractor.swift index a06a64fa3..5e276b318 100644 --- a/Nynja/Modules/Message/Interactor/MessageInteractor.swift +++ b/Nynja/Modules/Message/Interactor/MessageInteractor.swift @@ -975,6 +975,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 c53f3c856..1c207c69c 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 } - } -- GitLab From 1bf2509d2e3bebea7dfb4fa25e85446bd5383d3c Mon Sep 17 00:00:00 2001 From: Anton Poltoratskyi Date: Tue, 30 Oct 2018 12:59:07 +0200 Subject: [PATCH 29/64] [NY-4699] Remove hardcodes account statuses from profile and chat list. --- .../ChatListMessageCell/Model/ChatListMessageCellModel.swift | 5 ++--- .../Profile/View/DetailsView/ProfileDetailsView.swift | 2 -- 2 files changed, 2 insertions(+), 5 deletions(-) diff --git a/Nynja/Library/UI/Lists/TableView/Cells/ChatListMessageCell/Model/ChatListMessageCellModel.swift b/Nynja/Library/UI/Lists/TableView/Cells/ChatListMessageCell/Model/ChatListMessageCellModel.swift index 8d12b5eed..c5ee537d9 100644 --- a/Nynja/Library/UI/Lists/TableView/Cells/ChatListMessageCell/Model/ChatListMessageCellModel.swift +++ b/Nynja/Library/UI/Lists/TableView/Cells/ChatListMessageCell/Model/ChatListMessageCellModel.swift @@ -43,10 +43,9 @@ final class ChatListMessageCellModel: CellViewModel { // MARK: - Avatar private func setupAvatar(in cell: Cell) { - cell.avatarImageView.imageView + cell.avatarImageView + .imageView .setImage(url: model.photoURL, placeHolder: UIImage.nynja.Contacts.avaPlaceholder.image) - - cell.avatarImageView.update(model is Contact ? .color(UIColor.nynja.callGreen) : .none) } diff --git a/Nynja/Modules/Profile/View/DetailsView/ProfileDetailsView.swift b/Nynja/Modules/Profile/View/DetailsView/ProfileDetailsView.swift index cb5b5b71f..3a44d47a9 100644 --- a/Nynja/Modules/Profile/View/DetailsView/ProfileDetailsView.swift +++ b/Nynja/Modules/Profile/View/DetailsView/ProfileDetailsView.swift @@ -198,8 +198,6 @@ class ProfileDetailsView: UIView { phoneLabel.isHidden = false walletButton.isHidden = false infoView.isHidden = false - - avatarImageView.update(.color(UIColor.nynja.callGreen)) } // MARK: Recognizer -- GitLab From c4bbaff15b538dc66b69a0433751e4c13ef1ab1f Mon Sep 17 00:00:00 2001 From: Anton Poltoratskyi Date: Tue, 30 Oct 2018 14:54:50 +0200 Subject: [PATCH 30/64] [NY-4699] Added TypingDisplayModel. --- Nynja.xcodeproj/project.pbxproj | 8 ++ .../Interactor/MessageInteractor.swift | 6 +- .../Models/Statuses/ActionStatus.swift | 22 ++++- .../Message/Presenter/MessagePresenter.swift | 10 ++- .../Message/Protocols/MessageProtocols.swift | 2 +- Nynja/Statuses/AccountStatus.swift | 2 + Nynja/Statuses/TypingData.swift | 23 ++++++ Nynja/Statuses/TypingDisplayModel.swift | 29 +++++++ Nynja/Statuses/TypingStatusProvider.swift | 81 +++++++++++++++---- 9 files changed, 157 insertions(+), 26 deletions(-) create mode 100644 Nynja/Statuses/TypingData.swift create mode 100644 Nynja/Statuses/TypingDisplayModel.swift diff --git a/Nynja.xcodeproj/project.pbxproj b/Nynja.xcodeproj/project.pbxproj index 6ec362192..28dcfa7e2 100644 --- a/Nynja.xcodeproj/project.pbxproj +++ b/Nynja.xcodeproj/project.pbxproj @@ -910,6 +910,8 @@ 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 */; }; + 8542B8102188741100A286E5 /* TypingData.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8542B80F2188741100A286E5 /* TypingData.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 */; }; @@ -3141,6 +3143,8 @@ 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 = ""; }; + 8542B80F2188741100A286E5 /* TypingData.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TypingData.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 = ""; }; @@ -9109,6 +9113,8 @@ 85EB37FE21837304003A2D6F /* AccountStatus.swift */, 85EB37F72183659C003A2D6F /* AccountStatusProvider.swift */, 854834172186FADB002064E1 /* TypingStatusProvider.swift */, + 8542B811218879B100A286E5 /* TypingDisplayModel.swift */, + 8542B80F2188741100A286E5 /* TypingData.swift */, ); path = Statuses; sourceTree = ""; @@ -15339,6 +15345,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 */, @@ -16199,6 +16206,7 @@ 5A6237362268CC9BD4792230 /* EditUsernameViewController.swift in Sources */, A42D51C7206A361400EEB952 /* timeoutEvent.swift in Sources */, B7F5051D2061252100C28FA1 /* DataAndStorageItemsFactory.swift in Sources */, + 8542B8102188741100A286E5 /* TypingData.swift in Sources */, 267BE2B11FE13AB600C47E18 /* ParticipantsViewController.swift in Sources */, 8503B527205046A6006F0593 /* NotificationSettingsProtocols.swift in Sources */, FDECF3B609DB36ABEED91F70 /* EditUsernamePresenter.swift in Sources */, diff --git a/Nynja/Modules/Message/Interactor/MessageInteractor.swift b/Nynja/Modules/Message/Interactor/MessageInteractor.swift index 5e276b318..cbd024f04 100644 --- a/Nynja/Modules/Message/Interactor/MessageInteractor.swift +++ b/Nynja/Modules/Message/Interactor/MessageInteractor.swift @@ -189,12 +189,12 @@ final class MessageInteractor: BaseInteractor, MessageInteractorInputProtocol, H mqttService.addSubscriber(self) if let chatId = chat.id { - typingStatusProvider.addObserver(self, for: chatId) { [weak self] chatId, typingStatus in + typingStatusProvider.addObserver(self, for: chatId) { [weak self] chatId, typingInfo in guard let `self` = self else { return } - self.presenter?.actionStatusChanged(typingStatus) + self.presenter?.didReceiveTyping(typingInfo) - if case .done = typingStatus { + if case .done = typingInfo { return } dispatchAsyncMainThrotlle(key: "remove_typing_status", seconds: 10.0) { [weak self] in diff --git a/Nynja/Modules/Message/Models/Statuses/ActionStatus.swift b/Nynja/Modules/Message/Models/Statuses/ActionStatus.swift index 5c3c25c45..85e25e835 100644 --- a/Nynja/Modules/Message/Models/Statuses/ActionStatus.swift +++ b/Nynja/Modules/Message/Models/Statuses/ActionStatus.swift @@ -12,6 +12,27 @@ enum ActionStatus { case sending(SendingStatus) case recording(RecordingStatus) + 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 +68,4 @@ enum ActionStatus { self = .recording(.voice) } } - } diff --git a/Nynja/Modules/Message/Presenter/MessagePresenter.swift b/Nynja/Modules/Message/Presenter/MessagePresenter.swift index 4e558edb5..a5d12add6 100644 --- a/Nynja/Modules/Message/Presenter/MessagePresenter.swift +++ b/Nynja/Modules/Message/Presenter/MessagePresenter.swift @@ -939,11 +939,13 @@ class MessagePresenter: BasePresenter, MessagePresenterProtocol, MessageInteract lastStatus = status.title } - func actionStatusChanged(_ status: ActionStatus) { - if case .done = status { + func didReceiveTyping(_ typing: TypingDisplayModel) { + switch typing { + case .done: restoreStatus() - } else { - view.updateHeaderStatus(status.title) + case let .typing(sender, status): + let displayString = sender.displayName.flatMap { "\($0) \(status.title)" } ?? status.title + view.updateHeaderStatus(displayString) } } diff --git a/Nynja/Modules/Message/Protocols/MessageProtocols.swift b/Nynja/Modules/Message/Protocols/MessageProtocols.swift index 5258417d6..6e4d7e5ea 100644 --- a/Nynja/Modules/Message/Protocols/MessageProtocols.swift +++ b/Nynja/Modules/Message/Protocols/MessageProtocols.swift @@ -151,7 +151,7 @@ protocol MessageInteractorOutputProtocol: class, MentionFetchOutputProtocol, Mes func internetStatusChanged(_ status: InternetStatus) func presenceStatusChanged(_ status: PresenceStatus) - func actionStatusChanged(_ status: ActionStatus) + func didReceiveTyping(_ typing: TypingDisplayModel) func restoreStatus() func messageSent(_ localId: MessageLocalId) diff --git a/Nynja/Statuses/AccountStatus.swift b/Nynja/Statuses/AccountStatus.swift index f9f0890d4..2a1bfe2f6 100644 --- a/Nynja/Statuses/AccountStatus.swift +++ b/Nynja/Statuses/AccountStatus.swift @@ -8,6 +8,8 @@ typealias AccountId = String +typealias FeedId = String + enum AccountStatus { case active case inactive diff --git a/Nynja/Statuses/TypingData.swift b/Nynja/Statuses/TypingData.swift new file mode 100644 index 000000000..fc4d1b803 --- /dev/null +++ b/Nynja/Statuses/TypingData.swift @@ -0,0 +1,23 @@ +// +// TypingData.swift +// Nynja +// +// Created by Anton Poltoratskyi on 30.10.2018. +// Copyright © 2018 TecSynt Solutions. All rights reserved. +// + +struct TypingData { + let feedId: String + let displayInfo: DisplayInfo + + struct Sender { + let senderId: String + let senderName: String? + let status: ActionStatus + } + + enum DisplayInfo { + case p2p(Sender) + case room([Sender]) + } +} diff --git a/Nynja/Statuses/TypingDisplayModel.swift b/Nynja/Statuses/TypingDisplayModel.swift new file mode 100644 index 000000000..bed92f469 --- /dev/null +++ b/Nynja/Statuses/TypingDisplayModel.swift @@ -0,0 +1,29 @@ +// +// TypingDisplayModel.swift +// Nynja +// +// Created by Anton Poltoratskyi on 30.10.2018. +// Copyright © 2018 TecSynt Solutions. All rights reserved. +// + +enum TypingDisplayModel { + case typing(Sender, ActionStatus) + case done + + enum Sender { + case name(String) + case names([String]) + case none + + var displayName: String? { + switch self { + case let .name(name): + return name + case let .names(names): + return names.joined(separator: ", ") + case .none: + return nil + } + } + } +} diff --git a/Nynja/Statuses/TypingStatusProvider.swift b/Nynja/Statuses/TypingStatusProvider.swift index ecf1c9a46..db0701060 100644 --- a/Nynja/Statuses/TypingStatusProvider.swift +++ b/Nynja/Statuses/TypingStatusProvider.swift @@ -9,25 +9,24 @@ import Foundation protocol TypingStatusObservable: class { - typealias Callback = (AccountId, ActionStatus) -> Void + typealias Callback = (FeedId, TypingDisplayModel) -> Void func addObserver(_ observer: AnyObject, callback: @escaping Callback) - func addObserver(_ observer: AnyObject, for key: AccountId, callback: @escaping Callback) + func addObserver(_ observer: AnyObject, for key: FeedId, callback: @escaping Callback) func removeObserver(_ observer: AnyObject) - func removeObserver(_ observer: AnyObject, for key: AccountId) - func notify(_ key: AccountId, with value: ActionStatus) + func removeObserver(_ observer: AnyObject, for key: FeedId) + func notify(_ key: FeedId, with value: TypingDisplayModel) } protocol TypingStatusProvider: TypingStatusObservable { - func status(for accountId: AccountId) -> ActionStatus - func update(_ status: ActionStatus, for accountId: AccountId) + func typingStatus(for feedId: FeedId) -> TypingDisplayModel? } final class TypingStatusProviderImpl: TypingStatusProvider, KeyedObservableContainer, TypingHandlerDelegate, InitializeInjectable { - private var data: [AccountId: ActionStatus] = [:] + private var data: [FeedId: TypingData] = [:] - private(set) var observable = KeyedObservable() + private(set) var observable = KeyedObservable() // MARK: - Dependencies @@ -51,23 +50,71 @@ final class TypingStatusProviderImpl: TypingStatusProvider, KeyedObservableConta // MARK: - TypingStatusProvider - func status(for accountId: AccountId) -> ActionStatus { - return data[accountId] ?? .done - } - - func update(_ status: ActionStatus, for accountId: AccountId) { - data[accountId] = status - observable.notify(accountId, with: status) + func typingStatus(for feedId: FeedId) -> TypingDisplayModel? { + return data[feedId].flatMap { displayInfo(for: $0) } } // MARK: - TypingHandlerDelegate func didReceiveTyping(_ typing: Typing) { - guard let accountId = typing.phone_id, let typingType = typing.type else { + guard let feedId = typing.phone_id, let typingType = typing.type else { return } let status = ActionStatus(typingModelType: typingType) - notify(accountId, with: status) + + // FIXME: implement logic for rooms. + let senderInfo = TypingData.Sender(senderId: feedId, senderName: nil, status: status) + + let data = TypingData(feedId: feedId, displayInfo: .p2p(senderInfo)) + + update(data) + } + + private func update(_ typingData: TypingData) { + let feedId = typingData.feedId + + guard let oldTyping = data[feedId] else { + save(typingData) + return + } + switch (oldTyping.displayInfo, typingData.displayInfo) { + case (.p2p, .p2p): + save(typingData) + + case let (.room(oldDisplayInfo), .room(newDisplayInfo)): + var displayInfo = oldDisplayInfo + displayInfo.append(contentsOf: newDisplayInfo) + + let newTypingModel = TypingData(feedId: feedId, displayInfo: .room(displayInfo)) + save(newTypingModel) + + default: + break + } + } + + private func save(_ typingData: TypingData) { + data[typingData.feedId] = typingData + observable.notify(typingData.feedId, with: displayInfo(for: typingData)) + } + + private func displayInfo(for typing: TypingData) -> TypingDisplayModel { + switch typing.displayInfo { + case let .p2p(senderInfo): + let sender: TypingDisplayModel.Sender = senderInfo.senderName.flatMap { .name($0) } ?? .none + return .typing(sender, senderInfo.status) + + case let .room(senderArrayInfo): + guard let lastStatus = senderArrayInfo.last?.status else { + break + } + let senders: TypingDisplayModel.Sender = .names(senderArrayInfo.compactMap { + // FIXME: + $0.senderName + }) + return .typing(senders, lastStatus) + } + return .done } } -- GitLab From 411a0533f53f375945b56a1d9bf0cba0e47409db Mon Sep 17 00:00:00 2001 From: Anton Poltoratskyi Date: Tue, 30 Oct 2018 16:39:38 +0200 Subject: [PATCH 31/64] [NY-4699] Conform ActionStatus to Equatable. --- Nynja/Modules/Message/Models/Statuses/ActionStatus.swift | 2 +- Nynja/Modules/Message/Models/Statuses/RecordingStatus.swift | 2 +- Nynja/Modules/Message/Models/Statuses/SendingStatus.swift | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Nynja/Modules/Message/Models/Statuses/ActionStatus.swift b/Nynja/Modules/Message/Models/Statuses/ActionStatus.swift index 85e25e835..0f0a4e9c0 100644 --- a/Nynja/Modules/Message/Models/Statuses/ActionStatus.swift +++ b/Nynja/Modules/Message/Models/Statuses/ActionStatus.swift @@ -6,7 +6,7 @@ // Copyright © 2017 TecSynt Solutions. All rights reserved. // -enum ActionStatus { +enum ActionStatus: Equatable { case done case typing case sending(SendingStatus) diff --git a/Nynja/Modules/Message/Models/Statuses/RecordingStatus.swift b/Nynja/Modules/Message/Models/Statuses/RecordingStatus.swift index bfb75367e..a65d33387 100644 --- a/Nynja/Modules/Message/Models/Statuses/RecordingStatus.swift +++ b/Nynja/Modules/Message/Models/Statuses/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/SendingStatus.swift index 7ab436017..3a5ad85bc 100644 --- a/Nynja/Modules/Message/Models/Statuses/SendingStatus.swift +++ b/Nynja/Modules/Message/Models/Statuses/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" -- GitLab From 525fb290ad9f84ee35728d0ac49e471a69e8348c Mon Sep 17 00:00:00 2001 From: Anton Poltoratskyi Date: Tue, 30 Oct 2018 16:46:21 +0200 Subject: [PATCH 32/64] [NY-4699] Prepare display model for rooms --- .../Message/Presenter/MessagePresenter.swift | 2 +- Nynja/Statuses/TypingData.swift | 6 ++-- Nynja/Statuses/TypingDisplayModel.swift | 29 +++++++++++++------ Nynja/Statuses/TypingStatusProvider.swift | 24 +++++++++------ 4 files changed, 39 insertions(+), 22 deletions(-) diff --git a/Nynja/Modules/Message/Presenter/MessagePresenter.swift b/Nynja/Modules/Message/Presenter/MessagePresenter.swift index a5d12add6..77c263d7e 100644 --- a/Nynja/Modules/Message/Presenter/MessagePresenter.swift +++ b/Nynja/Modules/Message/Presenter/MessagePresenter.swift @@ -944,7 +944,7 @@ class MessagePresenter: BasePresenter, MessagePresenterProtocol, MessageInteract case .done: restoreStatus() case let .typing(sender, status): - let displayString = sender.displayName.flatMap { "\($0) \(status.title)" } ?? status.title + let displayString = sender?.displayName.flatMap { "\($0) \(status.title)" } ?? status.title view.updateHeaderStatus(displayString) } } diff --git a/Nynja/Statuses/TypingData.swift b/Nynja/Statuses/TypingData.swift index fc4d1b803..3fc7a2a9f 100644 --- a/Nynja/Statuses/TypingData.swift +++ b/Nynja/Statuses/TypingData.swift @@ -10,14 +10,14 @@ struct TypingData { let feedId: String let displayInfo: DisplayInfo - struct Sender { + struct SenderInfo { let senderId: String let senderName: String? let status: ActionStatus } enum DisplayInfo { - case p2p(Sender) - case room([Sender]) + case p2p(SenderInfo) + case room([SenderInfo]) } } diff --git a/Nynja/Statuses/TypingDisplayModel.swift b/Nynja/Statuses/TypingDisplayModel.swift index bed92f469..cb9155c5e 100644 --- a/Nynja/Statuses/TypingDisplayModel.swift +++ b/Nynja/Statuses/TypingDisplayModel.swift @@ -7,23 +7,34 @@ // enum TypingDisplayModel { - case typing(Sender, ActionStatus) + case typing(SenderInfo?, ActionStatus) case done - enum Sender { - case name(String) - case names([String]) - case none + enum SenderInfo { + case p2p(String) + case room([String]) var displayName: String? { switch self { - case let .name(name): + case let .p2p(name): return name - case let .names(names): + case let .room(names): return names.joined(separator: ", ") - case .none: - return nil } } } + + var displayName: String? { + guard case let .typing(senderInfo, _) = self else { + return nil + } + return senderInfo?.displayName + } + + var status: ActionStatus? { + guard case let .typing(_, status) = self else { + return nil + } + return status + } } diff --git a/Nynja/Statuses/TypingStatusProvider.swift b/Nynja/Statuses/TypingStatusProvider.swift index db0701060..8e2dd82da 100644 --- a/Nynja/Statuses/TypingStatusProvider.swift +++ b/Nynja/Statuses/TypingStatusProvider.swift @@ -64,7 +64,7 @@ final class TypingStatusProviderImpl: TypingStatusProvider, KeyedObservableConta let status = ActionStatus(typingModelType: typingType) // FIXME: implement logic for rooms. - let senderInfo = TypingData.Sender(senderId: feedId, senderName: nil, status: status) + let senderInfo = TypingData.SenderInfo(senderId: feedId, senderName: nil, status: status) let data = TypingData(feedId: feedId, displayInfo: .p2p(senderInfo)) @@ -84,9 +84,13 @@ final class TypingStatusProviderImpl: TypingStatusProvider, KeyedObservableConta case let (.room(oldDisplayInfo), .room(newDisplayInfo)): var displayInfo = oldDisplayInfo + displayInfo.removeAll { sender in + newDisplayInfo.contains { $0.senderId == sender.senderId } + } displayInfo.append(contentsOf: newDisplayInfo) let newTypingModel = TypingData(feedId: feedId, displayInfo: .room(displayInfo)) + save(newTypingModel) default: @@ -102,19 +106,21 @@ final class TypingStatusProviderImpl: TypingStatusProvider, KeyedObservableConta private func displayInfo(for typing: TypingData) -> TypingDisplayModel { switch typing.displayInfo { case let .p2p(senderInfo): - let sender: TypingDisplayModel.Sender = senderInfo.senderName.flatMap { .name($0) } ?? .none - return .typing(sender, senderInfo.status) + return .typing(senderInfo.senderName.flatMap { .p2p($0) }, senderInfo.status) case let .room(senderArrayInfo): - guard let lastStatus = senderArrayInfo.last?.status else { + guard !senderArrayInfo.isEmpty, let lastStatus = senderArrayInfo.last?.status else { break } - let senders: TypingDisplayModel.Sender = .names(senderArrayInfo.compactMap { - // FIXME: - $0.senderName - }) - return .typing(senders, lastStatus) + let members: [String] = senderArrayInfo.compactMap { + guard lastStatus == $0.status else { + return nil + } + return $0.senderName + } + return .typing(.room(members), lastStatus) } + return .done } } -- GitLab From 6a47735f38ffeb3b72546a99fe3d2dd706b0a5d9 Mon Sep 17 00:00:00 2001 From: Anton Poltoratskyi Date: Tue, 30 Oct 2018 17:38:02 +0200 Subject: [PATCH 33/64] [NY-4699] Refactored TypingStatusProvider --- Nynja.xcodeproj/project.pbxproj | 4 - Nynja/Statuses/TypingData.swift | 23 ---- Nynja/Statuses/TypingDisplayModel.swift | 12 +-- Nynja/Statuses/TypingStatusProvider.swift | 125 +++++++++++++++------- 4 files changed, 89 insertions(+), 75 deletions(-) delete mode 100644 Nynja/Statuses/TypingData.swift diff --git a/Nynja.xcodeproj/project.pbxproj b/Nynja.xcodeproj/project.pbxproj index 28dcfa7e2..63e89967f 100644 --- a/Nynja.xcodeproj/project.pbxproj +++ b/Nynja.xcodeproj/project.pbxproj @@ -910,7 +910,6 @@ 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 */; }; - 8542B8102188741100A286E5 /* TypingData.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8542B80F2188741100A286E5 /* TypingData.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 */; }; @@ -3143,7 +3142,6 @@ 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 = ""; }; - 8542B80F2188741100A286E5 /* TypingData.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TypingData.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 = ""; }; @@ -9114,7 +9112,6 @@ 85EB37F72183659C003A2D6F /* AccountStatusProvider.swift */, 854834172186FADB002064E1 /* TypingStatusProvider.swift */, 8542B811218879B100A286E5 /* TypingDisplayModel.swift */, - 8542B80F2188741100A286E5 /* TypingData.swift */, ); path = Statuses; sourceTree = ""; @@ -16206,7 +16203,6 @@ 5A6237362268CC9BD4792230 /* EditUsernameViewController.swift in Sources */, A42D51C7206A361400EEB952 /* timeoutEvent.swift in Sources */, B7F5051D2061252100C28FA1 /* DataAndStorageItemsFactory.swift in Sources */, - 8542B8102188741100A286E5 /* TypingData.swift in Sources */, 267BE2B11FE13AB600C47E18 /* ParticipantsViewController.swift in Sources */, 8503B527205046A6006F0593 /* NotificationSettingsProtocols.swift in Sources */, FDECF3B609DB36ABEED91F70 /* EditUsernamePresenter.swift in Sources */, diff --git a/Nynja/Statuses/TypingData.swift b/Nynja/Statuses/TypingData.swift deleted file mode 100644 index 3fc7a2a9f..000000000 --- a/Nynja/Statuses/TypingData.swift +++ /dev/null @@ -1,23 +0,0 @@ -// -// TypingData.swift -// Nynja -// -// Created by Anton Poltoratskyi on 30.10.2018. -// Copyright © 2018 TecSynt Solutions. All rights reserved. -// - -struct TypingData { - let feedId: String - let displayInfo: DisplayInfo - - struct SenderInfo { - let senderId: String - let senderName: String? - let status: ActionStatus - } - - enum DisplayInfo { - case p2p(SenderInfo) - case room([SenderInfo]) - } -} diff --git a/Nynja/Statuses/TypingDisplayModel.swift b/Nynja/Statuses/TypingDisplayModel.swift index cb9155c5e..24e962cf4 100644 --- a/Nynja/Statuses/TypingDisplayModel.swift +++ b/Nynja/Statuses/TypingDisplayModel.swift @@ -10,17 +10,11 @@ enum TypingDisplayModel { case typing(SenderInfo?, ActionStatus) case done - enum SenderInfo { - case p2p(String) - case room([String]) + struct SenderInfo { + var senders: [String] var displayName: String? { - switch self { - case let .p2p(name): - return name - case let .room(names): - return names.joined(separator: ", ") - } + return senders.joined(separator: ", ") } } diff --git a/Nynja/Statuses/TypingStatusProvider.swift b/Nynja/Statuses/TypingStatusProvider.swift index 8e2dd82da..5d2404131 100644 --- a/Nynja/Statuses/TypingStatusProvider.swift +++ b/Nynja/Statuses/TypingStatusProvider.swift @@ -23,9 +23,11 @@ protocol TypingStatusProvider: TypingStatusObservable { } final class TypingStatusProviderImpl: TypingStatusProvider, KeyedObservableContainer, TypingHandlerDelegate, InitializeInjectable { - + private var data: [FeedId: TypingData] = [:] + private var workItems: [FeedId: DispatchWorkItem] = [:] + private(set) var observable = KeyedObservable() @@ -64,63 +66,108 @@ final class TypingStatusProviderImpl: TypingStatusProvider, KeyedObservableConta let status = ActionStatus(typingModelType: typingType) // FIXME: implement logic for rooms. - let senderInfo = TypingData.SenderInfo(senderId: feedId, senderName: nil, status: status) + let senderInfo = TypingInfo(feed: .p2p(feedId), senderId: feedId, senderName: nil, status: status) - let data = TypingData(feedId: feedId, displayInfo: .p2p(senderInfo)) - - update(data) + update(with: senderInfo) } - private func update(_ typingData: TypingData) { - let feedId = typingData.feedId + private func update(with typingInfo: TypingInfo) { + let feedId = typingInfo.feed.identifier guard let oldTyping = data[feedId] else { - save(typingData) + // Save and notify if didn't exists any other typing status for this feed + switch typingInfo.feed { + case .p2p: + save(.p2p(typingInfo), for: feedId) + case .room: + save(.room([typingInfo]), for: feedId) + } return } - switch (oldTyping.displayInfo, typingData.displayInfo) { - case (.p2p, .p2p): - save(typingData) - - case let (.room(oldDisplayInfo), .room(newDisplayInfo)): - var displayInfo = oldDisplayInfo - displayInfo.removeAll { sender in - newDisplayInfo.contains { $0.senderId == sender.senderId } - } - displayInfo.append(contentsOf: newDisplayInfo) - - let newTypingModel = TypingData(feedId: feedId, displayInfo: .room(displayInfo)) + + switch oldTyping { + case .p2p: + save(.p2p(typingInfo), for: feedId) + case let .room(oldTypingSendersInfo): + var newTypingInfo = oldTypingSendersInfo - save(newTypingModel) + newTypingInfo.removeAll { typingInfo.senderId == $0.senderId } + newTypingInfo.append(typingInfo) - default: - break + save(.room(newTypingInfo), for: feedId) } } - private func save(_ typingData: TypingData) { - data[typingData.feedId] = typingData - observable.notify(typingData.feedId, with: displayInfo(for: typingData)) + private func save(_ typing: TypingData, for feedId: FeedId) { + let workItem = DispatchWorkItem { + + } + data[feedId] = typing + observable.notify(feedId, with: displayInfo(for: typing)) } private func displayInfo(for typing: TypingData) -> TypingDisplayModel { - switch typing.displayInfo { - case let .p2p(senderInfo): - return .typing(senderInfo.senderName.flatMap { .p2p($0) }, senderInfo.status) - - case let .room(senderArrayInfo): - guard !senderArrayInfo.isEmpty, let lastStatus = senderArrayInfo.last?.status else { - break + let typingInfo = typing.senders + + guard !typingInfo.isEmpty, let lastStatus = typingInfo.last?.status else { + return .done + } + let senders: [String] = typingInfo.compactMap { + guard lastStatus == $0.status else { + return nil + } + return $0.senderName + } + let senderInfo = TypingDisplayModel.SenderInfo(senders: senders) + + return .typing(senderInfo, lastStatus) + } +} + + +// MARK: - Private Data Types + +private extension TypingStatusProviderImpl { + + enum TypingData { + case p2p(TypingInfo) + case room([TypingInfo]) + + var senders: [TypingInfo] { + switch self { + case let .p2p(sender): + return [sender] + case let .room(senders): + return senders } - let members: [String] = senderArrayInfo.compactMap { - guard lastStatus == $0.status else { - return nil + } + } + + final class TypingInfo { + enum Feed { + case p2p(FeedId) + case room(FeedId) + + var identifier: String { + switch self { + case let .p2p(id): + return id + case let .room(id): + return id } - return $0.senderName } - return .typing(.room(members), lastStatus) } - return .done + let feed: Feed + let senderId: String + let senderName: String? + let status: ActionStatus + + init(feed: Feed, senderId: String, senderName: String?, status: ActionStatus) { + self.feed = feed + self.senderId = senderId + self.senderName = senderName + self.status = status + } } } -- GitLab From 1a56ee869f97fc142d3ec97a2e67a79caf89a052 Mon Sep 17 00:00:00 2001 From: Anton Poltoratskyi Date: Tue, 30 Oct 2018 17:59:10 +0200 Subject: [PATCH 34/64] [NY-4699] Implemented restoring status in TypingStatusProvider. --- .../Interactor/MessageInteractor.swift | 11 +------- Nynja/Statuses/TypingStatusProvider.swift | 27 +++++++++++++------ 2 files changed, 20 insertions(+), 18 deletions(-) diff --git a/Nynja/Modules/Message/Interactor/MessageInteractor.swift b/Nynja/Modules/Message/Interactor/MessageInteractor.swift index cbd024f04..b200a435b 100644 --- a/Nynja/Modules/Message/Interactor/MessageInteractor.swift +++ b/Nynja/Modules/Message/Interactor/MessageInteractor.swift @@ -190,16 +190,7 @@ final class MessageInteractor: BaseInteractor, MessageInteractorInputProtocol, H if let chatId = chat.id { typingStatusProvider.addObserver(self, for: chatId) { [weak self] chatId, typingInfo in - guard let `self` = self else { return } - - self.presenter?.didReceiveTyping(typingInfo) - - if case .done = typingInfo { - return - } - dispatchAsyncMainThrotlle(key: "remove_typing_status", seconds: 10.0) { [weak self] in - self?.presenter?.restoreStatus() - } + self?.presenter?.didReceiveTyping(typingInfo) } } ConnectionService.shared.addSubscriber(self) diff --git a/Nynja/Statuses/TypingStatusProvider.swift b/Nynja/Statuses/TypingStatusProvider.swift index 5d2404131..16472906e 100644 --- a/Nynja/Statuses/TypingStatusProvider.swift +++ b/Nynja/Statuses/TypingStatusProvider.swift @@ -98,18 +98,29 @@ final class TypingStatusProviderImpl: TypingStatusProvider, KeyedObservableConta } } - private func save(_ typing: TypingData, for feedId: FeedId) { + private func save(_ typing: TypingData?, for feedId: FeedId) { + data[feedId] = typing + + let displayInfo = self.displayInfo(for: typing) + observable.notify(feedId, with: displayInfo) + + workItems[feedId]?.cancel() + + if case .done = displayInfo { + return + } + let workItem = DispatchWorkItem { - + self.data[feedId] = nil + self.save(nil, for: feedId) } - data[feedId] = typing - observable.notify(feedId, with: displayInfo(for: typing)) + + workItems[feedId] = workItem + DispatchQueue.main.asyncAfter(deadline: .now() + 10, execute: workItem) } - private func displayInfo(for typing: TypingData) -> TypingDisplayModel { - let typingInfo = typing.senders - - guard !typingInfo.isEmpty, let lastStatus = typingInfo.last?.status else { + private func displayInfo(for typing: TypingData?) -> TypingDisplayModel { + guard let typingInfo = typing?.senders, !typingInfo.isEmpty, let lastStatus = typingInfo.last?.status else { return .done } let senders: [String] = typingInfo.compactMap { -- GitLab From e4aed91335f1d397c0ea8db49753e498a72a84a2 Mon Sep 17 00:00:00 2001 From: Anton Poltoratskyi Date: Tue, 30 Oct 2018 18:41:16 +0200 Subject: [PATCH 35/64] [NY-4699] Implemented dismiss work item for rooms. --- Nynja/Statuses/TypingStatusProvider.swift | 72 +++++++++++++---------- 1 file changed, 42 insertions(+), 30 deletions(-) diff --git a/Nynja/Statuses/TypingStatusProvider.swift b/Nynja/Statuses/TypingStatusProvider.swift index 16472906e..1f2defe9a 100644 --- a/Nynja/Statuses/TypingStatusProvider.swift +++ b/Nynja/Statuses/TypingStatusProvider.swift @@ -28,7 +28,9 @@ final class TypingStatusProviderImpl: TypingStatusProvider, KeyedObservableConta private var workItems: [FeedId: DispatchWorkItem] = [:] - private(set) var observable = KeyedObservable() + let observable = KeyedObservable() + + private let typingDismissInterval = 10.0 // MARK: - Dependencies @@ -67,56 +69,66 @@ final class TypingStatusProviderImpl: TypingStatusProvider, KeyedObservableConta // FIXME: implement logic for rooms. let senderInfo = TypingInfo(feed: .p2p(feedId), senderId: feedId, senderName: nil, status: status) - - update(with: senderInfo) + save(senderInfo) } - private func update(with typingInfo: TypingInfo) { + private func save(_ typingInfo: TypingInfo) { let feedId = typingInfo.feed.identifier - - guard let oldTyping = data[feedId] else { + + if let oldTyping = data[feedId] { + switch oldTyping { + case .p2p: + update(.p2p(typingInfo), for: feedId) + + case let .room(oldTypingSendersInfo): + var newTypingInfo = oldTypingSendersInfo + + newTypingInfo.removeAll { typingInfo.senderId == $0.senderId } + newTypingInfo.append(typingInfo) + + update(.room(newTypingInfo), for: feedId) + } + } else { // Save and notify if didn't exists any other typing status for this feed switch typingInfo.feed { case .p2p: - save(.p2p(typingInfo), for: feedId) + update(.p2p(typingInfo), for: feedId) case .room: - save(.room([typingInfo]), for: feedId) + update(.room([typingInfo]), for: feedId) } - return } - switch oldTyping { - case .p2p: - save(.p2p(typingInfo), for: feedId) - case let .room(oldTypingSendersInfo): - var newTypingInfo = oldTypingSendersInfo - - newTypingInfo.removeAll { typingInfo.senderId == $0.senderId } - newTypingInfo.append(typingInfo) - - save(.room(newTypingInfo), for: feedId) - } + dismiss(typingInfo, after: typingDismissInterval, for: feedId) } - private func save(_ typing: TypingData?, for feedId: FeedId) { + private func update(_ typing: TypingData?, for feedId: FeedId) { data[feedId] = typing let displayInfo = self.displayInfo(for: typing) observable.notify(feedId, with: displayInfo) - + } + + private func dismiss(_ typing: TypingInfo, after delay: TimeInterval, for feedId: FeedId) { workItems[feedId]?.cancel() - if case .done = displayInfo { - return - } - let workItem = DispatchWorkItem { - self.data[feedId] = nil - self.save(nil, for: feedId) + guard let currentTyping = self.data[feedId] else { + return + } + switch currentTyping { + case let .p2p(info) where info === typing: + self.update(nil, for: feedId) + + case let .room(info): + self.update(.room(info.filter { $0 !== typing }), for: feedId) + + default: + break + } } workItems[feedId] = workItem - DispatchQueue.main.asyncAfter(deadline: .now() + 10, execute: workItem) + DispatchQueue.main.asyncAfter(deadline: .now() + delay, execute: workItem) } private func displayInfo(for typing: TypingData?) -> TypingDisplayModel { @@ -136,7 +148,7 @@ final class TypingStatusProviderImpl: TypingStatusProvider, KeyedObservableConta } -// MARK: - Private Data Types +// MARK: - Inner Types private extension TypingStatusProviderImpl { -- GitLab From c0244f907c07928e22fca41d7330059ce5e8c614 Mon Sep 17 00:00:00 2001 From: Anton Poltoratskyi Date: Tue, 30 Oct 2018 19:04:25 +0200 Subject: [PATCH 36/64] [NY-4699] Make TypingStatusProvider asynchronous. --- .../Interactor/MessageInteractor.swift | 2 +- Nynja/Statuses/TypingStatusProvider.swift | 87 ++++++++++--------- 2 files changed, 49 insertions(+), 40 deletions(-) diff --git a/Nynja/Modules/Message/Interactor/MessageInteractor.swift b/Nynja/Modules/Message/Interactor/MessageInteractor.swift index b200a435b..50578f17a 100644 --- a/Nynja/Modules/Message/Interactor/MessageInteractor.swift +++ b/Nynja/Modules/Message/Interactor/MessageInteractor.swift @@ -178,7 +178,7 @@ final class MessageInteractor: BaseInteractor, MessageInteractorInputProtocol, H payloadParser = MessagePayloadParser() payloadBuilder = MessagePayloadBuilder() stickersProvider = StickersProvider(dependencies: .init(storage: StorageService.sharedInstance)) - typingStatusProvider = TypingStatusProviderImpl(dependencies: TypingHandler.shared) + typingStatusProvider = TypingStatusProviderImpl(dependencies: .init(typingHandler: TypingHandler.shared)) super.init() diff --git a/Nynja/Statuses/TypingStatusProvider.swift b/Nynja/Statuses/TypingStatusProvider.swift index 1f2defe9a..16d6666a9 100644 --- a/Nynja/Statuses/TypingStatusProvider.swift +++ b/Nynja/Statuses/TypingStatusProvider.swift @@ -19,23 +19,30 @@ protocol TypingStatusObservable: class { } protocol TypingStatusProvider: TypingStatusObservable { - func typingStatus(for feedId: FeedId) -> TypingDisplayModel? } final class TypingStatusProviderImpl: TypingStatusProvider, KeyedObservableContainer, TypingHandlerDelegate, InitializeInjectable { + let observable = KeyedObservable() + private var data: [FeedId: TypingData] = [:] private var workItems: [FeedId: DispatchWorkItem] = [:] - let observable = KeyedObservable() - private let typingDismissInterval = 10.0 + private let processingQueue: DispatchQueue + + private let notifyQueue: DispatchQueue + // MARK: - Dependencies - typealias Dependencies = TypingHandler + struct Dependencies { + let typingHandler: TypingHandler + let processingQueue = DispatchQueue.global(qos: .default) + let notifyQueue = DispatchQueue.main + } private let typingHandler: TypingHandler @@ -43,7 +50,9 @@ final class TypingStatusProviderImpl: TypingStatusProvider, KeyedObservableConta // MARK: - Init init(dependencies: Dependencies) { - typingHandler = dependencies + typingHandler = dependencies.typingHandler + processingQueue = dependencies.processingQueue + notifyQueue = dependencies.notifyQueue typingHandler.addObserver(self) } @@ -52,13 +61,6 @@ final class TypingStatusProviderImpl: TypingStatusProvider, KeyedObservableConta } - // MARK: - TypingStatusProvider - - func typingStatus(for feedId: FeedId) -> TypingDisplayModel? { - return data[feedId].flatMap { displayInfo(for: $0) } - } - - // MARK: - TypingHandlerDelegate func didReceiveTyping(_ typing: Typing) { @@ -68,11 +70,14 @@ final class TypingStatusProviderImpl: TypingStatusProvider, KeyedObservableConta let status = ActionStatus(typingModelType: typingType) // FIXME: implement logic for rooms. - let senderInfo = TypingInfo(feed: .p2p(feedId), senderId: feedId, senderName: nil, status: status) - save(senderInfo) + let senderInfo = SenderInfo(feed: .p2p(feedId), senderId: feedId, senderName: nil, status: status) + + processingQueue.async { + self.save(senderInfo) + } } - private func save(_ typingInfo: TypingInfo) { + private func save(_ typingInfo: SenderInfo) { let feedId = typingInfo.feed.identifier if let oldTyping = data[feedId] { @@ -101,34 +106,38 @@ final class TypingStatusProviderImpl: TypingStatusProvider, KeyedObservableConta dismiss(typingInfo, after: typingDismissInterval, for: feedId) } + private func dismiss(_ typing: SenderInfo, after delay: TimeInterval, for feedId: FeedId) { + workItems[feedId]?.cancel() + + let workItem = DispatchWorkItem { + self.remove(typing, for: feedId) + } + workItems[feedId] = workItem + + processingQueue.asyncAfter(deadline: .now() + delay, execute: workItem) + } + private func update(_ typing: TypingData?, for feedId: FeedId) { data[feedId] = typing let displayInfo = self.displayInfo(for: typing) - observable.notify(feedId, with: displayInfo) + notifyQueue.async { + self.observable.notify(feedId, with: displayInfo) + } } - private func dismiss(_ typing: TypingInfo, after delay: TimeInterval, for feedId: FeedId) { - workItems[feedId]?.cancel() - - let workItem = DispatchWorkItem { - guard let currentTyping = self.data[feedId] else { - return - } - switch currentTyping { - case let .p2p(info) where info === typing: - self.update(nil, for: feedId) - - case let .room(info): - self.update(.room(info.filter { $0 !== typing }), for: feedId) - - default: - break + private func remove(_ typing: SenderInfo, for feedId: FeedId) { + guard let currentTyping = data[feedId] else { + return + } + switch currentTyping { + case let .p2p(info): + if info === typing { + update(nil, for: feedId) } + case let .room(info): + update(.room(info.filter { $0 !== typing }), for: feedId) } - - workItems[feedId] = workItem - DispatchQueue.main.asyncAfter(deadline: .now() + delay, execute: workItem) } private func displayInfo(for typing: TypingData?) -> TypingDisplayModel { @@ -153,10 +162,10 @@ final class TypingStatusProviderImpl: TypingStatusProvider, KeyedObservableConta private extension TypingStatusProviderImpl { enum TypingData { - case p2p(TypingInfo) - case room([TypingInfo]) + case p2p(SenderInfo) + case room([SenderInfo]) - var senders: [TypingInfo] { + var senders: [SenderInfo] { switch self { case let .p2p(sender): return [sender] @@ -166,7 +175,7 @@ private extension TypingStatusProviderImpl { } } - final class TypingInfo { + final class SenderInfo { enum Feed { case p2p(FeedId) case room(FeedId) -- GitLab From 60d93a21c7a1f08ef04fec9510b80f8bfda8f56f Mon Sep 17 00:00:00 2001 From: Anton Poltoratskyi Date: Tue, 30 Oct 2018 19:06:54 +0200 Subject: [PATCH 37/64] [NY-4699] Update MessageProtocols. --- Nynja/Modules/Message/Presenter/MessagePresenter.swift | 2 +- Nynja/Modules/Message/Protocols/MessageProtocols.swift | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/Nynja/Modules/Message/Presenter/MessagePresenter.swift b/Nynja/Modules/Message/Presenter/MessagePresenter.swift index 77c263d7e..fc4d0a0ce 100644 --- a/Nynja/Modules/Message/Presenter/MessagePresenter.swift +++ b/Nynja/Modules/Message/Presenter/MessagePresenter.swift @@ -949,7 +949,7 @@ class MessagePresenter: BasePresenter, MessagePresenterProtocol, MessageInteract } } - func restoreStatus() { + private func restoreStatus() { view.updateHeaderStatus(lastStatus) } diff --git a/Nynja/Modules/Message/Protocols/MessageProtocols.swift b/Nynja/Modules/Message/Protocols/MessageProtocols.swift index 6e4d7e5ea..89a876acc 100644 --- a/Nynja/Modules/Message/Protocols/MessageProtocols.swift +++ b/Nynja/Modules/Message/Protocols/MessageProtocols.swift @@ -152,7 +152,6 @@ protocol MessageInteractorOutputProtocol: class, MentionFetchOutputProtocol, Mes func internetStatusChanged(_ status: InternetStatus) func presenceStatusChanged(_ status: PresenceStatus) func didReceiveTyping(_ typing: TypingDisplayModel) - func restoreStatus() func messageSent(_ localId: MessageLocalId) func messageRead(_ localId: MessageLocalId) -- GitLab From 953163d554a6cf3c1c323b135bbfb87e8c26b549 Mon Sep 17 00:00:00 2001 From: Anton Poltoratskyi Date: Wed, 31 Oct 2018 10:53:11 +0200 Subject: [PATCH 38/64] [NY-4699] Implemented typing logic in chat header. --- Nynja.xcodeproj/project.pbxproj | 42 ++++++++-- Nynja/Generated/LocalizableConstants.swift | 6 +- .../Message/Models/Statuses/ChatStatus.swift | 13 +++ .../Statuses/ChatStatusDisplayInfo.swift | 12 +++ .../{ => Internet}/InternetStatus.swift | 0 .../{ => Presence}/PresenceStatus.swift | 0 .../Statuses/{ => Typing}/ActionStatus.swift | 0 .../{ => Typing}/RecordingStatus.swift | 0 .../Statuses/{ => Typing}/SendingStatus.swift | 0 .../Message/Presenter/MessagePresenter.swift | 84 +++++++++++-------- .../Message/Protocols/MessageProtocols.swift | 2 +- Nynja/Modules/Message/View/MessageVC.swift | 2 +- .../View/Views/AvatarView/AvatarView.swift | 39 ++++++--- Nynja/Resources/en.lproj/Localizable.strings | 6 +- Nynja/Statuses/TypingDisplayModel.swift | 2 +- Nynja/Statuses/TypingStatusProvider.swift | 2 +- 16 files changed, 146 insertions(+), 64 deletions(-) create mode 100644 Nynja/Modules/Message/Models/Statuses/ChatStatus.swift create mode 100644 Nynja/Modules/Message/Models/Statuses/ChatStatusDisplayInfo.swift rename Nynja/Modules/Message/Models/Statuses/{ => Internet}/InternetStatus.swift (100%) rename Nynja/Modules/Message/Models/Statuses/{ => Presence}/PresenceStatus.swift (100%) rename Nynja/Modules/Message/Models/Statuses/{ => Typing}/ActionStatus.swift (100%) rename Nynja/Modules/Message/Models/Statuses/{ => Typing}/RecordingStatus.swift (100%) rename Nynja/Modules/Message/Models/Statuses/{ => Typing}/SendingStatus.swift (100%) diff --git a/Nynja.xcodeproj/project.pbxproj b/Nynja.xcodeproj/project.pbxproj index 63e89967f..95085d324 100644 --- a/Nynja.xcodeproj/project.pbxproj +++ b/Nynja.xcodeproj/project.pbxproj @@ -984,6 +984,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 */; }; @@ -3193,6 +3195,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 = ""; }; @@ -8545,6 +8549,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 = ( @@ -10262,11 +10292,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 = ""; @@ -14752,6 +14782,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 */, @@ -15362,6 +15393,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 */, diff --git a/Nynja/Generated/LocalizableConstants.swift b/Nynja/Generated/LocalizableConstants.swift index ed092328b..2fb3716d7 100644 --- a/Nynja/Generated/LocalizableConstants.swift +++ b/Nynja/Generated/LocalizableConstants.swift @@ -616,11 +616,11 @@ 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") } /// meters static var meters: String { return localizable.tr("Localizable", "meters") } @@ -802,7 +802,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/Modules/Message/Models/Statuses/ChatStatus.swift b/Nynja/Modules/Message/Models/Statuses/ChatStatus.swift new file mode 100644 index 000000000..1cedf26b2 --- /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 000000000..dd1a460ba --- /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?, ActionStatus) +} 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 100% rename from Nynja/Modules/Message/Models/Statuses/ActionStatus.swift rename to Nynja/Modules/Message/Models/Statuses/Typing/ActionStatus.swift diff --git a/Nynja/Modules/Message/Models/Statuses/RecordingStatus.swift b/Nynja/Modules/Message/Models/Statuses/Typing/RecordingStatus.swift similarity index 100% rename from Nynja/Modules/Message/Models/Statuses/RecordingStatus.swift rename to Nynja/Modules/Message/Models/Statuses/Typing/RecordingStatus.swift diff --git a/Nynja/Modules/Message/Models/Statuses/SendingStatus.swift b/Nynja/Modules/Message/Models/Statuses/Typing/SendingStatus.swift similarity index 100% rename from Nynja/Modules/Message/Models/Statuses/SendingStatus.swift rename to Nynja/Modules/Message/Models/Statuses/Typing/SendingStatus.swift diff --git a/Nynja/Modules/Message/Presenter/MessagePresenter.swift b/Nynja/Modules/Message/Presenter/MessagePresenter.swift index fc4d0a0ce..836148571 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 case .done = status { + 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) @@ -75,7 +105,8 @@ class MessagePresenter: BasePresenter, MessagePresenterProtocol, MessageInteract } - //MARK: - MessagePresenterProtocol + // MARK: - MessagePresenterProtocol + weak var view: MessageViewProtocol! var wireFrame: MessageWireframeProtocol! var interactor: MessageInteractorInputProtocol! { @@ -926,31 +957,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 + headerStatus.presence = status } func didReceiveTyping(_ typing: TypingDisplayModel) { - switch typing { - case .done: - restoreStatus() - case let .typing(sender, status): - let displayString = sender?.displayName.flatMap { "\($0) \(status.title)" } ?? status.title - view.updateHeaderStatus(displayString) - } - } - - private func restoreStatus() { - view.updateHeaderStatus(lastStatus) + headerStatus.typing = typing } func messageSent(_ localId: String) { @@ -992,12 +1007,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 89a876acc..c501e69f7 100644 --- a/Nynja/Modules/Message/Protocols/MessageProtocols.swift +++ b/Nynja/Modules/Message/Protocols/MessageProtocols.swift @@ -289,7 +289,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 fb54b6343..6dcb86247 100644 --- a/Nynja/Modules/Message/View/MessageVC.swift +++ b/Nynja/Modules/Message/View/MessageVC.swift @@ -1116,7 +1116,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/Views/AvatarView/AvatarView.swift b/Nynja/Modules/Message/View/Views/AvatarView/AvatarView.swift index bf039f735..c7d450f76 100644 --- a/Nynja/Modules/Message/View/Views/AvatarView/AvatarView.swift +++ b/Nynja/Modules/Message/View/Views/AvatarView/AvatarView.swift @@ -15,28 +15,41 @@ final class AvatarView: BaseView { return [statusLabel, separatorView] } - var status: String? { + var status: ChatStatusDisplayInfo? { didSet { - if let title = status?.replacingOccurrences(of: "...", with: ""), title.contains("typing") { - statusLabel.isHidden = true - typingView.isHidden = false + guard let status = status else { + return + } + + switch status { + case let .text(statusString): + statusLabel.text = statusString + statusLabel.accessibilityValue = statusString + + statusLabel.isHidden = false + typingView.isHidden = true + + case let .typing(sender, status): + let indicator: TypingView.Appearance.Indicator + switch status { + case .recording: + indicator = .circle(UIColor.nynja.white) + case .sending, .typing, .done: + indicator = .dots(UIColor.nynja.white) + } - let appearance = TypingView.Appearance(indicator: .dots(UIColor.white), + let appearance = TypingView.Appearance(indicator: indicator, textColor: titleLabel.textColor, textFont: statusLabel.font, - senderInfo: "typing", - typingInfo: "", + senderInfo: sender?.displayName, + typingInfo: status.title, isTypingInfoPinned: false ) typingView.update(appearance) - } else { - statusLabel.isHidden = false - typingView.isHidden = true - statusLabel.text = status + statusLabel.isHidden = true + typingView.isHidden = false } - statusLabel.text = status - statusLabel.accessibilityValue = status } } diff --git a/Nynja/Resources/en.lproj/Localizable.strings b/Nynja/Resources/en.lproj/Localizable.strings index ebadea743..5d1b71a0f 100644 --- a/Nynja/Resources/en.lproj/Localizable.strings +++ b/Nynja/Resources/en.lproj/Localizable.strings @@ -482,9 +482,9 @@ "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"; // MARK: Sending Status "file"="file"; @@ -492,7 +492,7 @@ // MARK: Recording Status "video"="video"; -"recording"="...recording a"; +"recording"="recording"; // MARK: Presence status "active"="active"; diff --git a/Nynja/Statuses/TypingDisplayModel.swift b/Nynja/Statuses/TypingDisplayModel.swift index 24e962cf4..7da267f34 100644 --- a/Nynja/Statuses/TypingDisplayModel.swift +++ b/Nynja/Statuses/TypingDisplayModel.swift @@ -8,7 +8,7 @@ enum TypingDisplayModel { case typing(SenderInfo?, ActionStatus) - case done + case none struct SenderInfo { var senders: [String] diff --git a/Nynja/Statuses/TypingStatusProvider.swift b/Nynja/Statuses/TypingStatusProvider.swift index 16d6666a9..ba0be82e9 100644 --- a/Nynja/Statuses/TypingStatusProvider.swift +++ b/Nynja/Statuses/TypingStatusProvider.swift @@ -142,7 +142,7 @@ final class TypingStatusProviderImpl: TypingStatusProvider, KeyedObservableConta private func displayInfo(for typing: TypingData?) -> TypingDisplayModel { guard let typingInfo = typing?.senders, !typingInfo.isEmpty, let lastStatus = typingInfo.last?.status else { - return .done + return .none } let senders: [String] = typingInfo.compactMap { guard lastStatus == $0.status else { -- GitLab From a8a1ac73363df7c4c975936d1a58c898611e2e5c Mon Sep 17 00:00:00 2001 From: Anton Poltoratskyi Date: Wed, 31 Oct 2018 11:12:30 +0200 Subject: [PATCH 39/64] [NY-4699] Fixed left offset for typing view. --- Frameworks/NynjaUIKit/NynjaUIKit/Views/Typing/TypingView.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Frameworks/NynjaUIKit/NynjaUIKit/Views/Typing/TypingView.swift b/Frameworks/NynjaUIKit/NynjaUIKit/Views/Typing/TypingView.swift index 5ffd98f8d..9cc14b774 100644 --- a/Frameworks/NynjaUIKit/NynjaUIKit/Views/Typing/TypingView.swift +++ b/Frameworks/NynjaUIKit/NynjaUIKit/Views/Typing/TypingView.swift @@ -139,7 +139,7 @@ public final class TypingView: BaseView { } enum senderInfo { - static let leftOffset: CGFloat = 8 + static let leftOffset: CGFloat = 4 } } } -- GitLab From 3367cb9276973d310bebf4cf88b501d58b85785e Mon Sep 17 00:00:00 2001 From: Anton Poltoratskyi Date: Wed, 31 Oct 2018 11:23:13 +0200 Subject: [PATCH 40/64] [NY-4699] Don't remake layout if the same views exists in typing container. --- .../NynjaUIKit/Views/Typing/TypingView.swift | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/Frameworks/NynjaUIKit/NynjaUIKit/Views/Typing/TypingView.swift b/Frameworks/NynjaUIKit/NynjaUIKit/Views/Typing/TypingView.swift index 9cc14b774..6e3bc5c57 100644 --- a/Frameworks/NynjaUIKit/NynjaUIKit/Views/Typing/TypingView.swift +++ b/Frameworks/NynjaUIKit/NynjaUIKit/Views/Typing/TypingView.swift @@ -77,7 +77,7 @@ public final class TypingView: BaseView { // MARK: - Layout public func update(_ appearance: Appearance) { - indicatorContainer.subviews.forEach { $0.removeFromSuperview() } + switch appearance.indicator { case let .dots(color): @@ -93,6 +93,10 @@ public final class TypingView: BaseView { } 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 @@ -107,6 +111,10 @@ public final class TypingView: BaseView { } 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) -- GitLab From 6dffbd91e7dd406e0fd33373384eb81b5573b2d1 Mon Sep 17 00:00:00 2001 From: Anton Poltoratskyi Date: Wed, 31 Oct 2018 17:24:13 +0200 Subject: [PATCH 41/64] [NY-4699] Implemented notifying UI on chat lists. --- Nynja/Extensions/Models/StarExtension.swift | 4 ++ .../Cell/ChatListMessageContentView.swift | 14 ++--- .../Cell/ChatListMessageTableViewCell.swift | 53 +++++++++++++++++++ .../Model/ChatListMessageCellModel.swift | 7 ++- .../ChatsList/ChatsListProtocols.swift | 9 +++- .../Interactor/ChatsListInteractor.swift | 23 ++++++++ .../Presenter/ChatsListPresenter.swift | 8 +++ .../ChatsList/View/ChatListTableDS.swift | 11 +++- .../View/ChatsListViewController.swift | 9 +++- .../Presenter/Contact+DialogCellModel.swift | 5 +- .../Profile/Presenter/DialogCellModel.swift | 1 + .../Presenter/Room+DialogCellModel.swift | 5 +- 12 files changed, 129 insertions(+), 20 deletions(-) diff --git a/Nynja/Extensions/Models/StarExtension.swift b/Nynja/Extensions/Models/StarExtension.swift index 95cbaf53a..450e052fc 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/Library/UI/Lists/TableView/Cells/ChatListMessageCell/Cell/ChatListMessageContentView.swift b/Nynja/Library/UI/Lists/TableView/Cells/ChatListMessageCell/Cell/ChatListMessageContentView.swift index 71c5eda1e..1ad7e579f 100644 --- a/Nynja/Library/UI/Lists/TableView/Cells/ChatListMessageCell/Cell/ChatListMessageContentView.swift +++ b/Nynja/Library/UI/Lists/TableView/Cells/ChatListMessageCell/Cell/ChatListMessageContentView.swift @@ -75,17 +75,9 @@ final class ChatListMessageContentView: BaseView { textView.setup(sender: sender, image: image, text: text) } - func showTyping(sender: String?) { + func showTyping(with appearance: TypingView.Appearance) { typingView.isHidden = false textView.isHidden = true - - let appearance = TypingView.Appearance(indicator: .circle(UIColor.nynja.white), - textColor: titleLabel.textColor, - textFont: typingFont, - senderInfo: sender, - typingInfo: "typing", - isTypingInfoPinned: false) - typingView.update(appearance) } @@ -97,8 +89,8 @@ final class ChatListMessageContentView: BaseView { // MARK: - Layout - private let typingFont = UIFont.makeFont(with: FontFamily.NotoSans.regular.name, - height: Constraints.typingView.height.adjustedByWidth)! + static let typingFont = UIFont.makeFont(with: FontFamily.NotoSans.regular.name, + height: Constraints.typingView.height.adjustedByWidth)! private enum Constraints { diff --git a/Nynja/Library/UI/Lists/TableView/Cells/ChatListMessageCell/Cell/ChatListMessageTableViewCell.swift b/Nynja/Library/UI/Lists/TableView/Cells/ChatListMessageCell/Cell/ChatListMessageTableViewCell.swift index 0230b9458..7a9b71f5e 100644 --- a/Nynja/Library/UI/Lists/TableView/Cells/ChatListMessageCell/Cell/ChatListMessageTableViewCell.swift +++ b/Nynja/Library/UI/Lists/TableView/Cells/ChatListMessageCell/Cell/ChatListMessageTableViewCell.swift @@ -12,6 +12,10 @@ import NynjaUIKit final class ChatListMessageTableViewCell: UITableViewCell { + private var observable: TypingObservable? + private var feedId: FeedId? + + // MARK: - Views private(set) lazy var avatarImageView: AvatarStatusView = { @@ -106,6 +110,55 @@ final class ChatListMessageTableViewCell: UITableViewCell { messageContentView.isHidden = false separatorView.isHidden = false } + + func setup(observable: TypingObservable?, feedId: FeedId) { + self.observable = observable + self.feedId = feedId + + self.observable?.observeChanges(for: feedId) { [weak self] typing in + self?.handleTyping(typing) + } + } + + + // MARK: - Life Cycle + + override func prepareForReuse() { + super.prepareForReuse() + messageContentView.hideTyping() + + guard let feedId = feedId else { + return + } + observable?.removeObserver(for: feedId) + self.feedId = nil + } + + + private func handleTyping(_ typing: TypingDisplayModel) { + switch typing { + case let .typing(sender, status): + let indicator: TypingView.Appearance.Indicator + switch status { + case .recording: + indicator = .circle(UIColor.nynja.white) + case .sending, .typing, .done: + indicator = .dots(UIColor.nynja.white) + } + + let appearance = TypingView.Appearance(indicator: indicator, + textColor: UIColor.nynja.white, + textFont: ChatListMessageContentView.typingFont, + senderInfo: sender?.displayName, + typingInfo: status.title, + isTypingInfoPinned: false + ) + messageContentView.showTyping(with: appearance) + + case .none: + messageContentView.hideTyping() + } + } } // MARK: - Layout diff --git a/Nynja/Library/UI/Lists/TableView/Cells/ChatListMessageCell/Model/ChatListMessageCellModel.swift b/Nynja/Library/UI/Lists/TableView/Cells/ChatListMessageCell/Model/ChatListMessageCellModel.swift index c5ee537d9..5cfa7a1b4 100644 --- a/Nynja/Library/UI/Lists/TableView/Cells/ChatListMessageCell/Model/ChatListMessageCellModel.swift +++ b/Nynja/Library/UI/Lists/TableView/Cells/ChatListMessageCell/Model/ChatListMessageCellModel.swift @@ -17,8 +17,9 @@ final class ChatListMessageCellModel: CellViewModel { return "chatList_cell" } - private let payloadParser: MessagePayloadParserInput private let model: DialogCellModel + private let observable: TypingObservable? + private let payloadParser: MessagePayloadParserInput private weak var delegate: ChatListMessageCellModelDelegate? private var sender: String? { @@ -26,9 +27,11 @@ final class ChatListMessageCellModel: CellViewModel { } init(model: DialogCellModel, + observable: TypingObservable? = nil, payloadParser: MessagePayloadParserInput, delegate: ChatListMessageCellModelDelegate?) { self.model = model + self.observable = observable self.payloadParser = payloadParser self.delegate = delegate } @@ -37,6 +40,7 @@ final class ChatListMessageCellModel: CellViewModel { setupAvatar(in: cell) setupAccessory(in: cell) setupMessage(model.message, in: cell) + cell.setup(observable: observable, feedId: model.feedId) } @@ -74,7 +78,6 @@ final class ChatListMessageCellModel: CellViewModel { let type = SendMessageType(rawValue: mime) else { return } - cell.messageContentView.showTyping(sender: sender) switch type { case .text: diff --git a/Nynja/Modules/ChatsList/ChatsListProtocols.swift b/Nynja/Modules/ChatsList/ChatsListProtocols.swift index c5aafbdb5..cbce84e51 100644 --- a/Nynja/Modules/ChatsList/ChatsListProtocols.swift +++ b/Nynja/Modules/ChatsList/ChatsListProtocols.swift @@ -8,6 +8,11 @@ import UIKit +protocol TypingObservable: class { + func observeChanges(for feedId: FeedId, handler: @escaping (TypingDisplayModel) -> ()) + func removeObserver(for feedId: FeedId) +} + protocol ChatsListWireFrameProtocol: class { func presentChatsList(navigation: UINavigationController, main: MainWireFrame?, animated: Bool) @@ -28,7 +33,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 +55,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 02aa17311..c110a358c 100644 --- a/Nynja/Modules/ChatsList/Interactor/ChatsListInteractor.swift +++ b/Nynja/Modules/ChatsList/Interactor/ChatsListInteractor.swift @@ -12,10 +12,13 @@ class ChatsListInteractor: BaseInteractor, ChatsListInteractorInputProtocol, Ini private let storageService: StorageService private let conversationsProvider: ConversationsProviding + private let typingProvider: TypingStatusProvider private var chats: [Contact] = [] private var searchText: String = "" + private var typingHandlers: [FeedId: (TypingDisplayModel) -> ()] = [:] + // MARK: - InitializeInjectable @@ -27,6 +30,14 @@ class ChatsListInteractor: BaseInteractor, ChatsListInteractorInputProtocol, Ini required init(dependencies: Dependencies) { storageService = dependencies.storageService conversationsProvider = dependencies.conversationsProvider + // FIXME: move to factory + typingProvider = TypingStatusProviderImpl(dependencies: .init(typingHandler: TypingHandler.shared)) + + super.init() + } + + deinit { + typingProvider.removeObserver(self) } @@ -39,6 +50,10 @@ class ChatsListInteractor: BaseInteractor, ChatsListInteractorInputProtocol, Ini override func loadData() { super.loadData() fetchChats() + + typingProvider.addObserver(self) { [weak self] feedId, typing in + self?.typingHandlers[feedId]?(typing) + } } @@ -49,6 +64,14 @@ class ChatsListInteractor: BaseInteractor, ChatsListInteractorInputProtocol, Ini applyFilter(with: searchText) } + func observeChanges(for feedId: FeedId, handler: @escaping (TypingDisplayModel) -> ()) { + typingHandlers[feedId] = handler + } + + 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 6456463eb..2db4c11d7 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 174497229..1a8c94bc1 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 b5bd6f8a1..d4899b1ad 100644 --- a/Nynja/Modules/ChatsList/View/ChatsListViewController.swift +++ b/Nynja/Modules/ChatsList/View/ChatsListViewController.swift @@ -126,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 @@ -138,6 +138,13 @@ final class ChatsListViewController: BaseVC, ChatsListViewProtocol, BackSwipable extension ChatsListViewController: UITableViewDelegate { + func tableView(_ tableView: UITableView, didEndDisplaying cell: UITableViewCell, forRowAt indexPath: IndexPath) { + guard let feedId = dataSource.chatList[indexPath.row].phone_id else { + return + } + presenter.removeObserver(for: feedId) + } + func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { tableView.deselectRow(at: indexPath, animated: false) prepareForDissappear() diff --git a/Nynja/Modules/Profile/Presenter/Contact+DialogCellModel.swift b/Nynja/Modules/Profile/Presenter/Contact+DialogCellModel.swift index 1001ca59a..45a78599e 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 c247a3172..38e1392c2 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/Room+DialogCellModel.swift b/Nynja/Modules/Profile/Presenter/Room+DialogCellModel.swift index 1e8cd4a00..b22acc76f 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! : "" } -- GitLab From 2fb295ed60f46082a175b8c58b7d4a829cabb6ef Mon Sep 17 00:00:00 2001 From: Anton Poltoratskyi Date: Wed, 31 Oct 2018 18:05:49 +0200 Subject: [PATCH 42/64] [NY-4699] Fixed `done` typing status. --- .../Cell/ChatListMessageTableViewCell.swift | 25 +++++++++++++------ .../View/ChatsListViewController.swift | 7 ------ .../Models/Statuses/Typing/ActionStatus.swift | 7 ++++++ 3 files changed, 25 insertions(+), 14 deletions(-) diff --git a/Nynja/Library/UI/Lists/TableView/Cells/ChatListMessageCell/Cell/ChatListMessageTableViewCell.swift b/Nynja/Library/UI/Lists/TableView/Cells/ChatListMessageCell/Cell/ChatListMessageTableViewCell.swift index 7a9b71f5e..c12c21936 100644 --- a/Nynja/Library/UI/Lists/TableView/Cells/ChatListMessageCell/Cell/ChatListMessageTableViewCell.swift +++ b/Nynja/Library/UI/Lists/TableView/Cells/ChatListMessageCell/Cell/ChatListMessageTableViewCell.swift @@ -101,6 +101,10 @@ final class ChatListMessageTableViewCell: UITableViewCell { setup() } + deinit { + removeObserver() + } + // MARK: - Setup @@ -126,18 +130,17 @@ final class ChatListMessageTableViewCell: UITableViewCell { override func prepareForReuse() { super.prepareForReuse() messageContentView.hideTyping() - - guard let feedId = feedId else { - return - } - observable?.removeObserver(for: feedId) - self.feedId = nil + removeObserver() } - private func handleTyping(_ typing: TypingDisplayModel) { switch typing { case let .typing(sender, status): + guard !status.isDone else { + messageContentView.hideTyping() + break + } + let indicator: TypingView.Appearance.Indicator switch status { case .recording: @@ -159,6 +162,14 @@ final class ChatListMessageTableViewCell: UITableViewCell { messageContentView.hideTyping() } } + + private func removeObserver() { + guard let feedId = feedId else { + return + } + observable?.removeObserver(for: feedId) + self.feedId = nil + } } // MARK: - Layout diff --git a/Nynja/Modules/ChatsList/View/ChatsListViewController.swift b/Nynja/Modules/ChatsList/View/ChatsListViewController.swift index d4899b1ad..15702f53d 100644 --- a/Nynja/Modules/ChatsList/View/ChatsListViewController.swift +++ b/Nynja/Modules/ChatsList/View/ChatsListViewController.swift @@ -138,13 +138,6 @@ final class ChatsListViewController: BaseVC, ChatsListViewProtocol, BackSwipable extension ChatsListViewController: UITableViewDelegate { - func tableView(_ tableView: UITableView, didEndDisplaying cell: UITableViewCell, forRowAt indexPath: IndexPath) { - guard let feedId = dataSource.chatList[indexPath.row].phone_id else { - return - } - presenter.removeObserver(for: feedId) - } - func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { tableView.deselectRow(at: indexPath, animated: false) prepareForDissappear() diff --git a/Nynja/Modules/Message/Models/Statuses/Typing/ActionStatus.swift b/Nynja/Modules/Message/Models/Statuses/Typing/ActionStatus.swift index 0f0a4e9c0..13c65fdba 100644 --- a/Nynja/Modules/Message/Models/Statuses/Typing/ActionStatus.swift +++ b/Nynja/Modules/Message/Models/Statuses/Typing/ActionStatus.swift @@ -12,6 +12,13 @@ enum ActionStatus: Equatable { 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 -- GitLab From 23f2ecbe8ccc228324e10a50a35ac2987e8eadaa Mon Sep 17 00:00:00 2001 From: Anton Poltoratskyi Date: Wed, 31 Oct 2018 18:44:50 +0200 Subject: [PATCH 43/64] [NY-4699] Implemented thread-safe access to data dictionary inside a TypingStatusProvider. --- .../Interactor/ChatsListInteractor.swift | 1 + Nynja/Statuses/TypingStatusProvider.swift | 35 +++++++++++++++++-- 2 files changed, 33 insertions(+), 3 deletions(-) diff --git a/Nynja/Modules/ChatsList/Interactor/ChatsListInteractor.swift b/Nynja/Modules/ChatsList/Interactor/ChatsListInteractor.swift index c110a358c..0e5138c35 100644 --- a/Nynja/Modules/ChatsList/Interactor/ChatsListInteractor.swift +++ b/Nynja/Modules/ChatsList/Interactor/ChatsListInteractor.swift @@ -66,6 +66,7 @@ class ChatsListInteractor: BaseInteractor, ChatsListInteractorInputProtocol, Ini func observeChanges(for feedId: FeedId, handler: @escaping (TypingDisplayModel) -> ()) { typingHandlers[feedId] = handler + typingProvider.typingStatus(for: feedId).flatMap { handler($0) } } func removeObserver(for feedId: FeedId) { diff --git a/Nynja/Statuses/TypingStatusProvider.swift b/Nynja/Statuses/TypingStatusProvider.swift index ba0be82e9..1405e3d86 100644 --- a/Nynja/Statuses/TypingStatusProvider.swift +++ b/Nynja/Statuses/TypingStatusProvider.swift @@ -19,6 +19,7 @@ protocol TypingStatusObservable: class { } protocol TypingStatusProvider: TypingStatusObservable { + func typingStatus(for feedId: FeedId) -> TypingDisplayModel? } final class TypingStatusProviderImpl: TypingStatusProvider, KeyedObservableContainer, TypingHandlerDelegate, InitializeInjectable { @@ -31,6 +32,8 @@ final class TypingStatusProviderImpl: TypingStatusProvider, KeyedObservableConta private let typingDismissInterval = 10.0 + private let isolationQueue = DispatchQueue(label: "com.nynja.typing-status-provider", attributes: .concurrent) + private let processingQueue: DispatchQueue private let notifyQueue: DispatchQueue @@ -61,6 +64,13 @@ final class TypingStatusProviderImpl: TypingStatusProvider, KeyedObservableConta } + // MARK: - TypingStatusProvider + + func typingStatus(for feedId: FeedId) -> TypingDisplayModel? { + return typing(for: feedId).flatMap { displayInfo(for: $0) } + } + + // MARK: - TypingHandlerDelegate func didReceiveTyping(_ typing: Typing) { @@ -80,7 +90,7 @@ final class TypingStatusProviderImpl: TypingStatusProvider, KeyedObservableConta private func save(_ typingInfo: SenderInfo) { let feedId = typingInfo.feed.identifier - if let oldTyping = data[feedId] { + if let oldTyping = typing(for: feedId) { switch oldTyping { case .p2p: update(.p2p(typingInfo), for: feedId) @@ -118,7 +128,7 @@ final class TypingStatusProviderImpl: TypingStatusProvider, KeyedObservableConta } private func update(_ typing: TypingData?, for feedId: FeedId) { - data[feedId] = typing + set(typing, for: feedId) let displayInfo = self.displayInfo(for: typing) notifyQueue.async { @@ -127,7 +137,7 @@ final class TypingStatusProviderImpl: TypingStatusProvider, KeyedObservableConta } private func remove(_ typing: SenderInfo, for feedId: FeedId) { - guard let currentTyping = data[feedId] else { + guard let currentTyping = self.typing(for: feedId) else { return } switch currentTyping { @@ -154,6 +164,25 @@ final class TypingStatusProviderImpl: TypingStatusProvider, KeyedObservableConta return .typing(senderInfo, lastStatus) } + + + // 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 + } } -- GitLab From 37a2c95b331f2065818f18e30d2646cb0e9eb46b Mon Sep 17 00:00:00 2001 From: Anton Poltoratskyi Date: Wed, 31 Oct 2018 18:58:16 +0200 Subject: [PATCH 44/64] [NY-4699] Ask for typing status when chat screen is loaded. --- Nynja/Modules/Message/Interactor/MessageInteractor.swift | 7 +++++++ Nynja/Modules/Message/Presenter/MessagePresenter.swift | 1 + Nynja/Modules/Message/Protocols/MessageProtocols.swift | 1 + 3 files changed, 9 insertions(+) diff --git a/Nynja/Modules/Message/Interactor/MessageInteractor.swift b/Nynja/Modules/Message/Interactor/MessageInteractor.swift index 50578f17a..d57eb3f51 100644 --- a/Nynja/Modules/Message/Interactor/MessageInteractor.swift +++ b/Nynja/Modules/Message/Interactor/MessageInteractor.swift @@ -444,6 +444,13 @@ final class MessageInteractor: BaseInteractor, MessageInteractorInputProtocol, H presenter?.internetStatusChanged(.waiting) } } + + func askForTypingStatus() { + guard let feedId = chat.id, let typing = typingStatusProvider.typingStatus(for: feedId) else { + return + } + presenter?.didReceiveTyping(typing) + } // MARK: - Send Message func sendMessage(_ message: InputTextMessage) { diff --git a/Nynja/Modules/Message/Presenter/MessagePresenter.swift b/Nynja/Modules/Message/Presenter/MessagePresenter.swift index 836148571..52796e279 100644 --- a/Nynja/Modules/Message/Presenter/MessagePresenter.swift +++ b/Nynja/Modules/Message/Presenter/MessagePresenter.swift @@ -101,6 +101,7 @@ final class MessagePresenter: BasePresenter, MessagePresenterProtocol, MessageIn super.screenLoaded() interactor.askForInternetStatus() + interactor.askForTypingStatus() interactor.room.map { fetchMentionedMessages(in: $0) } } diff --git a/Nynja/Modules/Message/Protocols/MessageProtocols.swift b/Nynja/Modules/Message/Protocols/MessageProtocols.swift index c501e69f7..78e3eb66b 100644 --- a/Nynja/Modules/Message/Protocols/MessageProtocols.swift +++ b/Nynja/Modules/Message/Protocols/MessageProtocols.swift @@ -244,6 +244,7 @@ protocol MessageInteractorInputProtocol: BaseInteractorProtocol, MentionFetchInp func sender(for message: Message) -> MessageSender func askForInternetStatus() + func askForTypingStatus() func editMessage(_ message: InputTextMessage) func clearEditMessageObject() -- GitLab From 8e74ab3e44c8632aaaf1fc2e0dc6fbf3d0e7ceb1 Mon Sep 17 00:00:00 2001 From: Anton Poltoratskyi Date: Wed, 31 Oct 2018 19:22:54 +0200 Subject: [PATCH 45/64] [NY-4699] Pass typing data from GroupsListInteractor to group list cells. --- .../GroupsList/GroupsListProtocols.swift | 4 ++-- .../Interactor/GroupsListInteractor.swift | 20 +++++++++++++++++++ .../Presenter/GroupsListPresenter.swift | 8 ++++++++ .../GroupsList/View/GroupsListTableDS.swift | 15 +++++++++++--- .../View/GroupsListViewController.swift | 2 +- 5 files changed, 43 insertions(+), 6 deletions(-) diff --git a/Nynja/Modules/GroupsList/GroupsListProtocols.swift b/Nynja/Modules/GroupsList/GroupsListProtocols.swift index 6f973d065..f10ba5bd2 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 c16ee18b5..d0109e5dd 100644 --- a/Nynja/Modules/GroupsList/Interactor/GroupsListInteractor.swift +++ b/Nynja/Modules/GroupsList/Interactor/GroupsListInteractor.swift @@ -12,10 +12,13 @@ class GroupsListInteractor: BaseInteractor, GroupsListInteractorInputProtocol, I private let storageService: StorageService private let conversationsProvider: ConversationsProviding + private let typingProvider: TypingStatusProvider private var chats: [Room] = [] private var searchText: String = "" + private var typingHandlers: [FeedId: (TypingDisplayModel) -> ()] = [:] + // MARK: - InitializeInjectable @@ -27,6 +30,10 @@ class GroupsListInteractor: BaseInteractor, GroupsListInteractorInputProtocol, I required init(dependencies: Dependencies) { storageService = dependencies.storageService conversationsProvider = dependencies.conversationsProvider + // FIXME: move to factory + typingProvider = TypingStatusProviderImpl(dependencies: .init(typingHandler: TypingHandler.shared)) + + super.init() } // MARK: - BaseInteractor @@ -42,6 +49,10 @@ class GroupsListInteractor: BaseInteractor, GroupsListInteractorInputProtocol, I override func loadData() { super.loadData() fetchGroups() + + typingProvider.addObserver(self) { [weak self] feedId, typing in + self?.typingHandlers[feedId]?(typing) + } } @@ -52,6 +63,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 4cd181f2e..8600c93ae 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 c7a9dbc10..49470a986 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 81fac1e09..1c779d577 100644 --- a/Nynja/Modules/GroupsList/View/GroupsListViewController.swift +++ b/Nynja/Modules/GroupsList/View/GroupsListViewController.swift @@ -109,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 -- GitLab From c0e439f27f9ad9a45044cab9f3fa4833ab59bc52 Mon Sep 17 00:00:00 2001 From: Anton Poltoratskyi Date: Thu, 1 Nov 2018 13:20:00 +0200 Subject: [PATCH 46/64] [NY-4699] Fetch member alias by roomId and phoneId. --- Nynja/DB/Models/DBMember.swift | 46 ++++++++++++++----- Nynja/Services/Member/MemberDAO.swift | 6 +++ Nynja/Services/Member/MemberDAOProtocol.swift | 3 +- 3 files changed, 42 insertions(+), 13 deletions(-) diff --git a/Nynja/DB/Models/DBMember.swift b/Nynja/DB/Models/DBMember.swift index 2b456cbcb..1ef475ada 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/Services/Member/MemberDAO.swift b/Nynja/Services/Member/MemberDAO.swift index 9cf7ed62a..4375b187a 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 a8d461120..bb5b49b1a 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) - } -- GitLab From 1c87030595791cdf9b990e6c1da884bfb9b85899 Mon Sep 17 00:00:00 2001 From: Anton Poltoratskyi Date: Thu, 1 Nov 2018 14:09:16 +0200 Subject: [PATCH 47/64] [NY-4699] Implemented in-chat action based statuses for rooms. --- Nynja/MQTTModels/TypingExtension+BERT.swift | 18 ++++--- Nynja/ServerModel/Model/Typing.swift | 6 ++- Nynja/ServerModel/Source/Decoder.swift | 9 ++-- Nynja/ServerModel/Spec/Typing_Spec.swift | 6 +++ Nynja/Services/Models/TypingModel.swift | 4 +- Nynja/Statuses/TypingStatusProvider.swift | 14 ++++-- .../Messaging/TypingSenderService.swift | 47 ++++++++++--------- 7 files changed, 66 insertions(+), 38 deletions(-) diff --git a/Nynja/MQTTModels/TypingExtension+BERT.swift b/Nynja/MQTTModels/TypingExtension+BERT.swift index 54369b5a1..7f52c0327 100644 --- a/Nynja/MQTTModels/TypingExtension+BERT.swift +++ b/Nynja/MQTTModels/TypingExtension+BERT.swift @@ -9,15 +9,21 @@ import Foundation extension Typing { + func getBert() -> 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/ServerModel/Model/Typing.swift b/Nynja/ServerModel/Model/Typing.swift index f37b18267..e807bc87c 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 bf56d8420..844a4646b 100644 --- a/Nynja/ServerModel/Source/Decoder.swift +++ b/Nynja/ServerModel/Source/Decoder.swift @@ -302,10 +302,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 } @@ -538,3 +540,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 41ceda8a5..bdc43d54c 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/Models/TypingModel.swift b/Nynja/Services/Models/TypingModel.swift index 1306e2159..8f7464947 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/Statuses/TypingStatusProvider.swift b/Nynja/Statuses/TypingStatusProvider.swift index 1405e3d86..ddd2830fa 100644 --- a/Nynja/Statuses/TypingStatusProvider.swift +++ b/Nynja/Statuses/TypingStatusProvider.swift @@ -74,13 +74,17 @@ final class TypingStatusProviderImpl: TypingStatusProvider, KeyedObservableConta // MARK: - TypingHandlerDelegate func didReceiveTyping(_ typing: Typing) { - guard let feedId = typing.phone_id, let typingType = typing.type else { + guard let feedId = typing.feed_id, let typingType = typing.type else { return } + let senderId = typing.sender_id + let senderAlias = typing.sender_alias + + let feed: SenderInfo.Feed = senderId != nil ? .p2p(feedId) : .room(feedId) + let status = ActionStatus(typingModelType: typingType) - // FIXME: implement logic for rooms. - let senderInfo = SenderInfo(feed: .p2p(feedId), senderId: feedId, senderName: nil, status: status) + let senderInfo = SenderInfo(feed: feed, senderId: senderId, senderName: senderAlias, status: status) processingQueue.async { self.save(senderInfo) @@ -220,11 +224,11 @@ private extension TypingStatusProviderImpl { } let feed: Feed - let senderId: String + let senderId: String? let senderName: String? let status: ActionStatus - init(feed: Feed, senderId: String, senderName: String?, status: ActionStatus) { + init(feed: Feed, senderId: String?, senderName: String?, status: ActionStatus) { self.feed = feed self.senderId = senderId self.senderName = senderName diff --git a/Shared/Services/Messaging/TypingSenderService.swift b/Shared/Services/Messaging/TypingSenderService.swift index 5b6ac91d9..bc1cf19c7 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 } } -- GitLab From 170870c6f0d9e4c923a48d3cd47741c6a02627c1 Mon Sep 17 00:00:00 2001 From: Anton Poltoratskyi Date: Thu, 1 Nov 2018 14:19:09 +0200 Subject: [PATCH 48/64] [NY-4699] Refactored TypingStatusProvider. --- Nynja/Statuses/TypingStatusProvider.swift | 45 ++++++++++++++++++----- 1 file changed, 36 insertions(+), 9 deletions(-) diff --git a/Nynja/Statuses/TypingStatusProvider.swift b/Nynja/Statuses/TypingStatusProvider.swift index ddd2830fa..24de72bf5 100644 --- a/Nynja/Statuses/TypingStatusProvider.swift +++ b/Nynja/Statuses/TypingStatusProvider.swift @@ -80,11 +80,16 @@ final class TypingStatusProviderImpl: TypingStatusProvider, KeyedObservableConta let senderId = typing.sender_id let senderAlias = typing.sender_alias - let feed: SenderInfo.Feed = senderId != nil ? .p2p(feedId) : .room(feedId) + let feed: SenderInfo.Feed + if let senderId = typing.sender_id, let alias = typing.sender_alias { + feed = .room(feedId, senderId: senderId, senderName: alias) + } else { + feed = .p2p(feedId) + } let status = ActionStatus(typingModelType: typingType) - let senderInfo = SenderInfo(feed: feed, senderId: senderId, senderName: senderAlias, status: status) + let senderInfo = SenderInfo(feed: feed, status: status) processingQueue.async { self.save(senderInfo) @@ -211,27 +216,49 @@ private extension TypingStatusProviderImpl { final class SenderInfo { enum Feed { case p2p(FeedId) - case room(FeedId) + case room(FeedId, senderId: String, senderName: String) var identifier: String { switch self { case let .p2p(id): return id - case let .room(id): + case let .room(id, _, _): return id } } + + var senderId: String? { + switch self { + case let .room(_, senderId, _): + return senderId + case .p2p: + return nil + } + } + + var senderName: String? { + switch self { + case let .room(_, _, senderName): + return senderName + case .p2p: + return nil + } + } } let feed: Feed - let senderId: String? - let senderName: String? let status: ActionStatus - init(feed: Feed, senderId: String?, senderName: String?, status: ActionStatus) { + var senderId: String? { + return feed.senderId + } + + var senderName: String? { + return feed.senderName + } + + init(feed: Feed, status: ActionStatus) { self.feed = feed - self.senderId = senderId - self.senderName = senderName self.status = status } } -- GitLab From 7690fae46d7714c701807a1e012487fa1dd30d40 Mon Sep 17 00:00:00 2001 From: Anton Poltoratskyi Date: Thu, 1 Nov 2018 15:27:45 +0200 Subject: [PATCH 49/64] [NY-4699] Ignore typing from rooms without sender alias. --- Nynja/Statuses/TypingDisplayModel.swift | 6 ++++- Nynja/Statuses/TypingStatusProvider.swift | 31 +++++++++-------------- 2 files changed, 17 insertions(+), 20 deletions(-) diff --git a/Nynja/Statuses/TypingDisplayModel.swift b/Nynja/Statuses/TypingDisplayModel.swift index 7da267f34..1f548cc0d 100644 --- a/Nynja/Statuses/TypingDisplayModel.swift +++ b/Nynja/Statuses/TypingDisplayModel.swift @@ -14,7 +14,11 @@ enum TypingDisplayModel { var senders: [String] var displayName: String? { - return senders.joined(separator: ", ") + guard !senders.isEmpty, senders.count > 1 else { + return senders.first + } + // FIXME: localize + return "\(senders.count) people ...".localized } } diff --git a/Nynja/Statuses/TypingStatusProvider.swift b/Nynja/Statuses/TypingStatusProvider.swift index 24de72bf5..d766aab03 100644 --- a/Nynja/Statuses/TypingStatusProvider.swift +++ b/Nynja/Statuses/TypingStatusProvider.swift @@ -30,14 +30,14 @@ final class TypingStatusProviderImpl: TypingStatusProvider, KeyedObservableConta private var workItems: [FeedId: DispatchWorkItem] = [:] - private let typingDismissInterval = 10.0 - 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: - Dependencies @@ -77,11 +77,12 @@ final class TypingStatusProviderImpl: TypingStatusProvider, KeyedObservableConta guard let feedId = typing.feed_id, let typingType = typing.type else { return } - let senderId = typing.sender_id - let senderAlias = typing.sender_alias - + let feed: SenderInfo.Feed - if let senderId = typing.sender_id, let alias = typing.sender_alias { + if let senderId = typing.sender_id { + guard let alias = typing.sender_alias else { + return + } feed = .room(feedId, senderId: senderId, senderName: alias) } else { feed = .p2p(feedId) @@ -107,7 +108,7 @@ final class TypingStatusProviderImpl: TypingStatusProvider, KeyedObservableConta case let .room(oldTypingSendersInfo): var newTypingInfo = oldTypingSendersInfo - newTypingInfo.removeAll { typingInfo.senderId == $0.senderId } + newTypingInfo.removeAll { typingInfo.feed.senderId == $0.feed.senderId } newTypingInfo.append(typingInfo) update(.room(newTypingInfo), for: feedId) @@ -164,10 +165,10 @@ final class TypingStatusProviderImpl: TypingStatusProvider, KeyedObservableConta return .none } let senders: [String] = typingInfo.compactMap { - guard lastStatus == $0.status else { - return nil - } - return $0.senderName +// guard lastStatus == $0.status else { +// return nil +// } + return $0.feed.senderName } let senderInfo = TypingDisplayModel.SenderInfo(senders: senders) @@ -249,14 +250,6 @@ private extension TypingStatusProviderImpl { let feed: Feed let status: ActionStatus - var senderId: String? { - return feed.senderId - } - - var senderName: String? { - return feed.senderName - } - init(feed: Feed, status: ActionStatus) { self.feed = feed self.status = status -- GitLab From bfee43d27216c8c9e0a44a7006cb1bbad3576790 Mon Sep 17 00:00:00 2001 From: Anton Poltoratskyi Date: Thu, 1 Nov 2018 16:37:29 +0200 Subject: [PATCH 50/64] [NY-4699] Handle case when 2 or more people are typing in a group. --- Nynja/Generated/LocalizableConstants.swift | 6 +++ .../Cell/ChatListMessageTableViewCell.swift | 6 +-- .../Statuses/ChatStatusDisplayInfo.swift | 2 +- .../Message/Presenter/MessagePresenter.swift | 2 +- .../View/Views/AvatarView/AvatarView.swift | 25 ++++++--- Nynja/Resources/en.lproj/Localizable.strings | 2 + Nynja/Statuses/TypingDisplayModel.swift | 54 ++++++++++++++----- Nynja/Statuses/TypingStatusProvider.swift | 11 ++-- 8 files changed, 76 insertions(+), 32 deletions(-) diff --git a/Nynja/Generated/LocalizableConstants.swift b/Nynja/Generated/LocalizableConstants.swift index 8df7d47f9..cd000668d 100644 --- a/Nynja/Generated/LocalizableConstants.swift +++ b/Nynja/Generated/LocalizableConstants.swift @@ -642,6 +642,12 @@ internal extension String { static var messageStatusEdited: String { return localizable.tr("Localizable", "message_status_edited") } /// 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 diff --git a/Nynja/Library/UI/Lists/TableView/Cells/ChatListMessageCell/Cell/ChatListMessageTableViewCell.swift b/Nynja/Library/UI/Lists/TableView/Cells/ChatListMessageCell/Cell/ChatListMessageTableViewCell.swift index c12c21936..0199e67f9 100644 --- a/Nynja/Library/UI/Lists/TableView/Cells/ChatListMessageCell/Cell/ChatListMessageTableViewCell.swift +++ b/Nynja/Library/UI/Lists/TableView/Cells/ChatListMessageCell/Cell/ChatListMessageTableViewCell.swift @@ -142,10 +142,10 @@ final class ChatListMessageTableViewCell: UITableViewCell { } let indicator: TypingView.Appearance.Indicator - switch status { - case .recording: + switch status.action { + case .recording?: indicator = .circle(UIColor.nynja.white) - case .sending, .typing, .done: + default: indicator = .dots(UIColor.nynja.white) } diff --git a/Nynja/Modules/Message/Models/Statuses/ChatStatusDisplayInfo.swift b/Nynja/Modules/Message/Models/Statuses/ChatStatusDisplayInfo.swift index dd1a460ba..dfd9c1d6f 100644 --- a/Nynja/Modules/Message/Models/Statuses/ChatStatusDisplayInfo.swift +++ b/Nynja/Modules/Message/Models/Statuses/ChatStatusDisplayInfo.swift @@ -8,5 +8,5 @@ enum ChatStatusDisplayInfo { case text(String) - case typing(TypingDisplayModel.SenderInfo?, ActionStatus) + case typing(TypingDisplayModel.SenderInfo?, TypingDisplayModel.Status) } diff --git a/Nynja/Modules/Message/Presenter/MessagePresenter.swift b/Nynja/Modules/Message/Presenter/MessagePresenter.swift index 52796e279..d13ecf56f 100644 --- a/Nynja/Modules/Message/Presenter/MessagePresenter.swift +++ b/Nynja/Modules/Message/Presenter/MessagePresenter.swift @@ -54,7 +54,7 @@ final class MessagePresenter: BasePresenter, MessagePresenterProtocol, MessageIn switch typing { case let .typing(sender, status): - if case .done = status { + if status.isDone { fallthrough } view.updateHeaderStatus(.typing(sender, status)) diff --git a/Nynja/Modules/Message/View/Views/AvatarView/AvatarView.swift b/Nynja/Modules/Message/View/Views/AvatarView/AvatarView.swift index 633c993fd..e2b5566cc 100644 --- a/Nynja/Modules/Message/View/Views/AvatarView/AvatarView.swift +++ b/Nynja/Modules/Message/View/Views/AvatarView/AvatarView.swift @@ -26,15 +26,14 @@ final class AvatarView: BaseView { statusLabel.text = statusString statusLabel.accessibilityValue = statusString - statusLabel.isHidden = false - typingView.isHidden = true + hideTyping() case let .typing(sender, status): let indicator: TypingView.Appearance.Indicator - switch status { - case .recording: + switch status.action { + case .recording?: indicator = .circle(UIColor.nynja.white) - case .sending, .typing, .done: + default: indicator = .dots(UIColor.nynja.white) } @@ -47,8 +46,7 @@ final class AvatarView: BaseView { ) typingView.update(appearance) - statusLabel.isHidden = true - typingView.isHidden = false + showTyping() } } } @@ -192,4 +190,17 @@ final class AvatarView: BaseView { muteLeftConstraint?.update(offset: leftInset) } } + + + // MARK: - Layout + + private func showTyping() { + statusLabel.isHidden = true + typingView.isHidden = false + } + + private func hideTyping() { + statusLabel.isHidden = false + typingView.isHidden = true + } } diff --git a/Nynja/Resources/en.lproj/Localizable.strings b/Nynja/Resources/en.lproj/Localizable.strings index 7a5e97526..a82d4f762 100644 --- a/Nynja/Resources/en.lproj/Localizable.strings +++ b/Nynja/Resources/en.lproj/Localizable.strings @@ -495,6 +495,8 @@ "message_status_typing"="typing"; "message_new_messages"="New messages"; "message_sending"="sending"; +"message_typing_status_people"="%@ people"; +"message_typing_status_undefined"="..."; // MARK: Sending Status "file"="file"; diff --git a/Nynja/Statuses/TypingDisplayModel.swift b/Nynja/Statuses/TypingDisplayModel.swift index 1f548cc0d..99ce063bb 100644 --- a/Nynja/Statuses/TypingDisplayModel.swift +++ b/Nynja/Statuses/TypingDisplayModel.swift @@ -7,32 +7,60 @@ // enum TypingDisplayModel { - case typing(SenderInfo?, ActionStatus) + 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 { - var senders: [String] + let senders: [String] var displayName: String? { guard !senders.isEmpty, senders.count > 1 else { return senders.first } - // FIXME: localize - return "\(senders.count) people ...".localized + return String.localizable.messageTypingStatusPeople(String(senders.count)) } } - var displayName: String? { - guard case let .typing(senderInfo, _) = self else { - return nil + 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 + } } - return senderInfo?.displayName - } - - var status: ActionStatus? { - guard case let .typing(_, status) = self else { + + var action: ActionStatus? { + if case let .action(actionStatus) = self { + return actionStatus + } return nil } - return status + + var isDone: Bool { + return action?.isDone ?? false + } } } diff --git a/Nynja/Statuses/TypingStatusProvider.swift b/Nynja/Statuses/TypingStatusProvider.swift index d766aab03..a6f0d7853 100644 --- a/Nynja/Statuses/TypingStatusProvider.swift +++ b/Nynja/Statuses/TypingStatusProvider.swift @@ -164,15 +164,12 @@ final class TypingStatusProviderImpl: TypingStatusProvider, KeyedObservableConta guard let typingInfo = typing?.senders, !typingInfo.isEmpty, let lastStatus = typingInfo.last?.status else { return .none } - let senders: [String] = typingInfo.compactMap { -// guard lastStatus == $0.status else { -// return nil -// } - return $0.feed.senderName - } + let senders = typingInfo.compactMap { $0.feed.senderName } let senderInfo = TypingDisplayModel.SenderInfo(senders: senders) - return .typing(senderInfo, lastStatus) + let shouldDisplayStatus = !typingInfo.contains { $0.status != lastStatus } + + return .typing(senderInfo, shouldDisplayStatus ? .action(lastStatus) : .undefined) } -- GitLab From 62c029d4e780d731b8a88339763acb4f8665397d Mon Sep 17 00:00:00 2001 From: Anton Poltoratskyi Date: Thu, 1 Nov 2018 17:13:53 +0200 Subject: [PATCH 51/64] [NY-4699] Remove unused AccountStatusProvider. Update interface of TypingStatusProvider. --- Nynja.xcodeproj/project.pbxproj | 4 --- Nynja/Statuses/AccountStatusProvider.swift | 40 ---------------------- Nynja/Statuses/TypingStatusProvider.swift | 7 ++-- 3 files changed, 2 insertions(+), 49 deletions(-) delete mode 100644 Nynja/Statuses/AccountStatusProvider.swift diff --git a/Nynja.xcodeproj/project.pbxproj b/Nynja.xcodeproj/project.pbxproj index 6bc64895a..a42efb344 100644 --- a/Nynja.xcodeproj/project.pbxproj +++ b/Nynja.xcodeproj/project.pbxproj @@ -1133,7 +1133,6 @@ 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 */; }; - 85EB37F82183659C003A2D6F /* AccountStatusProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 85EB37F72183659C003A2D6F /* AccountStatusProvider.swift */; }; 85EB37FB21837235003A2D6F /* KeyedObservableContainer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 85EB37FA21837235003A2D6F /* KeyedObservableContainer.swift */; }; 85EB37FD21837253003A2D6F /* KeyedObservable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 85EB37FC21837253003A2D6F /* KeyedObservable.swift */; }; 85EB37FF21837304003A2D6F /* AccountStatus.swift in Sources */ = {isa = PBXBuildFile; fileRef = 85EB37FE21837304003A2D6F /* AccountStatus.swift */; }; @@ -3352,7 +3351,6 @@ 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 = ""; }; - 85EB37F72183659C003A2D6F /* AccountStatusProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountStatusProvider.swift; sourceTree = ""; }; 85EB37FA21837235003A2D6F /* KeyedObservableContainer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeyedObservableContainer.swift; sourceTree = ""; }; 85EB37FC21837253003A2D6F /* KeyedObservable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeyedObservable.swift; sourceTree = ""; }; 85EB37FE21837304003A2D6F /* AccountStatus.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountStatus.swift; sourceTree = ""; }; @@ -9238,7 +9236,6 @@ isa = PBXGroup; children = ( 85EB37FE21837304003A2D6F /* AccountStatus.swift */, - 85EB37F72183659C003A2D6F /* AccountStatusProvider.swift */, 854834172186FADB002064E1 /* TypingStatusProvider.swift */, 8542B811218879B100A286E5 /* TypingDisplayModel.swift */, ); @@ -15159,7 +15156,6 @@ 4B4266C1204D917800194BC1 /* ActionsView+Layout.swift in Sources */, AF440BA5CEBE5170D082FF60 /* LoginProtocols.swift in Sources */, 85C16C3520D2520E00EDB77E /* StickersDownloadingService.swift in Sources */, - 85EB37F82183659C003A2D6F /* AccountStatusProvider.swift in Sources */, 6D6234F81F1E158600EF375F /* HistoryCell.swift in Sources */, FEA655F62167777E00B44029 /* PaymentInteractor.swift in Sources */, E74EC9EF1FC2DE23007268E6 /* MemberTable.swift in Sources */, diff --git a/Nynja/Statuses/AccountStatusProvider.swift b/Nynja/Statuses/AccountStatusProvider.swift deleted file mode 100644 index 65d15f633..000000000 --- a/Nynja/Statuses/AccountStatusProvider.swift +++ /dev/null @@ -1,40 +0,0 @@ -// -// AccountStatusProvider.swift -// Nynja -// -// Created by Anton Poltoratskyi on 26.10.2018. -// Copyright © 2018 TecSynt Solutions. All rights reserved. -// - -import Foundation - -protocol AccountStatusObservable: class { - typealias Callback = (AccountId, AccountStatus) -> Void - - func addObserver(_ observer: AnyObject, callback: @escaping Callback) - func addObserver(_ observer: AnyObject, for key: AccountId, callback: @escaping Callback) - func removeObserver(_ observer: AnyObject) - func removeObserver(_ observer: AnyObject, for key: AccountId) - func notify(_ key: AccountId, with value: AccountStatus) -} - -protocol AccountStatusProvider: AccountStatusObservable { - func status(for accountId: AccountId) -> AccountStatus - func update(_ status: AccountStatus, for accountId: AccountId) -} - -final class AccountStatusProviderImpl: AccountStatusProvider, KeyedObservableContainer { - - private var data: [AccountId: AccountStatus] = [:] - - private(set) var observable = KeyedObservable() - - func status(for accountId: AccountId) -> AccountStatus { - return data[accountId] ?? .none - } - - func update(_ status: AccountStatus, for accountId: AccountId) { - data[accountId] = status - observable.notify(accountId, with: status) - } -} diff --git a/Nynja/Statuses/TypingStatusProvider.swift b/Nynja/Statuses/TypingStatusProvider.swift index a6f0d7853..aa6fde968 100644 --- a/Nynja/Statuses/TypingStatusProvider.swift +++ b/Nynja/Statuses/TypingStatusProvider.swift @@ -8,17 +8,14 @@ import Foundation -protocol TypingStatusObservable: class { +protocol TypingStatusProvider: 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 notify(_ key: FeedId, with value: TypingDisplayModel) -} - -protocol TypingStatusProvider: TypingStatusObservable { + func typingStatus(for feedId: FeedId) -> TypingDisplayModel? } -- GitLab From e0bc9e79459c87d3c763012e34722430afa2182d Mon Sep 17 00:00:00 2001 From: Anton Poltoratskyi Date: Thu, 1 Nov 2018 17:26:22 +0200 Subject: [PATCH 52/64] [NY-4699] Don't display typing from ourself. --- .../Interactor/ChatsListInteractor.swift | 5 ++++- .../Interactor/GroupsListInteractor.swift | 5 ++++- .../Message/Interactor/MessageInteractor.swift | 5 ++++- Nynja/Statuses/TypingStatusProvider.swift | 15 +++++++++++++-- 4 files changed, 25 insertions(+), 5 deletions(-) diff --git a/Nynja/Modules/ChatsList/Interactor/ChatsListInteractor.swift b/Nynja/Modules/ChatsList/Interactor/ChatsListInteractor.swift index 0e5138c35..7994cb33d 100644 --- a/Nynja/Modules/ChatsList/Interactor/ChatsListInteractor.swift +++ b/Nynja/Modules/ChatsList/Interactor/ChatsListInteractor.swift @@ -31,7 +31,10 @@ class ChatsListInteractor: BaseInteractor, ChatsListInteractorInputProtocol, Ini storageService = dependencies.storageService conversationsProvider = dependencies.conversationsProvider // FIXME: move to factory - typingProvider = TypingStatusProviderImpl(dependencies: .init(typingHandler: TypingHandler.shared)) + typingProvider = TypingStatusProviderImpl(dependencies: + .init(storageService: StorageService.sharedInstance, + typingHandler: TypingHandler.shared) + ) super.init() } diff --git a/Nynja/Modules/GroupsList/Interactor/GroupsListInteractor.swift b/Nynja/Modules/GroupsList/Interactor/GroupsListInteractor.swift index d0109e5dd..86de8260e 100644 --- a/Nynja/Modules/GroupsList/Interactor/GroupsListInteractor.swift +++ b/Nynja/Modules/GroupsList/Interactor/GroupsListInteractor.swift @@ -31,7 +31,10 @@ class GroupsListInteractor: BaseInteractor, GroupsListInteractorInputProtocol, I storageService = dependencies.storageService conversationsProvider = dependencies.conversationsProvider // FIXME: move to factory - typingProvider = TypingStatusProviderImpl(dependencies: .init(typingHandler: TypingHandler.shared)) + typingProvider = TypingStatusProviderImpl(dependencies: + .init(storageService: StorageService.sharedInstance, + typingHandler: TypingHandler.shared) + ) super.init() } diff --git a/Nynja/Modules/Message/Interactor/MessageInteractor.swift b/Nynja/Modules/Message/Interactor/MessageInteractor.swift index 9902585bc..e5dacf53a 100644 --- a/Nynja/Modules/Message/Interactor/MessageInteractor.swift +++ b/Nynja/Modules/Message/Interactor/MessageInteractor.swift @@ -178,7 +178,10 @@ final class MessageInteractor: BaseInteractor, MessageInteractorInputProtocol, H payloadParser = MessagePayloadParser() payloadBuilder = MessagePayloadBuilder() stickersProvider = StickersProvider(dependencies: .init(storage: StorageService.sharedInstance)) - typingStatusProvider = TypingStatusProviderImpl(dependencies: .init(typingHandler: TypingHandler.shared)) + typingStatusProvider = TypingStatusProviderImpl(dependencies: + .init(storageService: StorageService.sharedInstance, + typingHandler: TypingHandler.shared) + ) super.init() diff --git a/Nynja/Statuses/TypingStatusProvider.swift b/Nynja/Statuses/TypingStatusProvider.swift index aa6fde968..8e26b9816 100644 --- a/Nynja/Statuses/TypingStatusProvider.swift +++ b/Nynja/Statuses/TypingStatusProvider.swift @@ -39,17 +39,21 @@ final class TypingStatusProviderImpl: TypingStatusProvider, KeyedObservableConta // 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 init(dependencies: Dependencies) { + storageService = dependencies.storageService typingHandler = dependencies.typingHandler processingQueue = dependencies.processingQueue notifyQueue = dependencies.notifyQueue @@ -71,10 +75,9 @@ final class TypingStatusProviderImpl: TypingStatusProvider, KeyedObservableConta // MARK: - TypingHandlerDelegate func didReceiveTyping(_ typing: Typing) { - guard let feedId = typing.feed_id, let typingType = typing.type else { + guard let feedId = typing.feed_id, let typingType = typing.type, shouldHandleTyping(typing) else { return } - let feed: SenderInfo.Feed if let senderId = typing.sender_id { guard let alias = typing.sender_alias else { @@ -94,6 +97,14 @@ final class TypingStatusProviderImpl: TypingStatusProvider, KeyedObservableConta } } + private func shouldHandleTyping(_ typing: Typing) -> Bool { + // Don't handle typing from us (in group chat) + if let senderId = typing.sender_id, senderId == storageService.phoneId { + return false + } + return true + } + private func save(_ typingInfo: SenderInfo) { let feedId = typingInfo.feed.identifier -- GitLab From 9dcb2dcf3285b2d3071f535b428beaee71628673 Mon Sep 17 00:00:00 2001 From: Anton Poltoratskyi Date: Thu, 1 Nov 2018 17:36:40 +0200 Subject: [PATCH 53/64] [NY-4699] Rename TypingStatusProvider to TypingProvider. --- Nynja.xcodeproj/project.pbxproj | 8 ++++---- .../ChatsList/Interactor/ChatsListInteractor.swift | 4 ++-- .../GroupsList/Interactor/GroupsListInteractor.swift | 4 ++-- .../Modules/Message/Interactor/MessageInteractor.swift | 10 +++++----- ...TypingStatusProvider.swift => TypingProvider.swift} | 10 +++++----- 5 files changed, 18 insertions(+), 18 deletions(-) rename Nynja/Statuses/{TypingStatusProvider.swift => TypingProvider.swift} (96%) diff --git a/Nynja.xcodeproj/project.pbxproj b/Nynja.xcodeproj/project.pbxproj index a42efb344..aaadae856 100644 --- a/Nynja.xcodeproj/project.pbxproj +++ b/Nynja.xcodeproj/project.pbxproj @@ -972,7 +972,7 @@ 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 /* TypingStatusProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 854834172186FADB002064E1 /* TypingStatusProvider.swift */; }; + 854834182186FADB002064E1 /* TypingProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 854834172186FADB002064E1 /* TypingProvider.swift */; }; 8548341B2187449F002064E1 /* Observable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8548341A2187449F002064E1 /* Observable.swift */; }; 8548341D218744AC002064E1 /* ObservableContainer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8548341C218744AC002064E1 /* ObservableContainer.swift */; }; 854A4B2C2080D68200759152 /* CellWithArrowTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 854A4B2A2080D68200759152 /* CellWithArrowTableViewCell.swift */; }; @@ -3201,7 +3201,7 @@ 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 /* TypingStatusProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TypingStatusProvider.swift; sourceTree = ""; }; + 854834172186FADB002064E1 /* TypingProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TypingProvider.swift; sourceTree = ""; }; 8548341A2187449F002064E1 /* Observable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Observable.swift; sourceTree = ""; }; 8548341C218744AC002064E1 /* ObservableContainer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ObservableContainer.swift; sourceTree = ""; }; 854A4B2A2080D68200759152 /* CellWithArrowTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CellWithArrowTableViewCell.swift; sourceTree = ""; }; @@ -9236,7 +9236,7 @@ isa = PBXGroup; children = ( 85EB37FE21837304003A2D6F /* AccountStatus.swift */, - 854834172186FADB002064E1 /* TypingStatusProvider.swift */, + 854834172186FADB002064E1 /* TypingProvider.swift */, 8542B811218879B100A286E5 /* TypingDisplayModel.swift */, ); path = Statuses; @@ -16378,7 +16378,7 @@ 850D220020D2E7E20018BBA4 /* SelectionFeedbackInteractive.swift in Sources */, 43711F24FF65C36730467BFF /* EditPhotoViewController.swift in Sources */, A42D519F206A361400EEB952 /* messageEvent.swift in Sources */, - 854834182186FADB002064E1 /* TypingStatusProvider.swift in Sources */, + 854834182186FADB002064E1 /* TypingProvider.swift in Sources */, F11DF06520BD96D000F3E005 /* GalleryFilterType.swift in Sources */, FBCE841220E525A6003B7558 /* NetworkClient.swift in Sources */, 7A8FE56A8E5D02256D8BE936 /* EditPhotoPresenter.swift in Sources */, diff --git a/Nynja/Modules/ChatsList/Interactor/ChatsListInteractor.swift b/Nynja/Modules/ChatsList/Interactor/ChatsListInteractor.swift index 7994cb33d..3e2846761 100644 --- a/Nynja/Modules/ChatsList/Interactor/ChatsListInteractor.swift +++ b/Nynja/Modules/ChatsList/Interactor/ChatsListInteractor.swift @@ -12,7 +12,7 @@ class ChatsListInteractor: BaseInteractor, ChatsListInteractorInputProtocol, Ini private let storageService: StorageService private let conversationsProvider: ConversationsProviding - private let typingProvider: TypingStatusProvider + private let typingProvider: TypingProvider private var chats: [Contact] = [] private var searchText: String = "" @@ -31,7 +31,7 @@ class ChatsListInteractor: BaseInteractor, ChatsListInteractorInputProtocol, Ini storageService = dependencies.storageService conversationsProvider = dependencies.conversationsProvider // FIXME: move to factory - typingProvider = TypingStatusProviderImpl(dependencies: + typingProvider = TypingProviderImpl(dependencies: .init(storageService: StorageService.sharedInstance, typingHandler: TypingHandler.shared) ) diff --git a/Nynja/Modules/GroupsList/Interactor/GroupsListInteractor.swift b/Nynja/Modules/GroupsList/Interactor/GroupsListInteractor.swift index 86de8260e..a497798e9 100644 --- a/Nynja/Modules/GroupsList/Interactor/GroupsListInteractor.swift +++ b/Nynja/Modules/GroupsList/Interactor/GroupsListInteractor.swift @@ -12,7 +12,7 @@ class GroupsListInteractor: BaseInteractor, GroupsListInteractorInputProtocol, I private let storageService: StorageService private let conversationsProvider: ConversationsProviding - private let typingProvider: TypingStatusProvider + private let typingProvider: TypingProvider private var chats: [Room] = [] private var searchText: String = "" @@ -31,7 +31,7 @@ class GroupsListInteractor: BaseInteractor, GroupsListInteractorInputProtocol, I storageService = dependencies.storageService conversationsProvider = dependencies.conversationsProvider // FIXME: move to factory - typingProvider = TypingStatusProviderImpl(dependencies: + typingProvider = TypingProviderImpl(dependencies: .init(storageService: StorageService.sharedInstance, typingHandler: TypingHandler.shared) ) diff --git a/Nynja/Modules/Message/Interactor/MessageInteractor.swift b/Nynja/Modules/Message/Interactor/MessageInteractor.swift index e5dacf53a..200973fb3 100644 --- a/Nynja/Modules/Message/Interactor/MessageInteractor.swift +++ b/Nynja/Modules/Message/Interactor/MessageInteractor.swift @@ -99,7 +99,7 @@ final class MessageInteractor: BaseInteractor, MessageInteractorInputProtocol, H private var presenceProvider: PresenceStatusProvider! - private let typingStatusProvider: TypingStatusProvider + private let typingProvider: TypingProvider private let historyRequestFactory: HistoryRequestModelFactoryProtocol = HistoryRequestModelFactory() @@ -178,7 +178,7 @@ final class MessageInteractor: BaseInteractor, MessageInteractorInputProtocol, H payloadParser = MessagePayloadParser() payloadBuilder = MessagePayloadBuilder() stickersProvider = StickersProvider(dependencies: .init(storage: StorageService.sharedInstance)) - typingStatusProvider = TypingStatusProviderImpl(dependencies: + typingProvider = TypingProviderImpl(dependencies: .init(storageService: StorageService.sharedInstance, typingHandler: TypingHandler.shared) ) @@ -192,7 +192,7 @@ final class MessageInteractor: BaseInteractor, MessageInteractorInputProtocol, H mqttService.addSubscriber(self) if let chatId = chat.id { - typingStatusProvider.addObserver(self, for: chatId) { [weak self] chatId, typingInfo in + typingProvider.addObserver(self, for: chatId) { [weak self] chatId, typingInfo in self?.presenter?.didReceiveTyping(typingInfo) } } @@ -207,7 +207,7 @@ final class MessageInteractor: BaseInteractor, MessageInteractorInputProtocol, H deinit { callService.messageInteractorCallProtocol = nil mqttService.removeSubscriber(self) - typingStatusProvider.removeObserver(self) + typingProvider.removeObserver(self) MessageHandler.shared.removeSubscriber(self) HistoryHandler.shared.removeSubscriber(self) ConnectionService.shared.removeSubscriber(self) @@ -448,7 +448,7 @@ final class MessageInteractor: BaseInteractor, MessageInteractorInputProtocol, H } func askForTypingStatus() { - guard let feedId = chat.id, let typing = typingStatusProvider.typingStatus(for: feedId) else { + guard let feedId = chat.id, let typing = typingProvider.typingStatus(for: feedId) else { return } presenter?.didReceiveTyping(typing) diff --git a/Nynja/Statuses/TypingStatusProvider.swift b/Nynja/Statuses/TypingProvider.swift similarity index 96% rename from Nynja/Statuses/TypingStatusProvider.swift rename to Nynja/Statuses/TypingProvider.swift index 8e26b9816..bef782d56 100644 --- a/Nynja/Statuses/TypingStatusProvider.swift +++ b/Nynja/Statuses/TypingProvider.swift @@ -1,5 +1,5 @@ // -// TypingStatusProvider.swift +// TypingProvider.swift // Nynja // // Created by Anton Poltoratskyi on 29.10.2018. @@ -8,7 +8,7 @@ import Foundation -protocol TypingStatusProvider: class { +protocol TypingProvider: class { typealias Callback = (FeedId, TypingDisplayModel) -> Void func addObserver(_ observer: AnyObject, callback: @escaping Callback) @@ -19,7 +19,7 @@ protocol TypingStatusProvider: class { func typingStatus(for feedId: FeedId) -> TypingDisplayModel? } -final class TypingStatusProviderImpl: TypingStatusProvider, KeyedObservableContainer, TypingHandlerDelegate, InitializeInjectable { +final class TypingProviderImpl: TypingProvider, KeyedObservableContainer, TypingHandlerDelegate, InitializeInjectable { let observable = KeyedObservable() @@ -65,7 +65,7 @@ final class TypingStatusProviderImpl: TypingStatusProvider, KeyedObservableConta } - // MARK: - TypingStatusProvider + // MARK: - TypingProvider func typingStatus(for feedId: FeedId) -> TypingDisplayModel? { return typing(for: feedId).flatMap { displayInfo(for: $0) } @@ -203,7 +203,7 @@ final class TypingStatusProviderImpl: TypingStatusProvider, KeyedObservableConta // MARK: - Inner Types -private extension TypingStatusProviderImpl { +private extension TypingProviderImpl { enum TypingData { case p2p(SenderInfo) -- GitLab From bf425d649b2606885bc5327c5da88cd3889ac6ad Mon Sep 17 00:00:00 2001 From: Anton Poltoratskyi Date: Thu, 1 Nov 2018 17:49:35 +0200 Subject: [PATCH 54/64] [NY-4699] Move TypingProvider initialization to ServiceFactrory. --- Nynja.xcodeproj/project.pbxproj | 8 +++ .../Interactor/ChatsListInteractor.swift | 7 +-- .../WireFrame/ChatsListWireframe.swift | 8 ++- .../Interactor/GroupsListInteractor.swift | 7 +-- .../WireFrame/GroupsListWireframe.swift | 9 +-- .../MQTTHandlerFactoryProtocol.swift | 13 ++++ .../ServiceFactory/ServiceFactory.swift | 59 ++++++------------- .../ServiceFactoryProtocol.swift | 49 +++++++++++++++ 8 files changed, 102 insertions(+), 58 deletions(-) create mode 100644 Nynja/Services/ServiceFactory/MQTTHandlerFactoryProtocol.swift create mode 100644 Nynja/Services/ServiceFactory/ServiceFactoryProtocol.swift diff --git a/Nynja.xcodeproj/project.pbxproj b/Nynja.xcodeproj/project.pbxproj index aaadae856..4fac69477 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 */; }; @@ -3127,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 = ""; }; @@ -12903,6 +12907,8 @@ F11786EF20AC5474007A9A1B /* ServiceFactory */ = { isa = PBXGroup; children = ( + 851C6A51218B55AC0062B148 /* ServiceFactoryProtocol.swift */, + 851C6A53218B560B0062B148 /* MQTTHandlerFactoryProtocol.swift */, F11786F020AC5482007A9A1B /* ServiceFactory.swift */, ); name = ServiceFactory; @@ -15130,6 +15136,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 */, @@ -16565,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/Modules/ChatsList/Interactor/ChatsListInteractor.swift b/Nynja/Modules/ChatsList/Interactor/ChatsListInteractor.swift index 3e2846761..29ca6e092 100644 --- a/Nynja/Modules/ChatsList/Interactor/ChatsListInteractor.swift +++ b/Nynja/Modules/ChatsList/Interactor/ChatsListInteractor.swift @@ -25,16 +25,13 @@ class ChatsListInteractor: BaseInteractor, ChatsListInteractorInputProtocol, Ini struct Dependencies { let storageService: StorageService let conversationsProvider: ConversationsProviding + let typingProvider: TypingProvider } required init(dependencies: Dependencies) { storageService = dependencies.storageService conversationsProvider = dependencies.conversationsProvider - // FIXME: move to factory - typingProvider = TypingProviderImpl(dependencies: - .init(storageService: StorageService.sharedInstance, - typingHandler: TypingHandler.shared) - ) + typingProvider = dependencies.typingProvider super.init() } diff --git a/Nynja/Modules/ChatsList/WireFrame/ChatsListWireframe.swift b/Nynja/Modules/ChatsList/WireFrame/ChatsListWireframe.swift index 608c9433c..a1bbd89b0 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/GroupsList/Interactor/GroupsListInteractor.swift b/Nynja/Modules/GroupsList/Interactor/GroupsListInteractor.swift index a497798e9..9b1bf236a 100644 --- a/Nynja/Modules/GroupsList/Interactor/GroupsListInteractor.swift +++ b/Nynja/Modules/GroupsList/Interactor/GroupsListInteractor.swift @@ -25,16 +25,13 @@ class GroupsListInteractor: BaseInteractor, GroupsListInteractorInputProtocol, I struct Dependencies { let storageService: StorageService let conversationsProvider: ConversationsProviding + let typingProvider: TypingProvider } required init(dependencies: Dependencies) { storageService = dependencies.storageService conversationsProvider = dependencies.conversationsProvider - // FIXME: move to factory - typingProvider = TypingProviderImpl(dependencies: - .init(storageService: StorageService.sharedInstance, - typingHandler: TypingHandler.shared) - ) + typingProvider = dependencies.typingProvider super.init() } diff --git a/Nynja/Modules/GroupsList/WireFrame/GroupsListWireframe.swift b/Nynja/Modules/GroupsList/WireFrame/GroupsListWireframe.swift index 3db6d4df5..bb52986b2 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/Services/ServiceFactory/MQTTHandlerFactoryProtocol.swift b/Nynja/Services/ServiceFactory/MQTTHandlerFactoryProtocol.swift new file mode 100644 index 000000000..48fa93797 --- /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 f581ec61b..5b164cd7d 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,13 @@ final class ServiceFactory: SharedServiceFactory, ServiceFactoryProtocol { func makeHistoryRequestFactory() -> HistoryRequestModelFactoryProtocol { return HistoryRequestModelFactory() } + + func makeTypingProvider() -> TypingProvider { + // FIXME: typing handler must be injected + let dependencies = TypingProviderImpl.Dependencies(storageService: makeStorageService(), + typingHandler: makeTypingHandler()) + return TypingProviderImpl(dependencies: dependencies) + } func makeContactsProvider() -> ContactsProviding { return ContactsProvider() @@ -170,3 +136,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 000000000..f0dfad164 --- /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 +} -- GitLab From acdb36ca0ab19733e96d3ae6c265256304b5e818 Mon Sep 17 00:00:00 2001 From: Anton Poltoratskyi Date: Thu, 1 Nov 2018 17:56:46 +0200 Subject: [PATCH 55/64] [NY-4699] Remove unused code. --- Nynja.xcodeproj/project.pbxproj | 4 ---- Nynja/Statuses/AccountStatus.swift | 19 ------------------- Nynja/Statuses/TypingProvider.swift | 2 ++ 3 files changed, 2 insertions(+), 23 deletions(-) delete mode 100644 Nynja/Statuses/AccountStatus.swift diff --git a/Nynja.xcodeproj/project.pbxproj b/Nynja.xcodeproj/project.pbxproj index 4fac69477..7b08c754c 100644 --- a/Nynja.xcodeproj/project.pbxproj +++ b/Nynja.xcodeproj/project.pbxproj @@ -1137,7 +1137,6 @@ 85EB37F321831094003A2D6F /* ChatListMessageTextView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 85EB37F221831094003A2D6F /* ChatListMessageTextView.swift */; }; 85EB37FB21837235003A2D6F /* KeyedObservableContainer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 85EB37FA21837235003A2D6F /* KeyedObservableContainer.swift */; }; 85EB37FD21837253003A2D6F /* KeyedObservable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 85EB37FC21837253003A2D6F /* KeyedObservable.swift */; }; - 85EB37FF21837304003A2D6F /* AccountStatus.swift in Sources */ = {isa = PBXBuildFile; fileRef = 85EB37FE21837304003A2D6F /* AccountStatus.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 */; }; @@ -3357,7 +3356,6 @@ 85EB37F221831094003A2D6F /* ChatListMessageTextView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatListMessageTextView.swift; sourceTree = ""; }; 85EB37FA21837235003A2D6F /* KeyedObservableContainer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeyedObservableContainer.swift; sourceTree = ""; }; 85EB37FC21837253003A2D6F /* KeyedObservable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeyedObservable.swift; sourceTree = ""; }; - 85EB37FE21837304003A2D6F /* AccountStatus.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountStatus.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 = ""; }; @@ -9239,7 +9237,6 @@ 85EB37F9218365A6003A2D6F /* Statuses */ = { isa = PBXGroup; children = ( - 85EB37FE21837304003A2D6F /* AccountStatus.swift */, 854834172186FADB002064E1 /* TypingProvider.swift */, 8542B811218879B100A286E5 /* TypingDisplayModel.swift */, ); @@ -15303,7 +15300,6 @@ A49B81B320B4BB6400980D36 /* NynjaMTIConfig.swift in Sources */, 8EC2AF6B20053FC300807B20 /* GroupCollectionCell.swift in Sources */, 4B8996D8204EDA7700DCB183 /* JobDAOProtocol.swift in Sources */, - 85EB37FF21837304003A2D6F /* AccountStatus.swift in Sources */, A4CE80C320C95E7F00400713 /* CollectionDisplayMode.swift in Sources */, E74E53951FB45D6800463242 /* ScrollBar.swift in Sources */, FEA655CD2167777E00B44029 /* SeedVerificationWalletProtocols.swift in Sources */, diff --git a/Nynja/Statuses/AccountStatus.swift b/Nynja/Statuses/AccountStatus.swift deleted file mode 100644 index 2a1bfe2f6..000000000 --- a/Nynja/Statuses/AccountStatus.swift +++ /dev/null @@ -1,19 +0,0 @@ -// -// AccountStatus.swift -// Nynja -// -// Created by Anton Poltoratskyi on 26.10.2018. -// Copyright © 2018 TecSynt Solutions. All rights reserved. -// - -typealias AccountId = String - -typealias FeedId = String - -enum AccountStatus { - case active - case inactive - case busy - case offline - case none -} diff --git a/Nynja/Statuses/TypingProvider.swift b/Nynja/Statuses/TypingProvider.swift index bef782d56..3620f9c31 100644 --- a/Nynja/Statuses/TypingProvider.swift +++ b/Nynja/Statuses/TypingProvider.swift @@ -8,6 +8,8 @@ import Foundation +typealias FeedId = String + protocol TypingProvider: class { typealias Callback = (FeedId, TypingDisplayModel) -> Void -- GitLab From f804dd6e298d675fb494241d1ed9150ab8269c23 Mon Sep 17 00:00:00 2001 From: Anton Poltoratskyi Date: Thu, 1 Nov 2018 18:07:07 +0200 Subject: [PATCH 56/64] [NY-4699] Make TypingProvider to be singleton. --- .../Message/Interactor/MessageInteractor.swift | 5 +---- Nynja/Services/ServiceFactory/ServiceFactory.swift | 5 +---- Nynja/Statuses/TypingProvider.swift | 13 +++++++++++-- 3 files changed, 13 insertions(+), 10 deletions(-) diff --git a/Nynja/Modules/Message/Interactor/MessageInteractor.swift b/Nynja/Modules/Message/Interactor/MessageInteractor.swift index 200973fb3..431beb157 100644 --- a/Nynja/Modules/Message/Interactor/MessageInteractor.swift +++ b/Nynja/Modules/Message/Interactor/MessageInteractor.swift @@ -178,10 +178,7 @@ final class MessageInteractor: BaseInteractor, MessageInteractorInputProtocol, H payloadParser = MessagePayloadParser() payloadBuilder = MessagePayloadBuilder() stickersProvider = StickersProvider(dependencies: .init(storage: StorageService.sharedInstance)) - typingProvider = TypingProviderImpl(dependencies: - .init(storageService: StorageService.sharedInstance, - typingHandler: TypingHandler.shared) - ) + typingProvider = TypingProviderImpl.shared super.init() diff --git a/Nynja/Services/ServiceFactory/ServiceFactory.swift b/Nynja/Services/ServiceFactory/ServiceFactory.swift index 5b164cd7d..d4367c0fb 100644 --- a/Nynja/Services/ServiceFactory/ServiceFactory.swift +++ b/Nynja/Services/ServiceFactory/ServiceFactory.swift @@ -55,10 +55,7 @@ final class ServiceFactory: SharedServiceFactory, ServiceFactoryProtocol, MQTTHa } func makeTypingProvider() -> TypingProvider { - // FIXME: typing handler must be injected - let dependencies = TypingProviderImpl.Dependencies(storageService: makeStorageService(), - typingHandler: makeTypingHandler()) - return TypingProviderImpl(dependencies: dependencies) + return TypingProviderImpl.shared } func makeContactsProvider() -> ContactsProviding { diff --git a/Nynja/Statuses/TypingProvider.swift b/Nynja/Statuses/TypingProvider.swift index 3620f9c31..83dd04a2d 100644 --- a/Nynja/Statuses/TypingProvider.swift +++ b/Nynja/Statuses/TypingProvider.swift @@ -21,7 +21,7 @@ protocol TypingProvider: class { func typingStatus(for feedId: FeedId) -> TypingDisplayModel? } -final class TypingProviderImpl: TypingProvider, KeyedObservableContainer, TypingHandlerDelegate, InitializeInjectable { +final class TypingProviderImpl: TypingProvider, KeyedObservableContainer, TypingHandlerDelegate { let observable = KeyedObservable() @@ -38,6 +38,15 @@ final class TypingProviderImpl: TypingProvider, KeyedObservableContainer, Typing 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 { @@ -54,7 +63,7 @@ final class TypingProviderImpl: TypingProvider, KeyedObservableContainer, Typing // MARK: - Init - init(dependencies: Dependencies) { + private init(dependencies: Dependencies) { storageService = dependencies.storageService typingHandler = dependencies.typingHandler processingQueue = dependencies.processingQueue -- GitLab From 99d7cc1bfd074ac0beede83f74ed9e1bfadf7659 Mon Sep 17 00:00:00 2001 From: Anton Poltoratskyi Date: Fri, 2 Nov 2018 12:01:26 +0200 Subject: [PATCH 57/64] [NY-4699] Implemented ability to work correctly both with senderId and without senderId sent in Typing model in p2p chat. --- Nynja/Statuses/TypingProvider.swift | 156 ++++++++++------------------ 1 file changed, 53 insertions(+), 103 deletions(-) diff --git a/Nynja/Statuses/TypingProvider.swift b/Nynja/Statuses/TypingProvider.swift index 83dd04a2d..9edbc028e 100644 --- a/Nynja/Statuses/TypingProvider.swift +++ b/Nynja/Statuses/TypingProvider.swift @@ -89,19 +89,12 @@ final class TypingProviderImpl: TypingProvider, KeyedObservableContainer, Typing guard let feedId = typing.feed_id, let typingType = typing.type, shouldHandleTyping(typing) else { return } - let feed: SenderInfo.Feed - if let senderId = typing.sender_id { - guard let alias = typing.sender_alias else { - return - } - feed = .room(feedId, senderId: senderId, senderName: alias) - } else { - feed = .p2p(feedId) - } - let status = ActionStatus(typingModelType: typingType) - let senderInfo = SenderInfo(feed: feed, status: status) + let senderInfo = TypingSenderInfo(feedId: feedId, + senderId: typing.sender_id, + senderName: typing.sender_alias, + status: status) processingQueue.async { self.save(senderInfo) @@ -109,51 +102,33 @@ final class TypingProviderImpl: TypingProvider, KeyedObservableContainer, Typing } private func shouldHandleTyping(_ typing: Typing) -> Bool { - // Don't handle typing from us (in group chat) + // Ignore typing from myself (in group chat). if let senderId = typing.sender_id, senderId == storageService.phoneId { return false } return true } - private func save(_ typingInfo: SenderInfo) { - let feedId = typingInfo.feed.identifier + private func save(_ typingInfo: TypingSenderInfo) { + let feedId = typingInfo.feedId - if let oldTyping = typing(for: feedId) { - switch oldTyping { - case .p2p: - update(.p2p(typingInfo), for: feedId) - - case let .room(oldTypingSendersInfo): - var newTypingInfo = oldTypingSendersInfo - - newTypingInfo.removeAll { typingInfo.feed.senderId == $0.feed.senderId } - newTypingInfo.append(typingInfo) - - update(.room(newTypingInfo), for: 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 { - // Save and notify if didn't exists any other typing status for this feed - switch typingInfo.feed { - case .p2p: - update(.p2p(typingInfo), for: feedId) - case .room: - update(.room([typingInfo]), for: feedId) - } + typing = TypingData(senders: [typingInfo]) } - dismiss(typingInfo, after: typingDismissInterval, for: feedId) - } - - private func dismiss(_ typing: SenderInfo, after delay: TimeInterval, for feedId: FeedId) { - workItems[feedId]?.cancel() - - let workItem = DispatchWorkItem { - self.remove(typing, for: feedId) - } - workItems[feedId] = workItem + update(typing, for: feedId) - processingQueue.asyncAfter(deadline: .now() + delay, execute: workItem) + dismiss(typingInfo, after: typingDismissInterval, for: feedId) } private func update(_ typing: TypingData?, for feedId: FeedId) { @@ -165,25 +140,38 @@ final class TypingProviderImpl: TypingProvider, KeyedObservableContainer, Typing } } - private func remove(_ typing: SenderInfo, for feedId: FeedId) { - guard let currentTyping = self.typing(for: feedId) else { + private func remove(_ typing: TypingSenderInfo, for feedId: FeedId) { + guard let currentTypingData = self.typing(for: feedId) else { return } - switch currentTyping { - case let .p2p(info): - if info === typing { - update(nil, for: feedId) - } - case let .room(info): - update(.room(info.filter { $0 !== typing }), for: feedId) + var newData = currentTypingData + newData.senders.removeAll { $0 === typing } + + update(newData, for: feedId) + } + + + // MARK: - Dismiss Timer + + private func dismiss(_ typing: TypingSenderInfo, after delay: TimeInterval, for feedId: FeedId) { + workItems[feedId]?.cancel() + + let workItem = DispatchWorkItem { + self.remove(typing, for: feedId) } + workItems[feedId] = 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.feed.senderName } + let senders = typingInfo.compactMap { $0.senderName } let senderInfo = TypingDisplayModel.SenderInfo(senders: senders) let shouldDisplayStatus = !typingInfo.contains { $0.status != lastStatus } @@ -216,58 +204,20 @@ final class TypingProviderImpl: TypingProvider, KeyedObservableContainer, Typing private extension TypingProviderImpl { - enum TypingData { - case p2p(SenderInfo) - case room([SenderInfo]) - - var senders: [SenderInfo] { - switch self { - case let .p2p(sender): - return [sender] - case let .room(senders): - return senders - } - } + struct TypingData { + var senders: [TypingSenderInfo] } - final class SenderInfo { - enum Feed { - case p2p(FeedId) - case room(FeedId, senderId: String, senderName: String) - - var identifier: String { - switch self { - case let .p2p(id): - return id - case let .room(id, _, _): - return id - } - } - - var senderId: String? { - switch self { - case let .room(_, senderId, _): - return senderId - case .p2p: - return nil - } - } - - var senderName: String? { - switch self { - case let .room(_, _, senderName): - return senderName - case .p2p: - return nil - } - } - } - - let feed: Feed + final class TypingSenderInfo { + let feedId: FeedId + let senderId: String + let senderName: String? let status: ActionStatus - init(feed: Feed, status: ActionStatus) { - self.feed = feed + init(feedId: FeedId, senderId: String?, senderName: String?, status: ActionStatus) { + self.feedId = feedId + self.senderId = senderId ?? feedId + self.senderName = senderName self.status = status } } -- GitLab From 46c9af56123337fc30039949479d7913ee5f1822 Mon Sep 17 00:00:00 2001 From: Anton Poltoratskyi Date: Fri, 2 Nov 2018 12:11:50 +0200 Subject: [PATCH 58/64] [NY-4699] Added validation for tuple.elements in Decoder. --- Nynja/ServerModel/Source/Decoder.swift | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Nynja/ServerModel/Source/Decoder.swift b/Nynja/ServerModel/Source/Decoder.swift index 844a4646b..d6116cd2e 100644 --- a/Nynja/ServerModel/Source/Decoder.swift +++ b/Nynja/ServerModel/Source/Decoder.swift @@ -1,5 +1,7 @@ func parseObject(name: String, body:[Model], tuple: BertTuple) -> AnyObject? { + guard tuple.elements.count == body.count + 1 else { return nil } + switch name { case "writer": if body.count != 5 { return nil } -- GitLab From 16cf00d9fd18a2dcb706391ede1b75ef8b584a99 Mon Sep 17 00:00:00 2001 From: Anton Poltoratskyi Date: Fri, 2 Nov 2018 12:26:36 +0200 Subject: [PATCH 59/64] [NY-4699] Move TypingObservable to separate file. --- Nynja.xcodeproj/project.pbxproj | 4 ++++ Nynja/Modules/ChatsList/ChatsListProtocols.swift | 5 ----- Nynja/Statuses/TypingObservable.swift | 12 ++++++++++++ 3 files changed, 16 insertions(+), 5 deletions(-) create mode 100644 Nynja/Statuses/TypingObservable.swift diff --git a/Nynja.xcodeproj/project.pbxproj b/Nynja.xcodeproj/project.pbxproj index 7b08c754c..b5b008825 100644 --- a/Nynja.xcodeproj/project.pbxproj +++ b/Nynja.xcodeproj/project.pbxproj @@ -1104,6 +1104,7 @@ 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 */; }; 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 */; }; @@ -3326,6 +3327,7 @@ 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 = ""; }; 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 = ""; }; @@ -9238,6 +9240,7 @@ isa = PBXGroup; children = ( 854834172186FADB002064E1 /* TypingProvider.swift */, + 85CEFBBF218C5D9500760F9E /* TypingObservable.swift */, 8542B811218879B100A286E5 /* TypingDisplayModel.swift */, ); path = Statuses; @@ -15704,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 */, diff --git a/Nynja/Modules/ChatsList/ChatsListProtocols.swift b/Nynja/Modules/ChatsList/ChatsListProtocols.swift index cbce84e51..e9ee87810 100644 --- a/Nynja/Modules/ChatsList/ChatsListProtocols.swift +++ b/Nynja/Modules/ChatsList/ChatsListProtocols.swift @@ -8,11 +8,6 @@ import UIKit -protocol TypingObservable: class { - func observeChanges(for feedId: FeedId, handler: @escaping (TypingDisplayModel) -> ()) - func removeObserver(for feedId: FeedId) -} - protocol ChatsListWireFrameProtocol: class { func presentChatsList(navigation: UINavigationController, main: MainWireFrame?, animated: Bool) diff --git a/Nynja/Statuses/TypingObservable.swift b/Nynja/Statuses/TypingObservable.swift new file mode 100644 index 000000000..34a6b75e9 --- /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) +} -- GitLab From 2a4813b4adc33ae8e5b06edd6ac82dd7f5144a6b Mon Sep 17 00:00:00 2001 From: Anton Poltoratskyi Date: Fri, 2 Nov 2018 12:37:33 +0200 Subject: [PATCH 60/64] [NY-4699] Rename observables. --- Nynja.xcodeproj/project.pbxproj | 32 ++++----- Nynja/Observable/KeyedObservable.swift | 70 ++++++------------- .../Observable/KeyedObservableContainer.swift | 70 +++++++++++++------ Nynja/Observable/Observable.swift | 38 ++++------ Nynja/Observable/ObservableContainer.swift | 38 ++++++---- .../HandleServices/TypingHandler.swift | 4 +- Nynja/Statuses/TypingProvider.swift | 4 +- 7 files changed, 128 insertions(+), 128 deletions(-) diff --git a/Nynja.xcodeproj/project.pbxproj b/Nynja.xcodeproj/project.pbxproj index b5b008825..f42862bb3 100644 --- a/Nynja.xcodeproj/project.pbxproj +++ b/Nynja.xcodeproj/project.pbxproj @@ -975,8 +975,8 @@ 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 /* Observable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8548341A2187449F002064E1 /* Observable.swift */; }; - 8548341D218744AC002064E1 /* ObservableContainer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8548341C218744AC002064E1 /* ObservableContainer.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 */; }; @@ -1136,8 +1136,8 @@ 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 /* KeyedObservableContainer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 85EB37FA21837235003A2D6F /* KeyedObservableContainer.swift */; }; - 85EB37FD21837253003A2D6F /* KeyedObservable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 85EB37FC21837253003A2D6F /* KeyedObservable.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 */; }; @@ -3206,8 +3206,8 @@ 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 /* Observable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Observable.swift; sourceTree = ""; }; - 8548341C218744AC002064E1 /* ObservableContainer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ObservableContainer.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 = ""; }; @@ -3356,8 +3356,8 @@ 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 /* KeyedObservableContainer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeyedObservableContainer.swift; sourceTree = ""; }; - 85EB37FC21837253003A2D6F /* KeyedObservable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeyedObservable.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 = ""; }; @@ -8489,10 +8489,10 @@ 8548341921874434002064E1 /* Observable */ = { isa = PBXGroup; children = ( - 8548341A2187449F002064E1 /* Observable.swift */, - 8548341C218744AC002064E1 /* ObservableContainer.swift */, - 85EB37FC21837253003A2D6F /* KeyedObservable.swift */, - 85EB37FA21837235003A2D6F /* KeyedObservableContainer.swift */, + 8548341C218744AC002064E1 /* Observable.swift */, + 8548341A2187449F002064E1 /* ObservableContainer.swift */, + 85EB37FA21837235003A2D6F /* KeyedObservable.swift */, + 85EB37FC21837253003A2D6F /* KeyedObservableContainer.swift */, ); path = Observable; sourceTree = ""; @@ -15331,7 +15331,7 @@ 85D66A0420BD963C00FBD803 /* MessagePayloadBuilder.swift in Sources */, 004581212036073100F8E413 /* JobMessageTable.swift in Sources */, 85EB37F321831094003A2D6F /* ChatListMessageTextView.swift in Sources */, - 85EB37FD21837253003A2D6F /* KeyedObservable.swift in Sources */, + 85EB37FD21837253003A2D6F /* KeyedObservableContainer.swift in Sources */, 8572C3B62092315B00E4840C /* CollectionViewDataProxy.swift in Sources */, A45F110520B4218D00F45004 /* DisplayChatConfiguration.swift in Sources */, E7598F681FA1D8B90082FBE7 /* ProfileScheduledMesssageCell.swift in Sources */, @@ -15495,7 +15495,7 @@ F117871020ACF018007A9A1B /* CameraQualitySettingsProtocols.swift in Sources */, A44B4D5920CE9BDF00CA700A /* ImageCellViewModel.swift in Sources */, A415132020DBD58900C2C01F /* Link.swift in Sources */, - 85EB37FB21837235003A2D6F /* KeyedObservableContainer.swift in Sources */, + 85EB37FB21837235003A2D6F /* KeyedObservable.swift in Sources */, 852DF263203720E600A4F8B6 /* FileIcons.swift in Sources */, A43B25DB20AB1EE400FF8107 /* NewChannelInteractor.swift in Sources */, FBCE840F20E525A6003B7558 /* HTTPParameters.swift in Sources */, @@ -15720,7 +15720,7 @@ 850571222050B0AD00EDF794 /* NotificationAlertSoundsViewController.swift in Sources */, 6D6731101F29E1F4003E8F8F /* BottomCallView.swift in Sources */, 26245F40204EF58E00C8D3DD /* BaseViewProtocol.swift in Sources */, - 8548341D218744AC002064E1 /* ObservableContainer.swift in Sources */, + 8548341D218744AC002064E1 /* Observable.swift in Sources */, 4B1D7DFE2029C41C00703228 /* AboutItemsFactory.swift in Sources */, A4688DFC20652DE30013660D /* StorageChange.swift in Sources */, 5683555B8382F7F37FEE1AF5 /* ProfileWireframe.swift in Sources */, @@ -15985,7 +15985,7 @@ E70938371FBEDA2B006CCDC6 /* ProfileTable.swift in Sources */, A416DA602075341C00FBF1BA /* CLLocationCoordinate2D+Payload.swift in Sources */, A4679BAE20B2DD100021FE9C /* SubscribersSelectorInteractor.swift in Sources */, - 8548341B2187449F002064E1 /* Observable.swift in Sources */, + 8548341B2187449F002064E1 /* ObservableContainer.swift in Sources */, FEA655FD2167777F00B44029 /* TransferDetailsInteractor.swift in Sources */, E70F78B91FD6C64E00385565 /* ChatCheckpointTable.swift in Sources */, 4B06D30620287060003B275B /* WCDataManagerProtocol.swift in Sources */, diff --git a/Nynja/Observable/KeyedObservable.swift b/Nynja/Observable/KeyedObservable.swift index 6c1da682a..3ade428dc 100644 --- a/Nynja/Observable/KeyedObservable.swift +++ b/Nynja/Observable/KeyedObservable.swift @@ -6,67 +6,39 @@ // Copyright © 2018 TecSynt Solutions. All rights reserved. // -import Foundation +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) +} -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] = [:] +extension KeyedObservable { - 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, callback: @escaping Callback) { + observable.addObserver(observer, callback: callback) } - 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 addObserver(_ observer: AnyObject, for key: Key, callback: @escaping Callback) { + observable.addObserver(observer, for: key, callback: callback) } 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() + observable.removeObserver(observer) } func removeObserver(_ observer: AnyObject, for key: Key) { - lock.lock() - observers[key]?.removeAll { $0.object.value === observer || $0.object.value == nil } - lock.unlock() + observable.removeObserver(observer, for: key) } 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() + observable.notify(key, with: value) } } diff --git a/Nynja/Observable/KeyedObservableContainer.swift b/Nynja/Observable/KeyedObservableContainer.swift index ce8dfedc0..f728dfe2e 100644 --- a/Nynja/Observable/KeyedObservableContainer.swift +++ b/Nynja/Observable/KeyedObservableContainer.swift @@ -6,39 +6,67 @@ // Copyright © 2018 TecSynt Solutions. All rights reserved. // -protocol KeyedObservableContainer: class { - associatedtype Key: Hashable - associatedtype Value - typealias Callback = (Key, Value) -> Void - - var observable: KeyedObservable { 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) -} +import Foundation -extension KeyedObservableContainer { +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 Callback) { - observable.addObserver(observer, callback: callback) + 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 Callback) { - observable.addObserver(observer, for: key, callback: callback) + 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) { - observable.removeObserver(observer) + 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) { - observable.removeObserver(observer, for: key) + lock.lock() + observers[key]?.removeAll { $0.object.value === observer || $0.object.value == nil } + lock.unlock() } func notify(_ key: Key, with value: Value) { - observable.notify(key, with: 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 index fb149c81c..0f648eb7d 100644 --- a/Nynja/Observable/Observable.swift +++ b/Nynja/Observable/Observable.swift @@ -6,36 +6,26 @@ // Copyright © 2018 TecSynt Solutions. All rights reserved. // -import Foundation - -final class Observable { - - private typealias Observers = [WeakRef] +protocol Observable: class { + associatedtype Observer + var observable: ObservableContainer { get } - private let lock = NSLock() + func addObserver(_ observer: Observer) + func removeObserver(_ observer: Observer) + func notify(_ block: (Observer) -> Void) +} - private var observers: Observers = [] +extension Observable { - func addObserver(_ observer: T) { - let container = WeakRef(value: observer as AnyObject) - - lock.lock() - - observers.append(container) - - lock.unlock() + func addObserver(_ observer: Observer) { + observable.addObserver(observer) } - func removeObserver(_ observer: T) { - let observer = observer as AnyObject - lock.lock() - observers.removeAll { $0.value === observer || $0.value == nil } - lock.unlock() + func removeObserver(_ observer: Observer) { + observable.removeObserver(observer) } - func notify(_ block: (T) -> Void) { - lock.lock() - observers.forEach { ($0.value as? T).flatMap(block) } - lock.unlock() + func notify(_ block: (Observer) -> Void) { + observable.notify(block) } } diff --git a/Nynja/Observable/ObservableContainer.swift b/Nynja/Observable/ObservableContainer.swift index d995d3c90..183ee0dd5 100644 --- a/Nynja/Observable/ObservableContainer.swift +++ b/Nynja/Observable/ObservableContainer.swift @@ -6,26 +6,36 @@ // Copyright © 2018 TecSynt Solutions. All rights reserved. // -protocol ObservableContainer: class { - associatedtype Observer - var observable: Observable { get } +import Foundation + +final class ObservableContainer { - func addObserver(_ observer: Observer) - func removeObserver(_ observer: Observer) - func notify(_ block: (Observer) -> Void) -} + private typealias Observers = [WeakRef] + + private let lock = NSLock() -extension ObservableContainer { + private var observers: Observers = [] - func addObserver(_ observer: Observer) { - observable.addObserver(observer) + func addObserver(_ observer: T) { + let container = WeakRef(value: observer as AnyObject) + + lock.lock() + + observers.append(container) + + lock.unlock() } - func removeObserver(_ observer: Observer) { - observable.removeObserver(observer) + func removeObserver(_ observer: T) { + let observer = observer as AnyObject + lock.lock() + observers.removeAll { $0.value === observer || $0.value == nil } + lock.unlock() } - func notify(_ block: (Observer) -> Void) { - observable.notify(block) + func notify(_ block: (T) -> Void) { + lock.lock() + observers.forEach { ($0.value as? T).flatMap(block) } + lock.unlock() } } diff --git a/Nynja/Services/HandleServices/TypingHandler.swift b/Nynja/Services/HandleServices/TypingHandler.swift index 7774b0c4e..ef56ab9ab 100644 --- a/Nynja/Services/HandleServices/TypingHandler.swift +++ b/Nynja/Services/HandleServices/TypingHandler.swift @@ -12,7 +12,7 @@ protocol TypingHandlerDelegate: class { func didReceiveTyping(_ typing: Typing) } -final class TypingHandler: BaseHandler, ObservableContainer { +final class TypingHandler: BaseHandler, Observable { // MARK: - Singleton @@ -23,7 +23,7 @@ final class TypingHandler: BaseHandler, ObservableContainer { // MARK: - ObservableContainer - let observable = Observable() + let observable = ObservableContainer() // MARK: - Handler diff --git a/Nynja/Statuses/TypingProvider.swift b/Nynja/Statuses/TypingProvider.swift index 9edbc028e..840385e27 100644 --- a/Nynja/Statuses/TypingProvider.swift +++ b/Nynja/Statuses/TypingProvider.swift @@ -21,9 +21,9 @@ protocol TypingProvider: class { func typingStatus(for feedId: FeedId) -> TypingDisplayModel? } -final class TypingProviderImpl: TypingProvider, KeyedObservableContainer, TypingHandlerDelegate { +final class TypingProviderImpl: TypingProvider, KeyedObservable, TypingHandlerDelegate { - let observable = KeyedObservable() + let observable = KeyedObservableContainer() private var data: [FeedId: TypingData] = [:] -- GitLab From 7048ec049d1b5bce95d89def29571e5f36ede6ef Mon Sep 17 00:00:00 2001 From: Anton Poltoratskyi Date: Fri, 2 Nov 2018 12:42:38 +0200 Subject: [PATCH 61/64] [NY-4699] Remove unused code from TypingView. --- .../NynjaUIKit/NynjaUIKit/Views/Typing/TypingView.swift | 5 +---- .../Cell/ChatListMessageTableViewCell.swift | 3 +-- Nynja/Modules/Message/View/Views/AvatarView/AvatarView.swift | 3 +-- 3 files changed, 3 insertions(+), 8 deletions(-) diff --git a/Frameworks/NynjaUIKit/NynjaUIKit/Views/Typing/TypingView.swift b/Frameworks/NynjaUIKit/NynjaUIKit/Views/Typing/TypingView.swift index 6e3bc5c57..665f1f922 100644 --- a/Frameworks/NynjaUIKit/NynjaUIKit/Views/Typing/TypingView.swift +++ b/Frameworks/NynjaUIKit/NynjaUIKit/Views/Typing/TypingView.swift @@ -23,20 +23,17 @@ public final class TypingView: BaseView { public let textFont: UIFont public let senderInfo: String? public let typingInfo: String - public let isTypingInfoPinned: Bool public init(indicator: Indicator, textColor: UIColor, textFont: UIFont, senderInfo: String?, - typingInfo: String, - isTypingInfoPinned: Bool) { + typingInfo: String) { self.indicator = indicator self.textColor = textColor self.textFont = textFont self.senderInfo = senderInfo self.typingInfo = typingInfo - self.isTypingInfoPinned = isTypingInfoPinned } } diff --git a/Nynja/Library/UI/Lists/TableView/Cells/ChatListMessageCell/Cell/ChatListMessageTableViewCell.swift b/Nynja/Library/UI/Lists/TableView/Cells/ChatListMessageCell/Cell/ChatListMessageTableViewCell.swift index 0199e67f9..4885b1c82 100644 --- a/Nynja/Library/UI/Lists/TableView/Cells/ChatListMessageCell/Cell/ChatListMessageTableViewCell.swift +++ b/Nynja/Library/UI/Lists/TableView/Cells/ChatListMessageCell/Cell/ChatListMessageTableViewCell.swift @@ -153,8 +153,7 @@ final class ChatListMessageTableViewCell: UITableViewCell { textColor: UIColor.nynja.white, textFont: ChatListMessageContentView.typingFont, senderInfo: sender?.displayName, - typingInfo: status.title, - isTypingInfoPinned: false + typingInfo: status.title ) messageContentView.showTyping(with: appearance) diff --git a/Nynja/Modules/Message/View/Views/AvatarView/AvatarView.swift b/Nynja/Modules/Message/View/Views/AvatarView/AvatarView.swift index e2b5566cc..44212f7e3 100644 --- a/Nynja/Modules/Message/View/Views/AvatarView/AvatarView.swift +++ b/Nynja/Modules/Message/View/Views/AvatarView/AvatarView.swift @@ -41,8 +41,7 @@ final class AvatarView: BaseView { textColor: titleLabel.textColor, textFont: statusLabel.font, senderInfo: sender?.displayName, - typingInfo: status.title, - isTypingInfoPinned: false + typingInfo: status.title ) typingView.update(appearance) -- GitLab From c39ace9781980d4b1ef7a6c90c594a4ec837bb30 Mon Sep 17 00:00:00 2001 From: Anton Poltoratskyi Date: Fri, 2 Nov 2018 13:07:53 +0200 Subject: [PATCH 62/64] [NY-4699] Implemented Typing on Profile screen. --- .../Interactor/ChatsListInteractor.swift | 2 +- .../Interactor/GroupsListInteractor.swift | 7 +++- .../Interactor/ProfileInteractor.swift | 39 +++++++++++++++++-- .../Profile/Presenter/ProfilePresenter.swift | 10 +++++ Nynja/Modules/Profile/ProfileProtocols.swift | 4 +- .../Profile/View/ProfileViewController.swift | 5 ++- .../View/TableView/ProfileTablewViewDS.swift | 5 +++ .../Profile/WireFrame/ProfileWireframe.swift | 11 +++--- 8 files changed, 69 insertions(+), 14 deletions(-) diff --git a/Nynja/Modules/ChatsList/Interactor/ChatsListInteractor.swift b/Nynja/Modules/ChatsList/Interactor/ChatsListInteractor.swift index 29ca6e092..1dc1e96a3 100644 --- a/Nynja/Modules/ChatsList/Interactor/ChatsListInteractor.swift +++ b/Nynja/Modules/ChatsList/Interactor/ChatsListInteractor.swift @@ -49,11 +49,11 @@ class ChatsListInteractor: BaseInteractor, ChatsListInteractorInputProtocol, Ini override func loadData() { super.loadData() - fetchChats() typingProvider.addObserver(self) { [weak self] feedId, typing in self?.typingHandlers[feedId]?(typing) } + fetchChats() } diff --git a/Nynja/Modules/GroupsList/Interactor/GroupsListInteractor.swift b/Nynja/Modules/GroupsList/Interactor/GroupsListInteractor.swift index 9b1bf236a..030bc26d8 100644 --- a/Nynja/Modules/GroupsList/Interactor/GroupsListInteractor.swift +++ b/Nynja/Modules/GroupsList/Interactor/GroupsListInteractor.swift @@ -36,6 +36,11 @@ class GroupsListInteractor: BaseInteractor, GroupsListInteractorInputProtocol, I super.init() } + deinit { + typingProvider.removeObserver(self) + } + + // MARK: - BaseInteractor override var subscribes: [SubscribeType]? { @@ -48,11 +53,11 @@ class GroupsListInteractor: BaseInteractor, GroupsListInteractorInputProtocol, I override func loadData() { super.loadData() - fetchGroups() typingProvider.addObserver(self) { [weak self] feedId, typing in self?.typingHandlers[feedId]?(typing) } + fetchGroups() } diff --git a/Nynja/Modules/Profile/Interactor/ProfileInteractor.swift b/Nynja/Modules/Profile/Interactor/ProfileInteractor.swift index 0b73d2bbf..aa441303d 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/ProfilePresenter.swift b/Nynja/Modules/Profile/Presenter/ProfilePresenter.swift index 4728e19ed..806d35cd7 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/ProfileProtocols.swift b/Nynja/Modules/Profile/ProfileProtocols.swift index 045570511..83626aca3 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/ProfileViewController.swift b/Nynja/Modules/Profile/View/ProfileViewController.swift index 33f2fc770..81be85f01 100644 --- a/Nynja/Modules/Profile/View/ProfileViewController.swift +++ b/Nynja/Modules/Profile/View/ProfileViewController.swift @@ -91,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 diff --git a/Nynja/Modules/Profile/View/TableView/ProfileTablewViewDS.swift b/Nynja/Modules/Profile/View/TableView/ProfileTablewViewDS.swift index f5b2e40fa..1c7cf6a6b 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 119210b33..e64262d1e 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) -- GitLab From 28734ce98b306c1eabcb2ab043c82be5120e691c Mon Sep 17 00:00:00 2001 From: Anton Poltoratskyi Date: Fri, 2 Nov 2018 17:09:37 +0200 Subject: [PATCH 63/64] [NY-4699] Fixed wrong logic of dismiss timer. --- Nynja/Statuses/TypingProvider.swift | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/Nynja/Statuses/TypingProvider.swift b/Nynja/Statuses/TypingProvider.swift index 840385e27..ffb9895ed 100644 --- a/Nynja/Statuses/TypingProvider.swift +++ b/Nynja/Statuses/TypingProvider.swift @@ -128,7 +128,7 @@ final class TypingProviderImpl: TypingProvider, KeyedObservable, TypingHandlerDe update(typing, for: feedId) - dismiss(typingInfo, after: typingDismissInterval, for: feedId) + dismiss(typingInfo, after: typingDismissInterval) } private func update(_ typing: TypingData?, for feedId: FeedId) { @@ -140,7 +140,8 @@ final class TypingProviderImpl: TypingProvider, KeyedObservable, TypingHandlerDe } } - private func remove(_ typing: TypingSenderInfo, for feedId: FeedId) { + private func remove(_ typing: TypingSenderInfo) { + let feedId = typing.feedId guard let currentTypingData = self.typing(for: feedId) else { return } @@ -153,13 +154,15 @@ final class TypingProviderImpl: TypingProvider, KeyedObservable, TypingHandlerDe // MARK: - Dismiss Timer - private func dismiss(_ typing: TypingSenderInfo, after delay: TimeInterval, for feedId: FeedId) { - workItems[feedId]?.cancel() + private func dismiss(_ typing: TypingSenderInfo, after delay: TimeInterval) { + let dismissKey = "\(typing.feedId)\(typing.senderId)" + + workItems[dismissKey]?.cancel() let workItem = DispatchWorkItem { - self.remove(typing, for: feedId) + self.remove(typing) } - workItems[feedId] = workItem + workItems[dismissKey] = workItem processingQueue.asyncAfter(deadline: .now() + delay, execute: workItem) } -- GitLab From a36e4529eb41aa4d60b2627490fdb3c07302a1ec Mon Sep 17 00:00:00 2001 From: Anton Poltoratskyi Date: Fri, 2 Nov 2018 20:09:23 +0200 Subject: [PATCH 64/64] [NY-4699] Fixed layout of ChatListMessageTableViewCell. --- .../NynjaUIKit/Views/Typing/TypingView.swift | 1 - Nynja.xcodeproj/project.pbxproj | 8 +- .../Cell/ChatListMessageContentView.swift | 69 +++++++++++++++-- ...ft => ChatListMessageIndicatorsView.swift} | 76 ++++++------------- .../Cell/ChatListMessageTableViewCell.swift | 32 +------- .../Cell/CounterView.swift | 25 +++--- .../Model/ChatListMessageCellModel.swift | 4 +- 7 files changed, 111 insertions(+), 104 deletions(-) rename Nynja/Library/UI/Lists/TableView/Cells/ChatListMessageCell/Cell/{ChatListMessageAccessoryView.swift => ChatListMessageIndicatorsView.swift} (59%) diff --git a/Frameworks/NynjaUIKit/NynjaUIKit/Views/Typing/TypingView.swift b/Frameworks/NynjaUIKit/NynjaUIKit/Views/Typing/TypingView.swift index 665f1f922..9f001bf32 100644 --- a/Frameworks/NynjaUIKit/NynjaUIKit/Views/Typing/TypingView.swift +++ b/Frameworks/NynjaUIKit/NynjaUIKit/Views/Typing/TypingView.swift @@ -49,7 +49,6 @@ public final class TypingView: BaseView { private lazy var senderInfoLabel: UILabel = { let label = UILabel() - label.setContentCompressionResistancePriority(.required, for: .horizontal) addSubview(label) return label }() diff --git a/Nynja.xcodeproj/project.pbxproj b/Nynja.xcodeproj/project.pbxproj index f42862bb3..3b43cd31f 100644 --- a/Nynja.xcodeproj/project.pbxproj +++ b/Nynja.xcodeproj/project.pbxproj @@ -1045,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 */; }; @@ -1105,6 +1104,7 @@ 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 */; }; @@ -3271,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 = ""; }; @@ -3328,6 +3327,7 @@ 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 = ""; }; @@ -8805,8 +8805,8 @@ children = ( 8580BAD320BD98E600239D9D /* ChatListMessageTableViewCell.swift */, 8580BAD420BD98E600239D9D /* ChatListMessageContentView.swift */, - 8580BAD120BD98E600239D9D /* ChatListMessageAccessoryView.swift */, 85EB37F221831094003A2D6F /* ChatListMessageTextView.swift */, + 85CEFBC4218CAD8F00760F9E /* ChatListMessageIndicatorsView.swift */, 8580BAD220BD98E600239D9D /* CounterView.swift */, ); path = Cell; @@ -14950,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 */, @@ -15259,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 */, 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 1ad7e579f..afbdb2a0a 100644 --- a/Nynja/Library/UI/Lists/TableView/Cells/ChatListMessageCell/Cell/ChatListMessageContentView.swift +++ b/Nynja/Library/UI/Lists/TableView/Cells/ChatListMessageCell/Cell/ChatListMessageContentView.swift @@ -12,6 +12,9 @@ import NynjaUIKit final class ChatListMessageContentView: BaseView { + private static let dateFormatter = DialogDateConverter() + + // MARK: - Views private(set) lazy var titleLabel: UILabel = { @@ -20,9 +23,30 @@ 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 timeLabel: UILabel = { + let leftInfset = Constraints.timeLabel.leftInset.adjustedByWidth + + let height = Constraints.timeLabel.height.adjustedByWidth + let color = UIColor.nynja.manatee + + 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.right.equalToSuperview() + maker.left.equalTo(titleLabel.snp.right).offset(leftInfset) maker.height.equalTo(height) } @@ -35,7 +59,7 @@ final class ChatListMessageContentView: BaseView { addSubview(textView) textView.snp.makeConstraints { maker in maker.top.equalTo(titleLabel.snp.bottom) - maker.bottom.left.right.equalToSuperview() + maker.bottom.left.equalToSuperview() } return textView @@ -49,21 +73,36 @@ final class ChatListMessageContentView: BaseView { addSubview(typingView) typingView.snp.makeConstraints { maker in maker.top.equalTo(titleLabel.snp.bottom) - maker.left.right.equalToSuperview() + maker.left.equalToSuperview() maker.height.equalTo(height) } 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 + }() + // MARK: - Setup override func baseSetup() { super.baseSetup() - titleLabel.isHidden = false - textView.isHidden = false - typingView.isHidden = false + indicatorsView.isHidden = false } func setupTitle(_ title: String?) { @@ -75,6 +114,14 @@ final class ChatListMessageContentView: BaseView { textView.setup(sender: sender, image: image, text: text) } + func setup(date: Date) { + timeLabel.text = type(of: self).dateFormatter.toString(date) + } + + func setup(mentions: Bool, unreadCount: Int) { + indicatorsView.setup(mentions: mentions, unreadCount: unreadCount) + } + func showTyping(with appearance: TypingView.Appearance) { typingView.isHidden = false textView.isHidden = true @@ -98,8 +145,18 @@ final class ChatListMessageContentView: BaseView { static let height: CGFloat = 22 } + enum timeLabel { + static let leftInset: CGFloat = 8.0 + static let height: CGFloat = 17 + } + enum typingView { static let height: CGFloat = 20 } + + enum indicatorsView { + static let leftInset: CGFloat = 8.0 + static let topInset: CGFloat = 4.0 + } } } diff --git a/Nynja/Library/UI/Lists/TableView/Cells/ChatListMessageCell/Cell/ChatListMessageAccessoryView.swift b/Nynja/Library/UI/Lists/TableView/Cells/ChatListMessageCell/Cell/ChatListMessageIndicatorsView.swift similarity index 59% rename from Nynja/Library/UI/Lists/TableView/Cells/ChatListMessageCell/Cell/ChatListMessageAccessoryView.swift rename to Nynja/Library/UI/Lists/TableView/Cells/ChatListMessageCell/Cell/ChatListMessageIndicatorsView.swift index 0faef603f..b697a079d 100644 --- a/Nynja/Library/UI/Lists/TableView/Cells/ChatListMessageCell/Cell/ChatListMessageAccessoryView.swift +++ b/Nynja/Library/UI/Lists/TableView/Cells/ChatListMessageCell/Cell/ChatListMessageIndicatorsView.swift @@ -1,65 +1,48 @@ // -// ChatListMessageAccessoryView.swift +// ChatListMessageIndicatorsView.swift // Nynja // -// Created by Anton Poltoratskyi on 15.05.2018. +// Created by Anton Poltoratskyi on 02.11.2018. // Copyright © 2018 TecSynt Solutions. All rights reserved. // import UIKit import SnapKit -final class ChatListMessageAccessoryView: BaseView { - - private static let dateFormatter = DialogDateConverter() - +final class ChatListMessageIndicatorsView: BaseView { // MARK: - Views - private(set) lazy var timeLabel: UILabel = { - let height = Constraints.timeLabel.height.adjustedByWidth - let color = UIColor.nynja.manatee - - let label = UILabel(height: height, color: color, fontName: FontFamily.NotoSans.regular.name, textAlignment: .right) - label.setContentCompressionResistancePriority(.required, for: .horizontal) - - addSubview(label) - label.snp.makeConstraints { maker in - maker.top.left.right.equalToSuperview() - maker.height.equalTo(height) - } - - return label - }() + private var counterSuperviewLeftConstraint: Constraint? private(set) lazy var counterView: CounterView = { - let height = Constraints.countView.height.adjustedByWidth - let topInset = Constraints.countView.topInset.adjustedByWidth - let fontHeight = Constraints.countView.fontHeight.adjustedByWidth let font = UIFont.makeFont(with: FontFamily.NotoSans.regular.name, height: fontHeight)! let textColor = UIColor.nynja.white let inset = Constraints.countView.horizontalContentInset.adjustedByWidth - let view = CounterView(font: font, - textColor: textColor, - horizontalInset: inset) - + let view = CounterView(font: font, textColor: textColor, horizontalInset: inset) + + view.setContentCompressionResistancePriority(.required, for: .horizontal) + view.setContentHuggingPriority(.required, for: .horizontal) + view.backgroundColor = UIColor.nynja.mainRed addSubview(view) view.snp.makeConstraints { maker in - maker.top.equalTo(timeLabel.snp.bottom).offset(topInset) - maker.right.equalToSuperview() - maker.height.equalTo(height) + maker.top.bottom.right.equalToSuperview() + counterSuperviewLeftConstraint = maker.left.equalToSuperview().constraint } return view }() + + private var mentionCounterConstraint: Constraint? + private var mentionSuperviewConstraint: Constraint? + private(set) lazy var mentionIndicatorView: UIImageView = { let size = Constraints.mentionIndicatorView.size.adjustedByWidth - let topInset = Constraints.mentionIndicatorView.topInset.adjustedByWidth let rightInset = Constraints.mentionIndicatorView.rightInset.adjustedByWidth let imageView = UIImageView() @@ -69,19 +52,18 @@ final class ChatListMessageAccessoryView: BaseView { addSubview(imageView) imageView.snp.makeConstraints { maker in - maker.top.equalTo(timeLabel.snp.bottom).offset(topInset) + maker.top.bottom.equalToSuperview() + mentionCounterConstraint = maker.right.equalTo(counterView.snp.left).offset(-rightInset).constraint mentionSuperviewConstraint = maker.right.equalToSuperview().constraint - maker.left.greaterThanOrEqualToSuperview() + + maker.left.equalToSuperview() maker.width.height.equalTo(size) } return imageView }() - private var mentionCounterConstraint: Constraint? - private var mentionSuperviewConstraint: Constraint? - // MARK: - Setup @@ -92,15 +74,13 @@ final class ChatListMessageAccessoryView: BaseView { mentionSuperviewConstraint?.deactivate() } - func setup(date: Date) { - timeLabel.text = type(of: self).dateFormatter.toString(date) - } - func setup(mentions: Bool, unreadCount: Int) { mentionIndicatorView.isHidden = !mentions counterView.count = unreadCount if mentions { + counterSuperviewLeftConstraint?.deactivate() + if unreadCount <= 0 { mentionCounterConstraint?.deactivate() mentionSuperviewConstraint?.activate() @@ -108,31 +88,25 @@ final class ChatListMessageAccessoryView: BaseView { mentionCounterConstraint?.activate() mentionSuperviewConstraint?.deactivate() } + } else { + counterSuperviewLeftConstraint?.activate() } } } // MARK: - Layout -extension ChatListMessageAccessoryView { +private extension ChatListMessageIndicatorsView { enum Constraints { - - enum timeLabel { - static let height: CGFloat = 17 - } - + enum countView { - static let topInset: CGFloat = 4.0 - static let height: CGFloat = 18.0 - static let fontHeight: CGFloat = 17.0 static let horizontalContentInset: CGFloat = 2.0 } enum mentionIndicatorView { static let size: CGFloat = 18.0 - static let topInset: CGFloat = 4.0 static let rightInset: CGFloat = 16.0 } } diff --git a/Nynja/Library/UI/Lists/TableView/Cells/ChatListMessageCell/Cell/ChatListMessageTableViewCell.swift b/Nynja/Library/UI/Lists/TableView/Cells/ChatListMessageCell/Cell/ChatListMessageTableViewCell.swift index 4885b1c82..7e9e1b34d 100644 --- a/Nynja/Library/UI/Lists/TableView/Cells/ChatListMessageCell/Cell/ChatListMessageTableViewCell.swift +++ b/Nynja/Library/UI/Lists/TableView/Cells/ChatListMessageCell/Cell/ChatListMessageTableViewCell.swift @@ -41,36 +41,14 @@ final class ChatListMessageTableViewCell: UITableViewCell { private(set) lazy var messageContentView: ChatListMessageContentView = { let view = ChatListMessageContentView() - let width = Constraints.messageContentView.width.adjustedByWidth let leftInset = Constraints.messageContentView.leftInset.adjustedByWidth - let rightInset = Constraints.messageAccessoryView.minLeftInset.adjustedByWidth - - view.setContentHuggingPriority(.required, for: .horizontal) + let rightInset = Constraints.messageContentView.rightInset.adjustedByWidth contentView.addSubview(view) view.snp.makeConstraints { maker in maker.left.equalTo(avatarImageView.snp.right).offset(leftInset) - maker.right.lessThanOrEqualTo(messageAccessoryView.snp.left).offset(-rightInset) - maker.centerY.equalToSuperview() - } - - view.titleLabel.snp.makeConstraints { maker in - maker.centerY.equalTo(messageAccessoryView.timeLabel) - } - - return view - }() - - private(set) lazy var messageAccessoryView: ChatListMessageAccessoryView = { - let view = ChatListMessageAccessoryView() - - let height = Constraints.messageAccessoryView.height.adjustedByWidth - let rightInset = Constraints.messageAccessoryView.rightInset.adjustedByWidth - - contentView.addSubview(view) - view.snp.makeConstraints { maker in maker.right.equalToSuperview().inset(rightInset) - maker.height.equalTo(height) + maker.centerY.equalToSuperview() } return view @@ -191,12 +169,6 @@ extension ChatListMessageTableViewCell { enum messageContentView { static let leftInset: CGFloat = 16.0 - static let width: CGFloat = 260 - } - - enum messageAccessoryView { - static let height: CGFloat = ChatListMessageAccessoryView.Constraints.timeLabel.height + ChatListMessageAccessoryView.Constraints.countView.height - static let minLeftInset: CGFloat = 8.0 static let rightInset: CGFloat = 16.0 } diff --git a/Nynja/Library/UI/Lists/TableView/Cells/ChatListMessageCell/Cell/CounterView.swift b/Nynja/Library/UI/Lists/TableView/Cells/ChatListMessageCell/Cell/CounterView.swift index 8616b9efe..ca3c23fa9 100644 --- a/Nynja/Library/UI/Lists/TableView/Cells/ChatListMessageCell/Cell/CounterView.swift +++ b/Nynja/Library/UI/Lists/TableView/Cells/ChatListMessageCell/Cell/CounterView.swift @@ -21,12 +21,16 @@ final class CounterView: UIView { isHidden = true countLabel.isHidden = true } + invalidateIntrinsicContentSize() + setNeedsLayout() } } var font: UIFont = UIFont.makeFont(with: FontFamily.NotoSans.regular.name, height: CGFloat(17.0.adjustedByWidth))! { didSet { countLabel.font = font + invalidateIntrinsicContentSize() + setNeedsLayout() } } @@ -38,13 +42,20 @@ final class CounterView: UIView { var horizontalInset: CGFloat = 0 { didSet { - countLabel.snp.updateConstraints { maker in - maker.left.greaterThanOrEqualToSuperview().offset(horizontalInset) - maker.right.lessThanOrEqualToSuperview().offset(-horizontalInset) - } + invalidateIntrinsicContentSize() + setNeedsLayout() } } + override var intrinsicContentSize: CGSize { + let maxSize = CGSize(width: CGFloat.greatestFiniteMagnitude, height: CGFloat.greatestFiniteMagnitude) + + var size = countLabel.sizeThatFits(maxSize) + size.width += horizontalInset * 2 + + return size + } + // MARK: - Views @@ -59,8 +70,6 @@ final class CounterView: UIView { addSubview(label) label.snp.makeConstraints { maker in maker.center.equalToSuperview() - maker.left.greaterThanOrEqualToSuperview().offset(horizontalInset) - maker.right.lessThanOrEqualToSuperview().offset(-horizontalInset) } return label @@ -87,10 +96,6 @@ final class CounterView: UIView { private func setup() { countLabel.isHidden = false - snp.makeConstraints { maker in - maker.width.equalTo(self.snp.height).priority(.high) - maker.width.greaterThanOrEqualTo(self.snp.height) - } } diff --git a/Nynja/Library/UI/Lists/TableView/Cells/ChatListMessageCell/Model/ChatListMessageCellModel.swift b/Nynja/Library/UI/Lists/TableView/Cells/ChatListMessageCell/Model/ChatListMessageCellModel.swift index 5cfa7a1b4..1f3f4919d 100644 --- a/Nynja/Library/UI/Lists/TableView/Cells/ChatListMessageCell/Model/ChatListMessageCellModel.swift +++ b/Nynja/Library/UI/Lists/TableView/Cells/ChatListMessageCell/Model/ChatListMessageCellModel.swift @@ -59,9 +59,9 @@ final class ChatListMessageCellModel: CellViewModel { let unreadCount = min(Int(model.unreadMessagesCount), type(of: self).unreadCounterLimit) let shouldDisplayMention = model.hasMentions - cell.messageAccessoryView.setup(mentions: shouldDisplayMention, unreadCount: unreadCount) + cell.messageContentView.setup(mentions: shouldDisplayMention, unreadCount: unreadCount) if let createdDate = model.message?.createdDate { - cell.messageAccessoryView.setup(date: createdDate) + cell.messageContentView.setup(date: createdDate) } } -- GitLab