From 635ac630e43ec2c319468708428edb8b071f117e Mon Sep 17 00:00:00 2001 From: Kuldar-Daniel Kokorev Date: Tue, 28 Jun 2022 10:28:58 +0300 Subject: [PATCH 01/37] Visitor Transfer (#271) --- GliaWidgets.xcodeproj/project.pbxproj | 23 ++ GliaWidgets/Asset.swift | 2 + GliaWidgets/Component/Bubble/BubbleView.swift | 4 +- .../Component/Connect/ConnectStyle.swift | 6 + .../Component/Connect/ConnectView.swift | 38 ++- .../Operator/ConnectOperatorView.swift | 1 + .../Component/ImageView/ImageView.swift | 1 + .../ImageView/User/UserImageStyle.swift | 8 +- .../ImageView/User/UserImageView.swift | 36 +-- .../UnreadMessageIndicatorStyle.swift | 5 +- .../UnreadMessageIndicatorView.swift | 4 +- .../CoreSDKClient/CoreSDKClient.Mock.swift | 1 + GliaWidgets/GCD.Interface.swift | 8 +- GliaWidgets/GCD.Live.swift | 9 + GliaWidgets/GCD.Mock.swift | 14 ++ GliaWidgets/Glia.swift | 5 +- .../Interactor.Environment.Interface.swift | 1 + .../Interactor.Environment.Mock.swift | 5 +- GliaWidgets/Interactor/Interactor.swift | 23 +- GliaWidgets/L10n.swift | 8 + GliaWidgets/Lib/Section/Section.swift | 4 + .../Contents.json | 15 ++ .../Frame 46705692.pdf | Bin 0 -> 2051 bytes .../Resources/en.lproj/Localizable.strings | 2 + GliaWidgets/Theme/Theme+Call.swift | 12 +- GliaWidgets/Theme/Theme+Chat.swift | 16 +- GliaWidgets/Theme/Theme+MinimizedBubble.swift | 3 +- GliaWidgets/View/Chat/ChatView.swift | 36 +++ .../Message/OperatorChatMessageView.swift | 2 +- .../Call/CallViewController.swift | 6 +- .../Chat/ChatViewController.Mock.swift | 2 +- .../Chat/ChatViewController.swift | 8 +- .../EngagementViewController.swift | 15 +- .../ViewModel/Call/CallViewModel.swift | 19 +- GliaWidgets/ViewModel/Call/Data/Call.swift | 21 +- .../ViewModel/Chat/ChatViewModel.swift | 71 ++++-- .../ViewModel/Chat/Data/ChatItem.swift | 22 +- .../ViewModel/EngagementViewModel.swift | 4 +- GliaWidgetsTests/GCD.Failing.swift | 3 +- .../Interactor.Environment.Failing.swift | 5 +- .../Lib/ChatItem/ChatItem+Equatable.swift | 36 +++ GliaWidgetsTests/Mocks/GliaWidgets.Mock.swift | 18 ++ GliaWidgetsTests/Sources/CallTests.swift | 63 +++++ .../Sources/CallViewModelTests.swift | 121 ++++++++++ .../Sources/ChatViewModelTests.swift | 223 ++++++++---------- .../Sources/InteractorStateTests.swift | 20 ++ .../Sources/InteractorTests.swift | 39 ++- 47 files changed, 765 insertions(+), 223 deletions(-) create mode 100644 GliaWidgets/Resources/Assets.xcassets/Operator/operatorTransferring.imageset/Contents.json create mode 100644 GliaWidgets/Resources/Assets.xcassets/Operator/operatorTransferring.imageset/Frame 46705692.pdf create mode 100644 GliaWidgetsTests/Lib/ChatItem/ChatItem+Equatable.swift create mode 100644 GliaWidgetsTests/Mocks/GliaWidgets.Mock.swift create mode 100644 GliaWidgetsTests/Sources/CallTests.swift create mode 100644 GliaWidgetsTests/Sources/InteractorStateTests.swift diff --git a/GliaWidgets.xcodeproj/project.pbxproj b/GliaWidgets.xcodeproj/project.pbxproj index dde8f2a62..9487b83e5 100644 --- a/GliaWidgets.xcodeproj/project.pbxproj +++ b/GliaWidgets.xcodeproj/project.pbxproj @@ -367,6 +367,10 @@ EB2CBB1227D89F7D004F178E /* OnHoldOverlayStyle.swift in Sources */ = {isa = PBXBuildFile; fileRef = EB2CBB1127D89F7D004F178E /* OnHoldOverlayStyle.swift */; }; EB2CBB1527D8DB95004F178E /* OnHoldOverlayVisualEffectView.swift in Sources */ = {isa = PBXBuildFile; fileRef = EB2CBB1427D8DB95004F178E /* OnHoldOverlayVisualEffectView.swift */; }; EB750F53273BA9BB00BE5FBD /* GliaError.swift in Sources */ = {isa = PBXBuildFile; fileRef = EB750F52273BA9BB00BE5FBD /* GliaError.swift */; }; + EB9ADB51280EBD4E00FAE8A4 /* InteractorStateTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = EB9ADB50280EBD4E00FAE8A4 /* InteractorStateTests.swift */; }; + EB9ADB552828E66B00FAE8A4 /* CallTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = EB9ADB542828E66B00FAE8A4 /* CallTests.swift */; }; + EB9ADB5A2829089F00FAE8A4 /* ChatItem+Equatable.swift in Sources */ = {isa = PBXBuildFile; fileRef = EB9ADB592829089F00FAE8A4 /* ChatItem+Equatable.swift */; }; + FD01A52B483418AB02DCFBFC /* Pods_GliaWidgets.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 85639A838514258D976E1B2A /* Pods_GliaWidgets.framework */; }; EB95491C2850757400F567F0 /* ChatTextContentViewTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = EB95491B2850757400F567F0 /* ChatTextContentViewTests.swift */; }; /* End PBXBuildFile section */ @@ -792,6 +796,10 @@ EB2CBB1127D89F7D004F178E /* OnHoldOverlayStyle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OnHoldOverlayStyle.swift; sourceTree = ""; }; EB2CBB1427D8DB95004F178E /* OnHoldOverlayVisualEffectView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OnHoldOverlayVisualEffectView.swift; sourceTree = ""; }; EB750F52273BA9BB00BE5FBD /* GliaError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GliaError.swift; sourceTree = ""; }; + EB9ADB50280EBD4E00FAE8A4 /* InteractorStateTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InteractorStateTests.swift; sourceTree = ""; }; + EB9ADB542828E66B00FAE8A4 /* CallTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CallTests.swift; sourceTree = ""; }; + EB9ADB592829089F00FAE8A4 /* ChatItem+Equatable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ChatItem+Equatable.swift"; sourceTree = ""; }; + F08274A374F775EE39BFBDB1 /* Pods-GliaWidgetsTests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-GliaWidgetsTests.debug.xcconfig"; path = "Target Support Files/Pods-GliaWidgetsTests/Pods-GliaWidgetsTests.debug.xcconfig"; sourceTree = ""; }; EB95491B2850757400F567F0 /* ChatTextContentViewTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatTextContentViewTests.swift; sourceTree = ""; }; /* End PBXFileReference section */ @@ -1881,8 +1889,10 @@ 7512A57627BE8A6700319DF1 /* InteractorTests.swift */, 7512A57927BF9FCD00319DF1 /* ChatViewModelTests.swift */, 7512A5A627C3926500319DF1 /* GliaTests.swift */, + EB9ADB50280EBD4E00FAE8A4 /* InteractorStateTests.swift */, EB27E71C27FEBB620090B895 /* CallViewModelTests.swift */, EB03B00D27FFF6DD0058F6B1 /* CallViewTests.swift */, + EB9ADB542828E66B00FAE8A4 /* CallTests.swift */, EB95491B2850757400F567F0 /* ChatTextContentViewTests.swift */, 847A7642285A1914004044D1 /* FileUploadListViewTests.swift */, ); @@ -2036,6 +2046,7 @@ 9A3E1D8527B6B668005634EB /* Lib */ = { isa = PBXGroup; children = ( + EB9ADB582829089700FAE8A4 /* ChatItem */, 9A8130C027D9091800220BBD /* Download */, 9A3E1D9E27BA7B80005634EB /* Storage */, ); @@ -2202,6 +2213,14 @@ path = OnHoldOverlay; sourceTree = ""; }; + EB9ADB582829089700FAE8A4 /* ChatItem */ = { + isa = PBXGroup; + children = ( + EB9ADB592829089F00FAE8A4 /* ChatItem+Equatable.swift */, + ); + path = ChatItem; + sourceTree = ""; + }; /* End PBXGroup section */ /* Begin PBXHeadersBuildPhase section */ @@ -2929,9 +2948,13 @@ 7512A5A727C3926500319DF1 /* GliaTests.swift in Sources */, 9A3E1DA227BA7D00005634EB /* FileSystemStorage.Environment.Failing.swift in Sources */, 9A1992E127D6313500161AAE /* ImageView.Cache.Failing.swift in Sources */, + EB9ADB552828E66B00FAE8A4 /* CallTests.swift in Sources */, 9A3E1DA027BA7B9F005634EB /* FileSystemStorageTests.swift in Sources */, + 7512A57D27BFA37D00319DF1 /* CoreSdkClient.Salemove.swift in Sources */, + EB9ADB5A2829089F00FAE8A4 /* ChatItem+Equatable.swift in Sources */, EB27E71D27FEBB620090B895 /* CallViewModelTests.swift in Sources */, 9ACC25D227B4727500BC5335 /* CoreSDKClient.Failing.swift in Sources */, + EB9ADB51280EBD4E00FAE8A4 /* InteractorStateTests.swift in Sources */, 847A7643285A1914004044D1 /* FileUploadListViewTests.swift in Sources */, 9A1992E727D66C7400161AAE /* UIKitBased.Failing.swift in Sources */, 9AE05CB62805D2CB00871321 /* Interactor.Environment.Failing.swift in Sources */, diff --git a/GliaWidgets/Asset.swift b/GliaWidgets/Asset.swift index ea43ceb9a..e77d7c3e5 100644 --- a/GliaWidgets/Asset.swift +++ b/GliaWidgets/Asset.swift @@ -76,6 +76,7 @@ public enum Asset { public static let upgradeVideo = ImageAsset(name: "upgradeVideo") public static let mockImage = ImageAsset(name: "mock-image") public static let operatorPlaceholder = ImageAsset(name: "operatorPlaceholder") + public static let operatorTransferring = ImageAsset(name: "operatorTransferring") public static let surveyCheckboxChecked = ImageAsset(name: "survey-checkbox-checked") public static let surveyCheckbox = ImageAsset(name: "survey-checkbox") public static let surveyValidationError = ImageAsset(name: "survey-validation-error") @@ -111,6 +112,7 @@ public enum Asset { upgradeVideo, mockImage, operatorPlaceholder, + operatorTransferring, surveyCheckboxChecked, surveyCheckbox, surveyValidationError, diff --git a/GliaWidgets/Component/Bubble/BubbleView.swift b/GliaWidgets/Component/Bubble/BubbleView.swift index 1ee2d6b63..5dfc44305 100644 --- a/GliaWidgets/Component/Bubble/BubbleView.swift +++ b/GliaWidgets/Component/Bubble/BubbleView.swift @@ -117,7 +117,7 @@ class BubbleView: UIView { switch kind { case .userImage(url: let url): guard userImageView == nil else { - userImageView?.setImage(fromUrl: url, animated: true) + userImageView?.setOperatorImage(fromUrl: url, animated: true) break } let userImageView = UserImageView( @@ -129,7 +129,7 @@ class BubbleView: UIView { imageViewCache: environment.imageViewCache ) ) - userImageView.setImage(fromUrl: url, animated: true) + userImageView.setOperatorImage(fromUrl: url, animated: true) self.userImageView = userImageView setView(userImageView) } diff --git a/GliaWidgets/Component/Connect/ConnectStyle.swift b/GliaWidgets/Component/Connect/ConnectStyle.swift index 998b592dd..2090b3371 100644 --- a/GliaWidgets/Component/Connect/ConnectStyle.swift +++ b/GliaWidgets/Component/Connect/ConnectStyle.swift @@ -14,6 +14,9 @@ public struct ConnectStyle { /// Style of the connected state. The view in this state will be shown to the visitor when the operator has picked up the engagement and is successfully connected to the visitor. public var connected: ConnectStatusStyle + /// Style of the transferring state. The view in this state will be shown to the visitor when the operator has started a operator-to-queue engagement transfer for the visitor. + public var transferring: ConnectStatusStyle + /// Style of the onHold state. The view in this state will be shown to the visitor when the operator has successfully connected to the visitor and has put visitor on hold. public var onHold: ConnectStatusStyle @@ -23,6 +26,7 @@ public struct ConnectStyle { /// - queue: Style of the in-queue state. The view in this state will be shown to the visitor when they have requested an engagement and are waiting in a queue to be connected to an operator. /// - connecting: Style of the connecting state. The view in this state will be shown to the visitor when the operator has picked up the engagement but is still connecting to the visitor. /// - connected: Style of the connected state. The view in this state will be shown to the visitor when the operator has picked up the engagement and is successfully connected to the visitor. + /// - transferring: Style of the transferring state. The view in this state will be shown to the visitor when the operator has started a operator-to-queue engagement transfer for the visitor. /// - onHold: Style of the onHold state. The view in this state will be shown to the visitor when the operator has successfully connected to the visitor and has put visitor on hold. /// public init( @@ -30,12 +34,14 @@ public struct ConnectStyle { queue: ConnectStatusStyle, connecting: ConnectStatusStyle, connected: ConnectStatusStyle, + transferring: ConnectStatusStyle, onHold: ConnectStatusStyle ) { self.connectOperator = queueOperator self.queue = queue self.connecting = connecting self.connected = connected + self.transferring = transferring self.onHold = onHold } } diff --git a/GliaWidgets/Component/Connect/ConnectView.swift b/GliaWidgets/Component/Connect/ConnectView.swift index e6e1c1ee9..ef4d15673 100644 --- a/GliaWidgets/Component/Connect/ConnectView.swift +++ b/GliaWidgets/Component/Connect/ConnectView.swift @@ -6,6 +6,7 @@ class ConnectView: UIView { case queue case connecting(name: String?, imageUrl: String?) case connected(name: String?, imageUrl: String?) + case transferring } let operatorView: ConnectOperatorView @@ -17,6 +18,7 @@ class ConnectView: UIView { private var connectCounter: Int = 0 private var isShowing = false private let environment: Environment + private let stackView = UIStackView() init( with style: ConnectStyle, @@ -51,6 +53,7 @@ class ConnectView: UIView { hide(animated: animated) case .queue: stopConnectTimer() + operatorView.imageView.setPlaceholderImage(style.connectOperator.operatorImage.placeholderImage) operatorView.startAnimating(animated: animated) statusView.setFirstText(style.queue.firstText, animated: false) statusView.setSecondText(style.queue.secondText, animated: false) @@ -58,7 +61,8 @@ class ConnectView: UIView { show(animated: animated) case .connecting(let name, let imageUrl): operatorView.startAnimating(animated: animated) - operatorView.imageView.setImage(fromUrl: imageUrl, animated: true) + operatorView.imageView.setPlaceholderImage(style.connectOperator.operatorImage.placeholderImage) + operatorView.imageView.setOperatorImage(fromUrl: imageUrl, animated: true) let firstText = style.connecting.firstText?.withOperatorName(name) statusView.setFirstText(firstText, animated: animated) statusView.setSecondText(nil, animated: animated) @@ -68,7 +72,8 @@ class ConnectView: UIView { case .connected(let name, let imageUrl): stopConnectTimer() operatorView.stopAnimating(animated: animated) - operatorView.imageView.setImage(fromUrl: imageUrl, animated: true) + operatorView.imageView.setPlaceholderImage(style.connectOperator.operatorImage.placeholderImage) + operatorView.imageView.setOperatorImage(fromUrl: imageUrl, animated: true) if let name = name { let firstText = style.connected.firstText?.withOperatorName(name) let secondText = style.connected.secondText? @@ -82,6 +87,16 @@ class ConnectView: UIView { } statusView.setStyle(style.connected) show(animated: animated) + case .transferring: + stopConnectTimer() + operatorView.setSize(.normal, animated: true) + operatorView.startAnimating(animated: animated) + operatorView.imageView.setOperatorImage(nil, animated: true) + operatorView.imageView.setPlaceholderImage(style.connectOperator.operatorImage.transferringImage) + statusView.setFirstText(style.transferring.firstText, animated: true) + statusView.setSecondText(style.transferring.secondText, animated: true) + statusView.setStyle(style.transferring) + show(animated: animated) } } @@ -113,19 +128,18 @@ class ConnectView: UIView { private func setup() { setState(.none, animated: false) accessibilityElements = [operatorView, statusView] + + stackView.axis = .vertical + stackView.spacing = 10 } private func layout() { - addSubview(operatorView) - operatorView.autoPinEdge(toSuperviewEdge: .top, withInset: 10) - operatorView.autoPinEdge(toSuperviewEdge: .left) - operatorView.autoPinEdge(toSuperviewEdge: .right) - - addSubview(statusView) - statusView.autoPinEdge(.top, to: .bottom, of: operatorView, withOffset: 10) - statusView.autoPinEdge(toSuperviewEdge: .left) - statusView.autoPinEdge(toSuperviewEdge: .right) - statusView.autoPinEdge(toSuperviewEdge: .bottom, withInset: 10) + addSubview(stackView) + stackView.autoPinEdgesToSuperviewEdges(with: .init(top: 10, left: 0, bottom: 10, right: 0)) + stackView.addArrangedSubviews([ + operatorView, + statusView + ]) } } diff --git a/GliaWidgets/Component/Connect/Operator/ConnectOperatorView.swift b/GliaWidgets/Component/Connect/Operator/ConnectOperatorView.swift index 93b9b91a7..756aaee5f 100644 --- a/GliaWidgets/Component/Connect/Operator/ConnectOperatorView.swift +++ b/GliaWidgets/Component/Connect/Operator/ConnectOperatorView.swift @@ -64,6 +64,7 @@ class ConnectOperatorView: UIView { layout() } + @available(*, unavailable) required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } diff --git a/GliaWidgets/Component/ImageView/ImageView.swift b/GliaWidgets/Component/ImageView/ImageView.swift index dd2186f56..71490afea 100644 --- a/GliaWidgets/Component/ImageView/ImageView.swift +++ b/GliaWidgets/Component/ImageView/ImageView.swift @@ -9,6 +9,7 @@ class ImageView: UIImageView { super.init(frame: frame) } + @available(*, unavailable) required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } diff --git a/GliaWidgets/Component/ImageView/User/UserImageStyle.swift b/GliaWidgets/Component/ImageView/User/UserImageStyle.swift index d1baecedf..4933c9bc0 100644 --- a/GliaWidgets/Component/ImageView/User/UserImageStyle.swift +++ b/GliaWidgets/Component/ImageView/User/UserImageStyle.swift @@ -14,21 +14,27 @@ public struct UserImageStyle { /// Background color of the image (in case it has transparency). public var imageBackgroundColor: UIColor + /// Transferring image. It is shown if the visitor is being transferred to another operator. + public var transferringImage: UIImage? + /// /// - Parameters: /// - placeholderImage: Placeholder image. /// - placeholderColor: Color of the placeholder image. /// - placeholderBackgroundColor: Color of the placeholder background. /// - imageBackgroundColor: Background color of the image (in case it has transparency). + /// - transferringImage: Transferring image. It is shown if the visitor is being transferred to another operator. public init( placeholderImage: UIImage?, placeholderColor: UIColor, placeholderBackgroundColor: UIColor, - imageBackgroundColor: UIColor + imageBackgroundColor: UIColor, + transferringImage: UIImage? ) { self.placeholderImage = placeholderImage self.placeholderColor = placeholderColor self.placeholderBackgroundColor = placeholderBackgroundColor self.imageBackgroundColor = imageBackgroundColor + self.transferringImage = transferringImage } } diff --git a/GliaWidgets/Component/ImageView/User/UserImageView.swift b/GliaWidgets/Component/ImageView/User/UserImageView.swift index e2e81ecb6..730f1cd88 100644 --- a/GliaWidgets/Component/ImageView/User/UserImageView.swift +++ b/GliaWidgets/Component/ImageView/User/UserImageView.swift @@ -3,7 +3,7 @@ import UIKit class UserImageView: UIView { private let style: UserImageStyle private let placeholderImageView = UIImageView() - private let imageView: ImageView + private let operatorImageView: ImageView private let environment: Environment init( @@ -12,7 +12,7 @@ class UserImageView: UIView { ) { self.style = style self.environment = environment - self.imageView = ImageView( + self.operatorImageView = ImageView( environment: .init( data: environment.data, uuid: environment.uuid, @@ -37,17 +37,21 @@ class UserImageView: UIView { updatePlaceholderContentMode() } - func setImage(_ image: UIImage?, animated: Bool) { - changeImageVisibility(visible: image != nil) - imageView.setImage(image, animated: animated) + func setPlaceholderImage(_ image: UIImage?) { + placeholderImageView.image = image } - func setImage(fromUrl url: String?, animated: Bool) { - imageView.setImage( + func setOperatorImage(_ image: UIImage?, animated: Bool) { + changeOperatorImageVisibility(visible: image != nil) + operatorImageView.setImage(image, animated: animated) + } + + func setOperatorImage(fromUrl url: String?, animated: Bool) { + operatorImageView.setImage( from: url, animated: animated, imageReceived: { [weak self] image in - self?.changeImageVisibility(visible: image != nil) + self?.changeOperatorImageVisibility(visible: image != nil) } ) } @@ -55,22 +59,22 @@ class UserImageView: UIView { private func setup() { clipsToBounds = true - placeholderImageView.image = style.placeholderImage placeholderImageView.tintColor = style.placeholderColor placeholderImageView.backgroundColor = style.placeholderBackgroundColor + placeholderImageView.image = style.placeholderImage updatePlaceholderContentMode() - imageView.isHidden = true - imageView.contentMode = .scaleAspectFill - imageView.backgroundColor = style.imageBackgroundColor + operatorImageView.isHidden = true + operatorImageView.contentMode = .scaleAspectFill + operatorImageView.backgroundColor = style.imageBackgroundColor } private func layout() { addSubview(placeholderImageView) placeholderImageView.autoPinEdgesToSuperviewEdges() - addSubview(imageView) - imageView.autoPinEdgesToSuperviewEdges() + addSubview(operatorImageView) + operatorImageView.autoPinEdgesToSuperviewEdges() } private func updatePlaceholderContentMode() { @@ -84,9 +88,9 @@ class UserImageView: UIView { } } - private func changeImageVisibility(visible: Bool) { + private func changeOperatorImageVisibility(visible: Bool) { placeholderImageView.isHidden = visible - imageView.isHidden = !visible + operatorImageView.isHidden = !visible } } diff --git a/GliaWidgets/Component/UnreadMessageIndicator/UnreadMessageIndicatorStyle.swift b/GliaWidgets/Component/UnreadMessageIndicator/UnreadMessageIndicatorStyle.swift index 0d3918310..6a9349ef8 100644 --- a/GliaWidgets/Component/UnreadMessageIndicator/UnreadMessageIndicatorStyle.swift +++ b/GliaWidgets/Component/UnreadMessageIndicator/UnreadMessageIndicatorStyle.swift @@ -21,6 +21,7 @@ public struct UnreadMessageIndicatorStyle { /// - placeholderImage: Image that acts as a placeholder if the operator has no picture set. /// - placeholderColor: Color of the placeholder's image if the operator has no picture set. /// - placeholderBackgroundColor: Background color of the placeholder's image if the operator has no picture set. + /// - imageBackgroundColor: Background color of the operator's image. Visible when the operator's image contains transparent parts. /// - imageBackgroundColor: Background olor of the operator's image. Visible when the operator's image contains transparent parts. /// - accessibility: Accessibility related properties. public init( @@ -31,6 +32,7 @@ public struct UnreadMessageIndicatorStyle { placeholderColor: UIColor, placeholderBackgroundColor: UIColor, imageBackgroundColor: UIColor, + transferringImage: UIImage, accessibility: Accessibility = .unsupported ) { self.badge = BadgeStyle( @@ -42,7 +44,8 @@ public struct UnreadMessageIndicatorStyle { placeholderImage: placeholderImage, placeholderColor: placeholderColor, placeholderBackgroundColor: placeholderBackgroundColor, - imageBackgroundColor: imageBackgroundColor + imageBackgroundColor: imageBackgroundColor, + transferringImage: transferringImage ) self.accessibility = accessibility } diff --git a/GliaWidgets/Component/UnreadMessageIndicator/UnreadMessageIndicatorView.swift b/GliaWidgets/Component/UnreadMessageIndicator/UnreadMessageIndicatorView.swift index 9249d14b1..dc494e846 100644 --- a/GliaWidgets/Component/UnreadMessageIndicator/UnreadMessageIndicatorView.swift +++ b/GliaWidgets/Component/UnreadMessageIndicator/UnreadMessageIndicatorView.swift @@ -48,11 +48,11 @@ final class UnreadMessageIndicatorView: View { } func setImage(_ image: UIImage?, animated: Bool) { - userImageView.setImage(image, animated: animated) + userImageView.setOperatorImage(image, animated: animated) } func setImage(fromUrl url: String?, animated: Bool) { - userImageView.setImage(fromUrl: url, animated: animated) + userImageView.setOperatorImage(fromUrl: url, animated: animated) } override func setup() { diff --git a/GliaWidgets/CoreSDKClient/CoreSDKClient.Mock.swift b/GliaWidgets/CoreSDKClient/CoreSDKClient.Mock.swift index 76123cb5f..627bf940b 100644 --- a/GliaWidgets/CoreSDKClient/CoreSDKClient.Mock.swift +++ b/GliaWidgets/CoreSDKClient/CoreSDKClient.Mock.swift @@ -1,5 +1,6 @@ #if DEBUG import Foundation +import SalemoveSDK extension CoreSdkClient { static let mock = Self( diff --git a/GliaWidgets/GCD.Interface.swift b/GliaWidgets/GCD.Interface.swift index 5b2e8026e..cd0edadaf 100644 --- a/GliaWidgets/GCD.Interface.swift +++ b/GliaWidgets/GCD.Interface.swift @@ -1,7 +1,7 @@ import Dispatch struct GCD { - var mainQueue: DispatchQueue + var mainQueue: MainQueue var globalQueue: DispatchQueue } @@ -10,4 +10,10 @@ extension GCD { var async: (@escaping () -> Void) -> Void var asyncAfterDeadline: (DispatchTime, @escaping () -> Void) -> Void } + + struct MainQueue { + var async: (@escaping () -> Void) -> Void + var asyncIfNeeded: (@escaping () -> Void) -> Void + var asyncAfterDeadline: (DispatchTime, @escaping () -> Void) -> Void + } } diff --git a/GliaWidgets/GCD.Live.swift b/GliaWidgets/GCD.Live.swift index e1a4c655c..5564bed87 100644 --- a/GliaWidgets/GCD.Live.swift +++ b/GliaWidgets/GCD.Live.swift @@ -5,7 +5,16 @@ extension GCD { mainQueue: .init( async: { callback in Dispatch.DispatchQueue.main.async { + callback() + } + }, + asyncIfNeeded: { callback in + if Thread.isMainThread { callback() + } else { + Dispatch.DispatchQueue.main.async { + callback() + } } }, asyncAfterDeadline: { deadline, callback in diff --git a/GliaWidgets/GCD.Mock.swift b/GliaWidgets/GCD.Mock.swift index a50cff621..a53f5d4c4 100644 --- a/GliaWidgets/GCD.Mock.swift +++ b/GliaWidgets/GCD.Mock.swift @@ -15,3 +15,17 @@ extension GCD.DispatchQueue { } ) } + +extension GCD.MainQueue { + static let mock = Self( + async: { callback in + callback() + }, + asyncIfNeeded: { callback in + callback() + }, + asyncAfterDeadline: { _, callback in + callback() + } + ) +} diff --git a/GliaWidgets/Glia.swift b/GliaWidgets/Glia.swift index d042632dd..af1d1d571 100644 --- a/GliaWidgets/Glia.swift +++ b/GliaWidgets/Glia.swift @@ -72,7 +72,10 @@ public class Glia { with: sdkConfiguration, queueID: queueId, visitorContext: visitorContext, - environment: .init(coreSdk: environment.coreSdk) + environment: .init( + coreSdk: environment.coreSdk, + gcd: environment.gcd + ) ) } diff --git a/GliaWidgets/Interactor/Interactor.Environment.Interface.swift b/GliaWidgets/Interactor/Interactor.Environment.Interface.swift index 8f9aebdb7..712b293b5 100644 --- a/GliaWidgets/Interactor/Interactor.Environment.Interface.swift +++ b/GliaWidgets/Interactor/Interactor.Environment.Interface.swift @@ -1,5 +1,6 @@ extension Interactor { struct Environment { var coreSdk: CoreSdkClient + var gcd: GCD } } diff --git a/GliaWidgets/Interactor/Interactor.Environment.Mock.swift b/GliaWidgets/Interactor/Interactor.Environment.Mock.swift index 62326d3c2..bc3ed2a56 100644 --- a/GliaWidgets/Interactor/Interactor.Environment.Mock.swift +++ b/GliaWidgets/Interactor/Interactor.Environment.Mock.swift @@ -1,5 +1,8 @@ #if DEBUG extension Interactor.Environment { - static let mock = Self(coreSdk: .mock) + static let mock = Self( + coreSdk: .mock, + gcd: .mock + ) } #endif diff --git a/GliaWidgets/Interactor/Interactor.swift b/GliaWidgets/Interactor/Interactor.swift index 2c5c1b1f8..c75e3b17e 100644 --- a/GliaWidgets/Interactor/Interactor.swift +++ b/GliaWidgets/Interactor/Interactor.swift @@ -28,6 +28,8 @@ enum InteractorEvent { case screenShareError(error: CoreSdkClient.SalemoveError) case screenSharingStateChanged(to: CoreSdkClient.VisitorScreenSharingState) case error(CoreSdkClient.SalemoveError) + case engagementTransferred(CoreSdkClient.Operator?) + case engagementTransferring } class Interactor { @@ -91,13 +93,14 @@ class Interactor { observers.removeAll(where: { $0().0 === observer }) } - private func notify(_ event: InteractorEvent) { + func notify(_ event: InteractorEvent) { observers .compactMap { $0() } .filter { $0.0 != nil } .forEach { let handler = $0.1 - DispatchQueue.main.async { + + environment.gcd.mainQueue.asyncIfNeeded { handler(event) } } @@ -286,11 +289,18 @@ extension Interactor: CoreSdkClient.Interactable { } var onEngagementTransfer: CoreSdkClient.EngagementTransferBlock { - return { _ in } + return { [weak self] operators in + let engagedOperator = operators?.first + + self?.state = .engaged(engagedOperator) + self?.notify(.engagementTransferred(engagedOperator)) + } } var onEngagementTransferring: CoreSdkClient.EngagementTransferringBlock { - { } + return { [weak self] in + self?.notify(.engagementTransferring) + } } var onOperatorTypingStatusUpdate: CoreSdkClient.OperatorTypingStatusUpdate { @@ -363,10 +373,13 @@ extension InteractorState: Equatable { switch (lhs, rhs) { case (.none, .none), (.enqueueing, .enqueueing), - (.engaged, .engaged), (.enqueued, .enqueued), (.ended, .ended): return true + + case (.engaged(let lhsOperator), .engaged(let rhsOperator)): + return lhsOperator == rhsOperator + default: return false } diff --git a/GliaWidgets/L10n.swift b/GliaWidgets/L10n.swift index f63c7f288..2a68d4f22 100644 --- a/GliaWidgets/L10n.swift +++ b/GliaWidgets/L10n.swift @@ -390,6 +390,10 @@ public enum L10n { /// An MSR will be with you shortly. public static let secondText = L10n.tr("Localizable", "call.connect.queue.secondText") } + public enum Transferring { + /// Transferring + public static let firstText = L10n.tr("Localizable", "call.connect.transferring.firstText") + } } public enum EndButton { /// End @@ -585,6 +589,10 @@ public enum L10n { /// An MSR will be with you shortly. public static let secondText = L10n.tr("Localizable", "chat.connect.queue.secondText") } + public enum Transferring { + /// Transferring + public static let firstText = L10n.tr("Localizable", "chat.connect.transferring.firstText") + } } public enum Download { /// Download diff --git a/GliaWidgets/Lib/Section/Section.swift b/GliaWidgets/Lib/Section/Section.swift index 5366ee593..6f8ca45c0 100644 --- a/GliaWidgets/Lib/Section/Section.swift +++ b/GliaWidgets/Lib/Section/Section.swift @@ -36,4 +36,8 @@ class Section { guard index + 1 < itemCount else { return nil } return items_[index + 1] } + + func removeItem(at index: Int) { + items_.remove(at: index) + } } diff --git a/GliaWidgets/Resources/Assets.xcassets/Operator/operatorTransferring.imageset/Contents.json b/GliaWidgets/Resources/Assets.xcassets/Operator/operatorTransferring.imageset/Contents.json new file mode 100644 index 000000000..7dda8d1b8 --- /dev/null +++ b/GliaWidgets/Resources/Assets.xcassets/Operator/operatorTransferring.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "Frame 46705692.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "preserves-vector-representation" : true + } +} diff --git a/GliaWidgets/Resources/Assets.xcassets/Operator/operatorTransferring.imageset/Frame 46705692.pdf b/GliaWidgets/Resources/Assets.xcassets/Operator/operatorTransferring.imageset/Frame 46705692.pdf new file mode 100644 index 0000000000000000000000000000000000000000..b1cb9f054d3479bca1b47a28aa3a6338dfde0acb GIT binary patch literal 2051 zcmbW2O;6k~5Qgvm6>~v?1CBppJCP7lSxQy4Rq4_j>LJY<*osZsO{i#pJu}&49H0ki zR$67B9nU*{Ga2ulpPn6R8HCWFp#A$kDm zi0m}}Ue|H??gSPW^Ivr*ehDqraDkd`4*Bl+f4Y<=*xVLSZ_-kZN0Jwd(|WL`-ISuQb+pNxfI#^QMl~d6B=MV*`d_dn>Tg z+n|EOj%B8$H$}iMcGBm$52Iy4+acUrKXxY(G8V&?8|^2?mT@K~tT*del5rMaAI?Oj z4DTL=4|+^$e6z-?m~rKtiOEdwmBw1K(*!jd`@&IBNL)h`AeYt~oCRnsGc8MJN*BP( z5Uf`Qb`vOQ>IvGY&1ZtIT;g{oOw%1rYJpnJEo#)yz?}VMnxvI;KG=3 z@tldtOmFg*=Jhy?r^!u2dNeXyVAysX`<(RH)UGJuD#pVu>(3lKy?w1Xu~I*axW*Xn2WcSvTL@GY5Tq78N|m^Qws`s%j5e!TR48?S_dAa=(X zeS>$-IzT*L*hs@2G>ovN9wY5=;ZtPrG10{fwa2D;f1_Vd{T`vIyg literal 0 HcmV?d00001 diff --git a/GliaWidgets/Resources/en.lproj/Localizable.strings b/GliaWidgets/Resources/en.lproj/Localizable.strings index c1cef5dea..f50dffb71 100644 --- a/GliaWidgets/Resources/en.lproj/Localizable.strings +++ b/GliaWidgets/Resources/en.lproj/Localizable.strings @@ -66,6 +66,7 @@ "chat.connect.connecting.secondText" = ""; "chat.connect.connected.firstText" = "{operatorName}"; "chat.connect.connected.secondText" = "{operatorName} has joined the conversation."; +"chat.connect.transferring.firstText" = "Transferring"; "chat.message.enterMessagePlaceholder" = "Enter Message"; "chat.message.startEngagementPlaceholder" = "Send a message to start chatting"; "chat.message.choiceCardPlaceholder" = "Tap on the answer above"; @@ -138,6 +139,7 @@ "call.connect.connecting.secondText" = ""; "call.connect.connected.firstText" = "{operatorName}"; "call.connect.connected.secondText" = "{callDuration}"; +"call.connect.transferring.firstText" = "Transferring"; "call.operator.name" = "{operatorName}"; "call.topText" = "(By default your video will be off)"; "call.bottomText" = "You can continue browsing and we’ll connect you automatically."; diff --git a/GliaWidgets/Theme/Theme+Call.swift b/GliaWidgets/Theme/Theme+Call.swift index 9548a3b26..545f65474 100644 --- a/GliaWidgets/Theme/Theme+Call.swift +++ b/GliaWidgets/Theme/Theme+Call.swift @@ -58,7 +58,8 @@ extension Theme { placeholderImage: Asset.operatorPlaceholder.image, placeholderColor: color.baseLight, placeholderBackgroundColor: color.primary, - imageBackgroundColor: .clear + imageBackgroundColor: .clear, + transferringImage: Asset.operatorTransferring.image ) let queueOperator = ConnectOperatorStyle( operatorImage: operatorImage, @@ -121,11 +122,20 @@ extension Theme { isFontScalingEnabled: true ) ) + let transferring = ConnectStatusStyle( + firstText: Call.Connect.Transferring.firstText, + firstTextFont: font.header1, + firstTextFontColor: color.baseLight, + secondText: nil, + secondTextFont: font.subtitle, + secondTextFontColor: color.baseLight + ) let connect = ConnectStyle( queueOperator: queueOperator, queue: queue, connecting: connecting, connected: connected, + transferring: transferring, onHold: onHold ) let onHoldStyle = CallStyle.OnHoldStyle( diff --git a/GliaWidgets/Theme/Theme+Chat.swift b/GliaWidgets/Theme/Theme+Chat.swift index d7c3dc938..7f58e8adf 100644 --- a/GliaWidgets/Theme/Theme+Chat.swift +++ b/GliaWidgets/Theme/Theme+Chat.swift @@ -56,7 +56,8 @@ extension Theme { placeholderImage: Asset.operatorPlaceholder.image, placeholderColor: color.baseLight, placeholderBackgroundColor: color.primary, - imageBackgroundColor: .clear + imageBackgroundColor: .clear, + transferringImage: Asset.operatorTransferring.image ) let queueOperator = ConnectOperatorStyle( operatorImage: operatorImage, @@ -119,11 +120,20 @@ extension Theme { isFontScalingEnabled: true ) ) + let transferring = ConnectStatusStyle( + firstText: Chat.Connect.Transferring.firstText, + firstTextFont: font.header1, + firstTextFontColor: color.baseDark, + secondText: nil, + secondTextFont: font.subtitle, + secondTextFontColor: color.primary + ) let connect = ConnectStyle( queueOperator: queueOperator, queue: queue, connecting: connecting, connected: connected, + transferring: transferring, onHold: onHold ) let visitorText = ChatTextContentStyle( @@ -291,7 +301,8 @@ extension Theme { placeholderImage: Asset.operatorPlaceholder.image, placeholderColor: color.baseLight, placeholderBackgroundColor: color.primary, - imageBackgroundColor: .clear + imageBackgroundColor: .clear, + transferringImage: Asset.operatorTransferring.image ) let callBubble = BubbleStyle( userImage: userImage, @@ -308,6 +319,7 @@ extension Theme { placeholderColor: color.baseLight, placeholderBackgroundColor: color.primary, imageBackgroundColor: .clear, + transferringImage: Asset.operatorTransferring.image, accessibility: .init(label: Accessibility.Message.UnreadMessagesIndicator.label) ) return ChatStyle( diff --git a/GliaWidgets/Theme/Theme+MinimizedBubble.swift b/GliaWidgets/Theme/Theme+MinimizedBubble.swift index a14150a77..5f31db8b9 100644 --- a/GliaWidgets/Theme/Theme+MinimizedBubble.swift +++ b/GliaWidgets/Theme/Theme+MinimizedBubble.swift @@ -4,7 +4,8 @@ extension Theme { placeholderImage: Asset.operatorPlaceholder.image, placeholderColor: color.baseLight, placeholderBackgroundColor: color.primary, - imageBackgroundColor: color.primary + imageBackgroundColor: color.primary, + transferringImage: Asset.operatorTransferring.image ) let badge = BadgeStyle( font: font.caption, diff --git a/GliaWidgets/View/Chat/ChatView.swift b/GliaWidgets/View/Chat/ChatView.swift index 0bfa713b7..1db4b84c5 100644 --- a/GliaWidgets/View/Chat/ChatView.swift +++ b/GliaWidgets/View/Chat/ChatView.swift @@ -385,6 +385,36 @@ extension ChatView { self.tableView.reloadData() } return .callUpgrade(view) + case .operatorConnected(let name, let imageUrl): + let connectView = ConnectView( + with: style.connect, + environment: .init( + data: environment.data, + uuid: environment.uuid, + gcd: environment.gcd, + imageViewCache: environment.imageViewCache, + timerProviding: environment.timerProviding) + ) + connectView.setState( + .connected(name: name, imageUrl: imageUrl), + animated: false + ) + return .queueOperator(connectView) + case .transferring: + let connectView = ConnectView( + with: style.connect, + environment: .init( + data: environment.data, + uuid: environment.uuid, + gcd: environment.gcd, + imageViewCache: environment.imageViewCache, + timerProviding: environment.timerProviding) + ) + connectView.setState( + .transferring, + animated: false + ) + return .queueOperator(connectView) } } // swiftlint:enable function_body_length @@ -418,6 +448,12 @@ extension ChatView { // MARK: Call Bubble extension ChatView { + func setCallBubbleImage(with imageUrl: String?) { + guard let callBubble = callBubble else { return } + + callBubble.kind = .userImage(url: imageUrl) + } + func showCallBubble(with imageUrl: String?, animated: Bool) { guard callBubble == nil else { return } let callBubble = BubbleView( diff --git a/GliaWidgets/View/Chat/Message/OperatorChatMessageView.swift b/GliaWidgets/View/Chat/Message/OperatorChatMessageView.swift index e4f0d4ebc..7740bc942 100644 --- a/GliaWidgets/View/Chat/Message/OperatorChatMessageView.swift +++ b/GliaWidgets/View/Chat/Message/OperatorChatMessageView.swift @@ -53,7 +53,7 @@ class OperatorChatMessageView: ChatMessageView { } func setOperatorImage(fromUrl url: String?, animated: Bool) { - operatorImageView?.setImage(fromUrl: url, animated: animated) + operatorImageView?.setOperatorImage(fromUrl: url, animated: animated) } private func layout() { diff --git a/GliaWidgets/ViewController/Call/CallViewController.swift b/GliaWidgets/ViewController/Call/CallViewController.swift index dae78ada8..dcc019d1a 100644 --- a/GliaWidgets/ViewController/Call/CallViewController.swift +++ b/GliaWidgets/ViewController/Call/CallViewController.swift @@ -13,8 +13,6 @@ class CallViewController: EngagementViewController, MediaUpgradePresenter { } override public func loadView() { - super.loadView() - let view = viewFactory.makeCallView() self.view = view @@ -52,6 +50,7 @@ class CallViewController: EngagementViewController, MediaUpgradePresenter { view.didRotate() } + // swiftlint:disable function_body_length private func bind(viewModel: CallViewModel, to view: CallView) { view.header.showBackButton() view.header.showCloseButton() @@ -104,9 +103,12 @@ class CallViewController: EngagementViewController, MediaUpgradePresenter { view.localVideoView.streamView = streamView case .setVisitorOnHold(let isOnHold): view.isVisitrOnHold = isOnHold + case .transferring: + view.setConnectState(.transferring, animated: true) } } } + // swiftlint:enable function_body_length } private extension CallViewModel.CallButton { diff --git a/GliaWidgets/ViewController/Chat/ChatViewController.Mock.swift b/GliaWidgets/ViewController/Chat/ChatViewController.Mock.swift index 0bcbc70a8..2e4425e7f 100644 --- a/GliaWidgets/ViewController/Chat/ChatViewController.Mock.swift +++ b/GliaWidgets/ViewController/Chat/ChatViewController.Mock.swift @@ -44,7 +44,7 @@ extension ChatViewController { var localFileEnv = LocalFile.Environment.mock localFileEnv.localFileThumbnailQueue.addOperation = { $0() } localFileEnv.gcd.globalQueue = .init(async: { $0() }, asyncAfterDeadline: { $1() }) - localFileEnv.gcd.mainQueue = .init(async: { $0() }, asyncAfterDeadline: { $1() }) + localFileEnv.gcd.mainQueue = .init(async: { $0() }, asyncIfNeeded: { $0() }, asyncAfterDeadline: { $1() }) localFileEnv.uiImage.imageWithContentsOfFileAtPath = { _ in .mock } fileDownload.state.value = .downloaded( .mock( diff --git a/GliaWidgets/ViewController/Chat/ChatViewController.swift b/GliaWidgets/ViewController/Chat/ChatViewController.swift index ee836ecdd..0df4370e0 100644 --- a/GliaWidgets/ViewController/Chat/ChatViewController.swift +++ b/GliaWidgets/ViewController/Chat/ChatViewController.swift @@ -12,9 +12,9 @@ class ChatViewController: EngagementViewController, MediaUpgradePresenter, } override public func loadView() { - super.loadView() let view = viewFactory.makeChatView() self.view = view + bind(viewModel: viewModel, to: view) } @@ -122,6 +122,12 @@ class ChatViewController: EngagementViewController, MediaUpgradePresenter, view.setOperatorTypingIndicatorIsHidden(to: isHidden) case .setIsAttachmentButtonHidden(let isHidden): view.messageEntryView.isAttachmentButtonHidden = isHidden + case .transferring: + view.setConnectState(.transferring, animated: true) + case .setCallBubbleImage(let imageUrl): + view.setCallBubbleImage(with: imageUrl) + case .setUnreadMessageIndicatorImage(let imageUrl): + view.unreadMessageIndicatorView.setImage(fromUrl: imageUrl, animated: true) } } } diff --git a/GliaWidgets/ViewController/EngagementViewController.swift b/GliaWidgets/ViewController/EngagementViewController.swift index 6612c8453..c76bd9a33 100644 --- a/GliaWidgets/ViewController/EngagementViewController.swift +++ b/GliaWidgets/ViewController/EngagementViewController.swift @@ -7,7 +7,11 @@ class EngagementViewController: ViewController, AlertPresenter { init(viewModel: EngagementViewModel, viewFactory: ViewFactory) { self.viewModel = viewModel self.viewFactory = viewFactory + super.init(nibName: nil, bundle: nil) + + // bind it here, so that view property will be requested from subclasses and views created + bind(engagementViewModel: viewModel) } @available(*, unavailable) @@ -20,13 +24,6 @@ class EngagementViewController: ViewController, AlertPresenter { viewModel.event(.viewWillAppear) } - override func viewDidLoad() { - super.viewDidLoad() - if let view = self.view as? EngagementView { - bind(viewModel: viewModel, to: view) - } - } - override func viewDidAppear(_ animated: Bool) { super.viewDidAppear(animated) viewModel.event(.viewDidAppear) @@ -37,7 +34,9 @@ class EngagementViewController: ViewController, AlertPresenter { viewModel.event(.viewDidDisappear) } - private func bind(viewModel: EngagementViewModel, to view: EngagementView) { + func bind(engagementViewModel: EngagementViewModel) { + guard let view = view as? EngagementView else { return } + view.header.endButton.tap = { [weak self] in self?.viewModel.event(.closeTapped) } view.header.endScreenShareButton.tap = { [weak self] in self?.viewModel.event(.endScreenSharingTapped) } view.header.backButton.tap = { [weak self] in self?.viewModel.event(.backTapped) } diff --git a/GliaWidgets/ViewModel/Call/CallViewModel.swift b/GliaWidgets/ViewModel/Call/CallViewModel.swift index 509d7db1f..479ab7743 100644 --- a/GliaWidgets/ViewModel/Call/CallViewModel.swift +++ b/GliaWidgets/ViewModel/Call/CallViewModel.swift @@ -71,7 +71,9 @@ class CallViewModel: EngagementViewModel, ViewModel { override func start() { super.start() + update(for: call.kind.value) + update(for: interactor.state) switch startWith { case .engagement(let mediaType): @@ -91,9 +93,7 @@ class CallViewModel: EngagementViewModel, ViewModel { case .enqueueing: action?(.queue) case .engaged: - if case .engagement = startWith { - showConnecting() - } + showConnecting() let operatorName = Strings.Operator.name.withOperatorName( interactor.engagedOperator?.firstName @@ -256,12 +256,22 @@ class CallViewModel: EngagementViewModel, ViewModel { handleVideoStreamError(error) case .upgradeOffer(let offer, answer: let answer): offerMediaUpgrade(offer, answer: answer) + case .engagementTransferring: + onEngagementTransferring() default: break } } } +extension CallViewModel { + private func onEngagementTransferring() { + call.transfer() + durationCounter.stop() + action?(.transferring) + } +} + extension CallViewModel { private func offerMediaUpgrade( _ offer: CoreSdkClient.MediaUpgradeOffer, @@ -353,7 +363,7 @@ extension CallViewModel { guard self.call.state.value == .started else { return } self.call.duration.value = duration } - case .upgrading: + case .connecting: action?(.switchToUpgradeMode) showConnecting() case .ended: @@ -503,6 +513,7 @@ extension CallViewModel { case queue case connecting(name: String?, imageUrl: String?) case connected(name: String?, imageUrl: String?) + case transferring case setOperatorName(String?) case setTopTextHidden(Bool) case setBottomTextHidden(Bool) diff --git a/GliaWidgets/ViewModel/Call/Data/Call.swift b/GliaWidgets/ViewModel/Call/Data/Call.swift index c05bb4b6b..1cb5bc1a2 100644 --- a/GliaWidgets/ViewModel/Call/Data/Call.swift +++ b/GliaWidgets/ViewModel/Call/Data/Call.swift @@ -47,7 +47,7 @@ extension CallKind { enum CallState { case none case started - case upgrading + case connecting case ended } @@ -79,6 +79,15 @@ enum MediaStream { return nil } } + + var isNone: Bool { + switch self { + case .none: + return true + default: + return false + } + } } class MediaChannel { @@ -112,10 +121,16 @@ class Call { self.hasVisitorTurnedOffVideo = kind.mediaDirection == .oneWay } + func transfer() { + audio.stream.value = .none + video.stream.value = .none + state.value = .connecting + } + func upgrade(to offer: CoreSdkClient.MediaUpgradeOffer) { setKind(for: offer.type, direction: offer.direction) setNeededDirection(offer.direction, for: offer.type) - state.value = .upgrading + state.value = .connecting } func updateAudioStream(with stream: CoreSdkClient.AudioStreamable) { @@ -243,7 +258,7 @@ class Call { } private func updateStarted() { - guard [.none, .upgrading].contains(state.value) else { return } + guard [.none, .connecting].contains(state.value) else { return } switch kind.value { case .audio: diff --git a/GliaWidgets/ViewModel/Chat/ChatViewModel.swift b/GliaWidgets/ViewModel/Chat/ChatViewModel.swift index 1a8d0ad73..f09808a9e 100644 --- a/GliaWidgets/ViewModel/Chat/ChatViewModel.swift +++ b/GliaWidgets/ViewModel/Chat/ChatViewModel.swift @@ -12,10 +12,11 @@ class ChatViewModel: EngagementViewModel, ViewModel { Section(2), Section(3) ] - private var historySection: Section { sections[0] } - private var pendingSection: Section { sections[1] } - private var queueOperatorSection: Section { sections[2] } - private var messagesSection: Section { sections[3] } + var historySection: Section { sections[0] } + var pendingSection: Section { sections[1] } + var queueOperatorSection: Section { sections[2] } + var messagesSection: Section { sections[3] } + private let call: ObservableValue private var unreadMessages: UnreadMessagesHandler! private let isChatScrolledToBottom = ObservableValue(with: true) @@ -144,18 +145,8 @@ class ChatViewModel: EngagementViewModel, ViewModel { interactor.withConfiguration { [weak self] in guard let self = self else { return } if case .startEngagement = self.startAction, self.environment.chatStorage.isEmpty() { - let item = ChatItem(kind: .queueOperator) - - self.appendItem( - item, - to: self.queueOperatorSection, - animated: false - ) - self.enqueue(mediaType: .text) } - - self.update(for: self.interactor.state) } } @@ -164,12 +155,29 @@ class ChatViewModel: EngagementViewModel, ViewModel { switch state { case .enqueueing: + let item = ChatItem(kind: .queueOperator) + + appendItem( + item, + to: queueOperatorSection, + animated: false + ) + action?(.queue) - action?(.scrollToBottom(animated: false)) + action?(.scrollToBottom(animated: true)) case .engaged(let engagedOperator): let name = engagedOperator?.firstName let pictureUrl = engagedOperator?.picture?.url + let chatItem = ChatItem(kind: .operatorConnected(name: name, imageUrl: pictureUrl)) + + setItems([], to: queueOperatorSection) + appendItem( + chatItem, + to: messagesSection, + animated: false + ) + action?(.connected(name: name, imageUrl: pictureUrl)) action?(.setMessageEntryEnabled(true)) @@ -220,12 +228,42 @@ class ChatViewModel: EngagementViewModel, ViewModel { offerMediaUpgrade(offer, answer: answer) case .typingStatusUpdated(let status): typingStatusUpdated(status) + case .engagementTransferring: + onEngagementTransferring() + case .engagementTransferred: + onEngagementTransferred() default: break } } } +extension ChatViewModel { + private func onEngagementTransferring() { + action?(.setMessageEntryEnabled(false)) + appendItem(.init(kind: .transferring), to: messagesSection, animated: true) + action?(.scrollToBottom(animated: true)) + } + + private func onEngagementTransferred() { + action?(.setMessageEntryEnabled(true)) + + let engagedOperator = interactor.engagedOperator + action?(.setCallBubbleImage(imageUrl: engagedOperator?.picture?.url)) + action?(.setUnreadMessageIndicatorImage(imageUrl: engagedOperator?.picture?.url)) + + guard let transferringItemIndex = messagesSection.items.firstIndex(where: { + switch $0.kind { + case .transferring: return true + default: return false + } + }) else { return } + + messagesSection.removeItem(at: transferringItemIndex) + action?(.refreshSection(messagesSection.index)) + } +} + // MARK: Section management extension ChatViewModel { @@ -803,6 +841,7 @@ extension ChatViewModel { enum Action { case queue case connected(name: String?, imageUrl: String?) + case transferring case setMessageEntryEnabled(Bool) case setChoiceCardInputModeEnabled(Bool) case setMessageText(String) @@ -825,7 +864,9 @@ extension ChatViewModel { declined: () -> Void ) case showCallBubble(imageUrl: String?) + case setCallBubbleImage(imageUrl: String?) case updateUnreadMessageIndicator(itemCount: Int) + case setUnreadMessageIndicatorImage(imageUrl: String?) case setOperatorTypingIndicatorIsHiddenTo(Bool, _ isChatScrolledToBottom: Bool) case setIsAttachmentButtonHidden(Bool) } diff --git a/GliaWidgets/ViewModel/Chat/Data/ChatItem.swift b/GliaWidgets/ViewModel/Chat/Data/ChatItem.swift index 503f4c443..7ff39ef46 100644 --- a/GliaWidgets/ViewModel/Chat/Data/ChatItem.swift +++ b/GliaWidgets/ViewModel/Chat/Data/ChatItem.swift @@ -1,15 +1,6 @@ import Foundation class ChatItem { - enum Kind { - case queueOperator - case outgoingMessage(OutgoingMessage) - case visitorMessage(ChatMessage, status: String?) - case operatorMessage(ChatMessage, showsImage: Bool, imageUrl: String?) - case choiceCard(ChatMessage, showsImage: Bool, imageUrl: String?, isActive: Bool) - case callUpgrade(ObservableValue, duration: ObservableValue) - } - var isOperatorMessage: Bool { switch kind { case .operatorMessage, .choiceCard: @@ -42,3 +33,16 @@ class ChatItem { } } } + +extension ChatItem { + enum Kind { + case queueOperator + case outgoingMessage(OutgoingMessage) + case visitorMessage(ChatMessage, status: String?) + case operatorMessage(ChatMessage, showsImage: Bool, imageUrl: String?) + case choiceCard(ChatMessage, showsImage: Bool, imageUrl: String?, isActive: Bool) + case callUpgrade(ObservableValue, duration: ObservableValue) + case operatorConnected(name: String?, imageUrl: String?) + case transferring + } +} diff --git a/GliaWidgets/ViewModel/EngagementViewModel.swift b/GliaWidgets/ViewModel/EngagementViewModel.swift index 0f0d4bf4a..501063132 100644 --- a/GliaWidgets/ViewModel/EngagementViewModel.swift +++ b/GliaWidgets/ViewModel/EngagementViewModel.swift @@ -83,9 +83,7 @@ class EngagementViewModel { } } - func start() { - update(for: interactor.state) - } + func start() {} func enqueue(mediaType: CoreSdkClient.MediaType) { interactor.enqueueForEngagement( diff --git a/GliaWidgetsTests/GCD.Failing.swift b/GliaWidgetsTests/GCD.Failing.swift index 119636b37..9857abaea 100644 --- a/GliaWidgetsTests/GCD.Failing.swift +++ b/GliaWidgetsTests/GCD.Failing.swift @@ -4,7 +4,8 @@ extension GCD { static let failing = Self( mainQueue: .init( async: { _ in fail("\(Self.self).mainQueue.async") }, - asyncAfterDeadline: { _, _ in fail("\(Self.self).mainQueue.asyncAfter") } + asyncIfNeeded: { _ in fail("\(Self.self).mainQueue.asyncIfNeeded") }, + asyncAfterDeadline: { _, _ in fail("\(Self.self).globalQueue.asyncAfter") } ), globalQueue: .init( async: { _ in fail("\(Self.self).globalQueue.async") }, diff --git a/GliaWidgetsTests/Interactor/Interactor.Environment.Failing.swift b/GliaWidgetsTests/Interactor/Interactor.Environment.Failing.swift index e1170d4b4..173f29268 100644 --- a/GliaWidgetsTests/Interactor/Interactor.Environment.Failing.swift +++ b/GliaWidgetsTests/Interactor/Interactor.Environment.Failing.swift @@ -1,4 +1,7 @@ @testable import GliaWidgets extension Interactor.Environment { - static let failing = Self(coreSdk: .failing) + static let failing = Self( + coreSdk: .failing, + gcd: .failing + ) } diff --git a/GliaWidgetsTests/Lib/ChatItem/ChatItem+Equatable.swift b/GliaWidgetsTests/Lib/ChatItem/ChatItem+Equatable.swift new file mode 100644 index 000000000..0d920412a --- /dev/null +++ b/GliaWidgetsTests/Lib/ChatItem/ChatItem+Equatable.swift @@ -0,0 +1,36 @@ +@testable import GliaWidgets + +extension ChatItem.Kind: Equatable { + public static func == (lhs: ChatItem.Kind, rhs: ChatItem.Kind) -> Bool { + switch (lhs, rhs) { + case (.queueOperator, .queueOperator): + return true + + case (.outgoingMessage(let lhsMessage), .outgoingMessage(let rhsMessage)): + return lhsMessage.id == rhsMessage.id + + case (.visitorMessage(let lhsMessage, _), .visitorMessage(let rhsMessage, _)): + return lhsMessage.id == rhsMessage.id + + case (.operatorMessage(let lhsMessage, _, _), .operatorMessage(let rhsMessage, _, _)): + return lhsMessage.id == rhsMessage.id + + case (.choiceCard(let lhsMessage, _, _, _), .choiceCard(let rhsMessage, _, _, _)): + return lhsMessage.id == rhsMessage.id + + case (.callUpgrade(let lhsKind, let lhsDuration), .callUpgrade(let rhsKind, let rhsDuration)): + return lhsKind.value == rhsKind.value + && lhsDuration.value == rhsDuration.value + + case (.operatorConnected(let lhsName, let lhsImageUrl), .operatorConnected(let rhsName, let rhsImageUrl)): + return lhsName == rhsName + && lhsImageUrl == rhsImageUrl + + case (.transferring, .transferring): + return true + + default: + return false + } + } +} diff --git a/GliaWidgetsTests/Mocks/GliaWidgets.Mock.swift b/GliaWidgetsTests/Mocks/GliaWidgets.Mock.swift new file mode 100644 index 000000000..265476a8f --- /dev/null +++ b/GliaWidgetsTests/Mocks/GliaWidgets.Mock.swift @@ -0,0 +1,18 @@ +import Foundation +import UIKit +@testable import GliaWidgets + +extension Interactor { + + static func mock() throws -> Interactor { + .init( + with: try .mock(), + queueID: "4CC83BDF-1C04-4B05-87B3-4D558B8F6999", + visitorContext: .mock(), + environment: .init( + coreSdk: .failing, + gcd: .mock + ) + ) + } +} diff --git a/GliaWidgetsTests/Sources/CallTests.swift b/GliaWidgetsTests/Sources/CallTests.swift new file mode 100644 index 000000000..f116d6e32 --- /dev/null +++ b/GliaWidgetsTests/Sources/CallTests.swift @@ -0,0 +1,63 @@ +@testable import GliaWidgets +import XCTest + +class CallTests: XCTestCase { + func test_transfer() throws { + let call: Call = .mock(kind: .video(direction: .twoWay), environment: .mock) + let remoteAudioStream = CoreSdkClient.MockAudioStreamable.mock( + muteFunc: {}, + unmuteFunc: {}, + getIsMutedFunc: { false }, + setIsMutedFunc: { _ in }, + getIsRemoteFunc: { true }, + setIsRemoteFunc: { _ in } + ) + let remoteVideoStream = CoreSdkClient.MockVideoStreamable.mock( + getStreamViewFunc: { .init() }, + playVideoFunc: {}, + pauseFunc: {}, + resumeFunc: {}, + stopFunc: {}, + getIsPausedFunc: { false }, + setIsPausedFunc: { _ in }, + getIsRemoteFunc: { true }, + setIsRemoteFunc: { _ in } + ) + let localAudioStream = CoreSdkClient.MockAudioStreamable.mock( + muteFunc: {}, + unmuteFunc: {}, + getIsMutedFunc: { false }, + setIsMutedFunc: { _ in }, + getIsRemoteFunc: { false }, + setIsRemoteFunc: { _ in } + ) + let localVideoStream = CoreSdkClient.MockVideoStreamable.mock( + getStreamViewFunc: { .init() }, + playVideoFunc: {}, + pauseFunc: {}, + resumeFunc: {}, + stopFunc: {}, + getIsPausedFunc: { false }, + setIsPausedFunc: { _ in }, + getIsRemoteFunc: { false }, + setIsRemoteFunc: { _ in } + ) + + XCTAssertEqual(call.state.value, .none) + + call.updateAudioStream(with: localAudioStream) + call.updateAudioStream(with: remoteAudioStream) + call.updateVideoStream(with: localVideoStream) + call.updateVideoStream(with: remoteVideoStream) + + XCTAssertEqual(call.state.value, .started) + XCTAssertFalse(call.audio.stream.value.isNone) + XCTAssertFalse(call.video.stream.value.isNone) + + call.transfer() + + XCTAssertEqual(call.state.value, .connecting) + XCTAssertTrue(call.audio.stream.value.isNone) + XCTAssertTrue(call.video.stream.value.isNone) + } +} diff --git a/GliaWidgetsTests/Sources/CallViewModelTests.swift b/GliaWidgetsTests/Sources/CallViewModelTests.swift index f018e2738..847754e6d 100644 --- a/GliaWidgetsTests/Sources/CallViewModelTests.swift +++ b/GliaWidgetsTests/Sources/CallViewModelTests.swift @@ -181,4 +181,125 @@ class CallViewModelTests: XCTestCase { XCTAssertTrue(isLocalAudioStreamMuted) XCTAssertTrue(isLocalVideoStreamPaused) } + + func test_engagementTransferringReleasesRemoteAndLocalVideoAndShowsConnectingState() throws { + enum Calls { case showConnecting } + var calls: [Calls] = [] + + var interactorEnv: Interactor.Environment = .failing + interactorEnv.gcd.mainQueue.asyncIfNeeded = { $0() } + let interactor: Interactor = .mock(environment: interactorEnv) + + call = .init( + .video(direction: .twoWay), + environment: .mock + ) + + viewModel = .init( + interactor: interactor, + alertConfiguration: .mock(), + screenShareHandler: ScreenShareHandler(), + environment: .mock, + call: call, + unreadMessages: .init(with: 0), + startWith: .engagement(mediaType: .video) + ) + + viewModel.action = { action in + switch action { + case .connecting: + calls.append(.showConnecting) + + case .setRemoteVideo(let video): + XCTAssertNil(video) + + case .setLocalVideo(let video): + XCTAssertNil(video) + + default: + break + } + } + + interactor.notify(.engagementTransferring) + + XCTAssertEqual([.showConnecting], calls) + } + + func test_engagementTransferringReleasesStreams() throws { + var interactorEnv: Interactor.Environment = .failing + interactorEnv.gcd.mainQueue.asyncIfNeeded = { $0() } + let interactor: Interactor = .mock(environment: interactorEnv) + let remoteAudioStream = CoreSdkClient.MockAudioStreamable.mock( + muteFunc: {}, + unmuteFunc: {}, + getIsMutedFunc: { false }, + setIsMutedFunc: { _ in }, + getIsRemoteFunc: { true }, + setIsRemoteFunc: { _ in } + ) + let remoteVideoStream = CoreSdkClient.MockVideoStreamable.mock( + getStreamViewFunc: { .init() }, + playVideoFunc: {}, + pauseFunc: {}, + resumeFunc: {}, + stopFunc: {}, + getIsPausedFunc: { false }, + setIsPausedFunc: { _ in }, + getIsRemoteFunc: { true }, + setIsRemoteFunc: { _ in } + ) + let localAudioStream = CoreSdkClient.MockAudioStreamable.mock( + muteFunc: {}, + unmuteFunc: {}, + getIsMutedFunc: { false }, + setIsMutedFunc: { _ in }, + getIsRemoteFunc: { false }, + setIsRemoteFunc: { _ in } + ) + let localVideoStream = CoreSdkClient.MockVideoStreamable.mock( + getStreamViewFunc: { .init() }, + playVideoFunc: {}, + pauseFunc: {}, + resumeFunc: {}, + stopFunc: {}, + getIsPausedFunc: { false }, + setIsPausedFunc: { _ in }, + getIsRemoteFunc: { false }, + setIsRemoteFunc: { _ in } + ) + + call = .init( + .video(direction: .twoWay), + environment: .mock + ) + + viewModel = .init( + interactor: interactor, + alertConfiguration: .mock(), + screenShareHandler: ScreenShareHandler(), + environment: .mock, + call: call, + unreadMessages: .init(with: 0), + startWith: .engagement(mediaType: .video) + ) + + call.updateVideoStream(with: localVideoStream) + call.updateVideoStream(with: remoteVideoStream) + call.updateAudioStream(with: localAudioStream) + call.updateAudioStream(with: remoteAudioStream) + + XCTAssertEqual(call.state.value, .started) + XCTAssertFalse(call.audio.stream.value.localStream == nil) + XCTAssertFalse(call.audio.stream.value.remoteStream == nil) + XCTAssertFalse(call.video.stream.value.localStream == nil) + XCTAssertFalse(call.video.stream.value.remoteStream == nil) + + interactor.notify(.engagementTransferring) + + XCTAssertNil(call.video.stream.value.localStream) + XCTAssertNil(call.video.stream.value.remoteStream) + XCTAssertNil(call.audio.stream.value.localStream) + XCTAssertNil(call.audio.stream.value.remoteStream) + } } diff --git a/GliaWidgetsTests/Sources/ChatViewModelTests.swift b/GliaWidgetsTests/Sources/ChatViewModelTests.swift index e528e5bab..f641215ed 100644 --- a/GliaWidgetsTests/Sources/ChatViewModelTests.swift +++ b/GliaWidgetsTests/Sources/ChatViewModelTests.swift @@ -58,7 +58,10 @@ class ChatViewModelTests: XCTestCase { } func test__startCallsSDKConfigureWithInteractorAndСonfigureWithConfiguration() throws { - var interactorEnv = Interactor.Environment.init(coreSdk: .failing) + var interactorEnv = Interactor.Environment.init( + coreSdk: .failing, + gcd: .failing + ) enum Calls { case configureWithConfiguration, configureWithInteractor } @@ -79,173 +82,139 @@ class ChatViewModelTests: XCTestCase { viewModel.start() XCTAssertEqual(calls, [.configureWithInteractor, .configureWithConfiguration]) } - - func test__updateDoesNotCallSDKFetchSiteConfigurationsOnEnqueueingState() throws { - // Given - enum Calls { case fetchSiteConfigurations } - var calls: [Calls] = [] - let interactorEnv = Interactor.Environment.init(coreSdk: .failing) - let interactor = Interactor.mock(environment: interactorEnv) + + func test_onInteractorStateEngagedClearsChatQueueSection() throws { var viewModelEnv = ChatViewModel.Environment.failing viewModelEnv.fileManager.urlsForDirectoryInDomainMask = { _, _ in [.mock] } viewModelEnv.fileManager.createDirectoryAtUrlWithIntermediateDirectories = { _, _, _ in } - viewModelEnv.fetchSiteConfigurations = { _ in - calls.append(.fetchSiteConfigurations) - } - let viewModel = ChatViewModel.mock(interactor: interactor, environment: viewModelEnv) + viewModelEnv.fetchSiteConfigurations = { _ in } - // When - viewModel.update(for: .enqueueing) + let interactor: Interactor = try .mock() + let viewModel: ChatViewModel = .mock(interactor: interactor, environment: viewModelEnv) + let queueSectionIndex: Int = viewModel.queueOperatorSection.index + let mockOperator: CoreSdkClient.Operator = .mock() - // Then - XCTAssertEqual(calls, []) + XCTAssertEqual(0, viewModel.numberOfItems(in: queueSectionIndex)) + viewModel.update(for: .enqueueing) + XCTAssertEqual(1, viewModel.numberOfItems(in: queueSectionIndex)) + viewModel.update(for: .engaged(mockOperator)) + XCTAssertEqual(0, viewModel.numberOfItems(in: queueSectionIndex)) } - - func test__updateCallsSDKFetchSiteConfigurationsOnEngagedState() throws { - // Given - enum Calls { case fetchSiteConfigurations } - var calls: [Calls] = [] - let interactorEnv = Interactor.Environment.init(coreSdk: .failing) - let interactor = Interactor.mock(environment: interactorEnv) + + func test_onEngagementTransferringAddsTransferringItemToTheEndOfChat() throws { + var interactorEnv: Interactor.Environment = .failing + interactorEnv.gcd.mainQueue.asyncIfNeeded = { $0() } var viewModelEnv = ChatViewModel.Environment.failing viewModelEnv.fileManager.urlsForDirectoryInDomainMask = { _, _ in [.mock] } viewModelEnv.fileManager.createDirectoryAtUrlWithIntermediateDirectories = { _, _, _ in } - viewModelEnv.fetchSiteConfigurations = { _ in - calls.append(.fetchSiteConfigurations) - } - let viewModel = ChatViewModel.mock(interactor: interactor, environment: viewModelEnv) + viewModelEnv.chatStorage.messages = { _ in [] } + viewModelEnv.fromHistory = { true } + viewModelEnv.fetchSiteConfigurations = { _ in } + + let interactor: Interactor = .mock(environment: interactorEnv) + let viewModel: ChatViewModel = .mock(interactor: interactor, environment: viewModelEnv) + + interactor.onEngagementTransferring() + + let lastSectionIndex = viewModel.numberOfSections - 1 + let lastSectionLastItemIndex = viewModel.numberOfItems(in: lastSectionIndex) - 1 + let lastItemKind = viewModel.item(for: lastSectionLastItemIndex, in: lastSectionIndex).kind + + XCTAssertEqual(lastItemKind, .transferring) + } + + func test_onEngagementTransferRemovesTransferringItemFromChat() throws { + var interactorEnv: Interactor.Environment = .failing + interactorEnv.gcd.mainQueue.asyncIfNeeded = { $0() } + var viewModelEnv = ChatViewModel.Environment.failing + viewModelEnv.fileManager.urlsForDirectoryInDomainMask = { _, _ in [.mock] } + viewModelEnv.fileManager.createDirectoryAtUrlWithIntermediateDirectories = { _, _, _ in } + viewModelEnv.chatStorage.messages = { _ in [] } + viewModelEnv.fromHistory = { true } + viewModelEnv.fetchSiteConfigurations = { _ in } - // When - viewModel.update(for: .engaged(nil)) + let interactor: Interactor = .mock(environment: interactorEnv) + let viewModel: ChatViewModel = .mock(interactor: interactor, environment: viewModelEnv) + + interactor.onEngagementTransferring() - // Then - XCTAssertEqual(calls, [.fetchSiteConfigurations]) - } + let lastSectionIndex = viewModel.numberOfSections - 1 + let lastSectionLastItemIndex = viewModel.numberOfItems(in: lastSectionIndex) - 1 + let lastItemKind = viewModel.item(for: lastSectionLastItemIndex, in: lastSectionIndex).kind - func test__sendMessageCallsEnqueueNewEngagementOnNoneState() throws { - // Given - enum Call { - case configureWithConfiguration, configureWithInteractor, queueForEngagement, sendMessage - } - var calls: [Call] = [] - var coreSdk = CoreSdkClient.failing - coreSdk.sendMessagePreview = { _, _ in } - coreSdk.configureWithConfiguration = { _, completion in - calls.append(.configureWithConfiguration) - completion?() - } - coreSdk.configureWithInteractor = { _ in - calls.append(.configureWithInteractor) - } - coreSdk.queueForEngagement = { _, _, _, _, _, _ in - calls.append(.queueForEngagement) - } - coreSdk.sendMessageWithAttachment = { _, _, _ in - calls.append(.sendMessage) - } - let interactor = Interactor.mock(environment: .init(coreSdk: coreSdk)) + XCTAssertEqual(lastItemKind, .transferring) + let mockOperator: CoreSdkClient.Operator = .mock() + interactor.onEngagementTransfer([mockOperator]) + + XCTAssertFalse(viewModel.messagesSection.items.contains(where: { $0.kind == .transferring })) + } + + func test_onEngagementTransferAddsOperatorConnectedChatItemToTheEndOfChat() throws { + var interactorEnv: Interactor.Environment = .failing + interactorEnv.gcd.mainQueue.asyncIfNeeded = { $0() } var viewModelEnv = ChatViewModel.Environment.failing viewModelEnv.fileManager.urlsForDirectoryInDomainMask = { _, _ in [.mock] } viewModelEnv.fileManager.createDirectoryAtUrlWithIntermediateDirectories = { _, _, _ in } viewModelEnv.chatStorage.messages = { _ in [] } - let viewModel = ChatViewModel.mock(interactor: interactor, environment: viewModelEnv) + viewModelEnv.fromHistory = { true } + viewModelEnv.fetchSiteConfigurations = { _ in } - // When - // Sets message text - viewModel.event(.messageTextChanged("Message")) - // Puts outgoing message to pending messages and starts enqueueing engagement - viewModel.event(.sendTapped) + let interactor: Interactor = .mock(environment: interactorEnv) + let viewModel: ChatViewModel = .mock(interactor: interactor, environment: viewModelEnv) + let mockOperator: CoreSdkClient.Operator = .mock() + let mockItemKind: ChatItem.Kind = .operatorConnected(name: mockOperator.firstName, imageUrl: mockOperator.picture?.url) - // Then - XCTAssertEqual(calls, [ - .configureWithInteractor, - .configureWithConfiguration, - .queueForEngagement - ]) + interactor.onEngagementTransfer([mockOperator]) + + let lastSectionIndex = viewModel.numberOfSections - 1 + let lastSectionLastItemIndex = viewModel.numberOfItems(in: lastSectionIndex) - 1 + let lastItemKind = viewModel.item(for: lastSectionLastItemIndex, in: lastSectionIndex).kind + + XCTAssertEqual(lastItemKind, mockItemKind) } - func _test__sendMessageDoesNotCallEnqueueNewEngagementOnEnqueuedState() throws { + func test__updateDoesNotCallSDKFetchSiteConfigurationsOnEnqueueingState() throws { // Given - enum Call { case queueForEngagement, sendMessage } - var calls: [Call] = [] - var coreSdk = CoreSdkClient.failing - coreSdk.sendMessagePreview = { _, _ in } - coreSdk.configureWithConfiguration = { _, completion in - completion?() - } - coreSdk.configureWithInteractor = { _ in } - coreSdk.queueForEngagement = { _, _, _, _, _, _ in - #warning("Need to call competion after adding public init for QueueTicket core-sdk entity") - calls.append(.queueForEngagement) - } - coreSdk.sendMessageWithAttachment = { _, _, _ in - calls.append(.sendMessage) - } - let interactor = Interactor.mock(environment: .init(coreSdk: coreSdk)) - + enum Calls { case fetchSiteConfigurations } + var calls: [Calls] = [] + let interactorEnv = Interactor.Environment(coreSdk: .failing, gcd: .mock) + let interactor = Interactor.mock(environment: interactorEnv) var viewModelEnv = ChatViewModel.Environment.failing viewModelEnv.fileManager.urlsForDirectoryInDomainMask = { _, _ in [.mock] } viewModelEnv.fileManager.createDirectoryAtUrlWithIntermediateDirectories = { _, _, _ in } - viewModelEnv.chatStorage.messages = { _ in [] } - viewModelEnv.chatStorage.isEmpty = { true } + viewModelEnv.fetchSiteConfigurations = { _ in + calls.append(.fetchSiteConfigurations) + } let viewModel = ChatViewModel.mock(interactor: interactor, environment: viewModelEnv) // When - // Sets interactor state to enqueued - viewModel.start() - // Sets message text - viewModel.event(.messageTextChanged("Message")) - // Sends outgoing message - viewModel.event(.sendTapped) + viewModel.update(for: .enqueueing) // Then - XCTAssertEqual(calls, [.queueForEngagement]) + XCTAssertEqual(calls, []) } - func _test__sendMessageDoesNotCallEnqueueNewEngagementOnEngagedState() throws { + func test__updateCallsSDKFetchSiteConfigurationsOnEngagedState() throws { // Given - enum Call { case queueForEngagement, sendMessage } - var calls: [Call] = [] - var coreSdk = CoreSdkClient.failing - coreSdk.sendMessagePreview = { _, _ in } - coreSdk.configureWithConfiguration = { _, completion in - completion?() - } - coreSdk.configureWithInteractor = { _ in } - coreSdk.queueForEngagement = { _, _, _, _, _, _ in - #warning("Need to call competion after adding public init for QueueTicket core-sdk entity") - calls.append(.queueForEngagement) - } - coreSdk.requestEngagedOperator = { completion in - completion([.mock()], nil) - } - coreSdk.sendMessageWithAttachment = { _, _, _ in - calls.append(.sendMessage) - } - let interactor = Interactor.mock(environment: .init(coreSdk: coreSdk)) - + enum Calls { case fetchSiteConfigurations } + var calls: [Calls] = [] + let interactorEnv = Interactor.Environment.init(coreSdk: .failing, gcd: .mock) + let interactor = Interactor.mock(environment: interactorEnv) var viewModelEnv = ChatViewModel.Environment.failing viewModelEnv.fileManager.urlsForDirectoryInDomainMask = { _, _ in [.mock] } viewModelEnv.fileManager.createDirectoryAtUrlWithIntermediateDirectories = { _, _, _ in } - viewModelEnv.chatStorage.messages = { _ in [] } - viewModelEnv.chatStorage.isEmpty = { true } + viewModelEnv.fetchSiteConfigurations = { _ in + calls.append(.fetchSiteConfigurations) + } let viewModel = ChatViewModel.mock(interactor: interactor, environment: viewModelEnv) // When - - // Sets interactor state to enqueued - viewModel.start() - // Sets interactor state to engaged - interactor.start() - // Sets message text - viewModel.event(.messageTextChanged("Message")) - // Sends outgoing message - viewModel.event(.sendTapped) + viewModel.update(for: .engaged(nil)) // Then - XCTAssertEqual(calls, [.queueForEngagement, .sendMessage]) - } + XCTAssertEqual(calls, [.fetchSiteConfigurations]) + } } extension ChatChoiceCardOption { diff --git a/GliaWidgetsTests/Sources/InteractorStateTests.swift b/GliaWidgetsTests/Sources/InteractorStateTests.swift new file mode 100644 index 000000000..ceebd7bc8 --- /dev/null +++ b/GliaWidgetsTests/Sources/InteractorStateTests.swift @@ -0,0 +1,20 @@ +import XCTest + +@testable import GliaWidgets + +class InteractorStateTests: XCTestCase { + func test_engagedWithSameOperatorIsEqualState() throws { + let engagedOperator: CoreSdkClient.Operator = .mock() + let state1: InteractorState = .engaged(engagedOperator) + let state2: InteractorState = .engaged(engagedOperator) + + XCTAssertEqual(state1, state2) + } + + func test_engagedWithDifferentOperatorIsNotEqualState() throws { + let state1: InteractorState = .engaged(.mock()) + let state2: InteractorState = .engaged(.mock()) + + XCTAssertNotEqual(state1, state2) + } +} diff --git a/GliaWidgetsTests/Sources/InteractorTests.swift b/GliaWidgetsTests/Sources/InteractorTests.swift index 838d2b4a7..e298266ec 100644 --- a/GliaWidgetsTests/Sources/InteractorTests.swift +++ b/GliaWidgetsTests/Sources/InteractorTests.swift @@ -38,7 +38,7 @@ class InteractorTests: XCTestCase { with: mock.config, queueID: mock.queueId, visitorContext: mock.visitorContext, - environment: .init(coreSdk: coreSdk) + environment: .init(coreSdk: coreSdk, gcd: .failing) ) interactor.enqueueForEngagement(mediaType: .text) {} failure: { @@ -99,4 +99,41 @@ class InteractorTests: XCTestCase { XCTAssertEqual(callbacks, [.withConfiguration, .withConfiguration, .withConfiguration]) } + + func test_onEngagementTransfer() throws { + enum Call: Equatable { + case stateChanged(InteractorState) + case engagementTransferred(CoreSdkClient.Operator?) + } + + var calls = [Call]() + let mockOperator: CoreSdkClient.Operator = .mock() + + interactor = .init( + with: mock.config, + queueID: mock.queueId, + visitorContext: mock.visitorContext, + environment: .init(coreSdk: .failing, gcd: .mock) + ) + + interactor.addObserver(self, handler: { event in + switch event { + case .stateChanged(let state): + calls.append(.stateChanged(state)) + + case .engagementTransferred(let engagedOperator): + calls.append(.engagementTransferred(engagedOperator)) + + default: + break + } + }) + + interactor.onEngagementTransfer([mockOperator]) + + XCTAssertEqual(calls, [ + .stateChanged(.engaged(mockOperator)), + .engagementTransferred(mockOperator) + ]) + } } From d84831dae05de75e608e58aadd418fbaf613cde8 Mon Sep 17 00:00:00 2001 From: Yurii Dukhovnyi Date: Tue, 28 Jun 2022 10:41:14 +0300 Subject: [PATCH 02/37] Update: Glia Core SDK 0.34.0 --- GliaWidgets.podspec | 2 +- Package.swift | 4 ++-- Podfile.lock | 4 ++-- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/GliaWidgets.podspec b/GliaWidgets.podspec index 948ee9ad7..5ffb549e4 100644 --- a/GliaWidgets.podspec +++ b/GliaWidgets.podspec @@ -19,7 +19,7 @@ Pod::Spec.new do |s| } s.exclude_files = ['GliaWidgets/Window/**'] - s.dependency 'SalemoveSDK', '0.33.4' + s.dependency 'SalemoveSDK', '0.34.0' s.dependency 'PureLayout', '~>3.1' s.dependency 'lottie-ios', '3.2.3' end diff --git a/Package.swift b/Package.swift index 632a85788..fece39bcf 100644 --- a/Package.swift +++ b/Package.swift @@ -40,8 +40,8 @@ let package = Package( ), .binaryTarget( name: "SalemoveSDK", - url: "https://github.com/salemove/ios-bundle/releases/download/0.33.4/SalemoveSDK.xcframework.zip", - checksum: "894c7b4cb4e19b6e27efbbb1b16cd94b0e205ef3f554cbb2f776f7591ccfa10e" + url: "https://github.com/salemove/ios-bundle/releases/download/0.34.0/SalemoveSDK.xcframework.zip", + checksum: "f1fae4fa201e034f870a0f6f0e2e9048c11da722bc0eae4688c0ff0cf967022c" ), .target( name: "GliaWidgets", diff --git a/Podfile.lock b/Podfile.lock index 9bfa23fee..efa55ee1d 100644 --- a/Podfile.lock +++ b/Podfile.lock @@ -10,7 +10,7 @@ PODS: - lottie-ios (3.2.3) - PureLayout (3.1.9) - ReactiveSwift (6.5.0-xcf) - - SalemoveSDK (0.33.4): + - SalemoveSDK (0.34.0): - GliaCoreDependency (= 1.0) - ReactiveSwift (= 6.5.0-xcf) - TwilioVoice (= 6.3.1) @@ -44,7 +44,7 @@ SPEC CHECKSUMS: lottie-ios: c058aeafa76daa4cf64d773554bccc8385d0150e PureLayout: 5fb5e5429519627d60d079ccb1eaa7265ce7cf88 ReactiveSwift: 377d0a5621761a57c61829541b2ee0b9fdcd98f2 - SalemoveSDK: 561306fb67ad648412b7bc2528292e9df4cf3912 + SalemoveSDK: 2f2da45d864eb75da0d7a76f12e15f6bb2f91245 SnapshotTesting: 6141c48b6aa76ead61431ca665c14ab9a066c53b TwilioVoice: 098a959181d4607921f5822d3c9f13043ea4075b WebRTC-lib: 508fe02efa0c1a3a8867082a77d24c9be5d29aeb From 1312c336dd2639c810ff28c9edea461dca216f6f Mon Sep 17 00:00:00 2001 From: Yurii Dukhovnyi Date: Wed, 18 May 2022 15:32:03 +0300 Subject: [PATCH 03/37] Remove SceneDelegate The project can't use SceneDelegate and AppDelegate at the same time. Removed SceneDelegate to make AppDelegate work and pass necessary events to SalemoveAppDelegate. --- GliaWidgets.xcodeproj/project.pbxproj | 4 --- TestingApp/AppDelegate.swift | 16 ---------- TestingApp/Info.plist | 19 ------------ TestingApp/SceneDelegate.swift | 42 --------------------------- 4 files changed, 81 deletions(-) delete mode 100644 TestingApp/SceneDelegate.swift diff --git a/GliaWidgets.xcodeproj/project.pbxproj b/GliaWidgets.xcodeproj/project.pbxproj index 9487b83e5..9d8d1c2ae 100644 --- a/GliaWidgets.xcodeproj/project.pbxproj +++ b/GliaWidgets.xcodeproj/project.pbxproj @@ -338,7 +338,6 @@ 9AE9E4B727E1E30500BFE239 /* MockHelpers.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9AE9E4B627E1E30500BFE239 /* MockHelpers.swift */; }; AFBBF5782851C391004993B3 /* Glia.Deprecated.swift in Sources */ = {isa = PBXBuildFile; fileRef = AFBBF5772851C391004993B3 /* Glia.Deprecated.swift */; }; AFBBF57A28522591004993B3 /* Configuration.Deprecated.swift in Sources */ = {isa = PBXBuildFile; fileRef = AFBBF57928522591004993B3 /* Configuration.Deprecated.swift */; }; - C4119E04268F411D004DFEFB /* SceneDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4119E03268F411D004DFEFB /* SceneDelegate.swift */; }; C4119E06268F41D1004DFEFB /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = C4119E05268F41D1004DFEFB /* Main.storyboard */; }; C42463742673ABE10082C135 /* ScreenShareHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = C42463732673ABE10082C135 /* ScreenShareHandler.swift */; }; C43C12F92694B14900C37E1B /* GliaPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = C43C12F82694B14900C37E1B /* GliaPresenter.swift */; }; @@ -765,7 +764,6 @@ AFBBF5772851C391004993B3 /* Glia.Deprecated.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Glia.Deprecated.swift; sourceTree = ""; }; AFBBF57928522591004993B3 /* Configuration.Deprecated.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Configuration.Deprecated.swift; sourceTree = ""; }; C1B1D31D2A8244728847B885 /* Pods_GliaWidgetsTests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_GliaWidgetsTests.framework; sourceTree = BUILT_PRODUCTS_DIR; }; - C4119E03268F411D004DFEFB /* SceneDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SceneDelegate.swift; sourceTree = ""; }; C4119E05268F41D1004DFEFB /* Main.storyboard */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; path = Main.storyboard; sourceTree = ""; }; C42463732673ABE10082C135 /* ScreenShareHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ScreenShareHandler.swift; sourceTree = ""; }; C43C12F82694B14900C37E1B /* GliaPresenter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GliaPresenter.swift; sourceTree = ""; }; @@ -1105,7 +1103,6 @@ 1A60AFF32567CDFB00E53F53 /* Settings */, 1A4AD3D4256FE7DA00468BFB /* Extensions */, 1A205D7A25655CEC003AA3CD /* AppDelegate.swift */, - C4119E03268F411D004DFEFB /* SceneDelegate.swift */, 1A205D7E25655CEC003AA3CD /* ViewController.swift */, 1A205D8525655CEE003AA3CD /* LaunchScreen.storyboard */, 1A205D8825655CEE003AA3CD /* Info.plist */, @@ -2973,7 +2970,6 @@ files = ( 1A4AD3D9256FE9D300468BFB /* SettingsTextCell.swift in Sources */, 1A4AD3DC256FF67200468BFB /* SettingsFontCell.swift in Sources */, - C4119E04268F411D004DFEFB /* SceneDelegate.swift in Sources */, 1A205D7F25655CEC003AA3CD /* ViewController.swift in Sources */, 1A4AD3D6256FE7F800468BFB /* UIColor+Extensions.swift in Sources */, 1A205D7B25655CEC003AA3CD /* AppDelegate.swift in Sources */, diff --git a/TestingApp/AppDelegate.swift b/TestingApp/AppDelegate.swift index 5f0516b80..fa3f88dc7 100644 --- a/TestingApp/AppDelegate.swift +++ b/TestingApp/AppDelegate.swift @@ -29,20 +29,4 @@ class AppDelegate: UIResponder, UIApplicationDelegate { func applicationDidBecomeActive(_ application: UIApplication) { salemoveDelegate.applicationDidBecomeActive(application) } - - // MARK: UISceneSession Lifecycle - - @available(iOS 13.0, *) - func application(_ application: UIApplication, configurationForConnecting connectingSceneSession: UISceneSession, options: UIScene.ConnectionOptions) -> UISceneConfiguration { - // Called when a new scene session is being created. - // Use this method to select a configuration to create the new scene with. - return UISceneConfiguration(name: "Default Configuration", sessionRole: connectingSceneSession.role) - } - - @available(iOS 13.0, *) - func application(_ application: UIApplication, didDiscardSceneSessions sceneSessions: Set) { - // Called when the user discards a scene session. - // If any sessions were discarded while the application was not running, this will be called shortly after application:didFinishLaunchingWithOptions. - // Use this method to release any resources that were specific to the discarded scenes, as they will not return. - } } diff --git a/TestingApp/Info.plist b/TestingApp/Info.plist index c16e02478..dbb2191cb 100644 --- a/TestingApp/Info.plist +++ b/TestingApp/Info.plist @@ -26,25 +26,6 @@ This app needs microphone for audio/video calls and for recording video NSPhotoLibraryAddUsageDescription Save attachments - UIApplicationSceneManifest - - UIApplicationSupportsMultipleScenes - - UISceneConfigurations - - UIWindowSceneSessionRoleApplication - - - UISceneConfigurationName - Default Configuration - UISceneDelegateClassName - $(PRODUCT_MODULE_NAME).SceneDelegate - UISceneStoryboardFile - Main - - - - UIApplicationSupportsIndirectInputEvents UIBackgroundModes diff --git a/TestingApp/SceneDelegate.swift b/TestingApp/SceneDelegate.swift deleted file mode 100644 index fe486211c..000000000 --- a/TestingApp/SceneDelegate.swift +++ /dev/null @@ -1,42 +0,0 @@ -import UIKit - -@available(iOS 13.0, *) -class SceneDelegate: UIResponder, UIWindowSceneDelegate { - - var window: UIWindow? - - func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) { - // Use this method to optionally configure and attach the UIWindow `window` to the provided UIWindowScene `scene`. - // If using a storyboard, the `window` property will automatically be initialized and attached to the scene. - // This delegate does not imply the connecting scene or session are new (see `application:configurationForConnectingSceneSession` instead). - // guard let _ = (scene as? UIWindowScene) else { return } - } - - func sceneDidDisconnect(_ scene: UIScene) { - // Called as the scene is being released by the system. - // This occurs shortly after the scene enters the background, or when its session is discarded. - // Release any resources associated with this scene that can be re-created the next time the scene connects. - // The scene may re-connect later, as its session was not necessarily discarded (see `application:didDiscardSceneSessions` instead). - } - - func sceneDidBecomeActive(_ scene: UIScene) { - // Called when the scene has moved from an inactive state to an active state. - // Use this method to restart any tasks that were paused (or not yet started) when the scene was inactive. - } - - func sceneWillResignActive(_ scene: UIScene) { - // Called when the scene will move from an active state to an inactive state. - // This may occur due to temporary interruptions (ex. an incoming phone call). - } - - func sceneWillEnterForeground(_ scene: UIScene) { - // Called as the scene transitions from the background to the foreground. - // Use this method to undo the changes made on entering the background. - } - - func sceneDidEnterBackground(_ scene: UIScene) { - // Called as the scene transitions from the foreground to the background. - // Use this method to save data, release shared resources, and store enough scene-specific state information - // to restore the scene back to its current state. - } -} From 519b586098de9ad8401dbfd747e85adde196dca4 Mon Sep 17 00:00:00 2001 From: "e.egorov" Date: Mon, 27 Jun 2022 16:57:32 +0300 Subject: [PATCH 04/37] Removed app-token usage from TestingApp --- GliaWidgets.xcodeproj/project.pbxproj | 12 ++- GliaWidgets/Configuration.Deprecated.swift | 63 -------------- GliaWidgets/Configuration.Unavailable.swift | 46 ++++++++++ GliaWidgets/Unavailable.swift | 3 + .../Settings/SettingsViewController.swift | 84 +++++++------------ 5 files changed, 85 insertions(+), 123 deletions(-) delete mode 100644 GliaWidgets/Configuration.Deprecated.swift create mode 100644 GliaWidgets/Configuration.Unavailable.swift create mode 100644 GliaWidgets/Unavailable.swift diff --git a/GliaWidgets.xcodeproj/project.pbxproj b/GliaWidgets.xcodeproj/project.pbxproj index 9d8d1c2ae..f96f15fe8 100644 --- a/GliaWidgets.xcodeproj/project.pbxproj +++ b/GliaWidgets.xcodeproj/project.pbxproj @@ -243,6 +243,7 @@ 845E2F9B283FCA9000C04D56 /* Theme.Survey.Checkbox.swift in Sources */ = {isa = PBXBuildFile; fileRef = 845E2F9A283FCA9000C04D56 /* Theme.Survey.Checkbox.swift */; }; 845E2F9D283FCB1400C04D56 /* Theme.Survey.Checkbox.Accessibility.swift in Sources */ = {isa = PBXBuildFile; fileRef = 845E2F9C283FCB1400C04D56 /* Theme.Survey.Checkbox.Accessibility.swift */; }; 847A7643285A1914004044D1 /* FileUploadListViewTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 847A7642285A1914004044D1 /* FileUploadListViewTests.swift */; }; + 84A318A12869ECFC00CA1DE5 /* Unavailable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84A318A02869ECFC00CA1DE5 /* Unavailable.swift */; }; 84CFB7732822700000167258 /* Theme.Button.Accessibility.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84CFB7722822700000167258 /* Theme.Button.Accessibility.swift */; }; 9A0B7D1727DA6B74006D8637 /* Interactor.Mock.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9A0B7D1627DA6B74006D8637 /* Interactor.Mock.swift */; }; 9A186A3527F5CF3C0055886D /* FileUploadStyle.Accessibility.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9A186A3427F5CF3C0055886D /* FileUploadStyle.Accessibility.swift */; }; @@ -337,7 +338,7 @@ 9AE9E4B527E0EE2E00BFE239 /* CallViewControllerVoiceOverTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9AE9E4B427E0EE2E00BFE239 /* CallViewControllerVoiceOverTests.swift */; }; 9AE9E4B727E1E30500BFE239 /* MockHelpers.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9AE9E4B627E1E30500BFE239 /* MockHelpers.swift */; }; AFBBF5782851C391004993B3 /* Glia.Deprecated.swift in Sources */ = {isa = PBXBuildFile; fileRef = AFBBF5772851C391004993B3 /* Glia.Deprecated.swift */; }; - AFBBF57A28522591004993B3 /* Configuration.Deprecated.swift in Sources */ = {isa = PBXBuildFile; fileRef = AFBBF57928522591004993B3 /* Configuration.Deprecated.swift */; }; + AFBBF57A28522591004993B3 /* Configuration.Unavailable.swift in Sources */ = {isa = PBXBuildFile; fileRef = AFBBF57928522591004993B3 /* Configuration.Unavailable.swift */; }; C4119E06268F41D1004DFEFB /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = C4119E05268F41D1004DFEFB /* Main.storyboard */; }; C42463742673ABE10082C135 /* ScreenShareHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = C42463732673ABE10082C135 /* ScreenShareHandler.swift */; }; C43C12F92694B14900C37E1B /* GliaPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = C43C12F82694B14900C37E1B /* GliaPresenter.swift */; }; @@ -666,6 +667,7 @@ 845E2F9A283FCA9000C04D56 /* Theme.Survey.Checkbox.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Theme.Survey.Checkbox.swift; sourceTree = ""; }; 845E2F9C283FCB1400C04D56 /* Theme.Survey.Checkbox.Accessibility.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Theme.Survey.Checkbox.Accessibility.swift; sourceTree = ""; }; 847A7642285A1914004044D1 /* FileUploadListViewTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FileUploadListViewTests.swift; sourceTree = ""; }; + 84A318A02869ECFC00CA1DE5 /* Unavailable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Unavailable.swift; sourceTree = ""; }; 84CFB7722822700000167258 /* Theme.Button.Accessibility.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Theme.Button.Accessibility.swift; sourceTree = ""; }; 9A0B7D1627DA6B74006D8637 /* Interactor.Mock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Interactor.Mock.swift; sourceTree = ""; }; 9A186A3427F5CF3C0055886D /* FileUploadStyle.Accessibility.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FileUploadStyle.Accessibility.swift; sourceTree = ""; }; @@ -762,7 +764,7 @@ 9AE9E4B427E0EE2E00BFE239 /* CallViewControllerVoiceOverTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CallViewControllerVoiceOverTests.swift; sourceTree = ""; }; 9AE9E4B627E1E30500BFE239 /* MockHelpers.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockHelpers.swift; sourceTree = ""; }; AFBBF5772851C391004993B3 /* Glia.Deprecated.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Glia.Deprecated.swift; sourceTree = ""; }; - AFBBF57928522591004993B3 /* Configuration.Deprecated.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Configuration.Deprecated.swift; sourceTree = ""; }; + AFBBF57928522591004993B3 /* Configuration.Unavailable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Configuration.Unavailable.swift; sourceTree = ""; }; C1B1D31D2A8244728847B885 /* Pods_GliaWidgetsTests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_GliaWidgetsTests.framework; sourceTree = BUILT_PRODUCTS_DIR; }; C4119E05268F41D1004DFEFB /* Main.storyboard */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; path = Main.storyboard; sourceTree = ""; }; C42463732673ABE10082C135 /* ScreenShareHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ScreenShareHandler.swift; sourceTree = ""; }; @@ -1055,7 +1057,7 @@ 9A83D78227B18DF000681C9F /* Glia.Environment.Mock.swift */, EB750F52273BA9BB00BE5FBD /* GliaError.swift */, 1AC7A7542582594200567FF8 /* Configuration.swift */, - AFBBF57928522591004993B3 /* Configuration.Deprecated.swift */, + AFBBF57928522591004993B3 /* Configuration.Unavailable.swift */, 1A205D5B25655CB1003AA3CD /* GliaWidgets.h */, 1A205D5C25655CB1003AA3CD /* Info.plist */, 6B48213D2735873300F2900A /* Feature.swift */, @@ -1073,6 +1075,7 @@ 9AE0A75F2821904400725946 /* FontScaling.Environment.Interface.swift */, 9AE0A7612822AF3000725946 /* FontScaling.Environment.Live.swift */, 9AE0A7632822B02C00725946 /* FontScaling.Environment.Mock.swift */, + 84A318A02869ECFC00CA1DE5 /* Unavailable.swift */, ); path = GliaWidgets; sourceTree = ""; @@ -2808,7 +2811,7 @@ C4F176CD261D1543009D9F07 /* ChatFileDownloadContentView.swift in Sources */, 75F58EE127E7D5300065BA2D /* Survey.ViewController.Props.swift in Sources */, 754CC61527E27C42005676E9 /* Survey.Checkbox.swift in Sources */, - AFBBF57A28522591004993B3 /* Configuration.Deprecated.swift in Sources */, + AFBBF57A28522591004993B3 /* Configuration.Unavailable.swift in Sources */, 1A4AD3CA256E864800468BFB /* ThemeColorStyle.swift in Sources */, 9AB196DE27C3FFF400FD60AB /* Call.Environment.Mock.swift in Sources */, 1A0C143125B8547200B00695 /* EngagementStyle.swift in Sources */, @@ -2871,6 +2874,7 @@ 1AFB1E6825F7AE3C00CA460D /* ChatAttachment.swift in Sources */, 1A2DA73725EFBFE700032611 /* FileUploadListView.swift in Sources */, 9A186A3527F5CF3C0055886D /* FileUploadStyle.Accessibility.swift in Sources */, + 84A318A12869ECFC00CA1DE5 /* Unavailable.swift in Sources */, 1AE15E49257A6BD200A642C0 /* ConfirmationAlertConfiguration.swift in Sources */, C460C7782600BCF400449851 /* ChoiceCardOptionStyle.swift in Sources */, C43D7A1125FF92680064B1DA /* ChoiceCardStyle.swift in Sources */, diff --git a/GliaWidgets/Configuration.Deprecated.swift b/GliaWidgets/Configuration.Deprecated.swift deleted file mode 100644 index 2894094e5..000000000 --- a/GliaWidgets/Configuration.Deprecated.swift +++ /dev/null @@ -1,63 +0,0 @@ -import Foundation - -extension Configuration { - /// Deprecated. - @available(*, deprecated, message: "Use `authorizationMethod` instead.") - public var appToken: String { - if case .appToken(let value) = authorizationMethod { - return value - } else { - return "" - } - } - - /// Deprecated. - @available(*, deprecated, message: "Api token is not supported.") - public var apiToken: String { "" } - - /// Deprecated. - @available(*, deprecated, message: "Use Configuration(authorizationMethod:environment:site:visitorContext:) instead.") - public init( - appToken: String, - apiToken: String, - environment: Environment, - site: String - ) { - self.init( - authorizationMethod: .appToken(appToken), - environment: environment, - site: site, - visitorContext: nil - ) - } - - /// Deprecated. - @available(*, deprecated, message: "Use Configuration(authorizationMethod:environment:site:visitorContext:) instead.") - public init( - appToken: String, - environment: Environment, - site: String - ) { - self.init( - authorizationMethod: .appToken(appToken), - environment: environment, - site: site, - visitorContext: nil - ) - } - - /// Deprecated. - @available(*, deprecated, message: "Use Configuration(authorizationMethod:environment:site:visitorContext:) instead.") - public init( - authorizationMethod: AuthorizationMethod, - environment: Environment, - site: String - ) { - self.init( - authorizationMethod: authorizationMethod, - environment: environment, - site: site, - visitorContext: nil - ) - } -} diff --git a/GliaWidgets/Configuration.Unavailable.swift b/GliaWidgets/Configuration.Unavailable.swift new file mode 100644 index 000000000..aea19d63b --- /dev/null +++ b/GliaWidgets/Configuration.Unavailable.swift @@ -0,0 +1,46 @@ +import Foundation + +extension Configuration { + /// Unavailable. + @available(*, unavailable, message: "Use `authorizationMethod` instead.") + public var appToken: String { + unavailable() + } + + /// Unavailable. + @available(*, unavailable, message: "Api token is not supported.") + public var apiToken: String { + unavailable() + } + + /// Unavailable. + @available(*, unavailable, message: "Use Configuration(authorizationMethod:environment:site:visitorContext:) instead.") + public init( + appToken: String, + apiToken: String, + environment: Environment, + site: String + ) { + unavailable() + } + + /// Unavailable. + @available(*, unavailable, message: "Use Configuration(authorizationMethod:environment:site:visitorContext:) instead.") + public init( + appToken: String, + environment: Environment, + site: String + ) { + unavailable() + } + + /// Unavailable. + @available(*, unavailable, message: "Use Configuration(authorizationMethod:environment:site:visitorContext:) instead.") + public init( + authorizationMethod: AuthorizationMethod, + environment: Environment, + site: String + ) { + unavailable() + } +} diff --git a/GliaWidgets/Unavailable.swift b/GliaWidgets/Unavailable.swift new file mode 100644 index 000000000..eb8121365 --- /dev/null +++ b/GliaWidgets/Unavailable.swift @@ -0,0 +1,3 @@ +func unavailable(function: StaticString = #function) -> Never { + fatalError("This implementation for \(function) is not longer available.") +} diff --git a/TestingApp/Settings/SettingsViewController.swift b/TestingApp/Settings/SettingsViewController.swift index 964c791d5..8ca6c05a7 100644 --- a/TestingApp/Settings/SettingsViewController.swift +++ b/TestingApp/Settings/SettingsViewController.swift @@ -9,8 +9,6 @@ private struct Section { class SettingsViewController: UIViewController { private let tableView = UITableView(frame: .zero, style: .grouped) private var sections = [Section]() - private var authorizationMethodCell: SettingsSegmentedCell! - private var appTokenCell: SettingsTextCell! private var siteApiKeyIdCell: SettingsTextCell! private var siteApiKeySecretCell: SettingsTextCell! private var siteCell: SettingsTextCell! @@ -70,27 +68,24 @@ class SettingsViewController: UIViewController { updateConfigurationSection() } + @objc + func authMethodDidChange(sender: UISegmentedControl) { + updateConfigurationSection() + tableView.reloadData() + } + + @objc + private func doneTapped() { + saveConf() + theme = makeTheme() + dismiss(animated: true, completion: nil) + } +} + +// MARK: - Private +private extension SettingsViewController { // swiftlint:disable function_body_length private func createCells() { - authorizationMethodCell = SettingsSegmentedCell( - title: "Site authorization method", - items: "app-token", "site-api-key" - ) - authorizationMethodCell.segmentedControl.addTarget( - self, - action: #selector(authMethodDidChange(sender:)), - for: .valueChanged - ) - switch conf.authorizationMethod { - case .appToken: - authorizationMethodCell.segmentedControl.selectedSegmentIndex = 0 - case .siteApiKey: - authorizationMethodCell.segmentedControl.selectedSegmentIndex = 1 - } - appTokenCell = SettingsTextCell( - title: "App token:", - text: conf.appToken - ) siteApiKeyIdCell = SettingsTextCell( title: "Identifier:", text: conf.siteApiKeyId @@ -164,7 +159,7 @@ class SettingsViewController: UIViewController { mediumSubtitle2FontCell = SettingsFontCell(title: "Medium subtitle2", defaultFont: theme.font.mediumSubtitle2) captionFontCell = SettingsFontCell(title: "Caption", - defaultFont: theme.font.caption) + defaultFont: theme.font.caption) buttonLabelFontCell = SettingsFontCell(title: "Button label", defaultFont: theme.font.buttonLabel) var fontCells = [SettingsCell]() @@ -189,27 +184,14 @@ class SettingsViewController: UIViewController { tableView.reloadData() } - @objc - func authMethodDidChange(sender: UISegmentedControl) { - updateConfigurationSection() - tableView.reloadData() - } - private func updateConfigurationSection() { - var cells = [SettingsCell]() - cells.append(authorizationMethodCell) - switch authorizationMethodCell.segmentedControl.selectedSegmentIndex { - case 0: - cells.append(appTokenCell) - case 1: - cells.append(siteApiKeyIdCell) - cells.append(siteApiKeySecretCell) - default: - break - } - cells.append(siteCell) - cells.append(queueIDCell) - cells.append(visitorContextAssedIdCell) + let cells: [SettingsCell] = [ + siteApiKeyIdCell, + siteApiKeySecretCell, + siteCell, + queueIDCell, + visitorContextAssedIdCell + ] configurationSection = Section( title: "Glia configuration", cells: cells @@ -218,8 +200,6 @@ class SettingsViewController: UIViewController { } private func loadConf() -> Configuration { - let authorizationMethod = UserDefaults.standard.integer(forKey: "conf.authorizationMethod") - let appToken = UserDefaults.standard.string(forKey: "conf.appToken") ?? "" let siteApiKeyId = UserDefaults.standard.string(forKey: "conf.siteApiKeyId") ?? "" let siteApiKeySecret = UserDefaults.standard.string(forKey: "conf.siteApiKeySecret") ?? "" let site = UserDefaults.standard.string(forKey: "conf.site") ?? "" @@ -227,7 +207,10 @@ class SettingsViewController: UIViewController { let visitorContext = UUID(uuidString: visitorAssetId) .map(Configuration.VisitorContext.init(assetId:)) return Configuration( - authorizationMethod: authorizationMethod == 0 ? .appToken(appToken) : .siteApiKey(id: siteApiKeyId, secret: siteApiKeySecret), + authorizationMethod: .siteApiKey( + id: siteApiKeyId, + secret: siteApiKeySecret + ), environment: .beta, site: site, visitorContext: visitorContext @@ -252,11 +235,6 @@ class SettingsViewController: UIViewController { } private func saveConf() { - UserDefaults.standard.setValue( - authorizationMethodCell.segmentedControl.selectedSegmentIndex, - forKey: "conf.authorizationMethod" - ) - UserDefaults.standard.setValue(appTokenCell.textField.text ?? "", forKey: "conf.appToken") UserDefaults.standard.setValue(siteApiKeyIdCell.textField.text ?? "", forKey: "conf.siteApiKeyId") UserDefaults.standard.setValue(siteApiKeySecretCell.textField.text ?? "", forKey: "conf.siteApiKeySecret") UserDefaults.standard.setValue(siteCell.textField.text ?? "", forKey: "conf.site") @@ -295,12 +273,6 @@ class SettingsViewController: UIViewController { return Theme(colorStyle: colorStyle, fontStyle: fontStyle) } - - @objc private func doneTapped() { - saveConf() - theme = makeTheme() - dismiss(animated: true, completion: nil) - } } extension SettingsViewController: UITableViewDataSource { From 2842e19aa97eb18157802a9bb7793fccc85fd439 Mon Sep 17 00:00:00 2001 From: Kuldar-Daniel Kokorev Date: Thu, 30 Jun 2022 10:47:51 +0300 Subject: [PATCH 05/37] Move URL tap handling tests from View to ChatViewModel --- GliaWidgets.xcodeproj/project.pbxproj | 8 -- .../Coordinator/Call/CallCoordinator.swift | 4 +- .../Coordinator/Chat/ChatCoordinator.swift | 4 +- GliaWidgets/Coordinator/RootCoordinator.swift | 7 +- GliaWidgets/Glia.swift | 3 +- GliaWidgets/View/Chat/ChatView.swift | 16 +-- .../View/Chat/Message/ChatMessageView.swift | 16 +-- .../Content/ChoiceCard/ChoiceCardView.swift | 7 +- .../Content/Text/ChatTextContentView.swift | 31 +----- .../Message/OperatorChatMessageView.swift | 6 +- .../Chat/Message/VisitorChatMessageView.swift | 6 +- .../ChatViewModel.Environment.Interface.swift | 1 + .../Chat/ChatViewModel.Environment.Mock.swift | 3 +- .../ViewModel/Chat/ChatViewModel.swift | 19 +++- .../Sources/ChatTextContentViewTests.swift | 96 ------------------ .../Sources/ChatViewModelTests.swift | 99 ++++++++++++++++++- .../ChatViewModel.Environment.Failing.swift | 3 +- 17 files changed, 144 insertions(+), 185 deletions(-) delete mode 100644 GliaWidgetsTests/Sources/ChatTextContentViewTests.swift diff --git a/GliaWidgets.xcodeproj/project.pbxproj b/GliaWidgets.xcodeproj/project.pbxproj index f96f15fe8..294fb1630 100644 --- a/GliaWidgets.xcodeproj/project.pbxproj +++ b/GliaWidgets.xcodeproj/project.pbxproj @@ -370,8 +370,6 @@ EB9ADB51280EBD4E00FAE8A4 /* InteractorStateTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = EB9ADB50280EBD4E00FAE8A4 /* InteractorStateTests.swift */; }; EB9ADB552828E66B00FAE8A4 /* CallTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = EB9ADB542828E66B00FAE8A4 /* CallTests.swift */; }; EB9ADB5A2829089F00FAE8A4 /* ChatItem+Equatable.swift in Sources */ = {isa = PBXBuildFile; fileRef = EB9ADB592829089F00FAE8A4 /* ChatItem+Equatable.swift */; }; - FD01A52B483418AB02DCFBFC /* Pods_GliaWidgets.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 85639A838514258D976E1B2A /* Pods_GliaWidgets.framework */; }; - EB95491C2850757400F567F0 /* ChatTextContentViewTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = EB95491B2850757400F567F0 /* ChatTextContentViewTests.swift */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -799,8 +797,6 @@ EB9ADB50280EBD4E00FAE8A4 /* InteractorStateTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InteractorStateTests.swift; sourceTree = ""; }; EB9ADB542828E66B00FAE8A4 /* CallTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CallTests.swift; sourceTree = ""; }; EB9ADB592829089F00FAE8A4 /* ChatItem+Equatable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ChatItem+Equatable.swift"; sourceTree = ""; }; - F08274A374F775EE39BFBDB1 /* Pods-GliaWidgetsTests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-GliaWidgetsTests.debug.xcconfig"; path = "Target Support Files/Pods-GliaWidgetsTests/Pods-GliaWidgetsTests.debug.xcconfig"; sourceTree = ""; }; - EB95491B2850757400F567F0 /* ChatTextContentViewTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatTextContentViewTests.swift; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -1893,7 +1889,6 @@ EB27E71C27FEBB620090B895 /* CallViewModelTests.swift */, EB03B00D27FFF6DD0058F6B1 /* CallViewTests.swift */, EB9ADB542828E66B00FAE8A4 /* CallTests.swift */, - EB95491B2850757400F567F0 /* ChatTextContentViewTests.swift */, 847A7642285A1914004044D1 /* FileUploadListViewTests.swift */, ); path = Sources; @@ -2945,13 +2940,11 @@ 9A8130C227D9095200220BBD /* FileDownload.Failing.swift in Sources */, 9A19927027D3BCAE00161AAE /* GCD.Failing.swift in Sources */, 9A8130C427D9099F00220BBD /* FileDownload.Environment.Failing.swift in Sources */, - EB95491C2850757400F567F0 /* ChatTextContentViewTests.swift in Sources */, 7512A5A727C3926500319DF1 /* GliaTests.swift in Sources */, 9A3E1DA227BA7D00005634EB /* FileSystemStorage.Environment.Failing.swift in Sources */, 9A1992E127D6313500161AAE /* ImageView.Cache.Failing.swift in Sources */, EB9ADB552828E66B00FAE8A4 /* CallTests.swift in Sources */, 9A3E1DA027BA7B9F005634EB /* FileSystemStorageTests.swift in Sources */, - 7512A57D27BFA37D00319DF1 /* CoreSdkClient.Salemove.swift in Sources */, EB9ADB5A2829089F00FAE8A4 /* ChatItem+Equatable.swift in Sources */, EB27E71D27FEBB620090B895 /* CallViewModelTests.swift in Sources */, 9ACC25D227B4727500BC5335 /* CoreSDKClient.Failing.swift in Sources */, @@ -2963,7 +2956,6 @@ 9A3E1D9D27BA7741005634EB /* FoundationBased.Failing.swift in Sources */, 9A3E1D8427B67F1B005634EB /* Helper.swift in Sources */, 9A8130C627D90B3800220BBD /* FileSystemStorage.Failing.swift in Sources */, - 845E2F6A28365AD000C04D56 /* CoreSDKClient.Operator.Mock.swift in Sources */, 7512A57A27BF9FCD00319DF1 /* ChatViewModelTests.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; diff --git a/GliaWidgets/Coordinator/Call/CallCoordinator.swift b/GliaWidgets/Coordinator/Call/CallCoordinator.swift index 0d637323f..09bee8a42 100644 --- a/GliaWidgets/Coordinator/Call/CallCoordinator.swift +++ b/GliaWidgets/Coordinator/Call/CallCoordinator.swift @@ -72,7 +72,8 @@ class CallCoordinator: SubFlowCoordinator, FlowCoordinator { fromHistory: environment.fromHistory, fetchSiteConfigurations: environment.fetchSiteConfigurations, getCurrentEngagement: environment.getCurrentEngagement, - timerProviding: environment.timerProviding + timerProviding: environment.timerProviding, + uiApplication: environment.uiApplication ), call: call, unreadMessages: unreadMessages, @@ -123,5 +124,6 @@ extension CallCoordinator { var getCurrentEngagement: CoreSdkClient.GetCurrentEngagement var timerProviding: FoundationBased.Timer.Providing var submitSurveyAnswer: CoreSdkClient.SubmitSurveyAnswer + var uiApplication: UIKitBased.UIApplication } } diff --git a/GliaWidgets/Coordinator/Chat/ChatCoordinator.swift b/GliaWidgets/Coordinator/Chat/ChatCoordinator.swift index 5bb77f970..590a06c06 100644 --- a/GliaWidgets/Coordinator/Chat/ChatCoordinator.swift +++ b/GliaWidgets/Coordinator/Chat/ChatCoordinator.swift @@ -84,7 +84,8 @@ class ChatCoordinator: SubFlowCoordinator, FlowCoordinator { fromHistory: environment.fromHistory, fetchSiteConfigurations: environment.fetchSiteConfigurations, getCurrentEngagement: environment.getCurrentEngagement, - timerProviding: .live + timerProviding: .live, + uiApplication: environment.uiApplication ) ) viewModel.engagementDelegate = { [weak self] event in @@ -203,5 +204,6 @@ extension ChatCoordinator { var fetchSiteConfigurations: CoreSdkClient.FetchSiteConfigurations var getCurrentEngagement: CoreSdkClient.GetCurrentEngagement var submitSurveyAnswer: CoreSdkClient.SubmitSurveyAnswer + var uiApplication: UIKitBased.UIApplication } } diff --git a/GliaWidgets/Coordinator/RootCoordinator.swift b/GliaWidgets/Coordinator/RootCoordinator.swift index e10688e66..ebc8aec01 100644 --- a/GliaWidgets/Coordinator/RootCoordinator.swift +++ b/GliaWidgets/Coordinator/RootCoordinator.swift @@ -209,7 +209,8 @@ extension RootCoordinator { fromHistory: environment.fromHistory, fetchSiteConfigurations: environment.fetchSiteConfigurations, getCurrentEngagement: environment.getCurrentEngagement, - submitSurveyAnswer: environment.submitSurveyAnswer + submitSurveyAnswer: environment.submitSurveyAnswer, + uiApplication: environment.uiApplication ) ) coordinator.delegate = { [weak self] event in @@ -292,7 +293,8 @@ extension RootCoordinator { fetchSiteConfigurations: environment.fetchSiteConfigurations, getCurrentEngagement: environment.getCurrentEngagement, timerProviding: environment.timerProviding, - submitSurveyAnswer: environment.submitSurveyAnswer + submitSurveyAnswer: environment.submitSurveyAnswer, + uiApplication: environment.uiApplication ) ) coordinator.delegate = { [weak self] event in @@ -487,6 +489,7 @@ extension RootCoordinator { var fetchSiteConfigurations: CoreSdkClient.FetchSiteConfigurations var getCurrentEngagement: CoreSdkClient.GetCurrentEngagement var submitSurveyAnswer: CoreSdkClient.SubmitSurveyAnswer + var uiApplication: UIKitBased.UIApplication } } diff --git a/GliaWidgets/Glia.swift b/GliaWidgets/Glia.swift index af1d1d571..af7931ca9 100644 --- a/GliaWidgets/Glia.swift +++ b/GliaWidgets/Glia.swift @@ -211,7 +211,8 @@ public class Glia { timerProviding: environment.timerProviding, fetchSiteConfigurations: environment.coreSdk.fetchSiteConfigurations, getCurrentEngagement: environment.coreSdk.getCurrentEngagement, - submitSurveyAnswer: environment.coreSdk.submitSurveyAnswer + submitSurveyAnswer: environment.coreSdk.submitSurveyAnswer, + uiApplication: environment.uiApplication ) ) rootCoordinator?.delegate = { [weak self] event in diff --git a/GliaWidgets/View/Chat/ChatView.swift b/GliaWidgets/View/Chat/ChatView.swift index 1db4b84c5..69b505074 100644 --- a/GliaWidgets/View/Chat/ChatView.swift +++ b/GliaWidgets/View/Chat/ChatView.swift @@ -267,10 +267,7 @@ extension ChatView { return .queueOperator(connectView) case .outgoingMessage(let message): let view = VisitorChatMessageView( - with: style.visitorMessage, - environment: .init( - uiApplication: environment.uiApplication - ) + with: style.visitorMessage ) view.appendContent( .text( @@ -295,10 +292,7 @@ extension ChatView { return .outgoingMessage(view) case .visitorMessage(let message, let status): let view = VisitorChatMessageView( - with: style.visitorMessage, - environment: .init( - uiApplication: environment.uiApplication - ) + with: style.visitorMessage ) view.appendContent( .text( @@ -329,8 +323,7 @@ extension ChatView { data: environment.data, uuid: environment.uuid, gcd: environment.gcd, - imageViewCache: environment.imageViewCache, - uiApplication: environment.uiApplication + imageViewCache: environment.imageViewCache ) ) view.appendContent( @@ -362,8 +355,7 @@ extension ChatView { data: environment.data, uuid: environment.uuid, gcd: environment.gcd, - imageViewCache: environment.imageViewCache, - uiApplication: environment.uiApplication + imageViewCache: environment.imageViewCache ) ) let choiceCard = ChoiceCard(with: message, isActive: isActive) diff --git a/GliaWidgets/View/Chat/Message/ChatMessageView.swift b/GliaWidgets/View/Chat/Message/ChatMessageView.swift index b358388f6..98b1af46a 100644 --- a/GliaWidgets/View/Chat/Message/ChatMessageView.swift +++ b/GliaWidgets/View/Chat/Message/ChatMessageView.swift @@ -8,16 +8,13 @@ class ChatMessageView: UIView { var linkTapped: ((URL) -> Void)? private let contentAlignment: ChatMessageContentAlignment - private let environment: Environment init( with style: ChatMessageStyle, - contentAlignment: ChatMessageContentAlignment, - environment: Environment + contentAlignment: ChatMessageContentAlignment ) { self.style = style self.contentAlignment = contentAlignment - self.environment = environment super.init(frame: .zero) setup() } @@ -31,10 +28,7 @@ class ChatMessageView: UIView { case let .text(text, accProperties): let contentView = ChatTextContentView( with: style.text, - contentAlignment: contentAlignment, - environment: .init( - uiApplication: environment.uiApplication - ) + contentAlignment: contentAlignment ) contentView.text = text contentView.linkTapped = { [weak self] in self?.linkTapped?($0) } @@ -138,9 +132,3 @@ class ChatMessageView: UIView { } } } - -extension ChatMessageView { - struct Environment { - var uiApplication: UIKitBased.UIApplication - } -} diff --git a/GliaWidgets/View/Chat/Message/Content/ChoiceCard/ChoiceCardView.swift b/GliaWidgets/View/Chat/Message/Content/ChoiceCard/ChoiceCardView.swift index a91af3c12..f9b9668af 100644 --- a/GliaWidgets/View/Chat/Message/Content/ChoiceCard/ChoiceCardView.swift +++ b/GliaWidgets/View/Chat/Message/Content/ChoiceCard/ChoiceCardView.swift @@ -18,8 +18,7 @@ final class ChoiceCardView: OperatorChatMessageView { data: environment.data, uuid: environment.uuid, gcd: environment.gcd, - imageViewCache: environment.imageViewCache, - uiApplication: environment.uiApplication + imageViewCache: environment.imageViewCache ) ) } @@ -75,9 +74,6 @@ final class ChoiceCardView: OperatorChatMessageView { let textView = ChatTextContentView( with: style.text, contentAlignment: .left, - environment: .init( - uiApplication: environment.uiApplication - ), insets: .zero ) textView.text = choiceCard.text @@ -113,7 +109,6 @@ extension ChoiceCardView { var uuid: () -> UUID var gcd: GCD var imageViewCache: ImageView.Cache - var uiApplication: UIKitBased.UIApplication } } diff --git a/GliaWidgets/View/Chat/Message/Content/Text/ChatTextContentView.swift b/GliaWidgets/View/Chat/Message/Content/Text/ChatTextContentView.swift index 3bb2a75ee..b30ee826e 100644 --- a/GliaWidgets/View/Chat/Message/Content/Text/ChatTextContentView.swift +++ b/GliaWidgets/View/Chat/Message/Content/Text/ChatTextContentView.swift @@ -30,17 +30,14 @@ class ChatTextContentView: UIView { private let contentAlignment: ChatMessageContentAlignment private let contentView = UIView() private let kTextInsets: UIEdgeInsets - private let environment: Environment init( with style: ChatTextContentStyle, contentAlignment: ChatMessageContentAlignment, - environment: Environment, insets: UIEdgeInsets = UIEdgeInsets(top: 12, left: 16, bottom: 12, right: 16) ) { self.style = style self.contentAlignment = contentAlignment - self.environment = environment self.kTextInsets = insets super.init(frame: .zero) setup() @@ -109,34 +106,9 @@ extension ChatTextContentView: UITextViewDelegate { in characterRange: NSRange, interaction: UITextItemInteraction ) -> Bool { - handleUrl(url: URL) + linkTapped?(URL) return false } - - func handleUrl(url: URL) { - switch url.scheme?.lowercased() { - case "tel", - "mailto": - guard - environment.uiApplication.canOpenURL(url) - else { return } - - environment.uiApplication.open(url) - - case "http", - "https": - linkTapped?(url) - - default: - return - } - } -} - -extension ChatTextContentView { - struct Environment { - var uiApplication: UIKitBased.UIApplication - } } extension ChatTextContentView { @@ -157,7 +129,6 @@ extension ChatTextContentView { accessibility: .unsupported ), contentAlignment: .left, - environment: environment, insets: .zero ) } diff --git a/GliaWidgets/View/Chat/Message/OperatorChatMessageView.swift b/GliaWidgets/View/Chat/Message/OperatorChatMessageView.swift index 7740bc942..456af2318 100644 --- a/GliaWidgets/View/Chat/Message/OperatorChatMessageView.swift +++ b/GliaWidgets/View/Chat/Message/OperatorChatMessageView.swift @@ -39,10 +39,7 @@ class OperatorChatMessageView: ChatMessageView { self.environment = environment super.init( with: style, - contentAlignment: .left, - environment: .init( - uiApplication: environment.uiApplication - ) + contentAlignment: .left ) setup() layout() @@ -80,6 +77,5 @@ extension OperatorChatMessageView { var uuid: () -> UUID var gcd: GCD var imageViewCache: ImageView.Cache - var uiApplication: UIKitBased.UIApplication } } diff --git a/GliaWidgets/View/Chat/Message/VisitorChatMessageView.swift b/GliaWidgets/View/Chat/Message/VisitorChatMessageView.swift index 73d171fe1..a308cd93f 100644 --- a/GliaWidgets/View/Chat/Message/VisitorChatMessageView.swift +++ b/GliaWidgets/View/Chat/Message/VisitorChatMessageView.swift @@ -10,13 +10,11 @@ class VisitorChatMessageView: ChatMessageView { private let kInsets = UIEdgeInsets(top: 2, left: 88, bottom: 2, right: 16) init( - with style: VisitorChatMessageStyle, - environment: Environment + with style: VisitorChatMessageStyle ) { super.init( with: style, - contentAlignment: .right, - environment: environment + contentAlignment: .right ) setup(style: style) diff --git a/GliaWidgets/ViewModel/Chat/ChatViewModel.Environment.Interface.swift b/GliaWidgets/ViewModel/Chat/ChatViewModel.Environment.Interface.swift index 10282f364..da026b0de 100644 --- a/GliaWidgets/ViewModel/Chat/ChatViewModel.Environment.Interface.swift +++ b/GliaWidgets/ViewModel/Chat/ChatViewModel.Environment.Interface.swift @@ -17,5 +17,6 @@ extension EngagementViewModel { var fetchSiteConfigurations: CoreSdkClient.FetchSiteConfigurations var getCurrentEngagement: CoreSdkClient.GetCurrentEngagement var timerProviding: FoundationBased.Timer.Providing + var uiApplication: UIKitBased.UIApplication } } diff --git a/GliaWidgets/ViewModel/Chat/ChatViewModel.Environment.Mock.swift b/GliaWidgets/ViewModel/Chat/ChatViewModel.Environment.Mock.swift index 54a2f758e..1cd176df7 100644 --- a/GliaWidgets/ViewModel/Chat/ChatViewModel.Environment.Mock.swift +++ b/GliaWidgets/ViewModel/Chat/ChatViewModel.Environment.Mock.swift @@ -21,7 +21,8 @@ extension ChatViewModel.Environment { fromHistory: { true }, fetchSiteConfigurations: { _ in }, getCurrentEngagement: { return nil }, - timerProviding: .mock + timerProviding: .mock, + uiApplication: .mock ) } #endif diff --git a/GliaWidgets/ViewModel/Chat/ChatViewModel.swift b/GliaWidgets/ViewModel/Chat/ChatViewModel.swift index f09808a9e..29cc3324f 100644 --- a/GliaWidgets/ViewModel/Chat/ChatViewModel.swift +++ b/GliaWidgets/ViewModel/Chat/ChatViewModel.swift @@ -632,8 +632,23 @@ extension ChatViewModel { delegate?(.showFile(file)) } - private func linkTapped(_ url: URL) { - delegate?(.openLink(url)) + func linkTapped(_ url: URL) { + switch url.scheme?.lowercased() { + case "tel", + "mailto": + guard + environment.uiApplication.canOpenURL(url) + else { return } + + environment.uiApplication.open(url) + + case "http", + "https": + delegate?(.openLink(url)) + + default: + return + } } private func downloadTapped(_ download: FileDownload) { diff --git a/GliaWidgetsTests/Sources/ChatTextContentViewTests.swift b/GliaWidgetsTests/Sources/ChatTextContentViewTests.swift deleted file mode 100644 index bd5dc4843..000000000 --- a/GliaWidgetsTests/Sources/ChatTextContentViewTests.swift +++ /dev/null @@ -1,96 +0,0 @@ -import XCTest - -@testable import GliaWidgets - -class ChatTextContentViewTests: XCTestCase { - var view: ChatTextContentView! - - func test_handleUrlWithPhoneOpensURLWithUIApplication() throws { - enum Call: Equatable { case openUrl(URL) } - - var calls: [Call] = [] - var environment = ChatTextContentView.Environment( - uiApplication: .failing - ) - - environment.uiApplication.canOpenURL = { _ in true } - environment.uiApplication.open = { - calls.append(.openUrl($0)) - } - - view = .mock(environment: environment) - - let telUrl = URL(string: "tel:12345678")! - view.handleUrl(url: telUrl) - - XCTAssertEqual(calls, [.openUrl(telUrl)]) - } - - func test_handleUrlWithEmailOpensURLWithUIApplication() throws { - enum Call: Equatable { case openUrl(URL) } - - var calls: [Call] = [] - var environment = ChatTextContentView.Environment( - uiApplication: .failing - ) - - environment.uiApplication.canOpenURL = { _ in true } - environment.uiApplication.open = { - calls.append(.openUrl($0)) - } - - view = .mock(environment: environment) - - let mailUrl = URL(string: "mailto:mock@mock.mock")! - view.handleUrl(url: mailUrl) - - XCTAssertEqual(calls, [.openUrl(mailUrl)]) - } - - func test_handleUrlWithLinkOpensCalsLinkTapped() throws { - enum Call: Equatable { case linkTapped(URL) } - var calls: [Call] = [] - - let environment = ChatTextContentView.Environment( - uiApplication: .failing - ) - - view = .mock(environment: environment) - view.linkTapped = { - calls.append(.linkTapped($0)) - } - - let linkUrl = URL(string: "https://mock.mock")! - view.handleUrl(url: linkUrl) - - XCTAssertEqual(calls, [.linkTapped(linkUrl)]) - } - - func test_handleUrlWithRandomSchemeDoesNothing() throws { - enum Call: Equatable { - case linkTapped(URL) - case openUrl(URL) - } - - var calls: [Call] = [] - - var environment = ChatTextContentView.Environment( - uiApplication: .failing - ) - - environment.uiApplication.canOpenURL = { _ in true } - environment.uiApplication.open = { - calls.append(.openUrl($0)) - } - - view = .mock(environment: environment) - view.linkTapped = { - calls.append(.linkTapped($0)) - } - - let mockUrl = URL(string: "mock:mock")! - view.handleUrl(url: mockUrl) - - XCTAssertEqual(calls, []) - } -} diff --git a/GliaWidgetsTests/Sources/ChatViewModelTests.swift b/GliaWidgetsTests/Sources/ChatViewModelTests.swift index f641215ed..d0c72c8a5 100644 --- a/GliaWidgetsTests/Sources/ChatViewModelTests.swift +++ b/GliaWidgetsTests/Sources/ChatViewModelTests.swift @@ -47,7 +47,8 @@ class ChatViewModelTests: XCTestCase { fromHistory: { true }, fetchSiteConfigurations: { _ in }, getCurrentEngagement: { nil }, - timerProviding: .mock + timerProviding: .mock, + uiApplication: .mock ) ) @@ -215,6 +216,102 @@ class ChatViewModelTests: XCTestCase { // Then XCTAssertEqual(calls, [.fetchSiteConfigurations]) } + + func test_handleUrlWithPhoneOpensURLWithUIApplication() throws { + enum Call: Equatable { case openUrl(URL) } + + var calls: [Call] = [] + var viewModelEnv = ChatViewModel.Environment.failing + viewModelEnv.fileManager.urlsForDirectoryInDomainMask = { _, _ in [.mock] } + viewModelEnv.fileManager.createDirectoryAtUrlWithIntermediateDirectories = { _, _, _ in } + viewModelEnv.uiApplication.canOpenURL = { _ in true } + viewModelEnv.uiApplication.open = { + calls.append(.openUrl($0)) + } + let viewModel: ChatViewModel = .mock(interactor: .mock(), environment: viewModelEnv) + + let telUrl = URL(string: "tel:12345678")! + viewModel.linkTapped(telUrl) + + XCTAssertEqual(calls, [.openUrl(telUrl)]) + } + + func test_handleUrlWithEmailOpensURLWithUIApplication() throws { + enum Call: Equatable { case openUrl(URL) } + + var calls: [Call] = [] + var viewModelEnv = ChatViewModel.Environment.failing + viewModelEnv.fileManager.urlsForDirectoryInDomainMask = { _, _ in [.mock] } + viewModelEnv.fileManager.createDirectoryAtUrlWithIntermediateDirectories = { _, _, _ in } + viewModelEnv.uiApplication.canOpenURL = { _ in true } + viewModelEnv.uiApplication.open = { + calls.append(.openUrl($0)) + } + let viewModel: ChatViewModel = .mock(interactor: .mock(), environment: viewModelEnv) + + let mailUrl = URL(string: "mailto:mock@mock.mock")! + viewModel.linkTapped(mailUrl) + + XCTAssertEqual(calls, [.openUrl(mailUrl)]) + } + + func test_handleUrlWithLinkOpensCalsLinkTapped() throws { + enum Call: Equatable { case linkTapped(URL) } + var calls: [Call] = [] + var viewModelEnv = ChatViewModel.Environment.failing + viewModelEnv.fileManager.urlsForDirectoryInDomainMask = { _, _ in [.mock] } + viewModelEnv.fileManager.createDirectoryAtUrlWithIntermediateDirectories = { _, _, _ in } + let viewModel: ChatViewModel = .mock( + interactor: .mock(), + environment: viewModelEnv + ) + + viewModel.delegate = { event in + switch event { + case .openLink(let url): + calls.append(.linkTapped(url)) + + default: + break + } + } + + let linkUrl = URL(string: "https://mock.mock")! + viewModel.linkTapped(linkUrl) + + XCTAssertEqual(calls, [.linkTapped(linkUrl)]) + } + + func test_handleUrlWithRandomSchemeDoesNothing() throws { + enum Call: Equatable { + case linkTapped(URL) + case openUrl(URL) + } + + var calls: [Call] = [] + var viewModelEnv = ChatViewModel.Environment.failing + viewModelEnv.fileManager.urlsForDirectoryInDomainMask = { _, _ in [.mock] } + viewModelEnv.fileManager.createDirectoryAtUrlWithIntermediateDirectories = { _, _, _ in } + viewModelEnv.uiApplication.canOpenURL = { _ in true } + viewModelEnv.uiApplication.open = { + calls.append(.openUrl($0)) + } + let viewModel: ChatViewModel = .mock(interactor: .mock(), environment: viewModelEnv) + viewModel.delegate = { event in + switch event { + case .openLink(let url): + calls.append(.linkTapped(url)) + + default: + break + } + } + + let mockUrl = URL(string: "mock:mock")! + viewModel.linkTapped(mockUrl) + + XCTAssertEqual(calls, []) + } } extension ChatChoiceCardOption { diff --git a/GliaWidgetsTests/ViewModel/Chat/ChatViewModel.Environment.Failing.swift b/GliaWidgetsTests/ViewModel/Chat/ChatViewModel.Environment.Failing.swift index ae63a0a29..c0179624f 100644 --- a/GliaWidgetsTests/ViewModel/Chat/ChatViewModel.Environment.Failing.swift +++ b/GliaWidgetsTests/ViewModel/Chat/ChatViewModel.Environment.Failing.swift @@ -33,6 +33,7 @@ extension ChatViewModel.Environment { fail("\(Self.self).fetchSiteConfigurations") }, getCurrentEngagement: { nil }, - timerProviding: .mock + timerProviding: .mock, + uiApplication: .mock ) } From 82281ef14f35c7eb0831bfd3dda29269d1cb2b5b Mon Sep 17 00:00:00 2001 From: Kuldar-Daniel Kokorev Date: Thu, 30 Jun 2022 10:59:57 +0300 Subject: [PATCH 06/37] Mark required initialisers unavailable --- GliaWidgets/Component/Bubble/BubbleWindow.swift | 1 + GliaWidgets/Component/Connect/ConnectView.swift | 1 + .../Connect/Operator/Animation/ConnectAnimationView.swift | 1 + GliaWidgets/Component/Connect/Status/ConnectStatusView.swift | 1 + GliaWidgets/Component/ImageView/File/FilePreviewView.swift | 1 + GliaWidgets/View/Chat/Entry/Upload/FileUploadListView.swift | 1 + GliaWidgets/View/Chat/Entry/Upload/FileUploadView.swift | 1 + GliaWidgets/View/Chat/Message/ChatMessageView.swift | 1 + .../View/Chat/Message/Content/File/ChatFileContentView.swift | 1 + .../Content/File/Download/ChatFileDownloadContentView.swift | 1 + .../Message/Content/File/Image/ChatImageFileContentView.swift | 1 + .../View/Chat/Message/Content/Text/ChatTextContentView.swift | 1 + GliaWidgets/View/Chat/Message/OperatorChatMessageView.swift | 1 + GliaWidgets/View/Chat/Message/VisitorChatMessageView.swift | 1 + GliaWidgets/View/Chat/Upgrade/ChatCallUpgradeView.swift | 1 + .../ViewController/Common/Popover/PopoverViewController.swift | 1 + 16 files changed, 16 insertions(+) diff --git a/GliaWidgets/Component/Bubble/BubbleWindow.swift b/GliaWidgets/Component/Bubble/BubbleWindow.swift index 5f6e3f7fd..b123bfce8 100644 --- a/GliaWidgets/Component/Bubble/BubbleWindow.swift +++ b/GliaWidgets/Component/Bubble/BubbleWindow.swift @@ -34,6 +34,7 @@ class BubbleWindow: UIWindow { layout() } + @available(*, unavailable) required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } diff --git a/GliaWidgets/Component/Connect/ConnectView.swift b/GliaWidgets/Component/Connect/ConnectView.swift index ef4d15673..2bee2177a 100644 --- a/GliaWidgets/Component/Connect/ConnectView.swift +++ b/GliaWidgets/Component/Connect/ConnectView.swift @@ -40,6 +40,7 @@ class ConnectView: UIView { layout() } + @available(*, unavailable) required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } diff --git a/GliaWidgets/Component/Connect/Operator/Animation/ConnectAnimationView.swift b/GliaWidgets/Component/Connect/Operator/Animation/ConnectAnimationView.swift index f9deb15f5..16587e621 100644 --- a/GliaWidgets/Component/Connect/Operator/Animation/ConnectAnimationView.swift +++ b/GliaWidgets/Component/Connect/Operator/Animation/ConnectAnimationView.swift @@ -21,6 +21,7 @@ class ConnectAnimationView: UIView { layout() } + @available(*, unavailable) required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } diff --git a/GliaWidgets/Component/Connect/Status/ConnectStatusView.swift b/GliaWidgets/Component/Connect/Status/ConnectStatusView.swift index 321a094e2..42fed6aee 100644 --- a/GliaWidgets/Component/Connect/Status/ConnectStatusView.swift +++ b/GliaWidgets/Component/Connect/Status/ConnectStatusView.swift @@ -11,6 +11,7 @@ class ConnectStatusView: UIView { layout() } + @available(*, unavailable) required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } diff --git a/GliaWidgets/Component/ImageView/File/FilePreviewView.swift b/GliaWidgets/Component/ImageView/File/FilePreviewView.swift index 870953371..f9305237b 100644 --- a/GliaWidgets/Component/ImageView/File/FilePreviewView.swift +++ b/GliaWidgets/Component/ImageView/File/FilePreviewView.swift @@ -27,6 +27,7 @@ class FilePreviewView: UIView { layout() } + @available(*, unavailable) required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } diff --git a/GliaWidgets/View/Chat/Entry/Upload/FileUploadListView.swift b/GliaWidgets/View/Chat/Entry/Upload/FileUploadListView.swift index 349d337ea..ac75934c6 100644 --- a/GliaWidgets/View/Chat/Entry/Upload/FileUploadListView.swift +++ b/GliaWidgets/View/Chat/Entry/Upload/FileUploadListView.swift @@ -40,6 +40,7 @@ final class FileUploadListView: UIView { layout() } + @available(*, unavailable) required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } diff --git a/GliaWidgets/View/Chat/Entry/Upload/FileUploadView.swift b/GliaWidgets/View/Chat/Entry/Upload/FileUploadView.swift index 290790494..4c1e5404c 100644 --- a/GliaWidgets/View/Chat/Entry/Upload/FileUploadView.swift +++ b/GliaWidgets/View/Chat/Entry/Upload/FileUploadView.swift @@ -25,6 +25,7 @@ class FileUploadView: UIView { layout() } + @available(*, unavailable) required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } diff --git a/GliaWidgets/View/Chat/Message/ChatMessageView.swift b/GliaWidgets/View/Chat/Message/ChatMessageView.swift index b358388f6..a7468315c 100644 --- a/GliaWidgets/View/Chat/Message/ChatMessageView.swift +++ b/GliaWidgets/View/Chat/Message/ChatMessageView.swift @@ -22,6 +22,7 @@ class ChatMessageView: UIView { setup() } + @available(*, unavailable) required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } diff --git a/GliaWidgets/View/Chat/Message/Content/File/ChatFileContentView.swift b/GliaWidgets/View/Chat/Message/Content/File/ChatFileContentView.swift index ef5390140..dd65430c0 100644 --- a/GliaWidgets/View/Chat/Message/Content/File/ChatFileContentView.swift +++ b/GliaWidgets/View/Chat/Message/Content/File/ChatFileContentView.swift @@ -26,6 +26,7 @@ class ChatFileContentView: UIView { layout() } + @available(*, unavailable) required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } diff --git a/GliaWidgets/View/Chat/Message/Content/File/Download/ChatFileDownloadContentView.swift b/GliaWidgets/View/Chat/Message/Content/File/Download/ChatFileDownloadContentView.swift index bdd946e36..2a3f3529a 100644 --- a/GliaWidgets/View/Chat/Message/Content/File/Download/ChatFileDownloadContentView.swift +++ b/GliaWidgets/View/Chat/Message/Content/File/Download/ChatFileDownloadContentView.swift @@ -27,6 +27,7 @@ class ChatFileDownloadContentView: ChatFileContentView { layout() } + @available(*, unavailable) required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } diff --git a/GliaWidgets/View/Chat/Message/Content/File/Image/ChatImageFileContentView.swift b/GliaWidgets/View/Chat/Message/Content/File/Image/ChatImageFileContentView.swift index dd5ee10e2..fabb4c52e 100644 --- a/GliaWidgets/View/Chat/Message/Content/File/Image/ChatImageFileContentView.swift +++ b/GliaWidgets/View/Chat/Message/Content/File/Image/ChatImageFileContentView.swift @@ -25,6 +25,7 @@ class ChatImageFileContentView: ChatFileContentView { layout() } + @available(*, unavailable) required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } diff --git a/GliaWidgets/View/Chat/Message/Content/Text/ChatTextContentView.swift b/GliaWidgets/View/Chat/Message/Content/Text/ChatTextContentView.swift index 3bb2a75ee..feae22a9e 100644 --- a/GliaWidgets/View/Chat/Message/Content/Text/ChatTextContentView.swift +++ b/GliaWidgets/View/Chat/Message/Content/Text/ChatTextContentView.swift @@ -47,6 +47,7 @@ class ChatTextContentView: UIView { layout() } + @available(*, unavailable) required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } diff --git a/GliaWidgets/View/Chat/Message/OperatorChatMessageView.swift b/GliaWidgets/View/Chat/Message/OperatorChatMessageView.swift index 7740bc942..0276f9c39 100644 --- a/GliaWidgets/View/Chat/Message/OperatorChatMessageView.swift +++ b/GliaWidgets/View/Chat/Message/OperatorChatMessageView.swift @@ -48,6 +48,7 @@ class OperatorChatMessageView: ChatMessageView { layout() } + @available(*, unavailable) required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } diff --git a/GliaWidgets/View/Chat/Message/VisitorChatMessageView.swift b/GliaWidgets/View/Chat/Message/VisitorChatMessageView.swift index 73d171fe1..029291fb1 100644 --- a/GliaWidgets/View/Chat/Message/VisitorChatMessageView.swift +++ b/GliaWidgets/View/Chat/Message/VisitorChatMessageView.swift @@ -23,6 +23,7 @@ class VisitorChatMessageView: ChatMessageView { layout() } + @available(*, unavailable) required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } diff --git a/GliaWidgets/View/Chat/Upgrade/ChatCallUpgradeView.swift b/GliaWidgets/View/Chat/Upgrade/ChatCallUpgradeView.swift index 861197349..bd078bb19 100644 --- a/GliaWidgets/View/Chat/Upgrade/ChatCallUpgradeView.swift +++ b/GliaWidgets/View/Chat/Upgrade/ChatCallUpgradeView.swift @@ -25,6 +25,7 @@ class ChatCallUpgradeView: UIView { duration.removeObserver(self) } + @available(*, unavailable) required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } diff --git a/GliaWidgets/ViewController/Common/Popover/PopoverViewController.swift b/GliaWidgets/ViewController/Common/Popover/PopoverViewController.swift index 1466102bd..84ec96856 100644 --- a/GliaWidgets/ViewController/Common/Popover/PopoverViewController.swift +++ b/GliaWidgets/ViewController/Common/Popover/PopoverViewController.swift @@ -23,6 +23,7 @@ final class PopoverViewController: UIViewController { popoverPresentationController?.delegate = self } + @available(*, unavailable) required init?(coder _: NSCoder) { fatalError("init(coder:) has not been implemented") } From 8d8e6044987f8f92f7c07d749173a2b66d9a1457 Mon Sep 17 00:00:00 2001 From: Yurii Dukhovnyi Date: Fri, 1 Jul 2022 01:00:58 +0300 Subject: [PATCH 07/37] Add accessibility identifiers for using in AT environments --- TestingApp/Main.storyboard | 8 ++++++++ TestingApp/Settings/SettingsViewController.swift | 15 +++++++++++---- 2 files changed, 19 insertions(+), 4 deletions(-) diff --git a/TestingApp/Main.storyboard b/TestingApp/Main.storyboard index 24961637f..99f8a8d05 100644 --- a/TestingApp/Main.storyboard +++ b/TestingApp/Main.storyboard @@ -22,6 +22,7 @@