From e951f45bfae2995a6229d48ec3adafe947f92920 Mon Sep 17 00:00:00 2001 From: Rasmus Tauts Date: Thu, 6 Jul 2023 15:03:46 +0300 Subject: [PATCH 01/64] Parse GVA JSON data to platform-specific data structures This PR consists of metadata parsing into gva objects and laying the groundwork for the rendering. It also contains placeholders for the UI, because these will be implemented in a separate ticket. In addition, this PR refactors ChatMessage to address certain issues with current implementation of computed properties MOB-2360 --- GliaWidgets.xcodeproj/project.pbxproj | 8 +++ ...onversations.ChatWithTranscriptModel.swift | 2 +- ...ersations.TranscriptModel+CustomCard.swift | 2 +- .../View/Chat/Cells/ChatItemCell.swift | 12 +++++ GliaWidgets/Sources/View/Chat/ChatView.swift | 26 +++++++++- .../Chat/ChatViewModel+CustomCard.swift | 2 +- .../ViewModel/Chat/ChatViewModel.swift | 2 +- .../ViewModel/Chat/Data/ChatItem.swift | 39 ++++++++++---- .../ViewModel/Chat/Data/ChatMessage.swift | 30 +++++++++-- .../Chat/Data/ChatMessageCardType.swift | 11 ++++ .../Sources/ViewModel/Chat/Data/Gva.swift | 51 +++++++++++++++++++ .../ViewModel/Chat/ChatItemTests.swift | 2 + 12 files changed, 167 insertions(+), 20 deletions(-) create mode 100644 GliaWidgets/Sources/ViewModel/Chat/Data/ChatMessageCardType.swift create mode 100644 GliaWidgets/Sources/ViewModel/Chat/Data/Gva.swift diff --git a/GliaWidgets.xcodeproj/project.pbxproj b/GliaWidgets.xcodeproj/project.pbxproj index 8bb608d85..c5610af35 100644 --- a/GliaWidgets.xcodeproj/project.pbxproj +++ b/GliaWidgets.xcodeproj/project.pbxproj @@ -510,6 +510,8 @@ B757AD4073B49FF0A17D071D /* Pods_TestingApp.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = EE3A89412171262DF9CD8ABA /* Pods_TestingApp.framework */; }; C0175A0F2A55A624001FACDE /* ChatMessagaEntryViewTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0175A0E2A55A624001FACDE /* ChatMessagaEntryViewTests.swift */; }; C0175A112A55AA3E001FACDE /* ChatMessageEntryView.+Mock.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0175A102A55AA3E001FACDE /* ChatMessageEntryView.+Mock.swift */; }; + C0175A132A56E29E001FACDE /* ChatMessageCardType.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0175A122A56E29E001FACDE /* ChatMessageCardType.swift */; }; + C0175A152A56E2DD001FACDE /* Gva.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0175A142A56E2DD001FACDE /* Gva.swift */; }; C03A8047292BA76D00DDECA6 /* ChatViewControllerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = C03A8046292BA76D00DDECA6 /* ChatViewControllerTests.swift */; }; C03A8049292BC8DB00DDECA6 /* CallViewControllerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = C03A8048292BC8DB00DDECA6 /* CallViewControllerTests.swift */; }; C05AB01C295F416700AA381F /* VisitorCodeCloseButtonProperties.swift in Sources */ = {isa = PBXBuildFile; fileRef = C05AB01B295F416700AA381F /* VisitorCodeCloseButtonProperties.swift */; }; @@ -1157,6 +1159,8 @@ B45FBFA4E2F1D31E83A1CC3A /* Pods_GliaWidgets.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_GliaWidgets.framework; sourceTree = BUILT_PRODUCTS_DIR; }; C0175A0E2A55A624001FACDE /* ChatMessagaEntryViewTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatMessagaEntryViewTests.swift; sourceTree = ""; }; C0175A102A55AA3E001FACDE /* ChatMessageEntryView.+Mock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ChatMessageEntryView.+Mock.swift"; sourceTree = ""; }; + C0175A122A56E29E001FACDE /* ChatMessageCardType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatMessageCardType.swift; sourceTree = ""; }; + C0175A142A56E2DD001FACDE /* Gva.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Gva.swift; sourceTree = ""; }; C03A8046292BA76D00DDECA6 /* ChatViewControllerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatViewControllerTests.swift; sourceTree = ""; }; C03A8048292BC8DB00DDECA6 /* CallViewControllerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CallViewControllerTests.swift; sourceTree = ""; }; C05AB016295DA9FC00AA381F /* AlertViewController+VisitorCode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "AlertViewController+VisitorCode.swift"; sourceTree = ""; }; @@ -1728,6 +1732,8 @@ AFCF8A5B2A02AB3000B7ABB3 /* OutgoingMessage.Mock.swift */, C49A29F12614A85E00819269 /* ChoiceCard.swift */, C43D7A1425FF9A590064B1DA /* ChatChoiceCardOption.swift */, + C0175A122A56E29E001FACDE /* ChatMessageCardType.swift */, + C0175A142A56E2DD001FACDE /* Gva.swift */, ); path = Data; sourceTree = ""; @@ -3813,6 +3819,7 @@ 1ABD6C5D25B59D1C00D56EFA /* BubbleWindow.swift in Sources */, 75940964298D3889008B173A /* MessageRenderer.Web.swift in Sources */, 1AE15E38257A578B00A642C0 /* MessageAlertConfiguration.swift in Sources */, + C0175A132A56E29E001FACDE /* ChatMessageCardType.swift in Sources */, AFBBF5782851C391004993B3 /* Glia.Deprecated.swift in Sources */, 75940950298D3810008B173A /* IdCollection.swift in Sources */, 75FF151D27F4F8E000FE7BE2 /* Theme.Survey.InputQuestion.swift in Sources */, @@ -3997,6 +4004,7 @@ 84265E62298D7B2900D65842 /* ScreenSharingCoordinator+DelegateEvent.swift in Sources */, C0D2F048299272D100803B47 /* VideoCallView.ConnectOperatorView.swift in Sources */, 9A3E1D8A27B6B824005634EB /* FileDownload.Environment.Interface.swift in Sources */, + C0175A152A56E2DD001FACDE /* Gva.swift in Sources */, 9A8130B727D7578500220BBD /* LocalFile.Environment.Mock.swift in Sources */, 1A0C143F25B85DB400B00695 /* CallViewController.swift in Sources */, 3197F7B629F7C2E5008EE9F7 /* SecureConversations.SecureChatModel.swift in Sources */, diff --git a/GliaWidgets/SecureConversations/ChatTranscript/SecureConversations.ChatWithTranscriptModel.swift b/GliaWidgets/SecureConversations/ChatTranscript/SecureConversations.ChatWithTranscriptModel.swift index b25a8fe31..2bd86ea6c 100644 --- a/GliaWidgets/SecureConversations/ChatTranscript/SecureConversations.ChatWithTranscriptModel.swift +++ b/GliaWidgets/SecureConversations/ChatTranscript/SecureConversations.ChatWithTranscriptModel.swift @@ -932,7 +932,7 @@ extension SecureConversations.TranscriptModel { appendItem(item, to: pendingSection, animated: true) action?(.updateItemsUserImage(animated: true)) - let choiceCardInputModeEnabled = message.isChoiceCard || self.isInteractableCustomCard(message) + let choiceCardInputModeEnabled = message.cardType == .choiceCard || self.isInteractableCustomCard(message) action?(.setChoiceCardInputModeEnabled(choiceCardInputModeEnabled)) // Store info about choice card mode from which diff --git a/GliaWidgets/SecureConversations/ChatTranscript/SecureConversations.TranscriptModel+CustomCard.swift b/GliaWidgets/SecureConversations/ChatTranscript/SecureConversations.TranscriptModel+CustomCard.swift index d7653d550..14cb17f6b 100644 --- a/GliaWidgets/SecureConversations/ChatTranscript/SecureConversations.TranscriptModel+CustomCard.swift +++ b/GliaWidgets/SecureConversations/ChatTranscript/SecureConversations.TranscriptModel+CustomCard.swift @@ -5,6 +5,6 @@ import GliaCoreSDK extension SecureConversations.TranscriptModel { func isInteractableCustomCard(_ chatMessage: ChatMessage) -> Bool { let message = MessageRenderer.Message(chatMessage: chatMessage) - return chatMessage.isCustomCard && (isInteractableCard?(message) ?? false) + return chatMessage.cardType == .customCard && (isInteractableCard?(message) ?? false) } } diff --git a/GliaWidgets/Sources/View/Chat/Cells/ChatItemCell.swift b/GliaWidgets/Sources/View/Chat/Cells/ChatItemCell.swift index a77581d65..34f16c276 100644 --- a/GliaWidgets/Sources/View/Chat/Cells/ChatItemCell.swift +++ b/GliaWidgets/Sources/View/Chat/Cells/ChatItemCell.swift @@ -12,6 +12,10 @@ class ChatItemCell: UITableViewCell { case callUpgrade(ChatCallUpgradeView) case unreadMessagesDivider(UnreadMessageDividerView) case systemMessage(SystemMessageView) + case gvaResponseText(UIView) + case gvaPersistentButton(UIView) + case gvaQuickReply(UIView) + case gvaGallery(UIView) var view: UIView? { switch self { @@ -35,6 +39,14 @@ class ChatItemCell: UITableViewCell { return view case let .systemMessage(view): return view + case let .gvaResponseText(view): + return view + case let .gvaPersistentButton(view): + return view + case let .gvaQuickReply(view): + return view + case let .gvaGallery(view): + return view } } } diff --git a/GliaWidgets/Sources/View/Chat/ChatView.swift b/GliaWidgets/Sources/View/Chat/ChatView.swift index 531dfd198..3cd8d6e73 100644 --- a/GliaWidgets/Sources/View/Chat/ChatView.swift +++ b/GliaWidgets/Sources/View/Chat/ChatView.swift @@ -405,7 +405,7 @@ extension ChatView { } guard let contentView = messageRenderer?.render(message) else { - if chatMessage.isChoiceCard { + if chatMessage.cardType == .choiceCard { return choiceCardMessageContent( chatMessage, showsImage: showsImage, @@ -483,6 +483,30 @@ extension ChatView { ) case .systemMessage(let message): return systemMessageContent(message) + case let .gvaPersistentButton(_, button): + // Temporary, since UI hasn't been implemented + + let textView = UITextView() + textView.text = "Persistent Button: \(button.content)" + return .gvaPersistentButton(textView) + case let .gvaResponseText(_, text): + // Temporary, since UI hasn't been implemented + + let textView = UITextView() + textView.text = "Response Text: \(text.content)" + return .gvaResponseText(textView) + case let .gvaQuickReply(_, button): + // Temporary, since UI hasn't been implemented + + let textView = UITextView() + textView.text = "Quick Reply: \(button.content)" + return .gvaQuickReply(textView) + case let .gvaGallery(_, gallery): + // Temporary, since UI hasn't been implemented + + let textView = UITextView() + textView.text = "Gallery: \(gallery.type.rawValue)" + return .gvaGallery(textView) } } // swiftlint:enable function_body_length diff --git a/GliaWidgets/Sources/ViewModel/Chat/ChatViewModel+CustomCard.swift b/GliaWidgets/Sources/ViewModel/Chat/ChatViewModel+CustomCard.swift index d6e242998..58fd1892e 100644 --- a/GliaWidgets/Sources/ViewModel/Chat/ChatViewModel+CustomCard.swift +++ b/GliaWidgets/Sources/ViewModel/Chat/ChatViewModel+CustomCard.swift @@ -108,6 +108,6 @@ extension ChatViewModel { func isInteractableCustomCard(_ chatMessage: ChatMessage) -> Bool { let message = MessageRenderer.Message(chatMessage: chatMessage) - return chatMessage.isCustomCard && (isInteractableCard?(message) ?? false) + return chatMessage.cardType == .customCard && (isInteractableCard?(message) ?? false) } } diff --git a/GliaWidgets/Sources/ViewModel/Chat/ChatViewModel.swift b/GliaWidgets/Sources/ViewModel/Chat/ChatViewModel.swift index fda65df6e..96756d75d 100644 --- a/GliaWidgets/Sources/ViewModel/Chat/ChatViewModel.swift +++ b/GliaWidgets/Sources/ViewModel/Chat/ChatViewModel.swift @@ -565,7 +565,7 @@ extension ChatViewModel { appendItem(item, to: messagesSection, animated: true) action?(.updateItemsUserImage(animated: true)) - let choiceCardInputModeEnabled = message.isChoiceCard || self.isInteractableCustomCard(message) + let choiceCardInputModeEnabled = message.cardType == .choiceCard || self.isInteractableCustomCard(message) action?(.setChoiceCardInputModeEnabled(choiceCardInputModeEnabled)) // Store info about choice card mode from which diff --git a/GliaWidgets/Sources/ViewModel/Chat/Data/ChatItem.swift b/GliaWidgets/Sources/ViewModel/Chat/Data/ChatItem.swift index d1fea9bce..f9aebb1de 100644 --- a/GliaWidgets/Sources/ViewModel/Chat/Data/ChatItem.swift +++ b/GliaWidgets/Sources/ViewModel/Chat/Data/ChatItem.swift @@ -34,17 +34,32 @@ class ChatItem { switch message.sender { case .visitor: kind = .visitorMessage(message, status: nil) - case .operator where message.isCustomCard && isCustomCardSupported: - kind = .customCard( - message, - showsImage: false, - imageUrl: nil, - isActive: !fromHistory - ) case .operator: - kind = message.isChoiceCard ? - .choiceCard(message, showsImage: false, imageUrl: nil, isActive: !fromHistory) : - .operatorMessage(message, showsImage: false, imageUrl: message.operator?.pictureUrl) + switch message.cardType { + case .choiceCard: + kind = .choiceCard(message, showsImage: false, imageUrl: nil, isActive: !fromHistory) + case .customCard: + if isCustomCardSupported { + kind = .customCard( + message, + showsImage: false, + imageUrl: nil, + isActive: !fromHistory + ) + } else { + return nil + } + case let .gvaPersistenButton(button): + kind = .gvaPersistentButton(message, persistenButton: button) + case let .gvaResponseText(text): + kind = .gvaResponseText(message, responseText: text) + case let .gvaQuickReply(button): + kind = .gvaQuickReply(message, quickReply: button) + case let .gvaGallery(gallery): + kind = .gvaGallery(message, gallery: gallery) + case .none: + kind = .operatorMessage(message, showsImage: false, imageUrl: message.operator?.pictureUrl) + } case .system: kind = .systemMessage(message) case .omniguide, .unknown: @@ -68,5 +83,9 @@ extension ChatItem { case transferring case unreadMessageDivider case systemMessage(ChatMessage) + case gvaPersistentButton(ChatMessage, persistenButton: GvaButton) + case gvaResponseText(ChatMessage, responseText: GvaResponseText) + case gvaQuickReply(ChatMessage, quickReply: GvaButton) + case gvaGallery(ChatMessage, gallery: GvaGallery) } } diff --git a/GliaWidgets/Sources/ViewModel/Chat/Data/ChatMessage.swift b/GliaWidgets/Sources/ViewModel/Chat/Data/ChatMessage.swift index 5b4ade148..7f5b7f32b 100644 --- a/GliaWidgets/Sources/ViewModel/Chat/Data/ChatMessage.swift +++ b/GliaWidgets/Sources/ViewModel/Chat/Data/ChatMessage.swift @@ -34,12 +34,32 @@ class ChatMessage: Codable { var downloads = [FileDownload]() var metadata: MessageMetadata? - var isChoiceCard: Bool { - return attachment?.type == .singleChoice - } + var cardType: ChatMessageCardType { + if let response = try? metadata?.decode(GvaResponseText.self), response.type == .plainText { + return .gvaResponseText(response) + } + + if let response = try? metadata?.decode(GvaButton.self) { + if response.type == .persistentButtons { + return .gvaPersistenButton(response) + } else if response.type == .quickReplies { + return .gvaQuickReply(response) + } + } + + if let response = try? metadata?.decode(GvaGallery.self), response.type == .galleryCards { + return .gvaGallery(response) + } + + if attachment?.type == .singleChoice { + return .choiceCard + } + + if metadata == nil { + return .none + } - var isCustomCard: Bool { - metadata != nil + return .customCard } private enum CodingKeys: String, CodingKey { diff --git a/GliaWidgets/Sources/ViewModel/Chat/Data/ChatMessageCardType.swift b/GliaWidgets/Sources/ViewModel/Chat/Data/ChatMessageCardType.swift new file mode 100644 index 000000000..84777c962 --- /dev/null +++ b/GliaWidgets/Sources/ViewModel/Chat/Data/ChatMessageCardType.swift @@ -0,0 +1,11 @@ +import Foundation + +enum ChatMessageCardType: Equatable { + case choiceCard + case customCard + case gvaPersistenButton(GvaButton) + case gvaResponseText(GvaResponseText) + case gvaQuickReply(GvaButton) + case gvaGallery(GvaGallery) + case none +} diff --git a/GliaWidgets/Sources/ViewModel/Chat/Data/Gva.swift b/GliaWidgets/Sources/ViewModel/Chat/Data/Gva.swift new file mode 100644 index 000000000..6bad9f8fe --- /dev/null +++ b/GliaWidgets/Sources/ViewModel/Chat/Data/Gva.swift @@ -0,0 +1,51 @@ +import Foundation + +struct GvaResponseText: Decodable, Equatable { + let type: GvaCardType + let content: String +} + +struct GvaButton: Decodable, Equatable { + let type: GvaCardType + let content: String + let options: [GvaOption] +} + +struct GvaGallery: Decodable, Equatable { + let type: GvaCardType + let gallaryCards: [GvaGallaryCard] +} + +struct GvaGallaryCard: Decodable, Equatable { + let title: String + let subtitle: String? + let imageUrl: String? + let options: [GvaOption]? +} + +struct GvaOption: Decodable, Equatable { + let text: String + let value: String? + let url: String? + let urlTarget: String? + let destinationPdBroadcastEvent: String? +} + +enum GvaUrlTarget: String, Decodable { + case modal + case _self + case blank + + enum CodingKeys: String, CodingKey { + case modal + case _self = "self" + case blank + } +} + +enum GvaCardType: String, Decodable { + case persistentButtons + case quickReplies + case plainText + case galleryCards +} diff --git a/GliaWidgetsTests/ViewModel/Chat/ChatItemTests.swift b/GliaWidgetsTests/ViewModel/Chat/ChatItemTests.swift index 9189b4c4c..d422d0890 100644 --- a/GliaWidgetsTests/ViewModel/Chat/ChatItemTests.swift +++ b/GliaWidgetsTests/ViewModel/Chat/ChatItemTests.swift @@ -32,6 +32,8 @@ final class ChatItemTests: XCTestCase { switch kind { case .choiceCard, .customCard, .operatorMessage, .systemMessage: XCTAssertTrue(chatItem.isOperatorMessage) + case .gvaGallery, .gvaQuickReply, .gvaResponseText, .gvaPersistentButton: + XCTAssertTrue(chatItem.isOperatorMessage) case .unreadMessageDivider, .visitorMessage, .transferring, .outgoingMessage, .queueOperator, .operatorConnected, .callUpgrade: XCTAssertFalse(chatItem.isOperatorMessage) From 242f7e3f5df2985044f7592d384f2d43b7958dfc Mon Sep 17 00:00:00 2001 From: Rasmus Tauts Date: Tue, 11 Jul 2023 11:31:10 +0300 Subject: [PATCH 02/64] GVA Response Text UI This PR creates GVA Response text UI. This PR also splits ChatView.swift a little bit, since it was closing on 1000 lines of code which is not desired and hard to read. MOB-2363 --- GliaWidgets.xcodeproj/project.pbxproj | 24 +- .../View/Chat/ChatView.Accessibility.swift | 40 ++ .../View/Chat/ChatView.TableView.swift | 48 ++ GliaWidgets/Sources/View/Chat/ChatView.swift | 607 +++++++++--------- .../Chat/Message/GvaResponseTextView.swift | 84 +++ .../ViewModel/Chat/Data/ChatItem.swift | 4 +- .../Sources/ViewModel/Chat/Data/Gva.swift | 4 +- 7 files changed, 499 insertions(+), 312 deletions(-) create mode 100644 GliaWidgets/Sources/View/Chat/ChatView.Accessibility.swift create mode 100644 GliaWidgets/Sources/View/Chat/ChatView.TableView.swift create mode 100644 GliaWidgets/Sources/View/Chat/Message/GvaResponseTextView.swift diff --git a/GliaWidgets.xcodeproj/project.pbxproj b/GliaWidgets.xcodeproj/project.pbxproj index c5610af35..ca4abbbf4 100644 --- a/GliaWidgets.xcodeproj/project.pbxproj +++ b/GliaWidgets.xcodeproj/project.pbxproj @@ -512,6 +512,9 @@ C0175A112A55AA3E001FACDE /* ChatMessageEntryView.+Mock.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0175A102A55AA3E001FACDE /* ChatMessageEntryView.+Mock.swift */; }; C0175A132A56E29E001FACDE /* ChatMessageCardType.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0175A122A56E29E001FACDE /* ChatMessageCardType.swift */; }; C0175A152A56E2DD001FACDE /* Gva.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0175A142A56E2DD001FACDE /* Gva.swift */; }; + C0175A172A5D30D7001FACDE /* GvaResponseTextView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0175A162A5D30D7001FACDE /* GvaResponseTextView.swift */; }; + C0175A192A5D3C56001FACDE /* ChatView.Accessibility.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0175A182A5D3C56001FACDE /* ChatView.Accessibility.swift */; }; + C0175A1D2A5D4226001FACDE /* ChatView.TableView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0175A1C2A5D4226001FACDE /* ChatView.TableView.swift */; }; C03A8047292BA76D00DDECA6 /* ChatViewControllerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = C03A8046292BA76D00DDECA6 /* ChatViewControllerTests.swift */; }; C03A8049292BC8DB00DDECA6 /* CallViewControllerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = C03A8048292BC8DB00DDECA6 /* CallViewControllerTests.swift */; }; C05AB01C295F416700AA381F /* VisitorCodeCloseButtonProperties.swift in Sources */ = {isa = PBXBuildFile; fileRef = C05AB01B295F416700AA381F /* VisitorCodeCloseButtonProperties.swift */; }; @@ -1161,6 +1164,9 @@ C0175A102A55AA3E001FACDE /* ChatMessageEntryView.+Mock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ChatMessageEntryView.+Mock.swift"; sourceTree = ""; }; C0175A122A56E29E001FACDE /* ChatMessageCardType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatMessageCardType.swift; sourceTree = ""; }; C0175A142A56E2DD001FACDE /* Gva.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Gva.swift; sourceTree = ""; }; + C0175A162A5D30D7001FACDE /* GvaResponseTextView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GvaResponseTextView.swift; sourceTree = ""; }; + C0175A182A5D3C56001FACDE /* ChatView.Accessibility.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatView.Accessibility.swift; sourceTree = ""; }; + C0175A1C2A5D4226001FACDE /* ChatView.TableView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatView.TableView.swift; sourceTree = ""; }; C03A8046292BA76D00DDECA6 /* ChatViewControllerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatViewControllerTests.swift; sourceTree = ""; }; C03A8048292BC8DB00DDECA6 /* CallViewControllerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CallViewControllerTests.swift; sourceTree = ""; }; C05AB016295DA9FC00AA381F /* AlertViewController+VisitorCode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "AlertViewController+VisitorCode.swift"; sourceTree = ""; }; @@ -1945,15 +1951,17 @@ 1A60AFD12566990F00E53F53 /* Chat */ = { isa = PBXGroup; children = ( - 1A8B61D025C97481000D780E /* Upgrade */, - 1A5F813A2588B70A00A605DA /* Entry */, - 1A60B02B256BF7C200E53F53 /* Message */, 1AC7A7822583B65B00567FF8 /* Cells */, - 1A60AFDC25669A4200E53F53 /* ChatView.swift */, - 1A60AFE725669C5000E53F53 /* ChatStyle.swift */, 9AB3402627FCDD92006E0FE2 /* ChatStyle.Accessibility.swift */, - AF10ED8E29BF849A00E85309 /* UnreadMessageDividerView.swift */, + 1A60AFE725669C5000E53F53 /* ChatStyle.swift */, + C0175A182A5D3C56001FACDE /* ChatView.Accessibility.swift */, + C0175A1C2A5D4226001FACDE /* ChatView.TableView.swift */, + 1A60AFDC25669A4200E53F53 /* ChatView.swift */, + 1A5F813A2588B70A00A605DA /* Entry */, + 1A60B02B256BF7C200E53F53 /* Message */, AF10ED9029BF85C700E85309 /* UnreadMessageDividerStyle.swift */, + AF10ED8E29BF849A00E85309 /* UnreadMessageDividerView.swift */, + 1A8B61D025C97481000D780E /* Upgrade */, ); path = Chat; sourceTree = ""; @@ -2105,6 +2113,7 @@ 845E2F6F283CF94100C04D56 /* VisitorChatMessageStyle.Accessibility.swift */, 3197F7AE29E95527008EE9F7 /* SystemMessageView.swift */, 3197F7B029E958F4008EE9F7 /* SystemMessageStyle.swift */, + C0175A162A5D30D7001FACDE /* GvaResponseTextView.swift */, ); path = Message; sourceTree = ""; @@ -4191,6 +4200,7 @@ 9A186A3527F5CF3C0055886D /* FileUploadStyle.Accessibility.swift in Sources */, 84A318A12869ECFC00CA1DE5 /* Unavailable.swift in Sources */, 1AE15E49257A6BD200A642C0 /* ConfirmationAlertConfiguration.swift in Sources */, + C0175A172A5D30D7001FACDE /* GvaResponseTextView.swift in Sources */, C460C7782600BCF400449851 /* ChoiceCardOptionStyle.swift in Sources */, AF3D520F2983BC5600AD8E69 /* FileUploader.Environment.Mock.swift in Sources */, C43D7A1125FF92680064B1DA /* ChoiceCardStyle.swift in Sources */, @@ -4198,6 +4208,7 @@ 9AB196D827C3E27300FD60AB /* ChatViewModel.Environment.Mock.swift in Sources */, 3100EEFF293F7E0900D57F71 /* Theme+SecureConversationsWelcome.swift in Sources */, 9A8130BF27D7AEF700220BBD /* FileUpload.Mock.swift in Sources */, + C0175A1D2A5D4226001FACDE /* ChatView.TableView.swift in Sources */, 9A8130BD27D7A53300220BBD /* FileUpload.Environment.Mock.swift in Sources */, 1A60AFB22566821B00E53F53 /* Asset.swift in Sources */, C05AB01C295F416700AA381F /* VisitorCodeCloseButtonProperties.swift in Sources */, @@ -4255,6 +4266,7 @@ 1A2DA73125EFA77E00032611 /* FileUploader.swift in Sources */, 1A60B0272568070800E53F53 /* UIStackView+Extensions.swift in Sources */, 75940945298D378A008B173A /* CoreSDKClient.Mock.swift in Sources */, + C0175A192A5D3C56001FACDE /* ChatView.Accessibility.swift in Sources */, C08D776228F58A18000461E5 /* ColorType.swift in Sources */, 84EFB05B28AA90220005E270 /* CustomCardContainerView.swift in Sources */, 7594098A298D38C2008B173A /* CallVisualizer.VisitorCodeViewModel.Mock.swift in Sources */, diff --git a/GliaWidgets/Sources/View/Chat/ChatView.Accessibility.swift b/GliaWidgets/Sources/View/Chat/ChatView.Accessibility.swift new file mode 100644 index 000000000..dab7075ef --- /dev/null +++ b/GliaWidgets/Sources/View/Chat/ChatView.Accessibility.swift @@ -0,0 +1,40 @@ +import UIKit + +// MARK: - Accessibility +extension ChatView { + static func operatorAccessibilityMessage( + for chatMessage: ChatMessage, + `operator`: String, + isFontScalingEnabled: Bool + ) -> ChatMessageContent.TextAccessibilityProperties { + .init( + label: chatMessage.operator?.name ?? `operator`, + value: chatMessage.content, + isFontScalingEnabled: isFontScalingEnabled + ) + } + + static func visitorAccessibilityMessage( + for chatMessage: ChatMessage, + visitor: String, + isFontScalingEnabled: Bool + ) -> ChatMessageContent.TextAccessibilityProperties { + .init( + label: visitor, + value: chatMessage.content, + isFontScalingEnabled: isFontScalingEnabled + ) + } + + static func visitorAccessibilityOutgoingMessage( + for outgoingMessage: OutgoingMessage, + visitor: String, + isFontScalingEnabled: Bool + ) -> ChatMessageContent.TextAccessibilityProperties { + .init( + label: visitor, + value: outgoingMessage.content, + isFontScalingEnabled: isFontScalingEnabled + ) + } +} diff --git a/GliaWidgets/Sources/View/Chat/ChatView.TableView.swift b/GliaWidgets/Sources/View/Chat/ChatView.TableView.swift new file mode 100644 index 000000000..23f82567e --- /dev/null +++ b/GliaWidgets/Sources/View/Chat/ChatView.TableView.swift @@ -0,0 +1,48 @@ +import UIKit + +// MARK: - UITableViewDataSource + +extension ChatView: UITableViewDataSource { + func numberOfSections(in tableView: UITableView) -> Int { + return numberOfSections?() ?? 0 + } + + func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { + return numberOfRows?(section) ?? 0 + } + + func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { + guard + let item = itemForRow?(indexPath.row, indexPath.section), + let cell: ChatItemCell = tableView.dequeue(cellFor: indexPath) + else { return UITableViewCell() } + cell.content = content(for: item) + return cell + } + + func tableView(_ tableView: UITableView, willDisplay cell: UITableViewCell, forRowAt indexPath: IndexPath) { + guard + let cell = cell as? ChatItemCell, + case .customCard(let view) = cell.content + else { return } + view.willDisplayView?() + } +} + +// MARK: - UITableViewDelegate + +extension ChatView: UITableViewDelegate { + public func scrollViewWillBeginDragging(_ scrollView: UIScrollView) { + endEditing(true) + } + + func scrollViewDidScroll(_ scrollView: UIScrollView) { + chatScrolledToBottom?(isBottomReached(for: scrollView)) + } + + func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat { + guard let item = itemForRow?(indexPath.row, indexPath.section) else { return CGFloat.zero } + guard case .none = content(for: item) else { return UITableView.automaticDimension } + return CGFloat.zero + } +} diff --git a/GliaWidgets/Sources/View/Chat/ChatView.swift b/GliaWidgets/Sources/View/Chat/ChatView.swift index 3cd8d6e73..efa61cb8f 100644 --- a/GliaWidgets/Sources/View/Chat/ChatView.swift +++ b/GliaWidgets/Sources/View/Chat/ChatView.swift @@ -321,195 +321,71 @@ extension ChatView { } } - // swiftlint:disable function_body_length - private func content(for item: ChatItem) -> ChatItemCell.Content { + func content(for item: ChatItem) -> ChatItemCell.Content { switch item.kind { case .queueOperator: return .queueOperator(connectView) - case .outgoingMessage(let message): - let view = VisitorChatMessageView( - with: style.visitorMessage, - environment: .init(uiScreen: environment.uiScreen) - ) - view.appendContent( - .text( - message.content, - accessibility: Self.visitorAccessibilityOutgoingMessage( - for: message, - visitor: style.accessibility.visitor, - isFontScalingEnabled: style.accessibility.isFontScalingEnabled - ) - ), - animated: false - ) - view.appendContent( - .files( - message.files, - accessibility: .init(from: .visitor) - ), - animated: false - ) - view.fileTapped = { [weak self] in self?.fileTapped?($0) } - view.linkTapped = { [weak self] in self?.linkTapped?($0) } - return .outgoingMessage(view) - case .visitorMessage(let message, let status): - let view = VisitorChatMessageView( - with: style.visitorMessage, - environment: .init(uiScreen: environment.uiScreen) - ) - view.appendContent( - .text( - message.content, - accessibility: Self.visitorAccessibilityMessage( - for: message, - visitor: style.accessibility.visitor, - isFontScalingEnabled: style.accessibility.isFontScalingEnabled - ) - ), - animated: false - ) - view.appendContent( - .downloads( - message.downloads, - accessibility: .init(from: .visitor) - ), - animated: false + case let .outgoingMessage(message): + return outgoingMessageContent(message) + case let .visitorMessage(message, status): + return visitorMessageContent( + message, + status: status ) - view.downloadTapped = { [weak self] in self?.downloadTapped?($0) } - view.linkTapped = { [weak self] in self?.linkTapped?($0) } - view.status = status - return .visitorMessage(view) - case .operatorMessage(let message, let showsImage, let imageUrl): + case let .operatorMessage(message, showsImage, imageUrl): return operatorMessageContent( message, showsImage: showsImage, imageUrl: imageUrl ) - case .choiceCard(let message, let showsImage, let imageUrl, let isActive): + case let .choiceCard(message, showsImage, imageUrl, isActive): return choiceCardMessageContent( message, showsImage: showsImage, imageUrl: imageUrl, isActive: isActive ) - case .customCard(let chatMessage, let showsImage, let imageUrl, let isActive): - let message = MessageRenderer.Message(chatMessage: chatMessage) - // Response card should be shown by default even if option is selected. - let shouldShow = messageRenderer?.shouldShowCard(message) ?? true - // Response card is considered as noninteractable by default. - let isInteractable = messageRenderer?.isInteractable(message) ?? false - // Need to hide interactable response card if integrator returns `false` - // via shouldShowCard interface. - if !shouldShow, isInteractable { - return .none - } - - guard let contentView = messageRenderer?.render(message) else { - if chatMessage.cardType == .choiceCard { - return choiceCardMessageContent( - chatMessage, - showsImage: showsImage, - imageUrl: imageUrl, - isActive: isActive - ) - } - return operatorMessageContent( - chatMessage, - showsImage: showsImage, - imageUrl: imageUrl - ) - } - - let container = CustomCardContainerView() - if let webCardView = contentView as? WebMessageCardView { - webCardView.isUserInteractionEnabled = isActive - webCardView.delegate = self - webCardView.updateHeight(heightCache[chatMessage.id] ?? 0) - container.willDisplayView = webCardView.startLoading - } - - container.addContentView(contentView) - return .customCard(container) - case .callUpgrade(let kind, let duration): - let callStyle = callUpgradeStyle(for: kind.value) - let view = ChatCallUpgradeView( - with: callStyle, - duration: duration - ) - kind.addObserver(self) { [weak self] kind, _ in - guard let self = self else { return } - - view.style = self.callUpgradeStyle(for: kind) - self.refreshAll() - } - return .callUpgrade(view) - case .operatorConnected(let name, let imageUrl): - let connectView = ConnectView( - with: style.connect, - layout: .chat, - 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 + case let .customCard(chatMessage, showsImage, imageUrl, isActive): + return customCardContent( + chatMessage, + showsImage: showsImage, + imageUrl: imageUrl, + isActive: isActive ) - return .queueOperator(connectView) + case let .callUpgrade(kind, duration): + return callUpgradeContent(kind: kind, duration: duration) + case let .operatorConnected(name, imageUrl): + return operatorConnectedContent(name: name, imageUrl: imageUrl) case .transferring: - let connectView = ConnectView( - with: style.connect, - layout: .chat, - 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) + return transferringContent() case .unreadMessageDivider: - return .unreadMessagesDivider( - UnreadMessageDividerView( - style: style.unreadMessageDivider - ) - ) + return unreadMessageDividerContent() case .systemMessage(let message): return systemMessageContent(message) case let .gvaPersistentButton(_, button): // Temporary, since UI hasn't been implemented - let textView = UITextView() textView.text = "Persistent Button: \(button.content)" return .gvaPersistentButton(textView) - case let .gvaResponseText(_, text): - // Temporary, since UI hasn't been implemented - - let textView = UITextView() - textView.text = "Response Text: \(text.content)" - return .gvaResponseText(textView) + case let .gvaResponseText(message, text, showImage, imageUrl): + return gvaResponseTextContent( + message, + text: text, + showImage: showImage, + imageUrl: imageUrl + ) case let .gvaQuickReply(_, button): // Temporary, since UI hasn't been implemented - let textView = UITextView() textView.text = "Quick Reply: \(button.content)" return .gvaQuickReply(textView) case let .gvaGallery(_, gallery): // Temporary, since UI hasn't been implemented - let textView = UITextView() textView.text = "Gallery: \(gallery.type.rawValue)" return .gvaGallery(textView) } } - // swiftlint:enable function_body_length private func callUpgradeStyle(for callKind: CallKind) -> ChatCallUpgradeStyle { return callKind == .audio @@ -529,101 +405,15 @@ extension ChatView { } } - private func isBottomReached(for scrollView: UIScrollView) -> Bool { + func isBottomReached(for scrollView: UIScrollView) -> Bool { let chatBottomOffset = scrollView.contentSize.height - scrollView.frame.size.height let currentPositionOffset = scrollView.contentOffset.y + scrollView.contentInset.top return currentPositionOffset >= chatBottomOffset } - - private func operatorMessageContent( - _ message: ChatMessage, - showsImage: Bool, - imageUrl: String? - ) -> ChatItemCell.Content { - let view = OperatorChatMessageView( - with: style.operatorMessage, - environment: .init( - data: environment.data, - uuid: environment.uuid, - gcd: environment.gcd, - imageViewCache: environment.imageViewCache, - uiScreen: environment.uiScreen - ) - ) - view.appendContent( - .text( - message.content, - accessibility: Self.operatorAccessibilityMessage( - for: message, - operator: style.accessibility.operator, - isFontScalingEnabled: style.accessibility.isFontScalingEnabled - ) - ), - animated: false - ) - view.appendContent( - .downloads( - message.downloads, - accessibility: .init(from: .operator(message.operator?.name ?? style.accessibility.operator))), - animated: false - ) - view.downloadTapped = { [weak self] in self?.downloadTapped?($0) } - view.linkTapped = { [weak self] in self?.linkTapped?($0) } - view.showsOperatorImage = showsImage - view.setOperatorImage(fromUrl: imageUrl, animated: false) - return .operatorMessage(view) - } - - private func choiceCardMessageContent( - _ message: ChatMessage, - showsImage: Bool, - imageUrl: String?, - isActive: Bool - ) -> ChatItemCell.Content { - let view = ChoiceCardView( - with: style.choiceCard, - environment: .init( - data: environment.data, - uuid: environment.uuid, - gcd: environment.gcd, - imageViewCache: environment.imageViewCache, - uiScreen: environment.uiScreen - ) - ) - let choiceCard = ChoiceCard(with: message, isActive: isActive) - view.showsOperatorImage = showsImage - view.setOperatorImage(fromUrl: imageUrl, animated: false) - view.onOptionTapped = { self.choiceOptionSelected($0, message.id) } - view.appendContent(.choiceCard(choiceCard), animated: false) - return .choiceCard(view) - } - - private func systemMessageContent(_ message: ChatMessage) -> ChatItemCell.Content { - let view = SystemMessageView( - with: style.systemMessage, - environment: .init( - uiScreen: environment.uiScreen - ) - ) - - view.appendContent( - .text( - message.content, - accessibility: Self.operatorAccessibilityMessage( - for: message, - operator: style.accessibility.operator, - isFontScalingEnabled: style.accessibility.isFontScalingEnabled - ) - ), - animated: false - ) - - return .systemMessage(view) - } } -// MARK: WebMessageCardViewDelegate +// MARK: - WebMessageCardViewDelegate extension ChatView: WebMessageCardViewDelegate { func viewDidUpdateHeight( @@ -675,7 +465,7 @@ extension ChatView: WebMessageCardViewDelegate { } } -// MARK: Call Bubble +// MARK: - Call Bubble extension ChatView { func setCallBubbleImage(with imageUrl: String?) { @@ -745,7 +535,7 @@ extension ChatView { } } -// MARK: Keyboard +// MARK: - Keyboard extension ChatView { private func observeKeyboard() { @@ -780,88 +570,301 @@ extension ChatView { } } -// MARK: UITableViewDataSource +// MARK: - Message Content -extension ChatView: UITableViewDataSource { - func numberOfSections(in tableView: UITableView) -> Int { - return numberOfSections?() ?? 0 +extension ChatView { + private func operatorMessageContent( + _ message: ChatMessage, + showsImage: Bool, + imageUrl: String? + ) -> ChatItemCell.Content { + let view = OperatorChatMessageView( + with: style.operatorMessage, + environment: .init( + data: environment.data, + uuid: environment.uuid, + gcd: environment.gcd, + imageViewCache: environment.imageViewCache, + uiScreen: environment.uiScreen + ) + ) + view.appendContent( + .text( + message.content, + accessibility: Self.operatorAccessibilityMessage( + for: message, + operator: style.accessibility.operator, + isFontScalingEnabled: style.accessibility.isFontScalingEnabled + ) + ), + animated: false + ) + view.appendContent( + .downloads( + message.downloads, + accessibility: .init(from: .operator(message.operator?.name ?? style.accessibility.operator))), + animated: false + ) + view.downloadTapped = { [weak self] in self?.downloadTapped?($0) } + view.linkTapped = { [weak self] in self?.linkTapped?($0) } + view.showsOperatorImage = showsImage + view.setOperatorImage(fromUrl: imageUrl, animated: false) + return .operatorMessage(view) } - func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { - return numberOfRows?(section) ?? 0 + private func choiceCardMessageContent( + _ message: ChatMessage, + showsImage: Bool, + imageUrl: String?, + isActive: Bool + ) -> ChatItemCell.Content { + let view = ChoiceCardView( + with: style.choiceCard, + environment: .init( + data: environment.data, + uuid: environment.uuid, + gcd: environment.gcd, + imageViewCache: environment.imageViewCache, + uiScreen: environment.uiScreen + ) + ) + let choiceCard = ChoiceCard(with: message, isActive: isActive) + view.showsOperatorImage = showsImage + view.setOperatorImage(fromUrl: imageUrl, animated: false) + view.onOptionTapped = { self.choiceOptionSelected($0, message.id) } + view.appendContent(.choiceCard(choiceCard), animated: false) + return .choiceCard(view) } - func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { - guard - let item = itemForRow?(indexPath.row, indexPath.section), - let cell: ChatItemCell = tableView.dequeue(cellFor: indexPath) - else { return UITableViewCell() } - cell.content = content(for: item) - return cell + private func systemMessageContent(_ message: ChatMessage) -> ChatItemCell.Content { + let view = SystemMessageView( + with: style.systemMessage, + environment: .init( + uiScreen: environment.uiScreen + ) + ) + + view.appendContent( + .text( + message.content, + accessibility: Self.operatorAccessibilityMessage( + for: message, + operator: style.accessibility.operator, + isFontScalingEnabled: style.accessibility.isFontScalingEnabled + ) + ), + animated: false + ) + + return .systemMessage(view) } - func tableView(_ tableView: UITableView, willDisplay cell: UITableViewCell, forRowAt indexPath: IndexPath) { - guard - let cell = cell as? ChatItemCell, - case .customCard(let view) = cell.content - else { return } - view.willDisplayView?() + private func outgoingMessageContent(_ message: OutgoingMessage) -> ChatItemCell.Content { + let view = VisitorChatMessageView( + with: style.visitorMessage, + environment: .init(uiScreen: environment.uiScreen) + ) + view.appendContent( + .text( + message.content, + accessibility: Self.visitorAccessibilityOutgoingMessage( + for: message, + visitor: style.accessibility.visitor, + isFontScalingEnabled: style.accessibility.isFontScalingEnabled + ) + ), + animated: false + ) + view.appendContent( + .files( + message.files, + accessibility: .init(from: .visitor) + ), + animated: false + ) + view.fileTapped = { [weak self] in self?.fileTapped?($0) } + view.linkTapped = { [weak self] in self?.linkTapped?($0) } + return .outgoingMessage(view) } -} -// MARK: UITableViewDelegate + private func visitorMessageContent( + _ message: ChatMessage, + status: String? + ) -> ChatItemCell.Content { + let view = VisitorChatMessageView( + with: style.visitorMessage, + environment: .init(uiScreen: environment.uiScreen) + ) + view.appendContent( + .text( + message.content, + accessibility: Self.visitorAccessibilityMessage( + for: message, + visitor: style.accessibility.visitor, + isFontScalingEnabled: style.accessibility.isFontScalingEnabled + ) + ), + animated: false + ) + view.appendContent( + .downloads( + message.downloads, + accessibility: .init(from: .visitor) + ), + animated: false + ) + view.downloadTapped = { [weak self] in self?.downloadTapped?($0) } + view.linkTapped = { [weak self] in self?.linkTapped?($0) } + view.status = status -extension ChatView: UITableViewDelegate { - public func scrollViewWillBeginDragging(_ scrollView: UIScrollView) { - endEditing(true) + return .visitorMessage(view) } - func scrollViewDidScroll(_ scrollView: UIScrollView) { - chatScrolledToBottom?(isBottomReached(for: scrollView)) + private func customCardContent( + _ chatMessage: ChatMessage, + showsImage: Bool, + imageUrl: String?, + isActive: Bool + ) -> ChatItemCell.Content { + let message = MessageRenderer.Message(chatMessage: chatMessage) + // Response card should be shown by default even if option is selected. + let shouldShow = messageRenderer?.shouldShowCard(message) ?? true + // Response card is considered as noninteractable by default. + let isInteractable = messageRenderer?.isInteractable(message) ?? false + // Need to hide interactable response card if integrator returns `false` + // via shouldShowCard interface. + if !shouldShow, isInteractable { + return .none + } + + guard let contentView = messageRenderer?.render(message) else { + if chatMessage.cardType == .choiceCard { + return choiceCardMessageContent( + chatMessage, + showsImage: showsImage, + imageUrl: imageUrl, + isActive: isActive + ) + } + return operatorMessageContent( + chatMessage, + showsImage: showsImage, + imageUrl: imageUrl + ) + } + + let container = CustomCardContainerView() + if let webCardView = contentView as? WebMessageCardView { + webCardView.isUserInteractionEnabled = isActive + webCardView.delegate = self + webCardView.updateHeight(heightCache[chatMessage.id] ?? 0) + container.willDisplayView = webCardView.startLoading + } + + container.addContentView(contentView) + return .customCard(container) } - func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat { - guard let item = itemForRow?(indexPath.row, indexPath.section) else { return CGFloat.zero } - guard case .none = content(for: item) else { return UITableView.automaticDimension } - return CGFloat.zero + private func callUpgradeContent( + kind: ObservableValue, + duration: ObservableValue + ) -> ChatItemCell.Content { + let callStyle = callUpgradeStyle(for: kind.value) + let view = ChatCallUpgradeView( + with: callStyle, + duration: duration + ) + kind.addObserver(self) { [weak self] kind, _ in + guard let self = self else { return } + + view.style = self.callUpgradeStyle(for: kind) + self.refreshAll() + } + return .callUpgrade(view) } -} -// MARK: - Accessibility -extension ChatView { - static func operatorAccessibilityMessage( - for chatMessage: ChatMessage, - `operator`: String, - isFontScalingEnabled: Bool - ) -> ChatMessageContent.TextAccessibilityProperties { - .init( - label: chatMessage.operator?.name ?? `operator`, - value: chatMessage.content, - isFontScalingEnabled: isFontScalingEnabled + private func operatorConnectedContent( + name: String?, + imageUrl: String? + ) -> ChatItemCell.Content { + let connectView = ConnectView( + with: style.connect, + layout: .chat, + 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) } - static func visitorAccessibilityMessage( - for chatMessage: ChatMessage, - visitor: String, - isFontScalingEnabled: Bool - ) -> ChatMessageContent.TextAccessibilityProperties { - .init( - label: visitor, - value: chatMessage.content, - isFontScalingEnabled: isFontScalingEnabled + private func transferringContent() -> ChatItemCell.Content { + let connectView = ConnectView( + with: style.connect, + layout: .chat, + 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) + } + + private func unreadMessageDividerContent() -> ChatItemCell.Content { + let messageDivider = UnreadMessageDividerView(style: style.unreadMessageDivider) + return .unreadMessagesDivider(messageDivider) } - static func visitorAccessibilityOutgoingMessage( - for outgoingMessage: OutgoingMessage, - visitor: String, - isFontScalingEnabled: Bool - ) -> ChatMessageContent.TextAccessibilityProperties { - .init( - label: visitor, - value: outgoingMessage.content, - isFontScalingEnabled: isFontScalingEnabled + private func gvaResponseTextContent( + _ message: ChatMessage, + text: GvaResponseText, + showImage: Bool, + imageUrl: String? + ) -> ChatItemCell.Content { + let view = GvaResponseTextView( + with: style.operatorMessage, + environment: .init( + data: environment.data, + uuid: environment.uuid, + gcd: environment.gcd, + imageViewCache: environment.imageViewCache, + uiScreen: environment.uiScreen + ) + ) + view.appendContent( + .text( + text.content, + accessibility: Self.operatorAccessibilityMessage( + for: message, + operator: style.accessibility.operator, + isFontScalingEnabled: style.accessibility.isFontScalingEnabled + ) + ), + animated: false + ) + view.appendContent( + .downloads( + message.downloads, + accessibility: .init(from: .operator(message.operator?.name ?? style.accessibility.operator))), + animated: false ) + view.downloadTapped = { [weak self] in self?.downloadTapped?($0) } + view.linkTapped = { [weak self] in self?.linkTapped?($0) } + view.showsOperatorImage = showImage + view.setOperatorImage(fromUrl: imageUrl, animated: false) + return .gvaResponseText(view) } } diff --git a/GliaWidgets/Sources/View/Chat/Message/GvaResponseTextView.swift b/GliaWidgets/Sources/View/Chat/Message/GvaResponseTextView.swift new file mode 100644 index 000000000..e0254480c --- /dev/null +++ b/GliaWidgets/Sources/View/Chat/Message/GvaResponseTextView.swift @@ -0,0 +1,84 @@ +import UIKit + +final class GvaResponseTextView: ChatMessageView { + var showsOperatorImage: Bool = false { + didSet { + if showsOperatorImage { + guard operatorImageView == nil else { return } + let operatorImageView = UserImageView( + with: viewStyle.operatorImage, + environment: .init( + data: environment.data, + uuid: environment.uuid, + gcd: environment.gcd, + imageViewCache: environment.imageViewCache + ) + ) + self.operatorImageView = operatorImageView + operatorImageViewContainer.addSubview(operatorImageView) + operatorImageView.layoutInSuperview().activate() + } else { + operatorImageView?.removeFromSuperview() + operatorImageView = nil + } + } + } + + private let viewStyle: OperatorChatMessageStyle + private var operatorImageView: UserImageView? + private let operatorImageViewContainer = UIView().makeView() + private let imageViewInsets = UIEdgeInsets(top: 4, left: 8, bottom: 2, right: 60) + private let operatorImageViewSize: CGFloat = 28 + private let environment: Environment + + init( + with style: OperatorChatMessageStyle, + environment: Environment + ) { + viewStyle = style + self.environment = environment + super.init( + with: style, + contentAlignment: .left, + environment: .init(uiScreen: environment.uiScreen) + ) + setup() + layout() + } + + required init() { + fatalError("init() has not been implemented") + } + + func setOperatorImage(fromUrl url: String?, animated: Bool) { + operatorImageView?.setOperatorImage(fromUrl: url, animated: animated) + } + + private func layout() { + addSubview(operatorImageViewContainer) + operatorImageViewContainer.translatesAutoresizingMaskIntoConstraints = false + var constraints = [NSLayoutConstraint](); defer { constraints.activate() } + constraints += operatorImageViewContainer.match(value: operatorImageViewSize) + constraints += operatorImageViewContainer.leadingAnchor.constraint(equalTo: leadingAnchor, constant: imageViewInsets.left) + constraints += operatorImageViewContainer.bottomAnchor.constraint(equalTo: bottomAnchor) + constraints += operatorImageViewContainer.topAnchor.constraint(greaterThanOrEqualTo: topAnchor, constant: imageViewInsets.top) + + addSubview(contentViews) + contentViews.translatesAutoresizingMaskIntoConstraints = false + constraints += contentViews.leadingAnchor.constraint(equalTo: operatorImageViewContainer.trailingAnchor, constant: 8) + constraints += contentViews.topAnchor.constraint(equalTo: topAnchor, constant: imageViewInsets.top) + constraints += contentViews.trailingAnchor.constraint(lessThanOrEqualTo: trailingAnchor, constant: -imageViewInsets.right) + + constraints += contentViews.bottomAnchor.constraint(equalTo: bottomAnchor) + } +} + +extension GvaResponseTextView { + struct Environment { + var data: FoundationBased.Data + var uuid: () -> UUID + var gcd: GCD + var imageViewCache: ImageView.Cache + var uiScreen: UIKitBased.UIScreen + } +} diff --git a/GliaWidgets/Sources/ViewModel/Chat/Data/ChatItem.swift b/GliaWidgets/Sources/ViewModel/Chat/Data/ChatItem.swift index f9aebb1de..d065a70a7 100644 --- a/GliaWidgets/Sources/ViewModel/Chat/Data/ChatItem.swift +++ b/GliaWidgets/Sources/ViewModel/Chat/Data/ChatItem.swift @@ -52,7 +52,7 @@ class ChatItem { case let .gvaPersistenButton(button): kind = .gvaPersistentButton(message, persistenButton: button) case let .gvaResponseText(text): - kind = .gvaResponseText(message, responseText: text) + kind = .gvaResponseText(message, responseText: text, showImage: true, imageUrl: message.operator?.pictureUrl) case let .gvaQuickReply(button): kind = .gvaQuickReply(message, quickReply: button) case let .gvaGallery(gallery): @@ -84,7 +84,7 @@ extension ChatItem { case unreadMessageDivider case systemMessage(ChatMessage) case gvaPersistentButton(ChatMessage, persistenButton: GvaButton) - case gvaResponseText(ChatMessage, responseText: GvaResponseText) + case gvaResponseText(ChatMessage, responseText: GvaResponseText, showImage: Bool, imageUrl: String?) case gvaQuickReply(ChatMessage, quickReply: GvaButton) case gvaGallery(ChatMessage, gallery: GvaGallery) } diff --git a/GliaWidgets/Sources/ViewModel/Chat/Data/Gva.swift b/GliaWidgets/Sources/ViewModel/Chat/Data/Gva.swift index 6bad9f8fe..62c9254ac 100644 --- a/GliaWidgets/Sources/ViewModel/Chat/Data/Gva.swift +++ b/GliaWidgets/Sources/ViewModel/Chat/Data/Gva.swift @@ -13,10 +13,10 @@ struct GvaButton: Decodable, Equatable { struct GvaGallery: Decodable, Equatable { let type: GvaCardType - let gallaryCards: [GvaGallaryCard] + let galleryCards: [GvaGalleryCard] } -struct GvaGallaryCard: Decodable, Equatable { +struct GvaGalleryCard: Decodable, Equatable { let title: String let subtitle: String? let imageUrl: String? From 9fd216d39e7dfeac7c9cca841c739a7b9d536030 Mon Sep 17 00:00:00 2001 From: Rasmus Tauts Date: Wed, 12 Jul 2023 16:14:02 +0300 Subject: [PATCH 03/64] Handle HTML text in GVA messages This PR allows the HTML to be rendered in GVA messages. The conversion of string to attributed string happens in decoding for performance and proper layout reasons. --- GliaWidgets/Sources/View/Chat/ChatView.swift | 2 +- .../View/Chat/Message/ChatMessageView.swift | 12 ++++ .../Message/Content/ChatMessageContent.swift | 1 + .../Content/Text/ChatTextContentView.swift | 56 ++++++++++++++++--- .../Sources/ViewModel/Chat/Data/Gva.swift | 43 +++++++++++++- 5 files changed, 103 insertions(+), 11 deletions(-) diff --git a/GliaWidgets/Sources/View/Chat/ChatView.swift b/GliaWidgets/Sources/View/Chat/ChatView.swift index efa61cb8f..8f86dd25e 100644 --- a/GliaWidgets/Sources/View/Chat/ChatView.swift +++ b/GliaWidgets/Sources/View/Chat/ChatView.swift @@ -845,7 +845,7 @@ extension ChatView { ) ) view.appendContent( - .text( + .attributedText( text.content, accessibility: Self.operatorAccessibilityMessage( for: message, diff --git a/GliaWidgets/Sources/View/Chat/Message/ChatMessageView.swift b/GliaWidgets/Sources/View/Chat/Message/ChatMessageView.swift index fe3932591..feb79b21e 100644 --- a/GliaWidgets/Sources/View/Chat/Message/ChatMessageView.swift +++ b/GliaWidgets/Sources/View/Chat/Message/ChatMessageView.swift @@ -56,6 +56,18 @@ class ChatMessageView: BaseView { accessibilityProperties: accProperties ) appendContentViews(contentViews, animated: animated) + case let .attributedText(text, accProperties): + let contentView = ChatTextContentView( + with: style.text, + contentAlignment: contentAlignment + ) + contentView.attributedText = text + contentView.linkTapped = { [weak self] in self?.linkTapped?($0) } + contentView.accessibilityProperties = .init( + label: accProperties.label, + value: accProperties.value + ) + appendContentView(contentView, animated: animated) default: break } diff --git a/GliaWidgets/Sources/View/Chat/Message/Content/ChatMessageContent.swift b/GliaWidgets/Sources/View/Chat/Message/Content/ChatMessageContent.swift index 23cbc349f..5f78fceba 100644 --- a/GliaWidgets/Sources/View/Chat/Message/Content/ChatMessageContent.swift +++ b/GliaWidgets/Sources/View/Chat/Message/Content/ChatMessageContent.swift @@ -5,6 +5,7 @@ enum ChatMessageContent { case files([LocalFile], accessibility: ChatFileContentView.AccessibilityProperties) case downloads([FileDownload], accessibility: ChatFileContentView.AccessibilityProperties) case choiceCard(ChoiceCard) + case attributedText(NSAttributedString, accessibility: TextAccessibilityProperties) struct TextAccessibilityProperties { let label: String diff --git a/GliaWidgets/Sources/View/Chat/Message/Content/Text/ChatTextContentView.swift b/GliaWidgets/Sources/View/Chat/Message/Content/Text/ChatTextContentView.swift index 7f2f03538..2d9360aaf 100644 --- a/GliaWidgets/Sources/View/Chat/Message/Content/Text/ChatTextContentView.swift +++ b/GliaWidgets/Sources/View/Chat/Message/Content/Text/ChatTextContentView.swift @@ -6,6 +6,11 @@ class ChatTextContentView: BaseView { set { setText(newValue) } } + var attributedText: NSAttributedString? { + get { return textView.attributedText } + set { return setAttributedText(newValue) } + } + var accessibilityProperties: AccessibilityProperties { get { .init( @@ -89,19 +94,52 @@ class ChatTextContentView: BaseView { } private func setText(_ text: String?) { - if text == nil || text?.isEmpty == true { + guard let text, !text.isEmpty else { textView.removeFromSuperview() - } else { - if textView.superview == nil { - contentView.addSubview(textView) - textView.translatesAutoresizingMaskIntoConstraints = false - var constraints = [NSLayoutConstraint](); defer { constraints.activate() } - constraints += textView.layoutInSuperview(insets: kTextInsets) - } - textView.text = text + return + } + + if textView.superview == nil { + contentView.addSubview(textView) + textView.translatesAutoresizingMaskIntoConstraints = false + var constraints = [NSLayoutConstraint](); defer { constraints.activate() } + constraints += textView.layoutInSuperview(insets: kTextInsets) } + textView.text = text + textView.accessibilityIdentifier = text } + + private func setAttributedText(_ text: NSAttributedString?) { + guard let text, !text.string.isEmpty else { + textView.removeFromSuperview() + return + } + + if textView.superview == nil { + contentView.addSubview(textView) + textView.translatesAutoresizingMaskIntoConstraints = false + var constraints = [NSLayoutConstraint](); defer { constraints.activate() } + constraints += textView.layoutInSuperview(insets: kTextInsets) + } + + let attributes: [NSAttributedString.Key: Any] = [ + .font: style.textFont, + .foregroundColor: style.textColor + ] + + let attributedText = NSMutableAttributedString(attributedString: text) + attributedText.addAttributes( + attributes, + range: NSRange( + location: 0, + length: attributedText.length + ) + ) + + textView.attributedText = attributedText + textView.accessibilityIdentifier = text.string + } } extension ChatTextContentView: UITextViewDelegate { diff --git a/GliaWidgets/Sources/ViewModel/Chat/Data/Gva.swift b/GliaWidgets/Sources/ViewModel/Chat/Data/Gva.swift index 62c9254ac..ad96c9c57 100644 --- a/GliaWidgets/Sources/ViewModel/Chat/Data/Gva.swift +++ b/GliaWidgets/Sources/ViewModel/Chat/Data/Gva.swift @@ -2,7 +2,19 @@ import Foundation struct GvaResponseText: Decodable, Equatable { let type: GvaCardType - let content: String + let content: NSAttributedString + + enum CodingKeys: String, CodingKey { + case type, content + } + + init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + type = try container.decode(GvaCardType.self, forKey: .type) + let contentString = try container.decode(String.self, forKey: .content) + let modifiedString = contentString.replacingOccurrences(of: "\n", with: "
") + content = modifiedString.htmlToAttributedString ?? NSAttributedString(string: "") + } } struct GvaButton: Decodable, Equatable { @@ -49,3 +61,32 @@ enum GvaCardType: String, Decodable { case plainText case galleryCards } + +private extension StringProtocol { + var htmlToAttributedString: NSAttributedString? { + Data(utf8).htmlToAttributedString + } + var htmlToString: String { + htmlToAttributedString?.string ?? "" + } +} + +private extension Data { + var htmlToAttributedString: NSAttributedString? { + do { + let options: [NSAttributedString.DocumentReadingOptionKey: Any] = [ + .documentType: NSAttributedString.DocumentType.html, + .characterEncoding: String.Encoding.utf8.rawValue + ] + return try NSAttributedString( + data: self, + options: options, + documentAttributes: nil + ) + } catch { + debugPrint("HTML-string decoding failed with error:", error) + return nil + } + } + var htmlToString: String { htmlToAttributedString?.string ?? "" } +} From 9bb0a03926e4655468307751a8008a797bdd20c5 Mon Sep 17 00:00:00 2001 From: Egor Egorov Date: Fri, 14 Jul 2023 18:15:24 +0300 Subject: [PATCH 04/64] Added actions for GVA buttons Added unit tests for GVA button actions MOB-2378 --- GliaWidgets.xcodeproj/project.pbxproj | 30 ++++++- .../ViewModel/Chat/ChatViewModel+GVA.swift | 80 +++++++++++++++++++ .../ViewModel/Chat/ChatViewModel.swift | 2 +- .../ChatViewModelTests+Gva.swift | 77 ++++++++++++++++++ .../ChatViewModelTests.swift | 0 .../ChatViewModel/Mocks/GvaOption.Mock.swift | 20 +++++ 6 files changed, 207 insertions(+), 2 deletions(-) create mode 100644 GliaWidgets/Sources/ViewModel/Chat/ChatViewModel+GVA.swift create mode 100644 GliaWidgetsTests/Sources/ChatViewModel/ChatViewModelTests+Gva.swift rename GliaWidgetsTests/Sources/{ => ChatViewModel}/ChatViewModelTests.swift (100%) create mode 100644 GliaWidgetsTests/Sources/ChatViewModel/Mocks/GvaOption.Mock.swift diff --git a/GliaWidgets.xcodeproj/project.pbxproj b/GliaWidgets.xcodeproj/project.pbxproj index ca4abbbf4..2cc2cadcf 100644 --- a/GliaWidgets.xcodeproj/project.pbxproj +++ b/GliaWidgets.xcodeproj/project.pbxproj @@ -350,6 +350,9 @@ 846429802A45A1F200943BD6 /* OfferPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8464297F2A45A1F200943BD6 /* OfferPresenter.swift */; }; 846429832A45DA7500943BD6 /* AlertViewController+Mock.swift in Sources */ = {isa = PBXBuildFile; fileRef = 846429822A45DA7500943BD6 /* AlertViewController+Mock.swift */; }; 846429862A45DB4100943BD6 /* AlertViewController.Kind+Mock.swift in Sources */ = {isa = PBXBuildFile; fileRef = 846429852A45DB4100943BD6 /* AlertViewController.Kind+Mock.swift */; }; + 84681A8E2A5ED76300DD7406 /* ChatViewModel+GVA.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84681A8D2A5ED76300DD7406 /* ChatViewModel+GVA.swift */; }; + 84681A952A61844000DD7406 /* ChatViewModelTests+Gva.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84681A942A61844000DD7406 /* ChatViewModelTests+Gva.swift */; }; + 84681A982A61853300DD7406 /* GvaOption.Mock.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84681A972A61853300DD7406 /* GvaOption.Mock.swift */; }; 846A5C3429CB3A130049B29F /* ScreenShareHandler.Implementation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 846A5C3329CB3A130049B29F /* ScreenShareHandler.Implementation.swift */; }; 846A5C3629CB3E270049B29F /* ScreenShareHandler.Mock.swift in Sources */ = {isa = PBXBuildFile; fileRef = 846A5C3529CB3E270049B29F /* ScreenShareHandler.Mock.swift */; }; 846A5C3929D18D400049B29F /* ScreenShareHandlerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 846A5C3829D18D400049B29F /* ScreenShareHandlerTests.swift */; }; @@ -999,6 +1002,9 @@ 8464297F2A45A1F200943BD6 /* OfferPresenter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OfferPresenter.swift; sourceTree = ""; }; 846429822A45DA7500943BD6 /* AlertViewController+Mock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "AlertViewController+Mock.swift"; sourceTree = ""; }; 846429852A45DB4100943BD6 /* AlertViewController.Kind+Mock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "AlertViewController.Kind+Mock.swift"; sourceTree = ""; }; + 84681A8D2A5ED76300DD7406 /* ChatViewModel+GVA.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ChatViewModel+GVA.swift"; sourceTree = ""; }; + 84681A942A61844000DD7406 /* ChatViewModelTests+Gva.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ChatViewModelTests+Gva.swift"; sourceTree = ""; }; + 84681A972A61853300DD7406 /* GvaOption.Mock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GvaOption.Mock.swift; sourceTree = ""; }; 846A5C3329CB3A130049B29F /* ScreenShareHandler.Implementation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ScreenShareHandler.Implementation.swift; sourceTree = ""; }; 846A5C3529CB3E270049B29F /* ScreenShareHandler.Mock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ScreenShareHandler.Mock.swift; sourceTree = ""; }; 846A5C3829D18D400049B29F /* ScreenShareHandlerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ScreenShareHandlerTests.swift; sourceTree = ""; }; @@ -1987,6 +1993,7 @@ 9AB196DF27C401F600FD60AB /* ChatViewModel.Mock.swift */, 9AB196D527C3E1E400FD60AB /* ChatViewModel.Environment.Interface.swift */, 9AB196D727C3E27300FD60AB /* ChatViewModel.Environment.Mock.swift */, + 84681A8D2A5ED76300DD7406 /* ChatViewModel+GVA.swift */, ); path = Chat; sourceTree = ""; @@ -2450,11 +2457,11 @@ 7512A57827BF9FB800319DF1 /* Sources */ = { isa = PBXGroup; children = ( + 84681A932A61840700DD7406 /* ChatViewModel */, 846429812A45DA5900943BD6 /* AlertViewController */, 846A5C4329F6BEB60049B29F /* Glia */, 7512A57627BE8A6700319DF1 /* InteractorTests.swift */, AF29811429E6D76A0005BD55 /* FileDownloadTests.swift */, - 7512A57927BF9FCD00319DF1 /* ChatViewModelTests.swift */, EB9ADB50280EBD4E00FAE8A4 /* InteractorStateTests.swift */, EB27E71C27FEBB620090B895 /* CallViewModelTests.swift */, EB03B00D27FFF6DD0058F6B1 /* CallViewTests.swift */, @@ -2943,6 +2950,24 @@ path = Mocks; sourceTree = ""; }; + 84681A932A61840700DD7406 /* ChatViewModel */ = { + isa = PBXGroup; + children = ( + 84681A962A61851700DD7406 /* Mocks */, + 7512A57927BF9FCD00319DF1 /* ChatViewModelTests.swift */, + 84681A942A61844000DD7406 /* ChatViewModelTests+Gva.swift */, + ); + path = ChatViewModel; + sourceTree = ""; + }; + 84681A962A61851700DD7406 /* Mocks */ = { + isa = PBXGroup; + children = ( + 84681A972A61853300DD7406 /* GvaOption.Mock.swift */, + ); + path = Mocks; + sourceTree = ""; + }; 846A5C3729D18D220049B29F /* ScreenShareHandler */ = { isa = PBXGroup; children = ( @@ -3985,6 +4010,7 @@ 845E2F72283D068000C04D56 /* HeaderStyle.Accessibility.swift in Sources */, 1A1E30CB25F9FDC400850E68 /* ChatImageFileContentStyle.swift in Sources */, 75C48498299AC71F003EC223 /* Header+Playbook.swift in Sources */, + 84681A8E2A5ED76300DD7406 /* ChatViewModel+GVA.swift in Sources */, 1A60AF93256674F900E53F53 /* Color.swift in Sources */, 1A38A8BA258B94D60089DE7B /* ImageView.swift in Sources */, C0857DE528D470C1008D171D /* Theme+Shadow.swift in Sources */, @@ -4306,6 +4332,7 @@ files = ( C03A8049292BC8DB00DDECA6 /* CallViewControllerTests.swift in Sources */, 84D5B9662A15204400807F92 /* QuickLookBased.Failing.swift in Sources */, + 84681A982A61853300DD7406 /* GvaOption.Mock.swift in Sources */, AF6AB34B2989517100003645 /* FileUploader.Failing.swift in Sources */, EB03B00E27FFF6DD0058F6B1 /* CallViewTests.swift in Sources */, 3197F7AD29E6A5C8008EE9F7 /* SecureConversations.FileUploadListView.Mock.swift in Sources */, @@ -4347,6 +4374,7 @@ EB9ADB51280EBD4E00FAE8A4 /* InteractorStateTests.swift in Sources */, EB7A1508286D98000035AC62 /* FileUploader.Environment.Failing.swift in Sources */, AF2355A229C9EC7E007D9896 /* IdCollectionTests.swift in Sources */, + 84681A952A61844000DD7406 /* ChatViewModelTests+Gva.swift in Sources */, 847A7643285A1914004044D1 /* FileUploadListViewModelTests.swift in Sources */, 9A1992E727D66C7400161AAE /* UIKitBased.Failing.swift in Sources */, 846A5C3929D18D400049B29F /* ScreenShareHandlerTests.swift in Sources */, diff --git a/GliaWidgets/Sources/ViewModel/Chat/ChatViewModel+GVA.swift b/GliaWidgets/Sources/ViewModel/Chat/ChatViewModel+GVA.swift new file mode 100644 index 000000000..93b132935 --- /dev/null +++ b/GliaWidgets/Sources/ViewModel/Chat/ChatViewModel+GVA.swift @@ -0,0 +1,80 @@ +import Foundation +import GliaCoreSDK + +private extension String { + static let gvaOptionUrlTargetModal = "modal" + static let gvaOptionUrlTargetSelf = "self" +} + +extension ChatViewModel { + func gvaOptionAction(for option: GvaOption) -> Cmd { + // If option contains `url`, then it's URL Button, + // otherwise it's Postback Button and option should be sent + // to the server as `SingleChoiceOption` + if let urlString = option.url, + let url = URL(string: urlString) { + return urlButtonAction(url: url, urlTarget: option.urlTarget) + } else { + return postbackButtonAction(for: option) + } + } +} + +private extension ChatViewModel { + func urlButtonAction(url: URL, urlTarget: String?) -> Cmd { + .init { [weak self] in + guard let self = self else { return } + + let openUrl = { [weak self] url in + guard let self = self else { return } + guard self.environment.uiApplication.canOpenURL(url) else { return } + self.environment.uiApplication.open(url) + } + + switch url.scheme?.lowercased() { + case URLScheme.tel.rawValue, + URLScheme.mailto.rawValue, + URLScheme.http.rawValue, + URLScheme.https.rawValue: + // "tel" ,"mailto" and "http(s)"-based links should be opened by UIApplication + openUrl(url) + + default: + if urlTarget == .gvaOptionUrlTargetModal || + urlTarget == .gvaOptionUrlTargetSelf { + // if GvaOption.urlTarget is "modal" or "self", then button url is deeplink + // and should be opened by UIApplication, to provide integrator + // an ability to handle deeplinks they configured. + openUrl(url) + } else { + return + } + } + } + } + + func postbackButtonAction(for option: GvaOption) -> Cmd { + .init { [weak self] in + guard let value = option.value else { return } + let singleChoiceOption = SingleChoiceOption(text: option.text, value: value) + self?.environment.sendSelectedOptionValue(singleChoiceOption) { [weak self] result in + guard let self = self else { return } + switch result { + case let .success(message): + let chatMessage = ChatMessage(with: message) + if let item = ChatItem( + with: chatMessage, + isCustomCardSupported: self.isCustomCardSupported + ) { + self.appendItem(item, to: self.messagesSection, animated: true) + } + case .failure: + self.showAlert( + with: self.alertConfiguration.unexpectedError, + dismissed: nil + ) + } + } + } + } +} diff --git a/GliaWidgets/Sources/ViewModel/Chat/ChatViewModel.swift b/GliaWidgets/Sources/ViewModel/Chat/ChatViewModel.swift index 96756d75d..0493a421e 100644 --- a/GliaWidgets/Sources/ViewModel/Chat/ChatViewModel.swift +++ b/GliaWidgets/Sources/ViewModel/Chat/ChatViewModel.swift @@ -29,7 +29,7 @@ class ChatViewModel: EngagementViewModel, ViewModel { private var unreadMessages: UnreadMessagesHandler! private let isChatScrolledToBottom = ObservableValue(with: true) private let showsCallBubble: Bool - private let isCustomCardSupported: Bool + let isCustomCardSupported: Bool let fileUploadListModel: SecureConversations.FileUploadListViewModel private let downloader: FileDownloader diff --git a/GliaWidgetsTests/Sources/ChatViewModel/ChatViewModelTests+Gva.swift b/GliaWidgetsTests/Sources/ChatViewModel/ChatViewModelTests+Gva.swift new file mode 100644 index 000000000..4c2fe2383 --- /dev/null +++ b/GliaWidgetsTests/Sources/ChatViewModel/ChatViewModelTests+Gva.swift @@ -0,0 +1,77 @@ +@testable import GliaWidgets +import XCTest + +extension ChatViewModelTests { + func test_gvaLinkButtonAction() { + let options: [GvaOption] = [ + .mock(url: "http://mock.mock"), + .mock(url: "https://mock.mock"), + .mock(url: "mock://mock.self", urlTarget: "self"), + .mock(url: "mock://mock.modal", urlTarget: "modal"), + .mock(url: "mock://mock", urlTarget: "mock"), + .mock(url: "mailto:mock@mock.mock"), + .mock(url: "tel:12345678") + ] + var calls: [Call] = [] + var env = ChatViewModel.Environment.mock + env.uiApplication.canOpenURL = { _ in true } + env.uiApplication.open = { url in + calls.append(.openUrl(url.absoluteString)) + } + // To ensure `sendSelectedOptionValue` is not called in case of URL Button + env.sendSelectedOptionValue = { option, _ in + calls.append(.sendOption(option.text, option.value)) + } + viewModel = .mock(environment: env) + + options.forEach { + viewModel.gvaOptionAction(for: $0)() + } + + let expectedResult: [Call] = [ + .openUrl("http://mock.mock"), + .openUrl("https://mock.mock"), + .openUrl("mock://mock.self"), + .openUrl("mock://mock.modal"), + .openUrl("mailto:mock@mock.mock"), + .openUrl("tel:12345678") + ] + XCTAssertEqual(calls, expectedResult) + } + + func test_gvaPostbackButtonAction() { + let option = GvaOption.mock(text: "text", value: "value") + var calls: [Call] = [] + var env = ChatViewModel.Environment.mock + // To ensure `open` is not called in case of URL Button + env.uiApplication.open = { url in + calls.append(.openUrl(url.absoluteString)) + } + env.sendSelectedOptionValue = { option, _ in + calls.append(.sendOption(option.text, option.value)) + } + viewModel = .mock(environment: env) + + viewModel.gvaOptionAction(for: option)() + + XCTAssertEqual(calls, [.sendOption("text", "value")]) + } +} + +private extension ChatViewModelTests { + enum Call: Equatable { + case openUrl(String?) + case sendOption(String?, String?) + + static func == (lhs: Call, rhs: Call) -> Bool { + switch (lhs, rhs) { + case let (.openUrl(lhsUrl), openUrl(rhsUrl)): + return lhsUrl == rhsUrl + case let (.sendOption(lhsText, lhsValue), .sendOption(rhsText, rhsValue)): + return lhsText == rhsText && lhsValue == rhsValue + default: + return false + } + } + } +} diff --git a/GliaWidgetsTests/Sources/ChatViewModelTests.swift b/GliaWidgetsTests/Sources/ChatViewModel/ChatViewModelTests.swift similarity index 100% rename from GliaWidgetsTests/Sources/ChatViewModelTests.swift rename to GliaWidgetsTests/Sources/ChatViewModel/ChatViewModelTests.swift diff --git a/GliaWidgetsTests/Sources/ChatViewModel/Mocks/GvaOption.Mock.swift b/GliaWidgetsTests/Sources/ChatViewModel/Mocks/GvaOption.Mock.swift new file mode 100644 index 000000000..c838dc1e9 --- /dev/null +++ b/GliaWidgetsTests/Sources/ChatViewModel/Mocks/GvaOption.Mock.swift @@ -0,0 +1,20 @@ +import Foundation +@testable import GliaWidgets + +extension GvaOption { + static func mock( + text: String = "", + value: String? = nil, + url: String? = nil, + urlTarget: String? = nil, + destinationPdBroadcastEvent: String? = nil + ) -> GvaOption { + return .init( + text: text, + value: value, + url: url, + urlTarget: urlTarget, + destinationPdBroadcastEvent: destinationPdBroadcastEvent + ) + } +} From 08c529fd6ed27bbfdfd4fee0ccae83d4e45f956e Mon Sep 17 00:00:00 2001 From: Egor Egorov Date: Mon, 17 Jul 2023 14:52:59 +0300 Subject: [PATCH 05/64] Unsupported broadcast events alert action MOB-2388 --- GliaWidgets/L10n.swift | 2 ++ .../Resources/en.lproj/Localizable.strings | 2 ++ .../Theme/Alert/AlertConfiguration.Mock.swift | 3 ++- .../Theme/Alert/AlertConfiguration.swift | 8 +++++++- .../Theme/Theme+AlertConfiguration.swift | 8 +++++++- .../ViewModel/Chat/ChatViewModel+GVA.swift | 17 ++++++++++++++++- .../ChatViewModel/ChatViewModelTests+Gva.swift | 17 +++++++++++++++++ 7 files changed, 53 insertions(+), 4 deletions(-) diff --git a/GliaWidgets/L10n.swift b/GliaWidgets/L10n.swift index 539fe7127..0bdd5da9b 100644 --- a/GliaWidgets/L10n.swift +++ b/GliaWidgets/L10n.swift @@ -10,6 +10,8 @@ import Foundation // swiftlint:disable explicit_type_interface function_parameter_count identifier_name line_length // swiftlint:disable nesting type_body_length type_name vertical_whitespace_opening_braces public enum L10n { + /// This action is not currently supported on mobile + public static let gvaNotSupported = L10n.tr("Localizable", "gva_not_supported", fallback: "This action is not currently supported on mobile") /// Operator public static let `operator` = L10n.tr("Localizable", "operator", fallback: "Operator") /// Powered by diff --git a/GliaWidgets/Resources/en.lproj/Localizable.strings b/GliaWidgets/Resources/en.lproj/Localizable.strings index 6a1a6c89e..9156c4daf 100644 --- a/GliaWidgets/Resources/en.lproj/Localizable.strings +++ b/GliaWidgets/Resources/en.lproj/Localizable.strings @@ -263,3 +263,5 @@ "callVisualizer.screenSharing.accessibility.messageHint" = "Message label"; "callVisualizer.screenSharing.accessibility.buttonLabel" = "End screen sharing"; "callVisualizer.screenSharing.accessibility.buttonHint" = "Ends screen sharing"; + +"gva_not_supported" = "This action is not currently supported on mobile"; diff --git a/GliaWidgets/Sources/Theme/Alert/AlertConfiguration.Mock.swift b/GliaWidgets/Sources/Theme/Alert/AlertConfiguration.Mock.swift index 068d48ea4..97c29d64e 100644 --- a/GliaWidgets/Sources/Theme/Alert/AlertConfiguration.Mock.swift +++ b/GliaWidgets/Sources/Theme/Alert/AlertConfiguration.Mock.swift @@ -19,7 +19,8 @@ extension AlertConfiguration { unexpectedError: .mock(), apiError: .mock(), unavailableMessageCenter: .mock(), - unavailableMessageCenterForBeingUnauthenticated: .mock() + unavailableMessageCenterForBeingUnauthenticated: .mock(), + unsupportedGvaBroadcastError: .mock() ) } } diff --git a/GliaWidgets/Sources/Theme/Alert/AlertConfiguration.swift b/GliaWidgets/Sources/Theme/Alert/AlertConfiguration.swift index 1de31fdbe..726ad06fb 100644 --- a/GliaWidgets/Sources/Theme/Alert/AlertConfiguration.swift +++ b/GliaWidgets/Sources/Theme/Alert/AlertConfiguration.swift @@ -51,6 +51,9 @@ public struct AlertConfiguration { /// Configuration of the unavailable message center error alert due to unautheticated visitor. public var unavailableMessageCenterForBeingUnauthenticated: MessageAlertConfiguration + /// Configuration of the unsupported GVA broadcast events error alert. + public var unsupportedGvaBroadcastError: MessageAlertConfiguration + /// /// - Parameters: /// - leaveQueue: Configuration of the queue leaving confirmation alert. @@ -70,6 +73,7 @@ public struct AlertConfiguration { /// - apiError: Configuration of the API error alert. /// - unavailableMessageCenter: Configuration of the unavailable message center error alert. /// - unavailableMessageCenterForBeingUnauthenticated: Configuration of the unavailable message center error alert due to unautheticated visitor. + /// - unsupportedGvaBroadcastError: Configuration of the unsupported GVA broadcast events error alert. /// public init( leaveQueue: ConfirmationAlertConfiguration, @@ -88,7 +92,8 @@ public struct AlertConfiguration { unexpectedError: MessageAlertConfiguration, apiError: MessageAlertConfiguration, unavailableMessageCenter: MessageAlertConfiguration, - unavailableMessageCenterForBeingUnauthenticated: MessageAlertConfiguration + unavailableMessageCenterForBeingUnauthenticated: MessageAlertConfiguration, + unsupportedGvaBroadcastError: MessageAlertConfiguration ) { self.leaveQueue = leaveQueue self.endEngagement = endEngagement @@ -107,5 +112,6 @@ public struct AlertConfiguration { self.apiError = apiError self.unavailableMessageCenter = unavailableMessageCenter self.unavailableMessageCenterForBeingUnauthenticated = unavailableMessageCenterForBeingUnauthenticated + self.unsupportedGvaBroadcastError = unsupportedGvaBroadcastError } } diff --git a/GliaWidgets/Sources/Theme/Theme+AlertConfiguration.swift b/GliaWidgets/Sources/Theme/Theme+AlertConfiguration.swift index a50b7fca3..1dc57758a 100644 --- a/GliaWidgets/Sources/Theme/Theme+AlertConfiguration.swift +++ b/GliaWidgets/Sources/Theme/Theme+AlertConfiguration.swift @@ -130,6 +130,11 @@ extension Theme { shouldShowCloseButton: false ) + let unsupportedGvaBroadcastError = MessageAlertConfiguration( + title: L10n.gvaNotSupported, + message: nil + ) + return AlertConfiguration( leaveQueue: leaveQueue, endEngagement: endEngagement, @@ -147,7 +152,8 @@ extension Theme { unexpectedError: unexpected, apiError: api, unavailableMessageCenter: unavailableMessageCenter, - unavailableMessageCenterForBeingUnauthenticated: unavailableMessageCenterForBeingUnauthenticated + unavailableMessageCenterForBeingUnauthenticated: unavailableMessageCenterForBeingUnauthenticated, + unsupportedGvaBroadcastError: unsupportedGvaBroadcastError ) } } diff --git a/GliaWidgets/Sources/ViewModel/Chat/ChatViewModel+GVA.swift b/GliaWidgets/Sources/ViewModel/Chat/ChatViewModel+GVA.swift index 93b132935..068d1f9ec 100644 --- a/GliaWidgets/Sources/ViewModel/Chat/ChatViewModel+GVA.swift +++ b/GliaWidgets/Sources/ViewModel/Chat/ChatViewModel+GVA.swift @@ -8,10 +8,15 @@ private extension String { extension ChatViewModel { func gvaOptionAction(for option: GvaOption) -> Cmd { + // If `option.destinationPdBroadcastEvent` is specified, + // this is broadcast event button, which is not supported + // on mobile. So an alert should be shown. // If option contains `url`, then it's URL Button, // otherwise it's Postback Button and option should be sent // to the server as `SingleChoiceOption` - if let urlString = option.url, + if option.destinationPdBroadcastEvent != nil { + return broadcastEventButtonAction() + } else if let urlString = option.url, let url = URL(string: urlString) { return urlButtonAction(url: url, urlTarget: option.urlTarget) } else { @@ -77,4 +82,14 @@ private extension ChatViewModel { } } } + + func broadcastEventButtonAction() -> Cmd { + .init { [weak self] in + guard let self = self else { return } + self.showAlert( + with: self.alertConfiguration.unsupportedGvaBroadcastError, + dismissed: nil + ) + } + } } diff --git a/GliaWidgetsTests/Sources/ChatViewModel/ChatViewModelTests+Gva.swift b/GliaWidgetsTests/Sources/ChatViewModel/ChatViewModelTests+Gva.swift index 4c2fe2383..5e62c90c3 100644 --- a/GliaWidgetsTests/Sources/ChatViewModel/ChatViewModelTests+Gva.swift +++ b/GliaWidgetsTests/Sources/ChatViewModel/ChatViewModelTests+Gva.swift @@ -56,12 +56,27 @@ extension ChatViewModelTests { XCTAssertEqual(calls, [.sendOption("text", "value")]) } + + func test_broadcastEventAction() { + let option = GvaOption.mock(text: "text", destinationPdBroadcastEvent: "mock") + var calls: [Call] = [] + viewModel = .mock(environment: .mock) + viewModel.engagementAction = { action in + guard case .showAlert = action else { return } + calls.append(.showAlert) + } + + viewModel.gvaOptionAction(for: option)() + + XCTAssertEqual(calls, [.showAlert]) + } } private extension ChatViewModelTests { enum Call: Equatable { case openUrl(String?) case sendOption(String?, String?) + case showAlert static func == (lhs: Call, rhs: Call) -> Bool { switch (lhs, rhs) { @@ -69,6 +84,8 @@ private extension ChatViewModelTests { return lhsUrl == rhsUrl case let (.sendOption(lhsText, lhsValue), .sendOption(rhsText, rhsValue)): return lhsText == rhsText && lhsValue == rhsValue + case (.showAlert, .showAlert): + return true default: return false } From 54fd772aa48124fbc75d17bffef3fc95c55285a4 Mon Sep 17 00:00:00 2001 From: Rasmus Tauts Date: Tue, 18 Jul 2023 14:14:37 +0300 Subject: [PATCH 06/64] Add GVA Persistent button UI This PR adds GVA Persistent Button UI and connects the buttons to corresponding actions. Some paddings were also changed due to new requirements. This PR also fixes GVA button decoding to support NSAttributedString, and Gallery Card decoding (unrelated to the ticket). MOB-2370 --- GliaWidgets.xcodeproj/project.pbxproj | 8 ++ ...onversations.ChatWithTranscriptModel.swift | 3 + GliaWidgets/Sources/View/Chat/ChatView.swift | 54 ++++++++-- .../Message/Content/ChatMessageContent.swift | 1 + .../Content/Text/ChatTextContentStyle.swift | 2 +- .../Message/GVAPersistentButtonView.swift | 102 ++++++++++++++++++ .../GvaPersistentButtonOptionView.swift | 70 ++++++++++++ .../Message/OperatorChatMessageView.swift | 2 +- .../Chat/ChatViewController.swift | 4 + .../ViewModel/Chat/ChatViewModel.swift | 3 + .../ViewModel/Chat/Data/ChatItem.swift | 4 +- .../Sources/ViewModel/Chat/Data/Gva.swift | 17 ++- 12 files changed, 255 insertions(+), 15 deletions(-) create mode 100644 GliaWidgets/Sources/View/Chat/Message/GVAPersistentButtonView.swift create mode 100644 GliaWidgets/Sources/View/Chat/Message/GvaPersistentButtonOptionView.swift diff --git a/GliaWidgets.xcodeproj/project.pbxproj b/GliaWidgets.xcodeproj/project.pbxproj index 2cc2cadcf..af63aed45 100644 --- a/GliaWidgets.xcodeproj/project.pbxproj +++ b/GliaWidgets.xcodeproj/project.pbxproj @@ -518,6 +518,8 @@ C0175A172A5D30D7001FACDE /* GvaResponseTextView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0175A162A5D30D7001FACDE /* GvaResponseTextView.swift */; }; C0175A192A5D3C56001FACDE /* ChatView.Accessibility.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0175A182A5D3C56001FACDE /* ChatView.Accessibility.swift */; }; C0175A1D2A5D4226001FACDE /* ChatView.TableView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0175A1C2A5D4226001FACDE /* ChatView.TableView.swift */; }; + C0175A232A65614E001FACDE /* GVAPersistentButtonView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0175A222A65614E001FACDE /* GVAPersistentButtonView.swift */; }; + C0175A252A66A431001FACDE /* GvaPersistentButtonOptionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0175A242A66A431001FACDE /* GvaPersistentButtonOptionView.swift */; }; C03A8047292BA76D00DDECA6 /* ChatViewControllerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = C03A8046292BA76D00DDECA6 /* ChatViewControllerTests.swift */; }; C03A8049292BC8DB00DDECA6 /* CallViewControllerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = C03A8048292BC8DB00DDECA6 /* CallViewControllerTests.swift */; }; C05AB01C295F416700AA381F /* VisitorCodeCloseButtonProperties.swift in Sources */ = {isa = PBXBuildFile; fileRef = C05AB01B295F416700AA381F /* VisitorCodeCloseButtonProperties.swift */; }; @@ -1173,6 +1175,8 @@ C0175A162A5D30D7001FACDE /* GvaResponseTextView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GvaResponseTextView.swift; sourceTree = ""; }; C0175A182A5D3C56001FACDE /* ChatView.Accessibility.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatView.Accessibility.swift; sourceTree = ""; }; C0175A1C2A5D4226001FACDE /* ChatView.TableView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatView.TableView.swift; sourceTree = ""; }; + C0175A222A65614E001FACDE /* GVAPersistentButtonView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GVAPersistentButtonView.swift; sourceTree = ""; }; + C0175A242A66A431001FACDE /* GvaPersistentButtonOptionView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GvaPersistentButtonOptionView.swift; sourceTree = ""; }; C03A8046292BA76D00DDECA6 /* ChatViewControllerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatViewControllerTests.swift; sourceTree = ""; }; C03A8048292BC8DB00DDECA6 /* CallViewControllerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CallViewControllerTests.swift; sourceTree = ""; }; C05AB016295DA9FC00AA381F /* AlertViewController+VisitorCode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "AlertViewController+VisitorCode.swift"; sourceTree = ""; }; @@ -2121,6 +2125,8 @@ 3197F7AE29E95527008EE9F7 /* SystemMessageView.swift */, 3197F7B029E958F4008EE9F7 /* SystemMessageStyle.swift */, C0175A162A5D30D7001FACDE /* GvaResponseTextView.swift */, + C0175A222A65614E001FACDE /* GVAPersistentButtonView.swift */, + C0175A242A66A431001FACDE /* GvaPersistentButtonOptionView.swift */, ); path = Message; sourceTree = ""; @@ -4112,6 +4118,7 @@ AF3D520D2983B3DD00AD8E69 /* FileUploader.Environment.Interface.swift in Sources */, 1ABD6C5925B5758000D56EFA /* BubbleStyle.swift in Sources */, C0D2F08B29A4E95700803B47 /* ConnectView.Mock.swift in Sources */, + C0175A252A66A431001FACDE /* GvaPersistentButtonOptionView.swift in Sources */, AF10ED9129BF85C700E85309 /* UnreadMessageDividerStyle.swift in Sources */, 75AF8CF127DBB1F9009EEE2C /* SurveyViewController.View.swift in Sources */, 9AA64E142811B91C00FA56FF /* FontScaling.swift in Sources */, @@ -4138,6 +4145,7 @@ 1AC7A7552582594200567FF8 /* Configuration.swift in Sources */, 75940988298D38C2008B173A /* VisitorCodeCoordinator.swift in Sources */, 1A4674B225E907120078FA1C /* AttachmentSourceItemStyle.swift in Sources */, + C0175A232A65614E001FACDE /* GVAPersistentButtonView.swift in Sources */, 315BAB1A29ADFEBC00FF284B /* ConfirmationStyle+TitleStyle.swift in Sources */, 1A0C143625B85C3300B00695 /* CallView.swift in Sources */, 1AA738A72578DD0300E1120F /* ConnectStatusStyle.swift in Sources */, diff --git a/GliaWidgets/SecureConversations/ChatTranscript/SecureConversations.ChatWithTranscriptModel.swift b/GliaWidgets/SecureConversations/ChatTranscript/SecureConversations.ChatWithTranscriptModel.swift index 2bd86ea6c..ca34eaefa 100644 --- a/GliaWidgets/SecureConversations/ChatTranscript/SecureConversations.ChatWithTranscriptModel.swift +++ b/GliaWidgets/SecureConversations/ChatTranscript/SecureConversations.ChatWithTranscriptModel.swift @@ -471,6 +471,9 @@ extension SecureConversations { case .customCardOptionSelected: // Not supported for transcript. break + case .gvaButtonTapped: + // Not supported for transcript + break } } diff --git a/GliaWidgets/Sources/View/Chat/ChatView.swift b/GliaWidgets/Sources/View/Chat/ChatView.swift index 8f86dd25e..1a31ff0ef 100644 --- a/GliaWidgets/Sources/View/Chat/ChatView.swift +++ b/GliaWidgets/Sources/View/Chat/ChatView.swift @@ -23,6 +23,7 @@ class ChatView: EngagementView { var chatScrolledToBottom: ((Bool) -> Void)? var linkTapped: ((URL) -> Void)? var selectCustomCardOption: ((HtmlMetadata.Option, MessageRenderer.Message.Identifier) -> Void)? + var gvaButtonTapped: ((GvaOption) -> Void)? private let style: ChatStyle private var messageEntryViewBottomConstraint: NSLayoutConstraint! @@ -328,10 +329,7 @@ extension ChatView { case let .outgoingMessage(message): return outgoingMessageContent(message) case let .visitorMessage(message, status): - return visitorMessageContent( - message, - status: status - ) + return visitorMessageContent(message, status: status) case let .operatorMessage(message, showsImage, imageUrl): return operatorMessageContent( message, @@ -362,11 +360,13 @@ extension ChatView { return unreadMessageDividerContent() case .systemMessage(let message): return systemMessageContent(message) - case let .gvaPersistentButton(_, button): - // Temporary, since UI hasn't been implemented - let textView = UITextView() - textView.text = "Persistent Button: \(button.content)" - return .gvaPersistentButton(textView) + case let .gvaPersistentButton(message, button, showImage, imageUrl): + return gvaPersistenButtonContent( + message, + button: button, + showImage: showImage, + imageUrl: imageUrl + ) case let .gvaResponseText(message, text, showImage, imageUrl): return gvaResponseTextContent( message, @@ -867,4 +867,40 @@ extension ChatView { view.setOperatorImage(fromUrl: imageUrl, animated: false) return .gvaResponseText(view) } + + private func gvaPersistenButtonContent( + _ message: ChatMessage, + button: GvaButton, + showImage: Bool, + imageUrl: String? + ) -> ChatItemCell.Content { + let view = GvaPersistentButtonView( + with: style.choiceCard, + environment: .init( + data: environment.data, + uuid: environment.uuid, + gcd: environment.gcd, + imageViewCache: environment.imageViewCache, + uiScreen: environment.uiScreen + ) + ) + + view.appendContent( + .gvaPersistentButton(button), + animated: false + ) + + view.appendContent( + .downloads( + message.downloads, + accessibility: .init(from: .operator(message.operator?.name ?? style.accessibility.operator))), + animated: false + ) + view.onOptionTapped = { [weak self] in self?.gvaButtonTapped?($0) } + view.downloadTapped = { [weak self] in self?.downloadTapped?($0) } + view.linkTapped = { [weak self] in self?.linkTapped?($0) } + view.showsOperatorImage = showImage + view.setOperatorImage(fromUrl: imageUrl, animated: false) + return .gvaPersistentButton(view) + } } diff --git a/GliaWidgets/Sources/View/Chat/Message/Content/ChatMessageContent.swift b/GliaWidgets/Sources/View/Chat/Message/Content/ChatMessageContent.swift index 5f78fceba..e65386800 100644 --- a/GliaWidgets/Sources/View/Chat/Message/Content/ChatMessageContent.swift +++ b/GliaWidgets/Sources/View/Chat/Message/Content/ChatMessageContent.swift @@ -5,6 +5,7 @@ enum ChatMessageContent { case files([LocalFile], accessibility: ChatFileContentView.AccessibilityProperties) case downloads([FileDownload], accessibility: ChatFileContentView.AccessibilityProperties) case choiceCard(ChoiceCard) + case gvaPersistentButton(GvaButton) case attributedText(NSAttributedString, accessibility: TextAccessibilityProperties) struct TextAccessibilityProperties { diff --git a/GliaWidgets/Sources/View/Chat/Message/Content/Text/ChatTextContentStyle.swift b/GliaWidgets/Sources/View/Chat/Message/Content/Text/ChatTextContentStyle.swift index e7365b09e..931936652 100644 --- a/GliaWidgets/Sources/View/Chat/Message/Content/Text/ChatTextContentStyle.swift +++ b/GliaWidgets/Sources/View/Chat/Message/Content/Text/ChatTextContentStyle.swift @@ -33,7 +33,7 @@ public class ChatTextContentStyle { textColor: UIColor, textStyle: UIFont.TextStyle = .body, backgroundColor: UIColor, - cornerRadius: CGFloat = 10, + cornerRadius: CGFloat = 8.49, accessibility: Accessibility = .unsupported ) { self.textFont = textFont diff --git a/GliaWidgets/Sources/View/Chat/Message/GVAPersistentButtonView.swift b/GliaWidgets/Sources/View/Chat/Message/GVAPersistentButtonView.swift new file mode 100644 index 000000000..5c81a42df --- /dev/null +++ b/GliaWidgets/Sources/View/Chat/Message/GVAPersistentButtonView.swift @@ -0,0 +1,102 @@ +import UIKit + +final class GvaPersistentButtonView: OperatorChatMessageView { + var onOptionTapped: ((GvaOption) -> Void)! + + private let stackViewLayoutMargins = UIEdgeInsets(top: 16, left: 16, bottom: 16, right: 16) + + private let environment: Environment + + init(with style: ChoiceCardStyle, environment: Environment) { + self.environment = environment + super.init( + with: style, + environment: .init( + data: environment.data, + uuid: environment.uuid, + gcd: environment.gcd, + imageViewCache: environment.imageViewCache, + uiScreen: environment.uiScreen + ) + ) + } + + required init() { + fatalError("init() has not been implemented") + } + + override func appendContent(_ content: ChatMessageContent, animated: Bool) { + switch content { + case let .gvaPersistentButton(choiceCard): + let contentView = self.contentView(for: choiceCard) + appendContentView(contentView, animated: animated) + default: + break + } + } + + private func contentView(for persistentButton: GvaButton) -> UIView { + let containerView = UIView() + // TODO: Styling will be done in a subsequent PR + containerView.backgroundColor = UIColor(red: 0.953, green: 0.953, blue: 0.953, alpha: 1) + containerView.layer.cornerRadius = 8.49 + containerView.layer.borderWidth = 2 + // TODO: Styling will be done in a subsequent PR + containerView.layer.borderColor = UIColor.clear.cgColor + + let stackView = UIStackView() + stackView.axis = .vertical + stackView.spacing = 16 + stackView.isLayoutMarginsRelativeArrangement = true + stackView.layoutMargins = stackViewLayoutMargins + containerView.addSubview(stackView) + stackView.layoutInSuperview().activate() + + let tempChatTextContentStyle: ChatTextContentStyle = .init( + textFont: .font(weight: .regular, size: 16), + textColor: .black, + backgroundColor: .clear + ) + let textView = ChatTextContentView( + with: tempChatTextContentStyle, + contentAlignment: .left, + insets: .zero + ) + textView.attributedText = persistentButton.content + stackView.addArrangedSubview(textView) + setupAccessibilityProperties(for: textView) + + let optionViews: [UIView] = persistentButton.options.compactMap { option in + let optionView = GvaPersistentButtonOptionView(text: option.text) + optionView.tap = { [weak self] in + self?.onOptionTapped(option) + } + return optionView + } + stackView.addArrangedSubviews(optionViews) + + return containerView + } +} + +extension GvaPersistentButtonView { + struct Environment { + var data: FoundationBased.Data + var uuid: () -> UUID + var gcd: GCD + var imageViewCache: ImageView.Cache + var uiScreen: UIKitBased.UIScreen + } +} + +extension GvaPersistentButtonView { + func setupAccessibilityProperties(for imageView: ImageView) { + imageView.isAccessibilityElement = true + imageView.accessibilityLabel = "placeholder" // Will be implemented in another PR + } + + func setupAccessibilityProperties(for textView: ChatTextContentView) { + textView.accessibilityLabel = textView.text + textView.isAccessibilityElement = true + } +} diff --git a/GliaWidgets/Sources/View/Chat/Message/GvaPersistentButtonOptionView.swift b/GliaWidgets/Sources/View/Chat/Message/GvaPersistentButtonOptionView.swift new file mode 100644 index 000000000..b8ecddcb9 --- /dev/null +++ b/GliaWidgets/Sources/View/Chat/Message/GvaPersistentButtonOptionView.swift @@ -0,0 +1,70 @@ +import UIKit + +class GvaPersistentButtonOptionView: BaseView { + var tap: (() -> Void)? + + private let text: String? + private let textLabel = UILabel() + private let choiceButton = UIButton() + private let viewInsets = UIEdgeInsets(top: 10, left: 12, bottom: 10, right: 12) + + init(text: String?) { + self.text = text + super.init() + } + + @available(*, unavailable) + required init() { + fatalError("init(coder:) has not been implemented") + } + + override func setup() { + super.setup() + backgroundColor = .clear + // TODO: Styling will be done in a subsequent PR + layer.backgroundColor = UIColor.white.cgColor + layer.cornerRadius = 4 + textLabel.text = text + textLabel.font = .font(weight: .regular, size: 12) + textLabel.textColor = UIColor.black + textLabel.textAlignment = .center + textLabel.numberOfLines = 0 + textLabel.isAccessibilityElement = false + + choiceButton.addTarget(self, action: #selector(onTap), for: .touchUpInside) + } + + override func defineLayout() { + super.defineLayout() + var constraints = [NSLayoutConstraint](); defer { constraints.activate() } + + heightAnchor.constraint(equalToConstant: 42).isActive = true + addSubview(textLabel) + textLabel.translatesAutoresizingMaskIntoConstraints = false + constraints += textLabel.layoutInSuperview(insets: viewInsets) + + addSubview(choiceButton) + choiceButton.translatesAutoresizingMaskIntoConstraints = false + constraints += choiceButton.layoutInSuperview() + } + + private func applyStyle(_ style: ChoiceCardOptionStateStyle) { + setFontScalingEnabled( + style.accessibility.isFontScalingEnabled, + for: textLabel + ) + + UIView.transition(with: textLabel, duration: 0.2, options: .transitionCrossDissolve) { + self.layer.backgroundColor = style.backgroundColor.cgColor + self.textLabel.textColor = style.textColor + if let borderColor = style.borderColor { + self.layer.borderColor = borderColor.cgColor + self.layer.borderWidth = style.borderWidth + } + } + } + + @objc private func onTap() { + tap?() + } +} diff --git a/GliaWidgets/Sources/View/Chat/Message/OperatorChatMessageView.swift b/GliaWidgets/Sources/View/Chat/Message/OperatorChatMessageView.swift index 74e705172..8cbfe46cf 100644 --- a/GliaWidgets/Sources/View/Chat/Message/OperatorChatMessageView.swift +++ b/GliaWidgets/Sources/View/Chat/Message/OperatorChatMessageView.swift @@ -66,7 +66,7 @@ class OperatorChatMessageView: ChatMessageView { addSubview(contentViews) contentViews.translatesAutoresizingMaskIntoConstraints = false - constraints += contentViews.leadingAnchor.constraint(equalTo: operatorImageViewContainer.trailingAnchor, constant: 4) + constraints += contentViews.leadingAnchor.constraint(equalTo: operatorImageViewContainer.trailingAnchor, constant: 8) constraints += contentViews.topAnchor.constraint(equalTo: topAnchor, constant: imageViewInsets.top) constraints += contentViews.trailingAnchor.constraint(lessThanOrEqualTo: trailingAnchor, constant: -imageViewInsets.right) diff --git a/GliaWidgets/Sources/ViewController/Chat/ChatViewController.swift b/GliaWidgets/Sources/ViewController/Chat/ChatViewController.swift index 3e7afdd25..52d380045 100644 --- a/GliaWidgets/Sources/ViewController/Chat/ChatViewController.swift +++ b/GliaWidgets/Sources/ViewController/Chat/ChatViewController.swift @@ -93,6 +93,10 @@ final class ChatViewController: EngagementViewController, PopoverPresenter { viewModel.event(.customCardOptionSelected(option: option, messageId: messageId)) } + view.gvaButtonTapped = { option in + viewModel.event(.gvaButtonTapped(option)) + } + var viewModel = viewModel viewModel.action = { [weak self, weak view] action in diff --git a/GliaWidgets/Sources/ViewModel/Chat/ChatViewModel.swift b/GliaWidgets/Sources/ViewModel/Chat/ChatViewModel.swift index 0493a421e..6b5e4ab1d 100644 --- a/GliaWidgets/Sources/ViewModel/Chat/ChatViewModel.swift +++ b/GliaWidgets/Sources/ViewModel/Chat/ChatViewModel.swift @@ -295,6 +295,8 @@ extension ChatViewModel { linkTapped(url) case .customCardOptionSelected(let option, let messageId): sendSelectedCustomCardOption(option, for: messageId) + case .gvaButtonTapped(let option): + gvaOptionAction(for: option)() } } } @@ -824,6 +826,7 @@ extension ChatViewModel { option: HtmlMetadata.Option, messageId: MessageRenderer.Message.Identifier ) + case gvaButtonTapped(GvaOption) } enum Action { diff --git a/GliaWidgets/Sources/ViewModel/Chat/Data/ChatItem.swift b/GliaWidgets/Sources/ViewModel/Chat/Data/ChatItem.swift index d065a70a7..7b6fbe5c7 100644 --- a/GliaWidgets/Sources/ViewModel/Chat/Data/ChatItem.swift +++ b/GliaWidgets/Sources/ViewModel/Chat/Data/ChatItem.swift @@ -50,7 +50,7 @@ class ChatItem { return nil } case let .gvaPersistenButton(button): - kind = .gvaPersistentButton(message, persistenButton: button) + kind = .gvaPersistentButton(message, persistenButton: button, showImage: true, imageUrl: message.operator?.pictureUrl) case let .gvaResponseText(text): kind = .gvaResponseText(message, responseText: text, showImage: true, imageUrl: message.operator?.pictureUrl) case let .gvaQuickReply(button): @@ -83,7 +83,7 @@ extension ChatItem { case transferring case unreadMessageDivider case systemMessage(ChatMessage) - case gvaPersistentButton(ChatMessage, persistenButton: GvaButton) + case gvaPersistentButton(ChatMessage, persistenButton: GvaButton, showImage: Bool, imageUrl: String?) case gvaResponseText(ChatMessage, responseText: GvaResponseText, showImage: Bool, imageUrl: String?) case gvaQuickReply(ChatMessage, quickReply: GvaButton) case gvaGallery(ChatMessage, gallery: GvaGallery) diff --git a/GliaWidgets/Sources/ViewModel/Chat/Data/Gva.swift b/GliaWidgets/Sources/ViewModel/Chat/Data/Gva.swift index ad96c9c57..736a21566 100644 --- a/GliaWidgets/Sources/ViewModel/Chat/Data/Gva.swift +++ b/GliaWidgets/Sources/ViewModel/Chat/Data/Gva.swift @@ -19,13 +19,26 @@ struct GvaResponseText: Decodable, Equatable { struct GvaButton: Decodable, Equatable { let type: GvaCardType - let content: String + let content: NSAttributedString let options: [GvaOption] + + enum CodingKeys: String, CodingKey { + case type, content, options + } + + init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + type = try container.decode(GvaCardType.self, forKey: .type) + let contentString = try container.decode(String.self, forKey: .content) + let modifiedString = contentString.replacingOccurrences(of: "\n", with: "
") + content = modifiedString.htmlToAttributedString ?? NSAttributedString(string: "") + options = try container.decode([GvaOption].self, forKey: .options) + } } struct GvaGallery: Decodable, Equatable { let type: GvaCardType - let galleryCards: [GvaGalleryCard] + let content: [GvaGalleryCard] } struct GvaGalleryCard: Decodable, Equatable { From b17acb2d5579638a6e60aca08af9732c797a1d38 Mon Sep 17 00:00:00 2001 From: Igor Kravchenko Date: Mon, 17 Jul 2023 09:15:23 +0300 Subject: [PATCH 07/64] Adjust layout to match snapshots Address layout inconsistencies after PureLayout removal. MOB-2469 --- GliaWidgets.xcodeproj/project.pbxproj | 44 +++++++ .../SecureConversations.FileUploadView.swift | 22 +++- GliaWidgets/Sources/View/Chat/ChatView.swift | 12 +- .../Chat/Entry/ChatMessageEntryView.swift | 2 +- .../ChatFileDownloadContentView.swift | 2 +- .../Message/OperatorChatMessageView.swift | 2 +- .../AlertViewControllerLayoutTests.swift | 66 ++++++++++ SnapshotTests/BubbleViewLayoutTests.swift | 12 ++ SnapshotTests/BubbleViewVoiceOverTests.swift | 1 + .../CallViewControllerLayoutTests.swift | 65 +++++++++ .../CallViewControllerVoiceOverTests.swift | 1 - .../ChatCallUpgradeViewLayoutTests.swift | 23 ++++ .../ChatViewControllerLayoutTests.swift | 55 ++++++++ ...ScreenShareViewControllerLayoutTests.swift | 26 ++++ ...sationsConfirmationScreenLayoutTests.swift | 46 +++++++ ...onversationsWelcomeScreenLayoutTests.swift | 123 ++++++++++++++++++ SnapshotTests/SnapshotTestCase.swift | 2 +- .../SurveyViewControllerLayoutTests.swift | 35 +++++ .../VideoCallViewControllerLayoutTests.swift | 59 +++++++++ ...VisitorCodeViewControllerLayoutTests.swift | 106 +++++++++++++++ 20 files changed, 695 insertions(+), 9 deletions(-) create mode 100644 SnapshotTests/AlertViewControllerLayoutTests.swift create mode 100644 SnapshotTests/BubbleViewLayoutTests.swift create mode 100644 SnapshotTests/CallViewControllerLayoutTests.swift create mode 100644 SnapshotTests/ChatCallUpgradeViewLayoutTests.swift create mode 100644 SnapshotTests/ChatViewControllerLayoutTests.swift create mode 100644 SnapshotTests/ScreenShareViewControllerLayoutTests.swift create mode 100644 SnapshotTests/SecureConversationsConfirmationScreenLayoutTests.swift create mode 100644 SnapshotTests/SecureConversationsWelcomeScreenLayoutTests.swift create mode 100644 SnapshotTests/SurveyViewControllerLayoutTests.swift create mode 100644 SnapshotTests/VideoCallViewControllerLayoutTests.swift create mode 100644 SnapshotTests/VisitorCodeViewControllerLayoutTests.swift diff --git a/GliaWidgets.xcodeproj/project.pbxproj b/GliaWidgets.xcodeproj/project.pbxproj index af63aed45..42137d05c 100644 --- a/GliaWidgets.xcodeproj/project.pbxproj +++ b/GliaWidgets.xcodeproj/project.pbxproj @@ -469,6 +469,17 @@ AF10ED8F29BF849A00E85309 /* UnreadMessageDividerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = AF10ED8E29BF849A00E85309 /* UnreadMessageDividerView.swift */; }; AF10ED9129BF85C700E85309 /* UnreadMessageDividerStyle.swift in Sources */ = {isa = PBXBuildFile; fileRef = AF10ED9029BF85C700E85309 /* UnreadMessageDividerStyle.swift */; }; AF11F30728BE6F0C002ACEB4 /* UIAlertController+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = AF11F30628BE6F0C002ACEB4 /* UIAlertController+Extensions.swift */; }; + AF22C8852A6154780004BF3C /* AlertViewControllerLayoutTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = AF22C8842A6154780004BF3C /* AlertViewControllerLayoutTests.swift */; }; + AF22C8872A6182AF0004BF3C /* BubbleViewLayoutTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = AF22C8862A6182AF0004BF3C /* BubbleViewLayoutTests.swift */; }; + AF22C8892A6184C50004BF3C /* CallViewControllerLayoutTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = AF22C8882A6184C50004BF3C /* CallViewControllerLayoutTests.swift */; }; + AF22C88B2A6186A20004BF3C /* ChatCallUpgradeViewLayoutTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = AF22C88A2A6186A20004BF3C /* ChatCallUpgradeViewLayoutTests.swift */; }; + AF22C88D2A6188EF0004BF3C /* ChatViewControllerLayoutTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = AF22C88C2A6188EF0004BF3C /* ChatViewControllerLayoutTests.swift */; }; + AF22C88F2A618B9D0004BF3C /* ScreenShareViewControllerLayoutTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = AF22C88E2A618B9D0004BF3C /* ScreenShareViewControllerLayoutTests.swift */; }; + AF22C8912A6198FF0004BF3C /* SecureConversationsWelcomeScreenLayoutTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = AF22C8902A6198FF0004BF3C /* SecureConversationsWelcomeScreenLayoutTests.swift */; }; + AF22C8932A619F5C0004BF3C /* SecureConversationsConfirmationScreenLayoutTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = AF22C8922A619F5C0004BF3C /* SecureConversationsConfirmationScreenLayoutTests.swift */; }; + AF22C8952A61A6B80004BF3C /* SurveyViewControllerLayoutTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = AF22C8942A61A6B80004BF3C /* SurveyViewControllerLayoutTests.swift */; }; + AF22C8972A61A9BF0004BF3C /* VideoCallViewControllerLayoutTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = AF22C8962A61A9BF0004BF3C /* VideoCallViewControllerLayoutTests.swift */; }; + AF22C8992A61AE930004BF3C /* VisitorCodeViewControllerLayoutTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = AF22C8982A61AE930004BF3C /* VisitorCodeViewControllerLayoutTests.swift */; }; AF2355A229C9EC7E007D9896 /* IdCollectionTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = AF2355A129C9EC7E007D9896 /* IdCollectionTests.swift */; }; AF29810929E045CE0005BD55 /* TranscriptModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = AF29810829E045CE0005BD55 /* TranscriptModelTests.swift */; }; AF29810B29E0478E0005BD55 /* TranscriptModel.Environment.Failing.swift in Sources */ = {isa = PBXBuildFile; fileRef = AF29810A29E0478E0005BD55 /* TranscriptModel.Environment.Failing.swift */; }; @@ -1126,6 +1137,17 @@ AF10ED8E29BF849A00E85309 /* UnreadMessageDividerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UnreadMessageDividerView.swift; sourceTree = ""; }; AF10ED9029BF85C700E85309 /* UnreadMessageDividerStyle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UnreadMessageDividerStyle.swift; sourceTree = ""; }; AF11F30628BE6F0C002ACEB4 /* UIAlertController+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIAlertController+Extensions.swift"; sourceTree = ""; }; + AF22C8842A6154780004BF3C /* AlertViewControllerLayoutTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AlertViewControllerLayoutTests.swift; sourceTree = ""; }; + AF22C8862A6182AF0004BF3C /* BubbleViewLayoutTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BubbleViewLayoutTests.swift; sourceTree = ""; }; + AF22C8882A6184C50004BF3C /* CallViewControllerLayoutTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CallViewControllerLayoutTests.swift; sourceTree = ""; }; + AF22C88A2A6186A20004BF3C /* ChatCallUpgradeViewLayoutTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatCallUpgradeViewLayoutTests.swift; sourceTree = ""; }; + AF22C88C2A6188EF0004BF3C /* ChatViewControllerLayoutTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatViewControllerLayoutTests.swift; sourceTree = ""; }; + AF22C88E2A618B9D0004BF3C /* ScreenShareViewControllerLayoutTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ScreenShareViewControllerLayoutTests.swift; sourceTree = ""; }; + AF22C8902A6198FF0004BF3C /* SecureConversationsWelcomeScreenLayoutTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SecureConversationsWelcomeScreenLayoutTests.swift; sourceTree = ""; }; + AF22C8922A619F5C0004BF3C /* SecureConversationsConfirmationScreenLayoutTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SecureConversationsConfirmationScreenLayoutTests.swift; sourceTree = ""; }; + AF22C8942A61A6B80004BF3C /* SurveyViewControllerLayoutTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SurveyViewControllerLayoutTests.swift; sourceTree = ""; }; + AF22C8962A61A9BF0004BF3C /* VideoCallViewControllerLayoutTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VideoCallViewControllerLayoutTests.swift; sourceTree = ""; }; + AF22C8982A61AE930004BF3C /* VisitorCodeViewControllerLayoutTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VisitorCodeViewControllerLayoutTests.swift; sourceTree = ""; }; AF2355A129C9EC7E007D9896 /* IdCollectionTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IdCollectionTests.swift; sourceTree = ""; }; AF29810829E045CE0005BD55 /* TranscriptModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TranscriptModelTests.swift; sourceTree = ""; }; AF29810A29E0478E0005BD55 /* TranscriptModel.Environment.Failing.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TranscriptModel.Environment.Failing.swift; sourceTree = ""; }; @@ -3032,18 +3054,29 @@ isa = PBXGroup; children = ( 846E822728996A5C008EFBF0 /* AlertViewControllerTests.swift */, + AF22C8842A6154780004BF3C /* AlertViewControllerLayoutTests.swift */, 9AB3401227F71D5D006E0FE2 /* BubbleViewVoiceOverTests.swift */, + AF22C8862A6182AF0004BF3C /* BubbleViewLayoutTests.swift */, 9AE9E4B427E0EE2E00BFE239 /* CallViewControllerVoiceOverTests.swift */, + AF22C8882A6184C50004BF3C /* CallViewControllerLayoutTests.swift */, 9AB3402828002422006E0FE2 /* ChatCallUpgradeViewVoiceOverTests.swift */, + AF22C88A2A6186A20004BF3C /* ChatCallUpgradeViewLayoutTests.swift */, 9A1992D627D61F8100161AAE /* ChatViewControllerVoiceOverTests.swift */, + AF22C88C2A6188EF0004BF3C /* ChatViewControllerLayoutTests.swift */, C07FA04C29B0E41A00E9FB7F /* ScreenShareViewControllerTests.swift */, + AF22C88E2A618B9D0004BF3C /* ScreenShareViewControllerLayoutTests.swift */, 75CF8D9029C3A85C00CB1524 /* SecureConversationsWelcomeScreenTests.swift */, + AF22C8902A6198FF0004BF3C /* SecureConversationsWelcomeScreenLayoutTests.swift */, 75CF8DAC29C8F2B500CB1524 /* SecureConversationsConfirmationScreenTests.swift */, + AF22C8922A619F5C0004BF3C /* SecureConversationsConfirmationScreenLayoutTests.swift */, 9A1992D727D61F8100161AAE /* SnapshotTestCase.swift */, 8458769E2823FD18007AC3DF /* SurveyViewControllerVoiceOverTests.swift */, + AF22C8942A61A6B80004BF3C /* SurveyViewControllerLayoutTests.swift */, C07FA04D29B0E41A00E9FB7F /* VideoCallViewControllerTests.swift */, + AF22C8962A61A9BF0004BF3C /* VideoCallViewControllerLayoutTests.swift */, C07FA04E29B0E41A00E9FB7F /* VisitorCodeViewControllerTests.swift */, AF755FD92A71583900871E36 /* BubbleWindowLayoutTests.swift */, + AF22C8982A61AE930004BF3C /* VisitorCodeViewControllerLayoutTests.swift */, ); path = SnapshotTests; sourceTree = ""; @@ -4441,17 +4474,28 @@ files = ( 846E822828996A5C008EFBF0 /* AlertViewControllerTests.swift in Sources */, 75CF8D9129C3A85C00CB1524 /* SecureConversationsWelcomeScreenTests.swift in Sources */, + AF22C8972A61A9BF0004BF3C /* VideoCallViewControllerLayoutTests.swift in Sources */, 9AE9E4B527E0EE2E00BFE239 /* CallViewControllerVoiceOverTests.swift in Sources */, + AF22C8852A6154780004BF3C /* AlertViewControllerLayoutTests.swift in Sources */, + AF22C8872A6182AF0004BF3C /* BubbleViewLayoutTests.swift in Sources */, C07FA05029B0E41A00E9FB7F /* VideoCallViewControllerTests.swift in Sources */, AF755FDA2A71583900871E36 /* BubbleWindowLayoutTests.swift in Sources */, + AF22C88D2A6188EF0004BF3C /* ChatViewControllerLayoutTests.swift in Sources */, + AF22C8992A61AE930004BF3C /* VisitorCodeViewControllerLayoutTests.swift in Sources */, 9AB3401327F71D5D006E0FE2 /* BubbleViewVoiceOverTests.swift in Sources */, + AF22C8932A619F5C0004BF3C /* SecureConversationsConfirmationScreenLayoutTests.swift in Sources */, 9A1992D827D61F8100161AAE /* ChatViewControllerVoiceOverTests.swift in Sources */, + AF22C8892A6184C50004BF3C /* CallViewControllerLayoutTests.swift in Sources */, 8458769F2823FD18007AC3DF /* SurveyViewControllerVoiceOverTests.swift in Sources */, C07FA05129B0E41A00E9FB7F /* VisitorCodeViewControllerTests.swift in Sources */, 75CF8DAD29C8F2B500CB1524 /* SecureConversationsConfirmationScreenTests.swift in Sources */, 9AB3402928002422006E0FE2 /* ChatCallUpgradeViewVoiceOverTests.swift in Sources */, C07FA04F29B0E41A00E9FB7F /* ScreenShareViewControllerTests.swift in Sources */, + AF22C8952A61A6B80004BF3C /* SurveyViewControllerLayoutTests.swift in Sources */, 9A1992D927D61F8100161AAE /* SnapshotTestCase.swift in Sources */, + AF22C88F2A618B9D0004BF3C /* ScreenShareViewControllerLayoutTests.swift in Sources */, + AF22C8912A6198FF0004BF3C /* SecureConversationsWelcomeScreenLayoutTests.swift in Sources */, + AF22C88B2A6186A20004BF3C /* ChatCallUpgradeViewLayoutTests.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; diff --git a/GliaWidgets/SecureConversations/FileUploadListView/SecureConversations.FileUploadView.swift b/GliaWidgets/SecureConversations/FileUploadListView/SecureConversations.FileUploadView.swift index c5bc5dc7d..afcca1af1 100644 --- a/GliaWidgets/SecureConversations/FileUploadListView/SecureConversations.FileUploadView.swift +++ b/GliaWidgets/SecureConversations/FileUploadListView/SecureConversations.FileUploadView.swift @@ -86,15 +86,31 @@ extension SecureConversations { equalTo: contentView.topAnchor, constant: style.removeButtonTopRightOffset.height ) constraints += removeButton.trailingAnchor.constraint( - equalTo: contentView.trailingAnchor, constant: style.removeButtonTopRightOffset.height + equalTo: contentView.trailingAnchor, constant: style.removeButtonTopRightOffset.width ) constraints += removeButton.match(value: buttonSize) contentView.addSubview(infoLabel) infoLabel.translatesAutoresizingMaskIntoConstraints = false + + // In order for `infoLabel` to have minimum height, to prevent `stateLabel` + // from jumping when `infoLabel` is empty, we need add extra height constraint + // with respecting content compression resistance priority to it. + + // Add content compression resistance priority. + let lowPriority = UILayoutPriority(rawValue: 750) + infoLabel.setContentCompressionResistancePriority(lowPriority, for: .vertical) + + // Add minimum height constraint. + let highPriority = UILayoutPriority(rawValue: 1000) + // Set desired minimum height and assign priority. + let minHeightConstraint = infoLabel.heightAnchor.constraint(greaterThanOrEqualToConstant: 17) + minHeightConstraint.priority = highPriority + constraints += minHeightConstraint + constraints += infoLabel.leadingAnchor.constraint(equalTo: filePreviewView.trailingAnchor, constant: 12) constraints += infoLabel.trailingAnchor.constraint(lessThanOrEqualTo: removeButton.leadingAnchor, constant: -80) - constraints += infoLabel.topAnchor.constraint(equalTo: filePreviewView.firstBaselineAnchor, constant: 3) + constraints += infoLabel.topAnchor.constraint(equalTo: filePreviewView.firstBaselineAnchor, constant: 0) contentView.addSubview(stateLabel) stateLabel.translatesAutoresizingMaskIntoConstraints = false @@ -370,7 +386,7 @@ extension SecureConversations.FileUploadView.Style { contentInsets = UIEdgeInsets(top: 20, left: 10, bottom: 20, right: 10) cornerRadius = 4 backgroundColor = uploadStyle.backgroundColor - removeButtonTopRightOffset = .init(width: -5, height: -14) + removeButtonTopRightOffset = .init(width: 5, height: -14) } } } diff --git a/GliaWidgets/Sources/View/Chat/ChatView.swift b/GliaWidgets/Sources/View/Chat/ChatView.swift index 1a31ff0ef..43413cea9 100644 --- a/GliaWidgets/Sources/View/Chat/ChatView.swift +++ b/GliaWidgets/Sources/View/Chat/ChatView.swift @@ -162,7 +162,17 @@ class ChatView: EngagementView { ]) addSubview(messageEntryView) - messageEntryViewBottomConstraint = messageEntryView.layoutIn(safeAreaLayoutGuide, edges: .bottom).first + let messageEntryInsets = UIEdgeInsets( + top: 0, + left: 0, + bottom: 4.5, + right: 0 + ) + messageEntryViewBottomConstraint = messageEntryView.layoutIn( + safeAreaLayoutGuide, + edges: .bottom, + insets: messageEntryInsets + ).first constraints += messageEntryViewBottomConstraint constraints += messageEntryView.layoutIn(safeAreaLayoutGuide, edges: .horizontal) diff --git a/GliaWidgets/Sources/View/Chat/Entry/ChatMessageEntryView.swift b/GliaWidgets/Sources/View/Chat/Entry/ChatMessageEntryView.swift index 2436e17a9..da7df154b 100644 --- a/GliaWidgets/Sources/View/Chat/Entry/ChatMessageEntryView.swift +++ b/GliaWidgets/Sources/View/Chat/Entry/ChatMessageEntryView.swift @@ -173,7 +173,7 @@ class ChatMessageEntryView: BaseView { constraints += textView.trailingAnchor.constraint(equalTo: buttonsStackView.leadingAnchor, constant: -8) messageContainerView.addSubview(placeholderLabel) - placeholderLabel.isAccessibilityElement = false + placeholderLabel.isAccessibilityElement = true placeholderLabel.translatesAutoresizingMaskIntoConstraints = false placeholderLabel.numberOfLines = 0 diff --git a/GliaWidgets/Sources/View/Chat/Message/Content/File/Download/ChatFileDownloadContentView.swift b/GliaWidgets/Sources/View/Chat/Message/Content/File/Download/ChatFileDownloadContentView.swift index 8a8b01873..967a4e900 100644 --- a/GliaWidgets/Sources/View/Chat/Message/Content/File/Download/ChatFileDownloadContentView.swift +++ b/GliaWidgets/Sources/View/Chat/Message/Content/File/Download/ChatFileDownloadContentView.swift @@ -69,7 +69,7 @@ class ChatFileDownloadContentView: ChatFileContentView { contentView.addSubview(infoLabel) infoLabel.translatesAutoresizingMaskIntoConstraints = false - constraints += infoLabel.topAnchor.constraint(equalTo: filePreviewView.topAnchor) + constraints += infoLabel.topAnchor.constraint(equalTo: filePreviewView.topAnchor, constant: 4) constraints += infoLabel.leadingAnchor.constraint(equalTo: filePreviewView.trailingAnchor, constant: 12) constraints += infoLabel.layoutInSuperview(edges: .trailing) diff --git a/GliaWidgets/Sources/View/Chat/Message/OperatorChatMessageView.swift b/GliaWidgets/Sources/View/Chat/Message/OperatorChatMessageView.swift index 8cbfe46cf..b86d34310 100644 --- a/GliaWidgets/Sources/View/Chat/Message/OperatorChatMessageView.swift +++ b/GliaWidgets/Sources/View/Chat/Message/OperatorChatMessageView.swift @@ -61,7 +61,7 @@ class OperatorChatMessageView: ChatMessageView { constraints += operatorImageViewContainer.match(value: operatorImageViewSize) constraints += operatorImageViewContainer.leadingAnchor.constraint(equalTo: leadingAnchor, constant: imageViewInsets.left) - constraints += operatorImageViewContainer.bottomAnchor.constraint(equalTo: bottomAnchor, constant: imageViewInsets.bottom) + constraints += operatorImageViewContainer.bottomAnchor.constraint(equalTo: bottomAnchor, constant: -imageViewInsets.bottom) constraints += operatorImageViewContainer.topAnchor.constraint(greaterThanOrEqualTo: topAnchor, constant: imageViewInsets.top) addSubview(contentViews) diff --git a/SnapshotTests/AlertViewControllerLayoutTests.swift b/SnapshotTests/AlertViewControllerLayoutTests.swift new file mode 100644 index 000000000..df8c9861d --- /dev/null +++ b/SnapshotTests/AlertViewControllerLayoutTests.swift @@ -0,0 +1,66 @@ +@testable import GliaWidgets +import SnapshotTesting +import XCTest + +class AlertViewControllerLayoutTests: SnapshotTestCase { + func test_screenSharingOffer() { + let alert = alert(ofKind: .screenShareOffer( + .mock(), + accepted: {}, + declined: {} + )) + assertSnapshot( + matching: alert, + as: .image, + named: nameForDevice() + ) + } + + func test_mediaUpgradeOffer() { + let alert = alert(ofKind: .singleMediaUpgrade( + .mock(), + accepted: {}, + declined: {} + )) + assertSnapshot( + matching: alert, + as: .image, + named: nameForDevice() + ) + } + + func test_messageAlert() { + let alert = alert(ofKind: .message( + .mock(), + accessibilityIdentifier: nil, + dismissed: {} + )) + assertSnapshot( + matching: alert, + as: .image, + named: nameForDevice() + ) + } + + func test_singleAction() { + let alert = alert(ofKind: .singleAction( + .mock(), + accessibilityIdentifier: "mocked-accessibility-identifier", + actionTapped: {} + )) + assertSnapshot( + matching: alert, + as: .image, + named: nameForDevice() + ) + } + + private func alert(ofKind kind: AlertViewController.Kind) -> AlertViewController { + let viewController = AlertViewController( + kind: kind, + viewFactory: .mock() + ) + viewController.view.frame = UIScreen.main.bounds + return viewController + } +} diff --git a/SnapshotTests/BubbleViewLayoutTests.swift b/SnapshotTests/BubbleViewLayoutTests.swift new file mode 100644 index 000000000..47538040f --- /dev/null +++ b/SnapshotTests/BubbleViewLayoutTests.swift @@ -0,0 +1,12 @@ +@testable import GliaWidgets +import SnapshotTesting +import XCTest + +class BubbleViewLayoutTests: SnapshotTestCase { + func test_bubble() { + let bubble = ViewFactory.mock().makeBubbleView() + bubble.frame = .init(origin: .zero, size: .init(width: 50, height: 50)) + // If shadow will cause failing test locally or on CI, we should disable it. + assertSnapshot(matching: bubble, as: .image) + } +} diff --git a/SnapshotTests/BubbleViewVoiceOverTests.swift b/SnapshotTests/BubbleViewVoiceOverTests.swift index 27f45a2d6..c9b99f7ab 100644 --- a/SnapshotTests/BubbleViewVoiceOverTests.swift +++ b/SnapshotTests/BubbleViewVoiceOverTests.swift @@ -7,6 +7,7 @@ class BubbleViewVoiceOverTests: SnapshotTestCase { func test_bubble() { let bubble = ViewFactory.mock().makeBubbleView() bubble.frame = .init(origin: .zero, size: .init(width: 50, height: 50)) + // If shadow will cause failing test locally or on CI, we should disable it. assertSnapshot(matching: bubble, as: .accessibilityImage(precision: SnapshotTestCase.possiblePrecision)) } } diff --git a/SnapshotTests/CallViewControllerLayoutTests.swift b/SnapshotTests/CallViewControllerLayoutTests.swift new file mode 100644 index 000000000..2093dec4d --- /dev/null +++ b/SnapshotTests/CallViewControllerLayoutTests.swift @@ -0,0 +1,65 @@ +@testable import GliaWidgets +import SnapshotTesting +import XCTest + +class CallViewControllerLayoutTests: SnapshotTestCase { + func test_audioCallQueueState() throws { + let viewController = try CallViewController.mockAudioCallQueueState() + viewController.view.frame = UIScreen.main.bounds + assertSnapshot( + matching: viewController, + as: .image, + named: nameForDevice() + ) + } + + func test_audioCallConnectingState() throws { + let viewController = try CallViewController.mockAudioCallConnectingState() + viewController.view.frame = UIScreen.main.bounds + assertSnapshot( + matching: viewController, + as: .image, + named: nameForDevice() + ) + } + + func test_audioCallConnectedState() throws { + let viewController = try CallViewController.mockAudioCallConnectedState() + viewController.view.frame = UIScreen.main.bounds + assertSnapshot( + matching: viewController, + as: .image, + named: nameForDevice() + ) + } + + func test_mockVideoCallConnectingState() throws { + let viewController = try CallViewController.mockVideoCallConnectingState() + viewController.view.frame = UIScreen.main.bounds + assertSnapshot( + matching: viewController, + as: .image, + named: nameForDevice() + ) + } + + func test_mockVideoCallQueueState() throws { + let viewController = try CallViewController.mockVideoCallQueueState() + viewController.view.frame = UIScreen.main.bounds + assertSnapshot( + matching: viewController, + as: .image, + named: nameForDevice() + ) + } + + func test_mockVideoCallConnectedState() throws { + let viewController = try CallViewController.mockVideoCallConnectedState() + viewController.view.frame = UIScreen.main.bounds + assertSnapshot( + matching: viewController, + as: .image, + named: nameForDevice() + ) + } +} diff --git a/SnapshotTests/CallViewControllerVoiceOverTests.swift b/SnapshotTests/CallViewControllerVoiceOverTests.swift index f79003f09..316dbb008 100644 --- a/SnapshotTests/CallViewControllerVoiceOverTests.swift +++ b/SnapshotTests/CallViewControllerVoiceOverTests.swift @@ -63,5 +63,4 @@ class CallViewControllerVoiceOverTests: SnapshotTestCase { named: nameForDevice() ) } - } diff --git a/SnapshotTests/ChatCallUpgradeViewLayoutTests.swift b/SnapshotTests/ChatCallUpgradeViewLayoutTests.swift new file mode 100644 index 000000000..e9d4a210e --- /dev/null +++ b/SnapshotTests/ChatCallUpgradeViewLayoutTests.swift @@ -0,0 +1,23 @@ +@testable import GliaWidgets +import SnapshotTesting +import XCTest + +class ChatCallUpgradeViewLayoutTests: SnapshotTestCase { + func test_chatCallUpgradeViewToAudio() { + let upgradeView = ChatCallUpgradeView(with: Theme.mock().chat.audioUpgrade, duration: .init(with: .zero)) + upgradeView.frame = .init(origin: .zero, size: .init(width: 300, height: 120)) + assertSnapshot( + matching: upgradeView, + as: .image + ) + } + + func test_chatCallUpgradeViewToVideo() { + let upgradeView = ChatCallUpgradeView(with: Theme.mock().chat.videoUpgrade, duration: .init(with: .zero)) + upgradeView.frame = .init(origin: .zero, size: .init(width: 300, height: 120)) + assertSnapshot( + matching: upgradeView, + as: .image + ) + } +} diff --git a/SnapshotTests/ChatViewControllerLayoutTests.swift b/SnapshotTests/ChatViewControllerLayoutTests.swift new file mode 100644 index 000000000..75aedd062 --- /dev/null +++ b/SnapshotTests/ChatViewControllerLayoutTests.swift @@ -0,0 +1,55 @@ +@testable import GliaWidgets +import SnapshotTesting +import XCTest + +class ChatViewControllerLayoutTests: SnapshotTestCase { + func test_messagesFromHistory() { + let viewController = ChatViewController.mockHistoryMessagesScreen() + viewController.view.frame = UIScreen.main.bounds + assertSnapshot( + matching: viewController, + as: .image, + named: nameForDevice() + ) + } + + func test_visitorUploadedFileStates() throws { + let viewController = try ChatViewController.mockVisitorFileUploadStates() + viewController.view.frame = UIScreen.main.bounds + assertSnapshot( + matching: viewController, + as: .image, + named: nameForDevice() + ) + } + + func test_choiceCard() throws { + let viewController = try ChatViewController.mockChoiceCard() + viewController.view.frame = UIScreen.main.bounds + assertSnapshot( + matching: viewController, + as: .image, + named: nameForDevice() + ) + } + + func test_visitorFileDownloadStates() throws { + var chatMessages: [ChatMessage] = [] + let viewController = try ChatViewController.mockVisitorFileDownloadStates { messages in + chatMessages = messages + } + viewController.view.frame = UIScreen.main.bounds + viewController.view.setNeedsLayout() + viewController.view.layoutIfNeeded() + XCTAssertEqual(chatMessages.count, 4) + chatMessages[0].downloads[0].state.value = .none + chatMessages[1].downloads[0].state.value = .downloading(progress: .init(with: 0.5)) + chatMessages[2].downloads[0].state.value = .downloaded(.mock()) + chatMessages[3].downloads[0].state.value = .error(.deleted) + assertSnapshot( + matching: viewController, + as: .image, + named: self.nameForDevice() + ) + } +} diff --git a/SnapshotTests/ScreenShareViewControllerLayoutTests.swift b/SnapshotTests/ScreenShareViewControllerLayoutTests.swift new file mode 100644 index 000000000..3d724121a --- /dev/null +++ b/SnapshotTests/ScreenShareViewControllerLayoutTests.swift @@ -0,0 +1,26 @@ +@testable import GliaWidgets +import SnapshotTesting +import XCTest + +class ScreenShareViewControllerLayoutTests: SnapshotTestCase { + func testScreenShareViewController() { + let props: CallVisualizer.ScreenSharingViewController.Props = .init( + screenSharingViewProps: .init( + style: .mock(), + header: .mock( + title: L10n.CallVisualizer.ScreenSharing.title, + backButton: .init(style: .mock(image: Asset.back.image)) + ), + endScreenSharing: .mock() + ) + ) + let screenShareViewController = CallVisualizer.ScreenSharingViewController(props: props) + screenShareViewController.view.frame = UIScreen.main.bounds + + assertSnapshot( + matching: screenShareViewController, + as: .image, + named: nameForDevice() + ) + } +} diff --git a/SnapshotTests/SecureConversationsConfirmationScreenLayoutTests.swift b/SnapshotTests/SecureConversationsConfirmationScreenLayoutTests.swift new file mode 100644 index 000000000..41a2645fe --- /dev/null +++ b/SnapshotTests/SecureConversationsConfirmationScreenLayoutTests.swift @@ -0,0 +1,46 @@ +@testable import GliaWidgets +import SnapshotTesting +import XCTest + +class SecureConversationsConfirmationScreenLayoutTests: SnapshotTestCase { + let theme = Theme.mock() + + func test_confirmationView() { + let props = Self.makeConfirmationProps(style: theme.secureConversationsConfirmation) + let viewController = SecureConversations.ConfirmationViewController( + viewModel: .init(environment: .init(confirmationStyle: theme.defaultSecureConversationsConfirmationStyle)), + viewFactory: .mock(theme: theme, messageRenderer: nil, environment: .mock), + props: props + ) + viewController.view.frame = UIScreen.main.bounds + + assertSnapshot( + matching: viewController.view, + as: .image, + named: self.nameForDevice() + ) + } + + // MARK: - Helpers + + static func headerProps() -> Header.Props { + .mock( + title: "Secure Conversations", + backButton: .init(style: .mock(image: Asset.back.image)), + closeButton: .init(style: .mock(image: Asset.close.image)) + ) + } + + static func makeConfirmationProps( + headerProps: Header.Props = headerProps(), + style: SecureConversations.ConfirmationStyle + ) -> SecureConversations.ConfirmationViewController.Props { + .init( + confirmationViewProps: .init( + style: style, + header: headerProps, + checkMessageButtonTap: .nop + ) + ) + } +} diff --git a/SnapshotTests/SecureConversationsWelcomeScreenLayoutTests.swift b/SnapshotTests/SecureConversationsWelcomeScreenLayoutTests.swift new file mode 100644 index 000000000..fa8beb3b8 --- /dev/null +++ b/SnapshotTests/SecureConversationsWelcomeScreenLayoutTests.swift @@ -0,0 +1,123 @@ +@testable import GliaWidgets +import SnapshotTesting +import XCTest + +class SecureConversationsWelcomeScreenLayoutTests: SnapshotTestCase { + let theme = Theme.mock() + + func test_welcomeView() { + let props = Self.makeWelcomeProps( + theme: theme.secureConversationsWelcome, + uploads: [] + ) + let viewController = SecureConversations.WelcomeViewController( + viewFactory: .mock(theme: theme, messageRenderer: nil, environment: .mock), + props: .welcome(props), + environment: .init(gcd: .live, uiScreen: .mock, notificationCenter: .mock) + ) + viewController.view.frame = UIScreen.main.bounds + + assertSnapshot( + matching: viewController.view, + as: .image, + named: self.nameForDevice() + ) + } + + func test_welcomeWithAttachments() { + let props = Self.makeWelcomeProps( + theme: theme.secureConversationsWelcome, + uploads: [ + uploadedFileProps(), + failedFileUploadProps() + ] + ) + let viewController = SecureConversations.WelcomeViewController( + viewFactory: .mock(theme: theme, messageRenderer: nil, environment: .mock), + props: .welcome(props), + environment: .init(gcd: .live, uiScreen: .mock, notificationCenter: .mock) + ) + viewController.view.frame = UIScreen.main.bounds + + assertSnapshot( + matching: viewController.view, + as: .image, + named: self.nameForDevice() + ) + } + + func test_welcomeViewController_withValidationError() { + let props = Self.makeWelcomeProps(theme: theme.secureConversationsWelcome, warningMessage: "This is warning message") + let viewController = SecureConversations.WelcomeViewController( + viewFactory: .mock(theme: theme, messageRenderer: nil, environment: .mock), + props: .welcome(props), + environment: .init(gcd: .live, uiScreen: .mock, notificationCenter: .mock) + ) + viewController.view.frame = UIScreen.main.bounds + + assertSnapshot( + matching: viewController.view, + as: .image, + named: self.nameForDevice() + ) + } + + // MARK: - Helpers + + func uploadedFileProps() -> SecureConversations.FileUploadView.Props { + .init( + id: "id-a", + style: .messageCenter(theme.secureConversationsWelcome.attachmentListStyle.item), + state: .uploaded(.init(localFile: .mock())), + removeTapped: .nop + ) + } + + func failedFileUploadProps() -> SecureConversations.FileUploadView.Props { + .init( + id: "id-b", + style: .messageCenter(theme.secureConversationsWelcome.attachmentListStyle.item), + state: .error(.network), + removeTapped: .nop + ) + } + + static func headerProps() -> Header.Props { + .mock( + title: "Secure Conversations", + backButton: .init(style: .mock(image: Asset.back.image)), + closeButton: .init(style: .mock(image: Asset.close.image)) + ) + } + + static func makeWelcomeProps( + theme: SecureConversations.WelcomeStyle, + headerProps: Header.Props = headerProps(), + uploads: [SecureConversations.FileUploadView.Props] = [], + warningMessage: String = "" + ) -> SecureConversations.WelcomeView.Props { + .init( + style: theme, + checkMessageButtonTap: .nop, + filePickerButton: .init(isEnabled: true, tap: .nop), + sendMessageButton: .active(.nop), + messageTextViewProps: .active( + .init( + style: theme.messageTextViewStyle, + text: "Lorem Ipsum is simply dummy text of the printing and typesetting industry.", + textChanged: .nop, + activeChanged: .nop + ) + ), + warningMessage: .init(text: warningMessage, animated: false), + fileUploadListProps: .init( + maxUnscrollableViews: 3, + style: .chat(.mock), + uploads: .init(uploads), + isScrollingEnabled: true + ), + headerProps: headerProps, + isUiHidden: false + ) + } +} diff --git a/SnapshotTests/SnapshotTestCase.swift b/SnapshotTests/SnapshotTestCase.swift index 556233fac..ebffe284c 100644 --- a/SnapshotTests/SnapshotTestCase.swift +++ b/SnapshotTests/SnapshotTestCase.swift @@ -38,7 +38,7 @@ extension SnapshotTestCase { ) ] - static let possiblePrecision: Float = 0.99 + static let possiblePrecision: Float = 1.0 private static func checkSimulatorEnvironment() { guard SnapshotTestCase.testedDevices.contains(where: { $0.matchesCurrentDevice() }) else { diff --git a/SnapshotTests/SurveyViewControllerLayoutTests.swift b/SnapshotTests/SurveyViewControllerLayoutTests.swift new file mode 100644 index 000000000..17ca7fd41 --- /dev/null +++ b/SnapshotTests/SurveyViewControllerLayoutTests.swift @@ -0,0 +1,35 @@ +@testable import GliaWidgets +import SnapshotTesting +import XCTest + +class SurveyViewControllerLayoutTests: SnapshotTestCase { + func test_emptySurvey() { + let viewController = Survey.ViewController(viewFactory: .mock(), environment: .init(notificationCenter: .mock), props: .emptyPropsMock()) + viewController.view.frame = UIScreen.main.bounds + assertSnapshot( + matching: viewController, + as: .image, + named: nameForDevice() + ) + } + + func test_filledSurvey() { + let viewController = Survey.ViewController(viewFactory: .mock(), environment: .init(notificationCenter: .mock), props: .filledPropsMock()) + viewController.view.frame = UIScreen.main.bounds + assertSnapshot( + matching: viewController, + as: .image, + named: nameForDevice() + ) + } + + func test_emptySurveyErrorState() { + let viewController = Survey.ViewController(viewFactory: .mock(), environment: .init(notificationCenter: .mock), props: .errorPropsMock()) + viewController.view.frame = UIScreen.main.bounds + assertSnapshot( + matching: viewController, + as: .image, + named: nameForDevice() + ) + } +} diff --git a/SnapshotTests/VideoCallViewControllerLayoutTests.swift b/SnapshotTests/VideoCallViewControllerLayoutTests.swift new file mode 100644 index 000000000..a535999f7 --- /dev/null +++ b/SnapshotTests/VideoCallViewControllerLayoutTests.swift @@ -0,0 +1,59 @@ +@testable import GliaWidgets +import SnapshotTesting +import XCTest + +class VideoCallViewControllerLayoutTests: SnapshotTestCase { + func testVideoCallViewController() { + let videoCallViewProps: CallVisualizer.VideoCallView.Props = .mock( + buttonBarProps: .mock( + style: .mock( + chatButton: .mock(), + videButton: .mock( + inactive: .activeMock( + image: Asset.callVideoActive.image, + title: L10n.Call.Buttons.Video.title, + accessibility: .init(label: L10n.Call.Accessibility.Buttons.Video.Active.label) + ) + ), + muteButton: .mock( + inactive: .inactiveMock( + image: Asset.callMuteInactive.image, + title: L10n.Call.Buttons.Mute.Inactive.title, + accessibility: .init(label: L10n.Call.Accessibility.Buttons.Mute.Inactive.label) + ) + ), + speakerButton: .mock( + inactive: .inactiveMock( + image: Asset.callSpeakerInactive.image, + title: L10n.Call.Buttons.Speaker.title, + accessibility: .init(label: L10n.Call.Accessibility.Buttons.Speaker.Inactive.label) + ) + ), + minimizeButton: .mock( + inactive: .inactiveMock( + image: Asset.callMiminize.image, + title: L10n.Call.Buttons.Minimize.title, + accessibility: .init(label: L10n.Call.Accessibility.Buttons.Minimize.Inactive.label) + ) + ), + badge: .mock() + ) + ), + headerProps: .mock( + title: "Video", + effect: .blur, + backButton: .init(style: .mock(image: Asset.back.image)) + ) + ) + let props: CallVisualizer.VideoCallViewController.Props = .init(videoCallViewProps: videoCallViewProps) + + let videoCallViewController: CallVisualizer.VideoCallViewController = .mock(props: props) + videoCallViewController.view.frame = UIScreen.main.bounds + + assertSnapshot( + matching: videoCallViewController, + as: .image, + named: nameForDevice() + ) + } +} diff --git a/SnapshotTests/VisitorCodeViewControllerLayoutTests.swift b/SnapshotTests/VisitorCodeViewControllerLayoutTests.swift new file mode 100644 index 000000000..ddf554e06 --- /dev/null +++ b/SnapshotTests/VisitorCodeViewControllerLayoutTests.swift @@ -0,0 +1,106 @@ +@testable import GliaWidgets +import SnapshotTesting +import XCTest + +class VisitorCodeViewControllerLayoutTests: SnapshotTestCase { + func testVisitorCodeAlertWhenLoading() { + let props: CallVisualizer.VisitorCodeViewController.Props = .init( + visitorCodeViewProps: .init( + viewType: .alert(closeButtonTap: .nop), + viewState: .loading + ) + ) + let visitorCodeViewController = CallVisualizer.VisitorCodeViewController(props: props) + visitorCodeViewController.view.frame = UIScreen.main.bounds + + assertSnapshot( + matching: visitorCodeViewController, + as: .image, + named: nameForDevice() + ) + } + + func testVisitorCodeAlertWhenError() { + let props: CallVisualizer.VisitorCodeViewController.Props = .init( + visitorCodeViewProps: .init( + viewType: .alert(closeButtonTap: .nop), + viewState: .error(refreshTap: .nop) + ) + ) + let visitorCodeViewController = CallVisualizer.VisitorCodeViewController(props: props) + visitorCodeViewController.view.frame = UIScreen.main.bounds + + assertSnapshot( + matching: visitorCodeViewController, + as: .accessibilityImage(precision: Self.possiblePrecision), + named: nameForDevice() + ) + } + + func testVisitorCodeAlertWhenSuccess() { + let props: CallVisualizer.VisitorCodeViewController.Props = .init( + visitorCodeViewProps: .init( + viewType: .alert(closeButtonTap: .nop), + viewState: .success(visitorCode: "12345") + ) + ) + let visitorCodeViewController = CallVisualizer.VisitorCodeViewController(props: props) + visitorCodeViewController.view.frame = UIScreen.main.bounds + + assertSnapshot( + matching: visitorCodeViewController, + as: .accessibilityImage(precision: Self.possiblePrecision), + named: nameForDevice() + ) + } + + func testVisitorCodeEmbeddedWhenLoading() { + let props: CallVisualizer.VisitorCodeViewController.Props = .init( + visitorCodeViewProps: .init( + viewType: .embedded, + viewState: .loading + ) + ) + let visitorCodeViewController = CallVisualizer.VisitorCodeViewController(props: props) + visitorCodeViewController.view.frame = UIScreen.main.bounds + + assertSnapshot( + matching: visitorCodeViewController, + as: .accessibilityImage(precision: Self.possiblePrecision), + named: nameForDevice() + ) + } + + func testVisitorCodeEmbeddedWhenError() { + let props: CallVisualizer.VisitorCodeViewController.Props = .init( + visitorCodeViewProps: .init( + viewType: .embedded, + viewState: .error(refreshTap: .nop) + ) + ) + let visitorCodeViewController = CallVisualizer.VisitorCodeViewController(props: props) + visitorCodeViewController.view.frame = UIScreen.main.bounds + + assertSnapshot( + matching: visitorCodeViewController, + as: .accessibilityImage(precision: Self.possiblePrecision), + named: nameForDevice() + ) + } + func testVisitorCodeEmbeddedWhenSuccess() { + let props: CallVisualizer.VisitorCodeViewController.Props = .init( + visitorCodeViewProps: .init( + viewType: .embedded, + viewState: .success(visitorCode: "12345") + ) + ) + let visitorCodeViewController = CallVisualizer.VisitorCodeViewController(props: props) + visitorCodeViewController.view.frame = UIScreen.main.bounds + + assertSnapshot( + matching: visitorCodeViewController, + as: .accessibilityImage(precision: Self.possiblePrecision), + named: nameForDevice() + ) + } +} From f94359bac704496c16156aa43bb42cce61af7e90 Mon Sep 17 00:00:00 2001 From: Rasmus Tauts Date: Wed, 19 Jul 2023 15:21:47 +0300 Subject: [PATCH 08/64] Apply Unified Customization to Persistent Button This PR adds Unified customization to Persistent button, together with default theme. In GVAPersistentButtonVIew the ContainerView was moved to a stand-alone class so layoutSubviews() could be used properly, since gradient color can only be applied in that method. In addition, it adds system message to unified customization since it wasn't applied before but was actually ready to be used. MOB-2372 --- GliaWidgets.xcodeproj/project.pbxproj | 32 ++- .../RemoteConfiguration+Chat.swift | 12 ++ GliaWidgets/Sources/Theme/Theme+Chat.swift | 3 +- GliaWidgets/Sources/Theme/Theme+Gva.swift | 29 +++ GliaWidgets/Sources/Theme/ThemeColor.swift | 7 +- GliaWidgets/Sources/View/Chat/ChatStyle.swift | 18 +- GliaWidgets/Sources/View/Chat/ChatView.swift | 2 +- .../GvaPersistentButtonOptionView.swift | 31 ++- .../Chat/GVA/GvaPersistentButtonStyle.swift | 182 ++++++++++++++++++ .../GvaPersistentButtonView.swift} | 63 ++++-- .../GvaResponseTextView.swift | 0 .../Sources/View/Chat/GVA/GvaStyle.swift | 23 +++ 12 files changed, 366 insertions(+), 36 deletions(-) create mode 100644 GliaWidgets/Sources/Theme/Theme+Gva.swift rename GliaWidgets/Sources/View/Chat/{Message => GVA}/GvaPersistentButtonOptionView.swift (72%) create mode 100644 GliaWidgets/Sources/View/Chat/GVA/GvaPersistentButtonStyle.swift rename GliaWidgets/Sources/View/Chat/{Message/GVAPersistentButtonView.swift => GVA/GvaPersistentButtonView.swift} (63%) rename GliaWidgets/Sources/View/Chat/{Message => GVA}/GvaResponseTextView.swift (100%) create mode 100644 GliaWidgets/Sources/View/Chat/GVA/GvaStyle.swift diff --git a/GliaWidgets.xcodeproj/project.pbxproj b/GliaWidgets.xcodeproj/project.pbxproj index 42137d05c..2ba1dc3a1 100644 --- a/GliaWidgets.xcodeproj/project.pbxproj +++ b/GliaWidgets.xcodeproj/project.pbxproj @@ -529,8 +529,11 @@ C0175A172A5D30D7001FACDE /* GvaResponseTextView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0175A162A5D30D7001FACDE /* GvaResponseTextView.swift */; }; C0175A192A5D3C56001FACDE /* ChatView.Accessibility.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0175A182A5D3C56001FACDE /* ChatView.Accessibility.swift */; }; C0175A1D2A5D4226001FACDE /* ChatView.TableView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0175A1C2A5D4226001FACDE /* ChatView.TableView.swift */; }; - C0175A232A65614E001FACDE /* GVAPersistentButtonView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0175A222A65614E001FACDE /* GVAPersistentButtonView.swift */; }; + C0175A232A65614E001FACDE /* GvaPersistentButtonView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0175A222A65614E001FACDE /* GvaPersistentButtonView.swift */; }; C0175A252A66A431001FACDE /* GvaPersistentButtonOptionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0175A242A66A431001FACDE /* GvaPersistentButtonOptionView.swift */; }; + C0175A282A67D470001FACDE /* GvaPersistentButtonStyle.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0175A272A67D470001FACDE /* GvaPersistentButtonStyle.swift */; }; + C0175A2A2A67D499001FACDE /* GvaStyle.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0175A292A67D499001FACDE /* GvaStyle.swift */; }; + C0175A2C2A67E2E9001FACDE /* Theme+Gva.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0175A2B2A67E2E9001FACDE /* Theme+Gva.swift */; }; C03A8047292BA76D00DDECA6 /* ChatViewControllerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = C03A8046292BA76D00DDECA6 /* ChatViewControllerTests.swift */; }; C03A8049292BC8DB00DDECA6 /* CallViewControllerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = C03A8048292BC8DB00DDECA6 /* CallViewControllerTests.swift */; }; C05AB01C295F416700AA381F /* VisitorCodeCloseButtonProperties.swift in Sources */ = {isa = PBXBuildFile; fileRef = C05AB01B295F416700AA381F /* VisitorCodeCloseButtonProperties.swift */; }; @@ -1197,8 +1200,11 @@ C0175A162A5D30D7001FACDE /* GvaResponseTextView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GvaResponseTextView.swift; sourceTree = ""; }; C0175A182A5D3C56001FACDE /* ChatView.Accessibility.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatView.Accessibility.swift; sourceTree = ""; }; C0175A1C2A5D4226001FACDE /* ChatView.TableView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatView.TableView.swift; sourceTree = ""; }; - C0175A222A65614E001FACDE /* GVAPersistentButtonView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GVAPersistentButtonView.swift; sourceTree = ""; }; + C0175A222A65614E001FACDE /* GvaPersistentButtonView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GvaPersistentButtonView.swift; sourceTree = ""; }; C0175A242A66A431001FACDE /* GvaPersistentButtonOptionView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GvaPersistentButtonOptionView.swift; sourceTree = ""; }; + C0175A272A67D470001FACDE /* GvaPersistentButtonStyle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GvaPersistentButtonStyle.swift; sourceTree = ""; }; + C0175A292A67D499001FACDE /* GvaStyle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GvaStyle.swift; sourceTree = ""; }; + C0175A2B2A67E2E9001FACDE /* Theme+Gva.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Theme+Gva.swift"; sourceTree = ""; }; C03A8046292BA76D00DDECA6 /* ChatViewControllerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatViewControllerTests.swift; sourceTree = ""; }; C03A8048292BC8DB00DDECA6 /* CallViewControllerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CallViewControllerTests.swift; sourceTree = ""; }; C05AB016295DA9FC00AA381F /* AlertViewController+VisitorCode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "AlertViewController+VisitorCode.swift"; sourceTree = ""; }; @@ -1890,6 +1896,7 @@ C0857DEC28D4831E008D171D /* Theme+Text.swift */, C06A7587296ECD75006B69A2 /* Theme+VisitorCode.swift */, 84265DEF2983E62100D65842 /* Theme+ScreenSharing.swift */, + C0175A2B2A67E2E9001FACDE /* Theme+Gva.swift */, ); path = Theme; sourceTree = ""; @@ -1983,6 +1990,7 @@ 1A60AFD12566990F00E53F53 /* Chat */ = { isa = PBXGroup; children = ( + C0175A262A67D431001FACDE /* GVA */, 1AC7A7822583B65B00567FF8 /* Cells */, 9AB3402627FCDD92006E0FE2 /* ChatStyle.Accessibility.swift */, 1A60AFE725669C5000E53F53 /* ChatStyle.swift */, @@ -2146,9 +2154,6 @@ 845E2F6F283CF94100C04D56 /* VisitorChatMessageStyle.Accessibility.swift */, 3197F7AE29E95527008EE9F7 /* SystemMessageView.swift */, 3197F7B029E958F4008EE9F7 /* SystemMessageStyle.swift */, - C0175A162A5D30D7001FACDE /* GvaResponseTextView.swift */, - C0175A222A65614E001FACDE /* GVAPersistentButtonView.swift */, - C0175A242A66A431001FACDE /* GvaPersistentButtonOptionView.swift */, ); path = Message; sourceTree = ""; @@ -3194,6 +3199,18 @@ path = SecureConversations; sourceTree = ""; }; + C0175A262A67D431001FACDE /* GVA */ = { + isa = PBXGroup; + children = ( + C0175A162A5D30D7001FACDE /* GvaResponseTextView.swift */, + C0175A222A65614E001FACDE /* GvaPersistentButtonView.swift */, + C0175A242A66A431001FACDE /* GvaPersistentButtonOptionView.swift */, + C0175A272A67D470001FACDE /* GvaPersistentButtonStyle.swift */, + C0175A292A67D499001FACDE /* GvaStyle.swift */, + ); + path = GVA; + sourceTree = ""; + }; C05E3EDC29C99DEE0013BC81 /* ProximityManager */ = { isa = PBXGroup; children = ( @@ -4084,6 +4101,7 @@ 3197F7B629F7C2E5008EE9F7 /* SecureConversations.SecureChatModel.swift in Sources */, 1A0C9A7125C16F4A00815406 /* Theme+Alert.swift in Sources */, 1A38A8AC258B65D00089DE7B /* ChatMessageStyle.swift in Sources */, + C0175A282A67D470001FACDE /* GvaPersistentButtonStyle.swift in Sources */, C49A29F22614A85E00819269 /* ChoiceCard.swift in Sources */, 1A0C9A6D25C16EED00815406 /* Theme+Call.swift in Sources */, 1A60AFAF256680EF00E53F53 /* L10n.swift in Sources */, @@ -4108,6 +4126,7 @@ 845E2F7F283F99F200C04D56 /* Theme.Survey.ValidationError.swift in Sources */, 1A5F81402588B7BD00A605DA /* ChatMessageEntryStyle.swift in Sources */, C0D2F03D29914BB300803B47 /* VideoCallViewModel.DelegateEvent.swift in Sources */, + C0175A2A2A67D499001FACDE /* GvaStyle.swift in Sources */, 1A8B61D625C974D0000D780E /* ChatCallUpgradeStyle.swift in Sources */, 3100D92D296E946600DEC9CE /* SecureConversations.ConfirmationViewModel.swift in Sources */, 3100EEFB293F363100D57F71 /* SecureConversations.WelcomeView.swift in Sources */, @@ -4178,7 +4197,7 @@ 1AC7A7552582594200567FF8 /* Configuration.swift in Sources */, 75940988298D38C2008B173A /* VisitorCodeCoordinator.swift in Sources */, 1A4674B225E907120078FA1C /* AttachmentSourceItemStyle.swift in Sources */, - C0175A232A65614E001FACDE /* GVAPersistentButtonView.swift in Sources */, + C0175A232A65614E001FACDE /* GvaPersistentButtonView.swift in Sources */, 315BAB1A29ADFEBC00FF284B /* ConfirmationStyle+TitleStyle.swift in Sources */, 1A0C143625B85C3300B00695 /* CallView.swift in Sources */, 1AA738A72578DD0300E1120F /* ConnectStatusStyle.swift in Sources */, @@ -4287,6 +4306,7 @@ EB2CBB1527D8DB95004F178E /* OnHoldOverlayVisualEffectView.swift in Sources */, AFEF5C7129929601005C3D8D /* SecureConversations.FilePreviewView.swift in Sources */, 1A4AD3AF256D283700468BFB /* ChatMessageView.swift in Sources */, + C0175A2C2A67E2E9001FACDE /* Theme+Gva.swift in Sources */, 1A0EDF6D25E7AC100076D1AD /* UIImage+Extensions.swift in Sources */, 6E60DD5827146DDB001422EF /* SingleActionAlertConfiguration.swift in Sources */, C49A29E32614A29700819269 /* FilePreviewStyle.swift in Sources */, diff --git a/GliaWidgets/Sources/RemoteConfiguration/RemoteConfiguration+Chat.swift b/GliaWidgets/Sources/RemoteConfiguration/RemoteConfiguration+Chat.swift index e51e7ca25..013311b7e 100644 --- a/GliaWidgets/Sources/RemoteConfiguration/RemoteConfiguration+Chat.swift +++ b/GliaWidgets/Sources/RemoteConfiguration/RemoteConfiguration+Chat.swift @@ -17,6 +17,18 @@ extension RemoteConfiguration { let typingIndicator: Color? let newMessagesDividerColor: Color? let newMessagesDividerText: Text? + let systemMessage: MessageBalloon? + let gva: Gva? + } + + struct Gva: Codable { + let persistentButton: GvaPersistentButton? + } + + struct GvaPersistentButton: Codable { + let title: Text? + let background: Layer? + let button: Button? } struct Header: Codable { diff --git a/GliaWidgets/Sources/Theme/Theme+Chat.swift b/GliaWidgets/Sources/Theme/Theme+Chat.swift index 3f7740d23..f6e325b82 100644 --- a/GliaWidgets/Sources/Theme/Theme+Chat.swift +++ b/GliaWidgets/Sources/Theme/Theme+Chat.swift @@ -377,7 +377,8 @@ extension Theme { secureTranscriptTitle: Chat.SecureTranscript.headerTitle, secureTranscriptHeader: secureTranscriptHeader, unreadMessageDivider: unreadMessageDivider, - systemMessage: systemMessage + systemMessage: systemMessage, + gliaVirtualAssistant: gliaVirtualAssistantStyle ) } diff --git a/GliaWidgets/Sources/Theme/Theme+Gva.swift b/GliaWidgets/Sources/Theme/Theme+Gva.swift new file mode 100644 index 000000000..1ec90552a --- /dev/null +++ b/GliaWidgets/Sources/Theme/Theme+Gva.swift @@ -0,0 +1,29 @@ +import UIKit + +extension Theme { + var gliaVirtualAssistantStyle: GliaVirtualAssistantStyle { + let font = ThemeFontStyle.default.font + + let persistentButton: GvaPersistentButtonStyle = .init( + title: .init( + textFont: font.bodyText, + textColor: .black, + backgroundColor: .clear + ), + backgroundColor: .fill(color: color.lightGrey), + cornerRadius: 10, + borderWidth: 0, + borderColor: .clear, + button: .init( + textFont: font.caption, + textColor: .black, + backgroundColor: .fill(color: color.background), + cornerRadius: 5, + borderColor: .clear, + borderWidth: 0 + ) + ) + + return .init(persistentButton: persistentButton) + } +} diff --git a/GliaWidgets/Sources/Theme/ThemeColor.swift b/GliaWidgets/Sources/Theme/ThemeColor.swift index 2ead2b999..59f9f351a 100644 --- a/GliaWidgets/Sources/Theme/ThemeColor.swift +++ b/GliaWidgets/Sources/Theme/ThemeColor.swift @@ -26,6 +26,9 @@ public struct ThemeColor { /// Negative system color. By default used as a background color for "End Engagement" button, negative action button in alerts and as file download/upload error icon, progress bar and text color. public var systemNegative: UIColor + /// Light grey color. By default used as a background for gva persistent buttons and gallery cards. + public var lightGrey: UIColor + /// /// - Parameters: /// - primary: Primary color used by Widgets. By default used as a header background, visitor chat message background, positive alert button background and in many other places. @@ -44,7 +47,8 @@ public struct ThemeColor { baseDark: UIColor? = nil, baseShade: UIColor? = nil, background: UIColor? = nil, - systemNegative: UIColor? = nil + systemNegative: UIColor? = nil, + lightGrey: UIColor? = nil ) { self.primary = primary ?? Color.primary self.secondary = secondary ?? Color.secondary @@ -54,5 +58,6 @@ public struct ThemeColor { self.baseShade = baseShade ?? Color.baseShade self.background = background ?? Color.background self.systemNegative = systemNegative ?? Color.systemNegative + self.lightGrey = lightGrey ?? Color.lightGrey } } diff --git a/GliaWidgets/Sources/View/Chat/ChatStyle.swift b/GliaWidgets/Sources/View/Chat/ChatStyle.swift index 17a15b5c2..f42a531df 100644 --- a/GliaWidgets/Sources/View/Chat/ChatStyle.swift +++ b/GliaWidgets/Sources/View/Chat/ChatStyle.swift @@ -47,8 +47,12 @@ public class ChatStyle: EngagementStyle { /// Style for divider of unread messages in secure messaging transcript. public var unreadMessageDivider: UnreadMessageDividerStyle + /// Style of the system message public var systemMessage: SystemMessageStyle + /// Style of the Glia Virtual Assistant + public var gliaVirtualAssistant: GliaVirtualAssistantStyle + /// /// - Parameters: /// - header: Style of the view's header (navigation bar area) when the screen is displaying live chat. @@ -90,7 +94,8 @@ public class ChatStyle: EngagementStyle { secureTranscriptTitle: String, secureTranscriptHeader: HeaderStyle, unreadMessageDivider: UnreadMessageDividerStyle, - systemMessage: SystemMessageStyle + systemMessage: SystemMessageStyle, + gliaVirtualAssistant: GliaVirtualAssistantStyle ) { self.title = title self.visitorMessage = visitorMessage @@ -108,6 +113,7 @@ public class ChatStyle: EngagementStyle { self.secureTranscriptHeader = secureTranscriptHeader self.unreadMessageDivider = unreadMessageDivider self.systemMessage = systemMessage + self.gliaVirtualAssistant = gliaVirtualAssistant super.init( header: header, @@ -172,6 +178,16 @@ public class ChatStyle: EngagementStyle { text: configuration?.newMessagesDividerText, assetBuilder: assetsBuilder ) + systemMessage.apply( + configuration: configuration?.systemMessage, + assetsBuilder: assetsBuilder + ) + + gliaVirtualAssistant.apply( + configuration: configuration?.gva, + assetBuilder: assetsBuilder + ) + configuration?.background?.color.unwrap { switch $0.type { case .fill: diff --git a/GliaWidgets/Sources/View/Chat/ChatView.swift b/GliaWidgets/Sources/View/Chat/ChatView.swift index 43413cea9..d90a15ff0 100644 --- a/GliaWidgets/Sources/View/Chat/ChatView.swift +++ b/GliaWidgets/Sources/View/Chat/ChatView.swift @@ -885,7 +885,7 @@ extension ChatView { imageUrl: String? ) -> ChatItemCell.Content { let view = GvaPersistentButtonView( - with: style.choiceCard, + with: style, environment: .init( data: environment.data, uuid: environment.uuid, diff --git a/GliaWidgets/Sources/View/Chat/Message/GvaPersistentButtonOptionView.swift b/GliaWidgets/Sources/View/Chat/GVA/GvaPersistentButtonOptionView.swift similarity index 72% rename from GliaWidgets/Sources/View/Chat/Message/GvaPersistentButtonOptionView.swift rename to GliaWidgets/Sources/View/Chat/GVA/GvaPersistentButtonOptionView.swift index b8ecddcb9..39705c5b5 100644 --- a/GliaWidgets/Sources/View/Chat/Message/GvaPersistentButtonOptionView.swift +++ b/GliaWidgets/Sources/View/Chat/GVA/GvaPersistentButtonOptionView.swift @@ -7,8 +7,13 @@ class GvaPersistentButtonOptionView: BaseView { private let textLabel = UILabel() private let choiceButton = UIButton() private let viewInsets = UIEdgeInsets(top: 10, left: 12, bottom: 10, right: 12) + private let style: GvaPersistentButtonStyle.ButtonStyle - init(text: String?) { + init( + style: GvaPersistentButtonStyle.ButtonStyle, + text: String? + ) { + self.style = style self.text = text super.init() } @@ -20,13 +25,13 @@ class GvaPersistentButtonOptionView: BaseView { override func setup() { super.setup() - backgroundColor = .clear - // TODO: Styling will be done in a subsequent PR - layer.backgroundColor = UIColor.white.cgColor - layer.cornerRadius = 4 + layer.cornerRadius = style.cornerRadius + layer.borderWidth = style.borderWidth + layer.borderColor = style.borderColor.cgColor + textLabel.text = text - textLabel.font = .font(weight: .regular, size: 12) - textLabel.textColor = UIColor.black + textLabel.font = style.textFont + textLabel.textColor = style.textColor textLabel.textAlignment = .center textLabel.numberOfLines = 0 textLabel.isAccessibilityElement = false @@ -48,6 +53,18 @@ class GvaPersistentButtonOptionView: BaseView { constraints += choiceButton.layoutInSuperview() } + override func layoutSubviews() { + switch style.backgroundColor { + case .fill(let color): + backgroundColor = color + case .gradient(let colors): + makeGradientBackground( + colors: colors, + cornerRadius: style.cornerRadius + ) + } + } + private func applyStyle(_ style: ChoiceCardOptionStateStyle) { setFontScalingEnabled( style.accessibility.isFontScalingEnabled, diff --git a/GliaWidgets/Sources/View/Chat/GVA/GvaPersistentButtonStyle.swift b/GliaWidgets/Sources/View/Chat/GVA/GvaPersistentButtonStyle.swift new file mode 100644 index 000000000..cb127e962 --- /dev/null +++ b/GliaWidgets/Sources/View/Chat/GVA/GvaPersistentButtonStyle.swift @@ -0,0 +1,182 @@ +import UIKit + +/// Style of the GVA Persistent Button container +public struct GvaPersistentButtonStyle { + /// Title of the container + public var title: ChatTextContentStyle + + /// Background of the container + public var backgroundColor: ColorType + + /// Corner radius of the container + public var cornerRadius: CGFloat + + /// Border width of the container + public var borderWidth: CGFloat + + /// Border color of the container + public var borderColor: UIColor + + /// Persistent Button + public var button: ButtonStyle + + /// Initialization of the object + public init( + title: ChatTextContentStyle, + backgroundColor: ColorType, + cornerRadius: CGFloat, + borderWidth: CGFloat, + borderColor: UIColor, + button: ButtonStyle + ) { + self.title = title + self.backgroundColor = backgroundColor + self.cornerRadius = cornerRadius + self.borderWidth = borderWidth + self.borderColor = borderColor + self.button = button + } + + mutating func apply( + _ configuration: RemoteConfiguration.GvaPersistentButton?, + assetBuilder: RemoteConfiguration.AssetsBuilder + ) { + applyBackgroundConfiguration(configuration?.background) + applyTitleConfiguration(configuration?.title, assetBuilder: assetBuilder) + button.apply(configuration?.button, assetBuilder: assetBuilder) + } + + mutating private func applyTitleConfiguration( + _ configuration: RemoteConfiguration.Text?, + assetBuilder: RemoteConfiguration.AssetsBuilder + ) { + UIFont.convertToFont( + uiFont: assetBuilder.fontBuilder(configuration?.font), + textStyle: title.textStyle + ).unwrap { title.textFont = $0 } + + configuration?.foreground?.value + .map { UIColor(hex: $0) } + .first + .unwrap { title.textColor = $0 } + } + + mutating private func applyBackgroundConfiguration(_ configuration: RemoteConfiguration.Layer?) { + configuration?.border?.value + .map { UIColor(hex: $0) } + .first + .unwrap { borderColor = $0 } + + configuration?.borderWidth + .unwrap { borderWidth = $0 } + + configuration?.cornerRadius + .unwrap { cornerRadius = $0 } + + configuration?.color.unwrap { + switch $0.type { + case .fill: + $0.value + .map { UIColor(hex: $0) } + .first + .unwrap { backgroundColor = .fill(color: $0) } + case .gradient: + let colors = $0.value.convertToCgColors() + backgroundColor = .gradient(colors: colors) + } + } + } +} + +extension GvaPersistentButtonStyle { + /// Style of the Persistent Button + public struct ButtonStyle { + /// Font of the button + public var textFont: UIFont + + /// Color of the button + public var textColor: UIColor + + /// Text style of the button + public var textStyle: UIFont.TextStyle + + /// Background of the button + public var backgroundColor: ColorType + + /// Corner radius of the button + public var cornerRadius: CGFloat + + /// Border color of the button + public var borderColor: UIColor + + /// Border width of the button + public var borderWidth: CGFloat + + init( + textFont: UIFont, + textColor: UIColor, + textStyle: UIFont.TextStyle = .title2, + backgroundColor: ColorType, + cornerRadius: CGFloat, + borderColor: UIColor, + borderWidth: CGFloat + ) { + self.textFont = textFont + self.textColor = textColor + self.textStyle = textStyle + self.backgroundColor = backgroundColor + self.cornerRadius = cornerRadius + self.borderColor = borderColor + self.borderWidth = borderWidth + } + + mutating func apply( + _ configuration: RemoteConfiguration.Button?, + assetBuilder: RemoteConfiguration.AssetsBuilder + ) { + applyTitleConfiguration(configuration?.text, assetBuilder: assetBuilder) + applyBackgroundConfiguration(configuration?.background) + } + + mutating private func applyTitleConfiguration( + _ configuration: RemoteConfiguration.Text?, + assetBuilder: RemoteConfiguration.AssetsBuilder + ) { + UIFont.convertToFont( + uiFont: assetBuilder.fontBuilder(configuration?.font), + textStyle: textStyle + ).unwrap { textFont = $0 } + + configuration?.foreground?.value + .map { UIColor(hex: $0) } + .first + .unwrap { textColor = $0 } + } + + mutating private func applyBackgroundConfiguration(_ configuration: RemoteConfiguration.Layer?) { + configuration?.border?.value + .map { UIColor(hex: $0) } + .first + .unwrap { borderColor = $0 } + + configuration?.borderWidth + .unwrap { borderWidth = $0 } + + configuration?.cornerRadius + .unwrap { cornerRadius = $0 } + + configuration?.color.unwrap { + switch $0.type { + case .fill: + $0.value + .map { UIColor(hex: $0) } + .first + .unwrap { backgroundColor = .fill(color: $0) } + case .gradient: + let colors = $0.value.convertToCgColors() + backgroundColor = .gradient(colors: colors) + } + } + } + } +} diff --git a/GliaWidgets/Sources/View/Chat/Message/GVAPersistentButtonView.swift b/GliaWidgets/Sources/View/Chat/GVA/GvaPersistentButtonView.swift similarity index 63% rename from GliaWidgets/Sources/View/Chat/Message/GVAPersistentButtonView.swift rename to GliaWidgets/Sources/View/Chat/GVA/GvaPersistentButtonView.swift index 5c81a42df..e66814247 100644 --- a/GliaWidgets/Sources/View/Chat/Message/GVAPersistentButtonView.swift +++ b/GliaWidgets/Sources/View/Chat/GVA/GvaPersistentButtonView.swift @@ -3,14 +3,16 @@ import UIKit final class GvaPersistentButtonView: OperatorChatMessageView { var onOptionTapped: ((GvaOption) -> Void)! + private let viewStyle: GvaPersistentButtonStyle private let stackViewLayoutMargins = UIEdgeInsets(top: 16, left: 16, bottom: 16, right: 16) private let environment: Environment - init(with style: ChoiceCardStyle, environment: Environment) { + init(with style: ChatStyle, environment: Environment) { + self.viewStyle = style.gliaVirtualAssistant.persistentButton self.environment = environment super.init( - with: style, + with: style.operatorMessage, environment: .init( data: environment.data, uuid: environment.uuid, @@ -27,8 +29,8 @@ final class GvaPersistentButtonView: OperatorChatMessageView { override func appendContent(_ content: ChatMessageContent, animated: Bool) { switch content { - case let .gvaPersistentButton(choiceCard): - let contentView = self.contentView(for: choiceCard) + case let .gvaPersistentButton(persistentButton): + let contentView = self.contentView(for: persistentButton) appendContentView(contentView, animated: animated) default: break @@ -36,14 +38,7 @@ final class GvaPersistentButtonView: OperatorChatMessageView { } private func contentView(for persistentButton: GvaButton) -> UIView { - let containerView = UIView() - // TODO: Styling will be done in a subsequent PR - containerView.backgroundColor = UIColor(red: 0.953, green: 0.953, blue: 0.953, alpha: 1) - containerView.layer.cornerRadius = 8.49 - containerView.layer.borderWidth = 2 - // TODO: Styling will be done in a subsequent PR - containerView.layer.borderColor = UIColor.clear.cgColor - + let containerView = ContainerView(style: viewStyle) let stackView = UIStackView() stackView.axis = .vertical stackView.spacing = 16 @@ -52,13 +47,8 @@ final class GvaPersistentButtonView: OperatorChatMessageView { containerView.addSubview(stackView) stackView.layoutInSuperview().activate() - let tempChatTextContentStyle: ChatTextContentStyle = .init( - textFont: .font(weight: .regular, size: 16), - textColor: .black, - backgroundColor: .clear - ) let textView = ChatTextContentView( - with: tempChatTextContentStyle, + with: viewStyle.title, contentAlignment: .left, insets: .zero ) @@ -67,7 +57,10 @@ final class GvaPersistentButtonView: OperatorChatMessageView { setupAccessibilityProperties(for: textView) let optionViews: [UIView] = persistentButton.options.compactMap { option in - let optionView = GvaPersistentButtonOptionView(text: option.text) + let optionView = GvaPersistentButtonOptionView( + style: viewStyle.button, + text: option.text + ) optionView.tap = { [weak self] in self?.onOptionTapped(option) } @@ -100,3 +93,35 @@ extension GvaPersistentButtonView { textView.isAccessibilityElement = true } } + +extension GvaPersistentButtonView { + final class ContainerView: BaseView { + + let style: GvaPersistentButtonStyle + + init(style: GvaPersistentButtonStyle) { + self.style = style + super.init() + } + + required init() { + fatalError("init() has not been implemented") + } + + override func layoutSubviews() { + layer.cornerRadius = style.cornerRadius + layer.borderWidth = style.borderWidth + layer.borderColor = style.borderColor.cgColor + + switch style.backgroundColor { + case .fill(let color): + backgroundColor = color + case .gradient(let colors): + makeGradientBackground( + colors: colors, + cornerRadius: style.cornerRadius + ) + } + } + } +} diff --git a/GliaWidgets/Sources/View/Chat/Message/GvaResponseTextView.swift b/GliaWidgets/Sources/View/Chat/GVA/GvaResponseTextView.swift similarity index 100% rename from GliaWidgets/Sources/View/Chat/Message/GvaResponseTextView.swift rename to GliaWidgets/Sources/View/Chat/GVA/GvaResponseTextView.swift diff --git a/GliaWidgets/Sources/View/Chat/GVA/GvaStyle.swift b/GliaWidgets/Sources/View/Chat/GVA/GvaStyle.swift new file mode 100644 index 000000000..0977a958a --- /dev/null +++ b/GliaWidgets/Sources/View/Chat/GVA/GvaStyle.swift @@ -0,0 +1,23 @@ +import UIKit + +/// Style of the GVA views +public struct GliaVirtualAssistantStyle { + /// Style of Persistent Button + public var persistentButton: GvaPersistentButtonStyle + + public init( + persistentButton: GvaPersistentButtonStyle + ) { + self.persistentButton = persistentButton + } + + mutating func apply( + configuration: RemoteConfiguration.Gva?, + assetBuilder: RemoteConfiguration.AssetsBuilder + ) { + persistentButton.apply( + configuration?.persistentButton, + assetBuilder: assetBuilder + ) + } +} From 09e68de5dbb6156958b7d932e38af0daac9aa1d5 Mon Sep 17 00:00:00 2001 From: Egor Egorov Date: Wed, 19 Jul 2023 16:27:20 +0300 Subject: [PATCH 09/64] Add QuickReplyButton UI Added QuickReplyView which displays left-aligned button's grid Added handling QR button actions Added displaying QR messages MOB-2395 --- GliaWidgets.xcodeproj/project.pbxproj | 48 +++++++ .../Button/AdjustedTouchAreaButton.swift | 24 ++++ .../Sources/Component/Button/Button.swift | 16 +-- GliaWidgets/Sources/Theme/Theme+Gva.swift | 14 +- GliaWidgets/Sources/View/Chat/ChatView.swift | 45 ++++--- .../Sources/View/Chat/GVA/GvaStyle.swift | 10 +- .../GVA/QuickReply/QuickReplyButtonCell.swift | 76 +++++++++++ .../QuickReply/QuickReplyButtonStyle.swift | 43 +++++++ .../Chat/GVA/QuickReply/QuickReplyView.swift | 120 ++++++++++++++++++ .../LeftAlignedCollectionViewFlowLayout.swift | 22 ++++ .../SelfSizingCollectionView.swift | 32 +++++ .../Chat/ChatViewController.swift | 2 + .../ViewModel/Chat/ChatViewModel+GVA.swift | 63 +++++---- .../ViewModel/Chat/ChatViewModel.swift | 11 +- .../ViewModel/Chat/Data/ChatItem.swift | 4 +- 15 files changed, 472 insertions(+), 58 deletions(-) create mode 100644 GliaWidgets/Sources/Component/Button/AdjustedTouchAreaButton.swift create mode 100644 GliaWidgets/Sources/View/Chat/GVA/QuickReply/QuickReplyButtonCell.swift create mode 100644 GliaWidgets/Sources/View/Chat/GVA/QuickReply/QuickReplyButtonStyle.swift create mode 100644 GliaWidgets/Sources/View/Chat/GVA/QuickReply/QuickReplyView.swift create mode 100644 GliaWidgets/Sources/View/Common/LeftAlignedCollectionViewFlowLayout/LeftAlignedCollectionViewFlowLayout.swift create mode 100644 GliaWidgets/Sources/View/Common/SelfSizingCollectionView/SelfSizingCollectionView.swift diff --git a/GliaWidgets.xcodeproj/project.pbxproj b/GliaWidgets.xcodeproj/project.pbxproj index 2ba1dc3a1..0712cc5d9 100644 --- a/GliaWidgets.xcodeproj/project.pbxproj +++ b/GliaWidgets.xcodeproj/project.pbxproj @@ -353,6 +353,12 @@ 84681A8E2A5ED76300DD7406 /* ChatViewModel+GVA.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84681A8D2A5ED76300DD7406 /* ChatViewModel+GVA.swift */; }; 84681A952A61844000DD7406 /* ChatViewModelTests+Gva.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84681A942A61844000DD7406 /* ChatViewModelTests+Gva.swift */; }; 84681A982A61853300DD7406 /* GvaOption.Mock.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84681A972A61853300DD7406 /* GvaOption.Mock.swift */; }; + 84681A9B2A669D8800DD7406 /* QuickReplyView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84681A9A2A669D8800DD7406 /* QuickReplyView.swift */; }; + 84681A9D2A669DB500DD7406 /* QuickReplyButtonCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84681A9C2A669DB500DD7406 /* QuickReplyButtonCell.swift */; }; + 84681A9F2A66B70400DD7406 /* LeftAlignedCollectionViewFlowLayout.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84681A9E2A66B70400DD7406 /* LeftAlignedCollectionViewFlowLayout.swift */; }; + 84681AA12A66B9F100DD7406 /* SelfSizingCollectionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84681AA02A66B9F100DD7406 /* SelfSizingCollectionView.swift */; }; + 84681AA32A66D90000DD7406 /* AdjustedTouchAreaButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84681AA22A66D90000DD7406 /* AdjustedTouchAreaButton.swift */; }; + 84681AA72A681EF900DD7406 /* QuickReplyButtonStyle.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84681AA62A681EF900DD7406 /* QuickReplyButtonStyle.swift */; }; 846A5C3429CB3A130049B29F /* ScreenShareHandler.Implementation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 846A5C3329CB3A130049B29F /* ScreenShareHandler.Implementation.swift */; }; 846A5C3629CB3E270049B29F /* ScreenShareHandler.Mock.swift in Sources */ = {isa = PBXBuildFile; fileRef = 846A5C3529CB3E270049B29F /* ScreenShareHandler.Mock.swift */; }; 846A5C3929D18D400049B29F /* ScreenShareHandlerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 846A5C3829D18D400049B29F /* ScreenShareHandlerTests.swift */; }; @@ -1021,6 +1027,12 @@ 84681A8D2A5ED76300DD7406 /* ChatViewModel+GVA.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ChatViewModel+GVA.swift"; sourceTree = ""; }; 84681A942A61844000DD7406 /* ChatViewModelTests+Gva.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ChatViewModelTests+Gva.swift"; sourceTree = ""; }; 84681A972A61853300DD7406 /* GvaOption.Mock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GvaOption.Mock.swift; sourceTree = ""; }; + 84681A9A2A669D8800DD7406 /* QuickReplyView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = QuickReplyView.swift; sourceTree = ""; }; + 84681A9C2A669DB500DD7406 /* QuickReplyButtonCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = QuickReplyButtonCell.swift; sourceTree = ""; }; + 84681A9E2A66B70400DD7406 /* LeftAlignedCollectionViewFlowLayout.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LeftAlignedCollectionViewFlowLayout.swift; sourceTree = ""; }; + 84681AA02A66B9F100DD7406 /* SelfSizingCollectionView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SelfSizingCollectionView.swift; sourceTree = ""; }; + 84681AA22A66D90000DD7406 /* AdjustedTouchAreaButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AdjustedTouchAreaButton.swift; sourceTree = ""; }; + 84681AA62A681EF900DD7406 /* QuickReplyButtonStyle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = QuickReplyButtonStyle.swift; sourceTree = ""; }; 846A5C3329CB3A130049B29F /* ScreenShareHandler.Implementation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ScreenShareHandler.Implementation.swift; sourceTree = ""; }; 846A5C3529CB3E270049B29F /* ScreenShareHandler.Mock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ScreenShareHandler.Mock.swift; sourceTree = ""; }; 846A5C3829D18D400049B29F /* ScreenShareHandlerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ScreenShareHandlerTests.swift; sourceTree = ""; }; @@ -2097,6 +2109,7 @@ 1AC7A7B42587A07D00567FF8 /* Action */, 1A60B0132567FC6400E53F53 /* Style */, 1A60B0142567FC7000E53F53 /* Button.swift */, + 84681AA22A66D90000DD7406 /* AdjustedTouchAreaButton.swift */, ); path = Button; sourceTree = ""; @@ -2189,6 +2202,8 @@ 1A63B2F0257A3F0F00508478 /* Common */ = { isa = PBXGroup; children = ( + 84681AA52A681E8400DD7406 /* LeftAlignedCollectionViewFlowLayout */, + 84681AA42A681E6900DD7406 /* SelfSizingCollectionView */, 1AA738B025790D4B00E1120F /* Alert */, ); path = Common; @@ -3001,6 +3016,32 @@ path = Mocks; sourceTree = ""; }; + 84681A992A669D5000DD7406 /* QuickReply */ = { + isa = PBXGroup; + children = ( + 84681A9A2A669D8800DD7406 /* QuickReplyView.swift */, + 84681A9C2A669DB500DD7406 /* QuickReplyButtonCell.swift */, + 84681AA62A681EF900DD7406 /* QuickReplyButtonStyle.swift */, + ); + path = QuickReply; + sourceTree = ""; + }; + 84681AA42A681E6900DD7406 /* SelfSizingCollectionView */ = { + isa = PBXGroup; + children = ( + 84681AA02A66B9F100DD7406 /* SelfSizingCollectionView.swift */, + ); + path = SelfSizingCollectionView; + sourceTree = ""; + }; + 84681AA52A681E8400DD7406 /* LeftAlignedCollectionViewFlowLayout */ = { + isa = PBXGroup; + children = ( + 84681A9E2A66B70400DD7406 /* LeftAlignedCollectionViewFlowLayout.swift */, + ); + path = LeftAlignedCollectionViewFlowLayout; + sourceTree = ""; + }; 846A5C3729D18D220049B29F /* ScreenShareHandler */ = { isa = PBXGroup; children = ( @@ -3202,6 +3243,7 @@ C0175A262A67D431001FACDE /* GVA */ = { isa = PBXGroup; children = ( + 84681A992A669D5000DD7406 /* QuickReply */, C0175A162A5D30D7001FACDE /* GvaResponseTextView.swift */, C0175A222A65614E001FACDE /* GvaPersistentButtonView.swift */, C0175A242A66A431001FACDE /* GvaPersistentButtonOptionView.swift */, @@ -4087,6 +4129,7 @@ 84D5B9642A151B6100807F92 /* QuickLookBased.Mock.swift in Sources */, 311CAFCD29F8FAE20067B59F /* SecureConversations.TranscriptModel+CustomCard.swift in Sources */, 9A19926B27D3BA8700161AAE /* ViewFactory.Environment.Mock.swift in Sources */, + 84681A9B2A669D8800DD7406 /* QuickReplyView.swift in Sources */, 1A6EB05725A717CB0007081A /* ChatMessage.swift in Sources */, 1AA738B225790D5A00E1120F /* AlertView.swift in Sources */, 845E2F8E283FB5B500C04D56 /* Theme.Survey.SingleQuestion.Accessibility.swift in Sources */, @@ -4141,6 +4184,7 @@ C0D2F08229A4E75200803B47 /* Header.Mock.swift in Sources */, 1A4AD3B3256D2A7600468BFB /* VisitorChatMessageStyle.swift in Sources */, 845E2F98283FC9A900C04D56 /* Theme.Survey.OptionButton.swift in Sources */, + 84681A9F2A66B70400DD7406 /* LeftAlignedCollectionViewFlowLayout.swift in Sources */, 84265E60298D7B2900D65842 /* ScreenSharingCoordinator+Environment.swift in Sources */, 9AB196DC27C3FFCC00FD60AB /* Call.Environment.Interface.swift in Sources */, 3197F7AF29E95527008EE9F7 /* SystemMessageView.swift in Sources */, @@ -4185,6 +4229,7 @@ 845A28FC28AFF092008558EA /* URLScheme.swift in Sources */, 75F58EE127E7D5300065BA2D /* Survey.ViewController.Props.swift in Sources */, 754CC61527E27C42005676E9 /* Survey.Checkbox.swift in Sources */, + 84681AA72A681EF900DD7406 /* QuickReplyButtonStyle.swift in Sources */, 1A4AD3CA256E864800468BFB /* ThemeColorStyle.swift in Sources */, AFA2FDF228907E9D00428E6D /* GliaViewController.Mock.swift in Sources */, 9AB196DE27C3FFF400FD60AB /* Call.Environment.Mock.swift in Sources */, @@ -4218,6 +4263,7 @@ 9A19926527D3BA3A00161AAE /* UIKitBased.Mock.swift in Sources */, C0D2F06B29A4DAA000803B47 /* VideoCallViewMock.swift in Sources */, 1AFB1E6225F7AE1300CA460D /* ChatEngagementFile.swift in Sources */, + 84681A9D2A669DB500DD7406 /* QuickReplyButtonCell.swift in Sources */, 3100EEF2293E214B00D57F71 /* SecureConversations.Coordinator.swift in Sources */, 1AA738AE2578E0D500E1120F /* ConnectAnimationView.swift in Sources */, 754CC61627E2816F005676E9 /* Survey.InputQuestionView.swift in Sources */, @@ -4279,6 +4325,7 @@ 755D186B29A6A5830009F5E8 /* WelcomeStyle+MessageTitleStyle.swift in Sources */, 75940981298D38C2008B173A /* VisitorCodeView+NumberView.swift in Sources */, 7529F2B427E1D503004D3581 /* Survey.swift in Sources */, + 84681AA12A66B9F100DD7406 /* SelfSizingCollectionView.swift in Sources */, 1A5F494B25CA86CA003E3678 /* Call.swift in Sources */, 84265E65298D7B2900D65842 /* ScreenSharingView.swift in Sources */, 9A1992ED27D6C19E00161AAE /* FileSystemStorage.Environment.Mock.swift in Sources */, @@ -4369,6 +4416,7 @@ 7594093D298D376B008B173A /* RemoteConfiguration+AssetsBuilder.swift in Sources */, 9A3E1D9B27B73246005634EB /* FoundationBased.Mock.swift in Sources */, 75FF151427F3A2D600FE7BE2 /* Theme+Survey.swift in Sources */, + 84681AA32A66D90000DD7406 /* AdjustedTouchAreaButton.swift in Sources */, 1A60AFE825669C5000E53F53 /* ChatStyle.swift in Sources */, 75940984298D38C2008B173A /* VisitorCodeViewController.swift in Sources */, 1A4AF5C725AEEA42002CD0F4 /* Operator+Extensions.swift in Sources */, diff --git a/GliaWidgets/Sources/Component/Button/AdjustedTouchAreaButton.swift b/GliaWidgets/Sources/Component/Button/AdjustedTouchAreaButton.swift new file mode 100644 index 000000000..134620016 --- /dev/null +++ b/GliaWidgets/Sources/Component/Button/AdjustedTouchAreaButton.swift @@ -0,0 +1,24 @@ +import UIKit + +class AdjustedTouchAreaButton: UIButton { + private var touchAreaInsets: TouchAreaInsets? + + init(touchAreaInsets: TouchAreaInsets? = nil) { + super.init(frame: .zero) + self.touchAreaInsets = touchAreaInsets + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override func point(inside point: CGPoint, with event: UIEvent?) -> Bool { + guard let insets = touchAreaInsets else { + return super.point(inside: point, with: event) + } + + let area = bounds.insetBy(dx: insets.dx, dy: insets.dy) + + return area.contains(point) + } +} diff --git a/GliaWidgets/Sources/Component/Button/Button.swift b/GliaWidgets/Sources/Component/Button/Button.swift index 0e655ddf9..ca50431c4 100644 --- a/GliaWidgets/Sources/Component/Button/Button.swift +++ b/GliaWidgets/Sources/Component/Button/Button.swift @@ -1,8 +1,10 @@ import UIKit -class Button: UIButton { +class Button: AdjustedTouchAreaButton { var tap: (() -> Void)? + var touchAreaInsets: TouchAreaInsets? + override var isEnabled: Bool { didSet { super.isEnabled = isEnabled @@ -16,7 +18,7 @@ class Button: UIButton { init(kind: ButtonKind, tap: (() -> Void)? = nil) { self.kind = kind self.tap = tap - super.init(frame: .zero) + super.init(touchAreaInsets: kind.properties.touchAreaInsets) setup() layout() } @@ -58,14 +60,4 @@ class Button: UIButton { @objc private func tapped() { tap?() } - - override func point(inside point: CGPoint, with event: UIEvent?) -> Bool { - guard let insets = kind.properties.touchAreaInsets else { - return super.point(inside: point, with: event) - } - - let area = bounds.insetBy(dx: insets.dx, dy: insets.dy) - - return area.contains(point) - } } diff --git a/GliaWidgets/Sources/Theme/Theme+Gva.swift b/GliaWidgets/Sources/Theme/Theme+Gva.swift index 1ec90552a..0b5ec77ef 100644 --- a/GliaWidgets/Sources/Theme/Theme+Gva.swift +++ b/GliaWidgets/Sources/Theme/Theme+Gva.swift @@ -24,6 +24,18 @@ extension Theme { ) ) - return .init(persistentButton: persistentButton) + let quickReplyButtonStyle: GvaQuickReplyButtonStyle = .init( + textFont: font.buttonLabel, + textColor: Color.primary, + backgroundColor: .fill(color: Color.baseLight), + cornerRadius: 10, + borderColor: Color.primary, + borderWidth: 1 + ) + + return .init( + persistentButton: persistentButton, + quickReplyButtonStyle: quickReplyButtonStyle + ) } } diff --git a/GliaWidgets/Sources/View/Chat/ChatView.swift b/GliaWidgets/Sources/View/Chat/ChatView.swift index d90a15ff0..dbba67a77 100644 --- a/GliaWidgets/Sources/View/Chat/ChatView.swift +++ b/GliaWidgets/Sources/View/Chat/ChatView.swift @@ -25,6 +25,7 @@ class ChatView: EngagementView { var selectCustomCardOption: ((HtmlMetadata.Option, MessageRenderer.Message.Identifier) -> Void)? var gvaButtonTapped: ((GvaOption) -> Void)? + private lazy var quickReplyView = QuickReplyView(style: style.gliaVirtualAssistant.quickReplyButtonStyle) private let style: ChatStyle private var messageEntryViewBottomConstraint: NSLayoutConstraint! private var callBubble: BubbleView? @@ -161,6 +162,10 @@ class ChatView: EngagementView { typingIndicatorView.heightAnchor.constraint(equalToConstant: 10) ]) + addSubview(quickReplyView) + constraints += quickReplyView.layoutIn(safeAreaLayoutGuide, edges: .horizontal) + constraints += quickReplyView.topAnchor.constraint(equalTo: tableAndIndicatorStack.bottomAnchor) + addSubview(messageEntryView) let messageEntryInsets = UIEdgeInsets( top: 0, @@ -176,7 +181,7 @@ class ChatView: EngagementView { constraints += messageEntryViewBottomConstraint constraints += messageEntryView.layoutIn(safeAreaLayoutGuide, edges: .horizontal) - constraints += messageEntryView.topAnchor.constraint(equalTo: tableAndIndicatorStack.bottomAnchor) + constraints += messageEntryView.topAnchor.constraint(equalTo: quickReplyView.bottomAnchor) addSubview(unreadMessageIndicatorView) unreadMessageIndicatorView.translatesAutoresizingMaskIntoConstraints = false @@ -310,7 +315,11 @@ extension ChatView { tableView.reloadData() } - func renderHeaderProps() { + func renderQuickReply(props: QuickReplyView.Props) { + quickReplyView.props = props + } + + private func renderHeaderProps() { header.props = props.header } } @@ -371,24 +380,28 @@ extension ChatView { case .systemMessage(let message): return systemMessageContent(message) case let .gvaPersistentButton(message, button, showImage, imageUrl): - return gvaPersistenButtonContent( + return gvaPersistentButtonContent( message, button: button, showImage: showImage, imageUrl: imageUrl ) case let .gvaResponseText(message, text, showImage, imageUrl): - return gvaResponseTextContent( + let view = gvaResponseTextView( message, - text: text, + text: text.content, showImage: showImage, imageUrl: imageUrl ) - case let .gvaQuickReply(_, button): - // Temporary, since UI hasn't been implemented - let textView = UITextView() - textView.text = "Quick Reply: \(button.content)" - return .gvaQuickReply(textView) + return .gvaResponseText(view) + case let .gvaQuickReply(message, button, showImage, imageUrl): + let view = gvaResponseTextView( + message, + text: button.content, + showImage: showImage, + imageUrl: imageUrl + ) + return .gvaQuickReply(view) case let .gvaGallery(_, gallery): // Temporary, since UI hasn't been implemented let textView = UITextView() @@ -838,12 +851,12 @@ extension ChatView { return .unreadMessagesDivider(messageDivider) } - private func gvaResponseTextContent( + private func gvaResponseTextView( _ message: ChatMessage, - text: GvaResponseText, + text: NSAttributedString, showImage: Bool, imageUrl: String? - ) -> ChatItemCell.Content { + ) -> GvaResponseTextView { let view = GvaResponseTextView( with: style.operatorMessage, environment: .init( @@ -856,7 +869,7 @@ extension ChatView { ) view.appendContent( .attributedText( - text.content, + text, accessibility: Self.operatorAccessibilityMessage( for: message, operator: style.accessibility.operator, @@ -875,10 +888,10 @@ extension ChatView { view.linkTapped = { [weak self] in self?.linkTapped?($0) } view.showsOperatorImage = showImage view.setOperatorImage(fromUrl: imageUrl, animated: false) - return .gvaResponseText(view) + return view } - private func gvaPersistenButtonContent( + private func gvaPersistentButtonContent( _ message: ChatMessage, button: GvaButton, showImage: Bool, diff --git a/GliaWidgets/Sources/View/Chat/GVA/GvaStyle.swift b/GliaWidgets/Sources/View/Chat/GVA/GvaStyle.swift index 0977a958a..a718b775e 100644 --- a/GliaWidgets/Sources/View/Chat/GVA/GvaStyle.swift +++ b/GliaWidgets/Sources/View/Chat/GVA/GvaStyle.swift @@ -5,10 +5,18 @@ public struct GliaVirtualAssistantStyle { /// Style of Persistent Button public var persistentButton: GvaPersistentButtonStyle + /// Style for Quick Reply buttons. + public var quickReplyButtonStyle: GvaQuickReplyButtonStyle + + /// - Parameters: + /// - persistentButton: Style of Persistent Button + /// - quickReplyButtonStyle: Style for Quick Reply buttons. public init( - persistentButton: GvaPersistentButtonStyle + persistentButton: GvaPersistentButtonStyle, + quickReplyButtonStyle: GvaQuickReplyButtonStyle ) { self.persistentButton = persistentButton + self.quickReplyButtonStyle = quickReplyButtonStyle } mutating func apply( diff --git a/GliaWidgets/Sources/View/Chat/GVA/QuickReply/QuickReplyButtonCell.swift b/GliaWidgets/Sources/View/Chat/GVA/QuickReply/QuickReplyButtonCell.swift new file mode 100644 index 000000000..ae540e3ca --- /dev/null +++ b/GliaWidgets/Sources/View/Chat/GVA/QuickReply/QuickReplyButtonCell.swift @@ -0,0 +1,76 @@ +import UIKit + +final class QuickReplyButtonCell: UICollectionViewCell { + static let identifier = "QuickReplyButtonCell" + + var props: Props = .nop { + didSet { + button.setTitle(props.title, for: .normal) + button.accessibilityLabel = props.title + } + } + + var style: GvaQuickReplyButtonStyle? { + didSet { + guard let style else { return } + applyStyle(style) + } + } + + private lazy var button: AdjustedTouchAreaButton = { + let button = AdjustedTouchAreaButton(touchAreaInsets: (dx: 0, dy: -8)) + button.translatesAutoresizingMaskIntoConstraints = false + button.contentEdgeInsets = .init(top: 6, left: 16, bottom: 6, right: 16) + button.addTarget(self, action: #selector(tap), for: .touchUpInside) + return button + }() + + override init(frame: CGRect) { + super.init(frame: frame) + contentView.addSubview(button) + NSLayoutConstraint.activate([ + button.heightAnchor.constraint(greaterThanOrEqualToConstant: 32), + button.topAnchor.constraint(equalTo: contentView.topAnchor, constant: 8), + button.leadingAnchor.constraint(equalTo: contentView.leadingAnchor), + button.trailingAnchor.constraint(equalTo: contentView.trailingAnchor), + button.bottomAnchor.constraint(equalTo: contentView.bottomAnchor, constant: -8) + ]) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + @objc private func tap() { + props.action() + } +} + +// MARK: - Private + +private extension QuickReplyButtonCell { + func applyStyle(_ style: GvaQuickReplyButtonStyle) { + switch style.backgroundColor { + case .fill(let color): + button.backgroundColor = color + case .gradient(let colors): + button.makeGradientBackground(colors: colors) + } + button.setTitleColor(style.textColor, for: .normal) + button.titleLabel?.font = style.textFont + button.layer.cornerRadius = style.cornerRadius + button.layer.borderColor = style.borderColor.cgColor + button.layer.borderWidth = style.borderWidth + } +} + +// MARK: - Props + +extension QuickReplyButtonCell { + struct Props { + let title: String + let action: Cmd + + static var nop: Props { .init(title: "", action: .nop) } + } +} diff --git a/GliaWidgets/Sources/View/Chat/GVA/QuickReply/QuickReplyButtonStyle.swift b/GliaWidgets/Sources/View/Chat/GVA/QuickReply/QuickReplyButtonStyle.swift new file mode 100644 index 000000000..80da59cf3 --- /dev/null +++ b/GliaWidgets/Sources/View/Chat/GVA/QuickReply/QuickReplyButtonStyle.swift @@ -0,0 +1,43 @@ +import UIKit + +/// Style of the GVA Quick Reply Button +public struct GvaQuickReplyButtonStyle { + /// Font of the button + public var textFont: UIFont + + /// Color of the button + public var textColor: UIColor + + /// Text style of the button + public var textStyle: UIFont.TextStyle + + /// Background of the button + public var backgroundColor: ColorType + + /// Corner radius of the button + public var cornerRadius: CGFloat + + /// Border color of the button + public var borderColor: UIColor + + /// Border width of the button + public var borderWidth: CGFloat + + init( + textFont: UIFont, + textColor: UIColor, + textStyle: UIFont.TextStyle = .title2, + backgroundColor: ColorType, + cornerRadius: CGFloat, + borderColor: UIColor, + borderWidth: CGFloat + ) { + self.textFont = textFont + self.textColor = textColor + self.textStyle = textStyle + self.backgroundColor = backgroundColor + self.cornerRadius = cornerRadius + self.borderColor = borderColor + self.borderWidth = borderWidth + } +} diff --git a/GliaWidgets/Sources/View/Chat/GVA/QuickReply/QuickReplyView.swift b/GliaWidgets/Sources/View/Chat/GVA/QuickReply/QuickReplyView.swift new file mode 100644 index 000000000..a0d2a58d3 --- /dev/null +++ b/GliaWidgets/Sources/View/Chat/GVA/QuickReply/QuickReplyView.swift @@ -0,0 +1,120 @@ +import UIKit + +final class QuickReplyView: BaseView { + var props: Props = .hidden { + didSet { + renderProps() + } + } + + let style: GvaQuickReplyButtonStyle + private lazy var collectionView: SelfSizingCollectionView = { + let layout = LeftAlignedCollectionViewFlowLayout() + layout.estimatedItemSize = UICollectionViewFlowLayout.automaticSize + layout.minimumLineSpacing = 0 + layout.minimumInteritemSpacing = 12 + layout.sectionInset = .init(top: 8, left: 16, bottom: 8, right: 16) + return SelfSizingCollectionView( + frame: .zero, + collectionViewLayout: layout + ) + }() + private var collectionViewHeightConstraint: NSLayoutConstraint? + + init(style: GvaQuickReplyButtonStyle) { + self.style = style + super.init() + } + + @available(*, unavailable) + required init() { + fatalError("init() has not been implemented") + } + + override func setup() { + super.setup() + collectionView.register( + QuickReplyButtonCell.self, + forCellWithReuseIdentifier: QuickReplyButtonCell.identifier + ) + collectionView.dataSource = self + + addSubview(collectionView) + collectionView.translatesAutoresizingMaskIntoConstraints = false + } + + override func defineLayout() { + super.defineLayout() + let height = collectionView.heightAnchor.constraint(equalToConstant: 0) + height.priority = .required + collectionViewHeightConstraint = height + + NSLayoutConstraint.activate([ + height, + collectionView.topAnchor.constraint(equalTo: safeAreaLayoutGuide.topAnchor), + collectionView.bottomAnchor.constraint(equalTo: safeAreaLayoutGuide.bottomAnchor), + collectionView.leadingAnchor.constraint(equalTo: safeAreaLayoutGuide.leadingAnchor), + collectionView.trailingAnchor.constraint(equalTo: safeAreaLayoutGuide.trailingAnchor) + ]) + } +} + +// MARK: - Private + +private extension QuickReplyView { + func renderProps() { + UIView.animate(withDuration: 0.3) { + switch self.props { + case .shown: + self.collectionViewHeightConstraint?.priority = .defaultLow + case .hidden: + self.collectionViewHeightConstraint?.priority = .required + } + self.layoutIfNeeded() + } + collectionView.reloadData() + } +} + +// MARK: - UICollectionViewDataSource + +extension QuickReplyView: UICollectionViewDataSource { + func collectionView( + _ collectionView: UICollectionView, + numberOfItemsInSection section: Int + ) -> Int { + props.buttons?.count ?? 0 + } + + func collectionView( + _ collectionView: UICollectionView, + cellForItemAt indexPath: IndexPath + ) -> UICollectionViewCell { + guard let props = props.buttons, + let cell = collectionView.dequeueReusableCell( + withReuseIdentifier: QuickReplyButtonCell.identifier, + for: indexPath + ) as? QuickReplyButtonCell else { + return UICollectionViewCell() + } + cell.style = style + cell.props = props[indexPath.item] + return cell + } +} + +// MARK: - Props + +extension QuickReplyView { + enum Props { + case shown([QuickReplyButtonCell.Props]) + case hidden + + var buttons: [QuickReplyButtonCell.Props]? { + switch self { + case let .shown(buttons): return buttons + case .hidden: return nil + } + } + } +} diff --git a/GliaWidgets/Sources/View/Common/LeftAlignedCollectionViewFlowLayout/LeftAlignedCollectionViewFlowLayout.swift b/GliaWidgets/Sources/View/Common/LeftAlignedCollectionViewFlowLayout/LeftAlignedCollectionViewFlowLayout.swift new file mode 100644 index 000000000..7519926eb --- /dev/null +++ b/GliaWidgets/Sources/View/Common/LeftAlignedCollectionViewFlowLayout/LeftAlignedCollectionViewFlowLayout.swift @@ -0,0 +1,22 @@ +import UIKit + +final class LeftAlignedCollectionViewFlowLayout: UICollectionViewFlowLayout { + override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? { + let attributes = super.layoutAttributesForElements(in: rect) + + var leftMargin = sectionInset.left + var maxY: CGFloat = -1.0 + attributes?.forEach { layoutAttribute in + if layoutAttribute.frame.origin.y >= maxY { + leftMargin = sectionInset.left + } + + layoutAttribute.frame.origin.x = leftMargin + + leftMargin += layoutAttribute.frame.width + minimumInteritemSpacing + maxY = max(layoutAttribute.frame.maxY, maxY) + } + + return attributes + } +} diff --git a/GliaWidgets/Sources/View/Common/SelfSizingCollectionView/SelfSizingCollectionView.swift b/GliaWidgets/Sources/View/Common/SelfSizingCollectionView/SelfSizingCollectionView.swift new file mode 100644 index 000000000..8bd3fd424 --- /dev/null +++ b/GliaWidgets/Sources/View/Common/SelfSizingCollectionView/SelfSizingCollectionView.swift @@ -0,0 +1,32 @@ +import UIKit + +final class SelfSizingCollectionView: UICollectionView { + override init(frame: CGRect, collectionViewLayout layout: UICollectionViewLayout) { + super.init(frame: frame, collectionViewLayout: layout) + commonInit() + } + + required init?(coder aDecoder: NSCoder) { + super.init(coder: aDecoder) + commonInit() + } + + private func commonInit() { + isScrollEnabled = false + } + + override var contentSize: CGSize { + didSet { + invalidateIntrinsicContentSize() + } + } + + override func reloadData() { + super.reloadData() + self.invalidateIntrinsicContentSize() + } + + override var intrinsicContentSize: CGSize { + return contentSize + } +} diff --git a/GliaWidgets/Sources/ViewController/Chat/ChatViewController.swift b/GliaWidgets/Sources/ViewController/Chat/ChatViewController.swift index 52d380045..4f1d00428 100644 --- a/GliaWidgets/Sources/ViewController/Chat/ChatViewController.swift +++ b/GliaWidgets/Sources/ViewController/Chat/ChatViewController.swift @@ -166,6 +166,8 @@ final class ChatViewController: EngagementViewController, PopoverPresenter { view?.unreadMessageIndicatorView.setImage(fromUrl: imageUrl, animated: true) case let .fileUploadListPropsUpdated(fileUploadListProps): view?.messageEntryView.uploadListView.props = fileUploadListProps + case let .quickReplyPropsUpdated(props): + view?.renderQuickReply(props: props) } self?.renderProps() } diff --git a/GliaWidgets/Sources/ViewModel/Chat/ChatViewModel+GVA.swift b/GliaWidgets/Sources/ViewModel/Chat/ChatViewModel+GVA.swift index 068d1f9ec..e4c3462b0 100644 --- a/GliaWidgets/Sources/ViewModel/Chat/ChatViewModel+GVA.swift +++ b/GliaWidgets/Sources/ViewModel/Chat/ChatViewModel+GVA.swift @@ -7,6 +7,17 @@ private extension String { } extension ChatViewModel { + func quickReplyOption(_ gvaOption: GvaOption) -> QuickReplyButtonCell.Props { + let action = Cmd { [weak self] in + self?.gvaOptionAction(for: gvaOption)() + self?.action?(.quickReplyPropsUpdated(.hidden)) + } + return .init( + title: gvaOption.text, + action: action + ) + } + func gvaOptionAction(for option: GvaOption) -> Cmd { // If `option.destinationPdBroadcastEvent` is specified, // this is broadcast event button, which is not supported @@ -23,6 +34,33 @@ extension ChatViewModel { return postbackButtonAction(for: option) } } + + func postbackButtonAction(for option: GvaOption) -> Cmd { + .init { [weak self] in + guard let value = option.value else { return } + let singleChoiceOption = SingleChoiceOption(text: option.text, value: value) + self?.environment.sendSelectedOptionValue(singleChoiceOption) { [weak self] result in + guard let self = self else { return } + switch result { + case let .success(message): + let chatMessage = ChatMessage(with: message) + let item = ChatItem( + with: chatMessage, + isCustomCardSupported: self.isCustomCardSupported + ) + if let item { + self.appendItem(item, to: self.messagesSection, animated: true) + } + self.action?(.scrollToBottom(animated: true)) + case .failure: + self.showAlert( + with: self.alertConfiguration.unexpectedError, + dismissed: nil + ) + } + } + } + } } private extension ChatViewModel { @@ -58,31 +96,6 @@ private extension ChatViewModel { } } - func postbackButtonAction(for option: GvaOption) -> Cmd { - .init { [weak self] in - guard let value = option.value else { return } - let singleChoiceOption = SingleChoiceOption(text: option.text, value: value) - self?.environment.sendSelectedOptionValue(singleChoiceOption) { [weak self] result in - guard let self = self else { return } - switch result { - case let .success(message): - let chatMessage = ChatMessage(with: message) - if let item = ChatItem( - with: chatMessage, - isCustomCardSupported: self.isCustomCardSupported - ) { - self.appendItem(item, to: self.messagesSection, animated: true) - } - case .failure: - self.showAlert( - with: self.alertConfiguration.unexpectedError, - dismissed: nil - ) - } - } - } - } - func broadcastEventButtonAction() -> Cmd { .init { [weak self] in guard let self = self else { return } diff --git a/GliaWidgets/Sources/ViewModel/Chat/ChatViewModel.swift b/GliaWidgets/Sources/ViewModel/Chat/ChatViewModel.swift index 6b5e4ab1d..8698e2d7e 100644 --- a/GliaWidgets/Sources/ViewModel/Chat/ChatViewModel.swift +++ b/GliaWidgets/Sources/ViewModel/Chat/ChatViewModel.swift @@ -577,9 +577,17 @@ extension ChatViewModel { if isChatBottomReached { action?(.scrollToBottom(animated: true)) } + + if case .gvaQuickReply(_, let button, _, _) = item.kind { + let props = button.options.map { quickReplyOption($0) } + action?(.quickReplyPropsUpdated(.shown(props))) + } } default: - break + // All Quick Reply buttons of the same set should disappear + // after the user taps on one of the buttons or when + // there is a new message from the user or GVA + action?(.quickReplyPropsUpdated(.hidden)) } } @@ -861,6 +869,7 @@ extension ChatViewModel { case setOperatorTypingIndicatorIsHiddenTo(Bool, _ isChatScrolledToBottom: Bool) case setAttachmentButtonVisibility(MediaPickerButtonVisibility) case fileUploadListPropsUpdated(SecureConversations.FileUploadListView.Props) + case quickReplyPropsUpdated(QuickReplyView.Props) } enum DelegateEvent { diff --git a/GliaWidgets/Sources/ViewModel/Chat/Data/ChatItem.swift b/GliaWidgets/Sources/ViewModel/Chat/Data/ChatItem.swift index 7b6fbe5c7..7a82a9d24 100644 --- a/GliaWidgets/Sources/ViewModel/Chat/Data/ChatItem.swift +++ b/GliaWidgets/Sources/ViewModel/Chat/Data/ChatItem.swift @@ -54,7 +54,7 @@ class ChatItem { case let .gvaResponseText(text): kind = .gvaResponseText(message, responseText: text, showImage: true, imageUrl: message.operator?.pictureUrl) case let .gvaQuickReply(button): - kind = .gvaQuickReply(message, quickReply: button) + kind = .gvaQuickReply(message, quickReply: button, showImage: true, imageUrl: message.operator?.pictureUrl) case let .gvaGallery(gallery): kind = .gvaGallery(message, gallery: gallery) case .none: @@ -85,7 +85,7 @@ extension ChatItem { case systemMessage(ChatMessage) case gvaPersistentButton(ChatMessage, persistenButton: GvaButton, showImage: Bool, imageUrl: String?) case gvaResponseText(ChatMessage, responseText: GvaResponseText, showImage: Bool, imageUrl: String?) - case gvaQuickReply(ChatMessage, quickReply: GvaButton) + case gvaQuickReply(ChatMessage, quickReply: GvaButton, showImage: Bool, imageUrl: String?) case gvaGallery(ChatMessage, gallery: GvaGallery) } } From 580f37dfec0e9ca056091cc8b3d0a86942e453d2 Mon Sep 17 00:00:00 2001 From: Egor Egorov Date: Mon, 24 Jul 2023 15:58:50 +0300 Subject: [PATCH 10/64] Add Quick Reply button to RemoteConfiguration MOB-2397 --- .../RemoteConfiguration+Chat.swift | 1 + GliaWidgets/Sources/Theme/Theme+Gva.swift | 4 +- GliaWidgets/Sources/View/Chat/ChatView.swift | 4 +- .../Sources/View/Chat/GVA/GvaStyle.swift | 12 +++-- .../QuickReply/QuickReplyButtonStyle.swift | 49 +++++++++++++++++++ .../ViewController/ViewController.swift | 2 +- 6 files changed, 64 insertions(+), 8 deletions(-) diff --git a/GliaWidgets/Sources/RemoteConfiguration/RemoteConfiguration+Chat.swift b/GliaWidgets/Sources/RemoteConfiguration/RemoteConfiguration+Chat.swift index 013311b7e..80db3eb51 100644 --- a/GliaWidgets/Sources/RemoteConfiguration/RemoteConfiguration+Chat.swift +++ b/GliaWidgets/Sources/RemoteConfiguration/RemoteConfiguration+Chat.swift @@ -23,6 +23,7 @@ extension RemoteConfiguration { struct Gva: Codable { let persistentButton: GvaPersistentButton? + let quickReplyButton: Button? } struct GvaPersistentButton: Codable { diff --git a/GliaWidgets/Sources/Theme/Theme+Gva.swift b/GliaWidgets/Sources/Theme/Theme+Gva.swift index 0b5ec77ef..b95e38312 100644 --- a/GliaWidgets/Sources/Theme/Theme+Gva.swift +++ b/GliaWidgets/Sources/Theme/Theme+Gva.swift @@ -24,7 +24,7 @@ extension Theme { ) ) - let quickReplyButtonStyle: GvaQuickReplyButtonStyle = .init( + let quickReplyButton: GvaQuickReplyButtonStyle = .init( textFont: font.buttonLabel, textColor: Color.primary, backgroundColor: .fill(color: Color.baseLight), @@ -35,7 +35,7 @@ extension Theme { return .init( persistentButton: persistentButton, - quickReplyButtonStyle: quickReplyButtonStyle + quickReplyButton: quickReplyButton ) } } diff --git a/GliaWidgets/Sources/View/Chat/ChatView.swift b/GliaWidgets/Sources/View/Chat/ChatView.swift index dbba67a77..2fd4dc8b9 100644 --- a/GliaWidgets/Sources/View/Chat/ChatView.swift +++ b/GliaWidgets/Sources/View/Chat/ChatView.swift @@ -25,7 +25,9 @@ class ChatView: EngagementView { var selectCustomCardOption: ((HtmlMetadata.Option, MessageRenderer.Message.Identifier) -> Void)? var gvaButtonTapped: ((GvaOption) -> Void)? - private lazy var quickReplyView = QuickReplyView(style: style.gliaVirtualAssistant.quickReplyButtonStyle) + private lazy var quickReplyView = QuickReplyView( + style: style.gliaVirtualAssistant.quickReplyButton + ) private let style: ChatStyle private var messageEntryViewBottomConstraint: NSLayoutConstraint! private var callBubble: BubbleView? diff --git a/GliaWidgets/Sources/View/Chat/GVA/GvaStyle.swift b/GliaWidgets/Sources/View/Chat/GVA/GvaStyle.swift index a718b775e..718197130 100644 --- a/GliaWidgets/Sources/View/Chat/GVA/GvaStyle.swift +++ b/GliaWidgets/Sources/View/Chat/GVA/GvaStyle.swift @@ -6,17 +6,17 @@ public struct GliaVirtualAssistantStyle { public var persistentButton: GvaPersistentButtonStyle /// Style for Quick Reply buttons. - public var quickReplyButtonStyle: GvaQuickReplyButtonStyle + public var quickReplyButton: GvaQuickReplyButtonStyle /// - Parameters: /// - persistentButton: Style of Persistent Button - /// - quickReplyButtonStyle: Style for Quick Reply buttons. + /// - quickReplyButton: Style for Quick Reply buttons. public init( persistentButton: GvaPersistentButtonStyle, - quickReplyButtonStyle: GvaQuickReplyButtonStyle + quickReplyButton: GvaQuickReplyButtonStyle ) { self.persistentButton = persistentButton - self.quickReplyButtonStyle = quickReplyButtonStyle + self.quickReplyButton = quickReplyButton } mutating func apply( @@ -27,5 +27,9 @@ public struct GliaVirtualAssistantStyle { configuration?.persistentButton, assetBuilder: assetBuilder ) + quickReplyButton.apply( + configuration?.quickReplyButton, + assetBuilder: assetBuilder + ) } } diff --git a/GliaWidgets/Sources/View/Chat/GVA/QuickReply/QuickReplyButtonStyle.swift b/GliaWidgets/Sources/View/Chat/GVA/QuickReply/QuickReplyButtonStyle.swift index 80da59cf3..5ce0f589e 100644 --- a/GliaWidgets/Sources/View/Chat/GVA/QuickReply/QuickReplyButtonStyle.swift +++ b/GliaWidgets/Sources/View/Chat/GVA/QuickReply/QuickReplyButtonStyle.swift @@ -40,4 +40,53 @@ public struct GvaQuickReplyButtonStyle { self.borderColor = borderColor self.borderWidth = borderWidth } + + mutating func apply( + _ configuration: RemoteConfiguration.Button?, + assetBuilder: RemoteConfiguration.AssetsBuilder + ) { + applyTitleConfiguration(configuration?.text, assetBuilder: assetBuilder) + applyBackgroundConfiguration(configuration?.background) + } + + mutating private func applyTitleConfiguration( + _ configuration: RemoteConfiguration.Text?, + assetBuilder: RemoteConfiguration.AssetsBuilder + ) { + UIFont.convertToFont( + uiFont: assetBuilder.fontBuilder(configuration?.font), + textStyle: textStyle + ).unwrap { textFont = $0 } + + configuration?.foreground?.value + .map { UIColor(hex: $0) } + .first + .unwrap { textColor = $0 } + } + + mutating private func applyBackgroundConfiguration(_ configuration: RemoteConfiguration.Layer?) { + configuration?.border?.value + .map { UIColor(hex: $0) } + .first + .unwrap { borderColor = $0 } + + configuration?.borderWidth + .unwrap { borderWidth = $0 } + + configuration?.cornerRadius + .unwrap { cornerRadius = $0 } + + configuration?.color.unwrap { + switch $0.type { + case .fill: + $0.value + .map { UIColor(hex: $0) } + .first + .unwrap { backgroundColor = .fill(color: $0) } + case .gradient: + let colors = $0.value.convertToCgColors() + backgroundColor = .gradient(colors: colors) + } + } + } } diff --git a/TestingApp/ViewController/ViewController.swift b/TestingApp/ViewController/ViewController.swift index 4a160f539..c7c718fdb 100644 --- a/TestingApp/ViewController/ViewController.swift +++ b/TestingApp/ViewController/ViewController.swift @@ -320,7 +320,7 @@ extension ViewController { func retrieveRemoteConfiguration(_ fileName: String) -> RemoteConfiguration? { guard - let url = Bundle.main.url(forResource: fileName, withExtension: "json"), + let url = Bundle.main.url(forResource: fileName, withExtension: "json", subdirectory: "UnifiedUI"), let jsonData = try? Data(contentsOf: url), let config = try? JSONDecoder().decode(RemoteConfiguration.self, from: .init(jsonData)) else { From 9ba22bdb9dc06a951c35177a7e520dcbc6d65e69 Mon Sep 17 00:00:00 2001 From: Igor Kravchenko Date: Mon, 24 Jul 2023 21:30:00 +0300 Subject: [PATCH 11/64] Introduce snapshot tests for dynamic type font Add initial snapshot tests for dynamic type fonts. Note that these tests reveal that some UI does not take into account dynamic font type, so appropriate tickets are to be created. MOB-2476 --- GliaWidgets.xcodeproj/project.pbxproj | 44 ++++++ ...rtViewControllerDynamicTypeFontTests.swift | 67 ++++++++++ .../BubbleViewDynamicTypeFontTests.swift | 16 +++ ...llViewControllerDynamicTypeFontTests.swift | 65 +++++++++ ...tCallUpgradeViewDynamicTypeFontTests.swift | 23 ++++ ...atViewControllerDynamicTypeFontTests.swift | 55 ++++++++ ...reViewControllerDynamicTypeFontTests.swift | 28 ++++ ...nfirmationScreenDynamicTypeFontTests.swift | 48 +++++++ ...onsWelcomeScreenDynamicTypeFontTests.swift | 125 ++++++++++++++++++ SnapshotTests/SnapshotTestCase.swift | 22 +++ ...eyViewControllerDynamicTypeFontTests.swift | 35 +++++ ...llViewControllerDynamicTypeFontTests.swift | 61 +++++++++ ...deViewControllerDynamicTypeFontTests.swift | 108 +++++++++++++++ 13 files changed, 697 insertions(+) create mode 100644 SnapshotTests/AlertViewControllerDynamicTypeFontTests.swift create mode 100644 SnapshotTests/BubbleViewDynamicTypeFontTests.swift create mode 100644 SnapshotTests/CallViewControllerDynamicTypeFontTests.swift create mode 100644 SnapshotTests/ChatCallUpgradeViewDynamicTypeFontTests.swift create mode 100644 SnapshotTests/ChatViewControllerDynamicTypeFontTests.swift create mode 100644 SnapshotTests/ScreenShareViewControllerDynamicTypeFontTests.swift create mode 100644 SnapshotTests/SecureConversationsConfirmationScreenDynamicTypeFontTests.swift create mode 100644 SnapshotTests/SecureConversationsWelcomeScreenDynamicTypeFontTests.swift create mode 100644 SnapshotTests/SurveyViewControllerDynamicTypeFontTests.swift create mode 100644 SnapshotTests/VideoCallViewControllerDynamicTypeFontTests.swift create mode 100644 SnapshotTests/VisitorCodeViewControllerDynamicTypeFontTests.swift diff --git a/GliaWidgets.xcodeproj/project.pbxproj b/GliaWidgets.xcodeproj/project.pbxproj index 0712cc5d9..dc2e63f6d 100644 --- a/GliaWidgets.xcodeproj/project.pbxproj +++ b/GliaWidgets.xcodeproj/project.pbxproj @@ -468,6 +468,17 @@ 9AE9E4B527E0EE2E00BFE239 /* CallViewControllerVoiceOverTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9AE9E4B427E0EE2E00BFE239 /* CallViewControllerVoiceOverTests.swift */; }; 9AE9E4B727E1E30500BFE239 /* MockHelpers.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9AE9E4B627E1E30500BFE239 /* MockHelpers.swift */; }; AD57658B6D7BB11749113284 /* Pods_SnapshotTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 9131A19E516E338E979C0C43 /* Pods_SnapshotTests.framework */; }; + AF03A7AF2A6E7DC40081887D /* AlertViewControllerDynamicTypeFontTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = AF03A7AE2A6E7DC40081887D /* AlertViewControllerDynamicTypeFontTests.swift */; }; + AF03A7B12A6E96870081887D /* BubbleViewDynamicTypeFontTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = AF03A7B02A6E96870081887D /* BubbleViewDynamicTypeFontTests.swift */; }; + AF03A7B32A6EA5490081887D /* CallViewControllerDynamicTypeFontTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = AF03A7B22A6EA5490081887D /* CallViewControllerDynamicTypeFontTests.swift */; }; + AF03A7B52A6EAA950081887D /* ChatCallUpgradeViewDynamicTypeFontTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = AF03A7B42A6EAA950081887D /* ChatCallUpgradeViewDynamicTypeFontTests.swift */; }; + AF03A7B72A6EAFBA0081887D /* ChatViewControllerDynamicTypeFontTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = AF03A7B62A6EAFBA0081887D /* ChatViewControllerDynamicTypeFontTests.swift */; }; + AF03A7B92A6ED1240081887D /* ScreenShareViewControllerDynamicTypeFontTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = AF03A7B82A6ED1240081887D /* ScreenShareViewControllerDynamicTypeFontTests.swift */; }; + AF03A7BB2A6ED73E0081887D /* SecureConversationsWelcomeScreenDynamicTypeFontTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = AF03A7BA2A6ED73E0081887D /* SecureConversationsWelcomeScreenDynamicTypeFontTests.swift */; }; + AF03A7BD2A6EDACF0081887D /* SecureConversationsConfirmationScreenDynamicTypeFontTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = AF03A7BC2A6EDACF0081887D /* SecureConversationsConfirmationScreenDynamicTypeFontTests.swift */; }; + AF03A7BF2A6EDCBF0081887D /* SurveyViewControllerDynamicTypeFontTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = AF03A7BE2A6EDCBF0081887D /* SurveyViewControllerDynamicTypeFontTests.swift */; }; + AF03A7C12A6EDE190081887D /* VideoCallViewControllerDynamicTypeFontTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = AF03A7C02A6EDE190081887D /* VideoCallViewControllerDynamicTypeFontTests.swift */; }; + AF03A7C32A6EDF490081887D /* VisitorCodeViewControllerDynamicTypeFontTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = AF03A7C22A6EDF490081887D /* VisitorCodeViewControllerDynamicTypeFontTests.swift */; }; AF0D26D629705FDF00816CCB /* SecureConversations.ActivityIndicator.swift in Sources */ = {isa = PBXBuildFile; fileRef = AF0D26D529705FDE00816CCB /* SecureConversations.ActivityIndicator.swift */; }; AF0D26D82971912A00816CCB /* SecureConversations.SendMessageButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = AF0D26D72971912A00816CCB /* SecureConversations.SendMessageButton.swift */; }; AF10ED8B29B7A4C000E85309 /* ChatViewModel+ChoiceCards.swift in Sources */ = {isa = PBXBuildFile; fileRef = AF10ED8A29B7A4C000E85309 /* ChatViewModel+ChoiceCards.swift */; }; @@ -1145,6 +1156,17 @@ 9AE9E4B427E0EE2E00BFE239 /* CallViewControllerVoiceOverTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CallViewControllerVoiceOverTests.swift; sourceTree = ""; }; 9AE9E4B627E1E30500BFE239 /* MockHelpers.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockHelpers.swift; sourceTree = ""; }; AB58EB188E5FABE4A07F2ACD /* Pods-GliaWidgets.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-GliaWidgets.release.xcconfig"; path = "Target Support Files/Pods-GliaWidgets/Pods-GliaWidgets.release.xcconfig"; sourceTree = ""; }; + AF03A7AE2A6E7DC40081887D /* AlertViewControllerDynamicTypeFontTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AlertViewControllerDynamicTypeFontTests.swift; sourceTree = ""; }; + AF03A7B02A6E96870081887D /* BubbleViewDynamicTypeFontTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BubbleViewDynamicTypeFontTests.swift; sourceTree = ""; }; + AF03A7B22A6EA5490081887D /* CallViewControllerDynamicTypeFontTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CallViewControllerDynamicTypeFontTests.swift; sourceTree = ""; }; + AF03A7B42A6EAA950081887D /* ChatCallUpgradeViewDynamicTypeFontTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatCallUpgradeViewDynamicTypeFontTests.swift; sourceTree = ""; }; + AF03A7B62A6EAFBA0081887D /* ChatViewControllerDynamicTypeFontTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatViewControllerDynamicTypeFontTests.swift; sourceTree = ""; }; + AF03A7B82A6ED1240081887D /* ScreenShareViewControllerDynamicTypeFontTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ScreenShareViewControllerDynamicTypeFontTests.swift; sourceTree = ""; }; + AF03A7BA2A6ED73E0081887D /* SecureConversationsWelcomeScreenDynamicTypeFontTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SecureConversationsWelcomeScreenDynamicTypeFontTests.swift; sourceTree = ""; }; + AF03A7BC2A6EDACF0081887D /* SecureConversationsConfirmationScreenDynamicTypeFontTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SecureConversationsConfirmationScreenDynamicTypeFontTests.swift; sourceTree = ""; }; + AF03A7BE2A6EDCBF0081887D /* SurveyViewControllerDynamicTypeFontTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SurveyViewControllerDynamicTypeFontTests.swift; sourceTree = ""; }; + AF03A7C02A6EDE190081887D /* VideoCallViewControllerDynamicTypeFontTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VideoCallViewControllerDynamicTypeFontTests.swift; sourceTree = ""; }; + AF03A7C22A6EDF490081887D /* VisitorCodeViewControllerDynamicTypeFontTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VisitorCodeViewControllerDynamicTypeFontTests.swift; sourceTree = ""; }; AF0D26D529705FDE00816CCB /* SecureConversations.ActivityIndicator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SecureConversations.ActivityIndicator.swift; sourceTree = ""; }; AF0D26D72971912A00816CCB /* SecureConversations.SendMessageButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SecureConversations.SendMessageButton.swift; sourceTree = ""; }; AF10ED8A29B7A4C000E85309 /* ChatViewModel+ChoiceCards.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ChatViewModel+ChoiceCards.swift"; sourceTree = ""; }; @@ -3101,28 +3123,39 @@ children = ( 846E822728996A5C008EFBF0 /* AlertViewControllerTests.swift */, AF22C8842A6154780004BF3C /* AlertViewControllerLayoutTests.swift */, + AF03A7AE2A6E7DC40081887D /* AlertViewControllerDynamicTypeFontTests.swift */, 9AB3401227F71D5D006E0FE2 /* BubbleViewVoiceOverTests.swift */, AF22C8862A6182AF0004BF3C /* BubbleViewLayoutTests.swift */, + AF03A7B02A6E96870081887D /* BubbleViewDynamicTypeFontTests.swift */, 9AE9E4B427E0EE2E00BFE239 /* CallViewControllerVoiceOverTests.swift */, AF22C8882A6184C50004BF3C /* CallViewControllerLayoutTests.swift */, + AF03A7B22A6EA5490081887D /* CallViewControllerDynamicTypeFontTests.swift */, 9AB3402828002422006E0FE2 /* ChatCallUpgradeViewVoiceOverTests.swift */, AF22C88A2A6186A20004BF3C /* ChatCallUpgradeViewLayoutTests.swift */, + AF03A7B42A6EAA950081887D /* ChatCallUpgradeViewDynamicTypeFontTests.swift */, 9A1992D627D61F8100161AAE /* ChatViewControllerVoiceOverTests.swift */, AF22C88C2A6188EF0004BF3C /* ChatViewControllerLayoutTests.swift */, + AF03A7B62A6EAFBA0081887D /* ChatViewControllerDynamicTypeFontTests.swift */, C07FA04C29B0E41A00E9FB7F /* ScreenShareViewControllerTests.swift */, AF22C88E2A618B9D0004BF3C /* ScreenShareViewControllerLayoutTests.swift */, + AF03A7B82A6ED1240081887D /* ScreenShareViewControllerDynamicTypeFontTests.swift */, 75CF8D9029C3A85C00CB1524 /* SecureConversationsWelcomeScreenTests.swift */, AF22C8902A6198FF0004BF3C /* SecureConversationsWelcomeScreenLayoutTests.swift */, + AF03A7BA2A6ED73E0081887D /* SecureConversationsWelcomeScreenDynamicTypeFontTests.swift */, 75CF8DAC29C8F2B500CB1524 /* SecureConversationsConfirmationScreenTests.swift */, AF22C8922A619F5C0004BF3C /* SecureConversationsConfirmationScreenLayoutTests.swift */, + AF03A7BC2A6EDACF0081887D /* SecureConversationsConfirmationScreenDynamicTypeFontTests.swift */, 9A1992D727D61F8100161AAE /* SnapshotTestCase.swift */, 8458769E2823FD18007AC3DF /* SurveyViewControllerVoiceOverTests.swift */, AF22C8942A61A6B80004BF3C /* SurveyViewControllerLayoutTests.swift */, + AF03A7BE2A6EDCBF0081887D /* SurveyViewControllerDynamicTypeFontTests.swift */, C07FA04D29B0E41A00E9FB7F /* VideoCallViewControllerTests.swift */, AF22C8962A61A9BF0004BF3C /* VideoCallViewControllerLayoutTests.swift */, + AF03A7C02A6EDE190081887D /* VideoCallViewControllerDynamicTypeFontTests.swift */, C07FA04E29B0E41A00E9FB7F /* VisitorCodeViewControllerTests.swift */, AF755FD92A71583900871E36 /* BubbleWindowLayoutTests.swift */, AF22C8982A61AE930004BF3C /* VisitorCodeViewControllerLayoutTests.swift */, + AF03A7C22A6EDF490081887D /* VisitorCodeViewControllerDynamicTypeFontTests.swift */, ); path = SnapshotTests; sourceTree = ""; @@ -4541,17 +4574,25 @@ buildActionMask = 2147483647; files = ( 846E822828996A5C008EFBF0 /* AlertViewControllerTests.swift in Sources */, + AF03A7BB2A6ED73E0081887D /* SecureConversationsWelcomeScreenDynamicTypeFontTests.swift in Sources */, 75CF8D9129C3A85C00CB1524 /* SecureConversationsWelcomeScreenTests.swift in Sources */, AF22C8972A61A9BF0004BF3C /* VideoCallViewControllerLayoutTests.swift in Sources */, + AF03A7BD2A6EDACF0081887D /* SecureConversationsConfirmationScreenDynamicTypeFontTests.swift in Sources */, + AF03A7B92A6ED1240081887D /* ScreenShareViewControllerDynamicTypeFontTests.swift in Sources */, 9AE9E4B527E0EE2E00BFE239 /* CallViewControllerVoiceOverTests.swift in Sources */, + AF03A7BF2A6EDCBF0081887D /* SurveyViewControllerDynamicTypeFontTests.swift in Sources */, AF22C8852A6154780004BF3C /* AlertViewControllerLayoutTests.swift in Sources */, AF22C8872A6182AF0004BF3C /* BubbleViewLayoutTests.swift in Sources */, + AF03A7C12A6EDE190081887D /* VideoCallViewControllerDynamicTypeFontTests.swift in Sources */, C07FA05029B0E41A00E9FB7F /* VideoCallViewControllerTests.swift in Sources */, AF755FDA2A71583900871E36 /* BubbleWindowLayoutTests.swift in Sources */, AF22C88D2A6188EF0004BF3C /* ChatViewControllerLayoutTests.swift in Sources */, + AF03A7B12A6E96870081887D /* BubbleViewDynamicTypeFontTests.swift in Sources */, + AF03A7AF2A6E7DC40081887D /* AlertViewControllerDynamicTypeFontTests.swift in Sources */, AF22C8992A61AE930004BF3C /* VisitorCodeViewControllerLayoutTests.swift in Sources */, 9AB3401327F71D5D006E0FE2 /* BubbleViewVoiceOverTests.swift in Sources */, AF22C8932A619F5C0004BF3C /* SecureConversationsConfirmationScreenLayoutTests.swift in Sources */, + AF03A7B32A6EA5490081887D /* CallViewControllerDynamicTypeFontTests.swift in Sources */, 9A1992D827D61F8100161AAE /* ChatViewControllerVoiceOverTests.swift in Sources */, AF22C8892A6184C50004BF3C /* CallViewControllerLayoutTests.swift in Sources */, 8458769F2823FD18007AC3DF /* SurveyViewControllerVoiceOverTests.swift in Sources */, @@ -4561,9 +4602,12 @@ C07FA04F29B0E41A00E9FB7F /* ScreenShareViewControllerTests.swift in Sources */, AF22C8952A61A6B80004BF3C /* SurveyViewControllerLayoutTests.swift in Sources */, 9A1992D927D61F8100161AAE /* SnapshotTestCase.swift in Sources */, + AF03A7B52A6EAA950081887D /* ChatCallUpgradeViewDynamicTypeFontTests.swift in Sources */, + AF03A7C32A6EDF490081887D /* VisitorCodeViewControllerDynamicTypeFontTests.swift in Sources */, AF22C88F2A618B9D0004BF3C /* ScreenShareViewControllerLayoutTests.swift in Sources */, AF22C8912A6198FF0004BF3C /* SecureConversationsWelcomeScreenLayoutTests.swift in Sources */, AF22C88B2A6186A20004BF3C /* ChatCallUpgradeViewLayoutTests.swift in Sources */, + AF03A7B72A6EAFBA0081887D /* ChatViewControllerDynamicTypeFontTests.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; diff --git a/SnapshotTests/AlertViewControllerDynamicTypeFontTests.swift b/SnapshotTests/AlertViewControllerDynamicTypeFontTests.swift new file mode 100644 index 000000000..ab1ab191e --- /dev/null +++ b/SnapshotTests/AlertViewControllerDynamicTypeFontTests.swift @@ -0,0 +1,67 @@ +import AccessibilitySnapshot +@testable import GliaWidgets +import SnapshotTesting +import XCTest + +final class AlertViewControllerDynamicTypeFontTests: SnapshotTestCase { + func test_screenSharingOffer_extra3Large() { + let alert = alert(ofKind: .screenShareOffer( + .mock(), + accepted: {}, + declined: {} + )) + assertSnapshot( + matching: alert, + as: .extra3LargeFontStrategy, + named: nameForDevice() + ) + } + + func test_mediaUpgradeOffer_extra3Large() { + let alert = alert(ofKind: .singleMediaUpgrade( + .mock(), + accepted: {}, + declined: {} + )) + assertSnapshot( + matching: alert, + as: .extra3LargeFontStrategy, + named: nameForDevice() + ) + } + + func test_messageAlert_extra3Large() { + let alert = alert(ofKind: .message( + .mock(), + accessibilityIdentifier: nil, + dismissed: {} + )) + assertSnapshot( + matching: alert, + as: .extra3LargeFontStrategy, + named: nameForDevice() + ) + } + + func test_singleAction_extra3Large() { + let alert = alert(ofKind: .singleAction( + .mock(), + accessibilityIdentifier: "mocked-accessibility-identifier", + actionTapped: {} + )) + assertSnapshot( + matching: alert, + as: .extra3LargeFontStrategy, + named: nameForDevice() + ) + } + + private func alert(ofKind kind: AlertViewController.Kind) -> AlertViewController { + let viewController = AlertViewController( + kind: kind, + viewFactory: .mock() + ) + viewController.view.frame = UIScreen.main.bounds + return viewController + } +} diff --git a/SnapshotTests/BubbleViewDynamicTypeFontTests.swift b/SnapshotTests/BubbleViewDynamicTypeFontTests.swift new file mode 100644 index 000000000..cb60ee7d2 --- /dev/null +++ b/SnapshotTests/BubbleViewDynamicTypeFontTests.swift @@ -0,0 +1,16 @@ +import AccessibilitySnapshot +@testable import GliaWidgets +import SnapshotTesting +import XCTest + +final class BubbleViewDynamicTypeFontTests: SnapshotTestCase { + func test_bubble_extra3Large() { + let bubble = ViewFactory.mock().makeBubbleView() + bubble.frame = .init(origin: .zero, size: .init(width: 50, height: 50)) + // If shadow will cause failing test locally or on CI, we should disable it. + assertSnapshot( + matching: bubble, + as: .extra3LargeFontStrategy + ) + } +} diff --git a/SnapshotTests/CallViewControllerDynamicTypeFontTests.swift b/SnapshotTests/CallViewControllerDynamicTypeFontTests.swift new file mode 100644 index 000000000..eb7ff9b11 --- /dev/null +++ b/SnapshotTests/CallViewControllerDynamicTypeFontTests.swift @@ -0,0 +1,65 @@ +@testable import GliaWidgets +import SnapshotTesting +import XCTest + +final class CallViewControllerDynamicTypeFontTests: SnapshotTestCase { + func test_audioCallQueueState_extra3Large() throws { + let viewController = try CallViewController.mockAudioCallQueueState() + viewController.view.frame = UIScreen.main.bounds + assertSnapshot( + matching: viewController, + as: .extra3LargeFontStrategy, + named: nameForDevice() + ) + } + + func test_audioCallConnectingState_extra3Large() throws { + let viewController = try CallViewController.mockAudioCallConnectingState() + viewController.view.frame = UIScreen.main.bounds + assertSnapshot( + matching: viewController, + as: .extra3LargeFontStrategy, + named: nameForDevice() + ) + } + + func test_audioCallConnectedState_extra3Large() throws { + let viewController = try CallViewController.mockAudioCallConnectedState() + viewController.view.frame = UIScreen.main.bounds + assertSnapshot( + matching: viewController, + as: .extra3LargeFontStrategy, + named: nameForDevice() + ) + } + + func test_mockVideoCallConnectingState_extra3Large() throws { + let viewController = try CallViewController.mockVideoCallConnectingState() + viewController.view.frame = UIScreen.main.bounds + assertSnapshot( + matching: viewController, + as: .extra3LargeFontStrategy, + named: nameForDevice() + ) + } + + func test_mockVideoCallQueueState_extra3Large() throws { + let viewController = try CallViewController.mockVideoCallQueueState() + viewController.view.frame = UIScreen.main.bounds + assertSnapshot( + matching: viewController, + as: .extra3LargeFontStrategy, + named: nameForDevice() + ) + } + + func test_mockVideoCallConnectedState_extra3Large() throws { + let viewController = try CallViewController.mockVideoCallConnectedState() + viewController.view.frame = UIScreen.main.bounds + assertSnapshot( + matching: viewController, + as: .extra3LargeFontStrategy, + named: nameForDevice() + ) + } +} diff --git a/SnapshotTests/ChatCallUpgradeViewDynamicTypeFontTests.swift b/SnapshotTests/ChatCallUpgradeViewDynamicTypeFontTests.swift new file mode 100644 index 000000000..b5e98f354 --- /dev/null +++ b/SnapshotTests/ChatCallUpgradeViewDynamicTypeFontTests.swift @@ -0,0 +1,23 @@ +@testable import GliaWidgets +import SnapshotTesting +import XCTest + +final class ChatCallUpgradeViewDynamicTypeFontTests: SnapshotTestCase { + func test_chatCallUpgradeViewToAudio_extra3Large() { + let upgradeView = ChatCallUpgradeView(with: Theme.mock().chat.audioUpgrade, duration: .init(with: .zero)) + upgradeView.frame = .init(origin: .zero, size: .init(width: 300, height: 120)) + assertSnapshot( + matching: upgradeView, + as: .extra3LargeFontStrategy + ) + } + + func test_chatCallUpgradeViewToVideo_extra3Large() { + let upgradeView = ChatCallUpgradeView(with: Theme.mock().chat.videoUpgrade, duration: .init(with: .zero)) + upgradeView.frame = .init(origin: .zero, size: .init(width: 300, height: 120)) + assertSnapshot( + matching: upgradeView, + as: .extra3LargeFontStrategy + ) + } +} diff --git a/SnapshotTests/ChatViewControllerDynamicTypeFontTests.swift b/SnapshotTests/ChatViewControllerDynamicTypeFontTests.swift new file mode 100644 index 000000000..e502d74c0 --- /dev/null +++ b/SnapshotTests/ChatViewControllerDynamicTypeFontTests.swift @@ -0,0 +1,55 @@ +@testable import GliaWidgets +import SnapshotTesting +import XCTest + +final class ChatViewControllerDynamicTypeFontTests: SnapshotTestCase { + func test_messagesFromHistory_extra3Large() { + let viewController = ChatViewController.mockHistoryMessagesScreen() + viewController.view.frame = UIScreen.main.bounds + assertSnapshot( + matching: viewController, + as: .extra3LargeFontStrategy, + named: nameForDevice() + ) + } + + func test_visitorUploadedFileStates_extra3Large() throws { + let viewController = try ChatViewController.mockVisitorFileUploadStates() + viewController.view.frame = UIScreen.main.bounds + assertSnapshot( + matching: viewController, + as: .extra3LargeFontStrategy, + named: nameForDevice() + ) + } + + func test_choiceCard_extra3Large() throws { + let viewController = try ChatViewController.mockChoiceCard() + viewController.view.frame = UIScreen.main.bounds + assertSnapshot( + matching: viewController, + as: .extra3LargeFontStrategy, + named: nameForDevice() + ) + } + + func test_visitorFileDownloadStates_extra3Large() throws { + var chatMessages: [ChatMessage] = [] + let viewController = try ChatViewController.mockVisitorFileDownloadStates { messages in + chatMessages = messages + } + viewController.view.frame = UIScreen.main.bounds + viewController.view.setNeedsLayout() + viewController.view.layoutIfNeeded() + XCTAssertEqual(chatMessages.count, 4) + chatMessages[0].downloads[0].state.value = .none + chatMessages[1].downloads[0].state.value = .downloading(progress: .init(with: 0.5)) + chatMessages[2].downloads[0].state.value = .downloaded(.mock()) + chatMessages[3].downloads[0].state.value = .error(.deleted) + assertSnapshot( + matching: viewController, + as: .extra3LargeFontStrategy, + named: self.nameForDevice() + ) + } +} diff --git a/SnapshotTests/ScreenShareViewControllerDynamicTypeFontTests.swift b/SnapshotTests/ScreenShareViewControllerDynamicTypeFontTests.swift new file mode 100644 index 000000000..9af9109a1 --- /dev/null +++ b/SnapshotTests/ScreenShareViewControllerDynamicTypeFontTests.swift @@ -0,0 +1,28 @@ +@testable import GliaWidgets +import SnapshotTesting +import XCTest + +// swiftlint:disable type_name +final class ScreenShareViewControllerDynamicTypeFontTests: SnapshotTestCase { + func testScreenShareViewController_extra3Large() { + let props: CallVisualizer.ScreenSharingViewController.Props = .init( + screenSharingViewProps: .init( + style: .mock(), + header: .mock( + title: L10n.CallVisualizer.ScreenSharing.title, + backButton: .init(style: .mock(image: Asset.back.image)) + ), + endScreenSharing: .mock() + ) + ) + let screenShareViewController = CallVisualizer.ScreenSharingViewController(props: props) + screenShareViewController.view.frame = UIScreen.main.bounds + + assertSnapshot( + matching: screenShareViewController, + as: .extra3LargeFontStrategy, + named: nameForDevice() + ) + } +} +// swiftlint:enable type_name diff --git a/SnapshotTests/SecureConversationsConfirmationScreenDynamicTypeFontTests.swift b/SnapshotTests/SecureConversationsConfirmationScreenDynamicTypeFontTests.swift new file mode 100644 index 000000000..134c38231 --- /dev/null +++ b/SnapshotTests/SecureConversationsConfirmationScreenDynamicTypeFontTests.swift @@ -0,0 +1,48 @@ +@testable import GliaWidgets +import SnapshotTesting +import XCTest + +// swiftlint:disable type_name +final class SecureConversationsConfirmationScreenDynamicTypeFontTests: SnapshotTestCase { + let theme = Theme.mock() + + func test_confirmationView_extra3Large() { + let props = Self.makeConfirmationProps(style: theme.secureConversationsConfirmation) + let viewController = SecureConversations.ConfirmationViewController( + viewModel: .init(environment: .init(confirmationStyle: theme.defaultSecureConversationsConfirmationStyle)), + viewFactory: .mock(theme: theme, messageRenderer: nil, environment: .mock), + props: props + ) + viewController.view.frame = UIScreen.main.bounds + + assertSnapshot( + matching: viewController.view, + as: .extra3LargeFontStrategy, + named: self.nameForDevice() + ) + } + + // MARK: - Helpers + + static func headerProps() -> Header.Props { + .mock( + title: "Secure Conversations", + backButton: .init(style: .mock(image: Asset.back.image)), + closeButton: .init(style: .mock(image: Asset.close.image)) + ) + } + + static func makeConfirmationProps( + headerProps: Header.Props = headerProps(), + style: SecureConversations.ConfirmationStyle + ) -> SecureConversations.ConfirmationViewController.Props { + .init( + confirmationViewProps: .init( + style: style, + header: headerProps, + checkMessageButtonTap: .nop + ) + ) + } +} +// swiftlint:enable type_name diff --git a/SnapshotTests/SecureConversationsWelcomeScreenDynamicTypeFontTests.swift b/SnapshotTests/SecureConversationsWelcomeScreenDynamicTypeFontTests.swift new file mode 100644 index 000000000..5bd896ffc --- /dev/null +++ b/SnapshotTests/SecureConversationsWelcomeScreenDynamicTypeFontTests.swift @@ -0,0 +1,125 @@ +@testable import GliaWidgets +import SnapshotTesting +import XCTest + +// swiftlint:disable type_name +final class SecureConversationsWelcomeScreenDynamicTypeFontTests: SnapshotTestCase { + let theme = Theme.mock() + + func test_welcomeView_extra3Large() { + let props = Self.makeWelcomeProps( + theme: theme.secureConversationsWelcome, + uploads: [] + ) + let viewController = SecureConversations.WelcomeViewController( + viewFactory: .mock(theme: theme, messageRenderer: nil, environment: .mock), + props: .welcome(props), + environment: .init(gcd: .live, uiScreen: .mock, notificationCenter: .mock) + ) + viewController.view.frame = UIScreen.main.bounds + + assertSnapshot( + matching: viewController.view, + as: .extra3LargeFontStrategy, + named: self.nameForDevice() + ) + } + + func test_welcomeWithAttachments_extra3Large() { + let props = Self.makeWelcomeProps( + theme: theme.secureConversationsWelcome, + uploads: [ + uploadedFileProps(), + failedFileUploadProps() + ] + ) + let viewController = SecureConversations.WelcomeViewController( + viewFactory: .mock(theme: theme, messageRenderer: nil, environment: .mock), + props: .welcome(props), + environment: .init(gcd: .live, uiScreen: .mock, notificationCenter: .mock) + ) + viewController.view.frame = UIScreen.main.bounds + + assertSnapshot( + matching: viewController.view, + as: .extra3LargeFontStrategy, + named: self.nameForDevice() + ) + } + + func test_welcomeViewController_withValidationError_extra3Large() { + let props = Self.makeWelcomeProps(theme: theme.secureConversationsWelcome, warningMessage: "This is warning message") + let viewController = SecureConversations.WelcomeViewController( + viewFactory: .mock(theme: theme, messageRenderer: nil, environment: .mock), + props: .welcome(props), + environment: .init(gcd: .live, uiScreen: .mock, notificationCenter: .mock) + ) + viewController.view.frame = UIScreen.main.bounds + + assertSnapshot( + matching: viewController.view, + as: .extra3LargeFontStrategy, + named: self.nameForDevice() + ) + } + + // MARK: - Helpers + + func uploadedFileProps() -> SecureConversations.FileUploadView.Props { + .init( + id: "id-a", + style: .messageCenter(theme.secureConversationsWelcome.attachmentListStyle.item), + state: .uploaded(.init(localFile: .mock())), + removeTapped: .nop + ) + } + + func failedFileUploadProps() -> SecureConversations.FileUploadView.Props { + .init( + id: "id-b", + style: .messageCenter(theme.secureConversationsWelcome.attachmentListStyle.item), + state: .error(.network), + removeTapped: .nop + ) + } + + static func headerProps() -> Header.Props { + .mock( + title: "Secure Conversations", + backButton: .init(style: .mock(image: Asset.back.image)), + closeButton: .init(style: .mock(image: Asset.close.image)) + ) + } + + static func makeWelcomeProps( + theme: SecureConversations.WelcomeStyle, + headerProps: Header.Props = headerProps(), + uploads: [SecureConversations.FileUploadView.Props] = [], + warningMessage: String = "" + ) -> SecureConversations.WelcomeView.Props { + .init( + style: theme, + checkMessageButtonTap: .nop, + filePickerButton: .init(isEnabled: true, tap: .nop), + sendMessageButton: .active(.nop), + messageTextViewProps: .active( + .init( + style: theme.messageTextViewStyle, + text: "Lorem Ipsum is simply dummy text of the printing and typesetting industry.", + textChanged: .nop, + activeChanged: .nop + ) + ), + warningMessage: .init(text: warningMessage, animated: false), + fileUploadListProps: .init( + maxUnscrollableViews: 3, + style: .chat(.mock), + uploads: .init(uploads), + isScrollingEnabled: true + ), + headerProps: headerProps, + isUiHidden: false + ) + } +} +// swiftlint:enable type_name diff --git a/SnapshotTests/SnapshotTestCase.swift b/SnapshotTests/SnapshotTestCase.swift index ebffe284c..d6d46b3e2 100644 --- a/SnapshotTests/SnapshotTestCase.swift +++ b/SnapshotTests/SnapshotTestCase.swift @@ -167,3 +167,25 @@ extension Snapshotting where Value == UIViewController, Format == UIImage { } } } + +extension Snapshotting where Value == UIViewController, Format == UIImage { + /// Strategy for making image references with largest dynamic font type for UIViewController. + static var extra3LargeFontStrategy: Self { + Self.image( + traits: UITraitCollection( + preferredContentSizeCategory: .accessibilityExtraExtraExtraLarge + ) + ) + } +} + +extension Snapshotting where Value == UIView, Format == UIImage { + /// Strategy for making image references with largest dynamic font type for UIView. + static var extra3LargeFontStrategy: Self { + Self.image( + traits: UITraitCollection( + preferredContentSizeCategory: .accessibilityExtraExtraExtraLarge + ) + ) + } +} diff --git a/SnapshotTests/SurveyViewControllerDynamicTypeFontTests.swift b/SnapshotTests/SurveyViewControllerDynamicTypeFontTests.swift new file mode 100644 index 000000000..2e7fe033a --- /dev/null +++ b/SnapshotTests/SurveyViewControllerDynamicTypeFontTests.swift @@ -0,0 +1,35 @@ +@testable import GliaWidgets +import SnapshotTesting +import XCTest + +final class SurveyViewControllerDynamicTypeFontTests: SnapshotTestCase { + func test_emptySurvey_extra3Large() { + let viewController = Survey.ViewController(viewFactory: .mock(), environment: .init(notificationCenter: .mock), props: .emptyPropsMock()) + viewController.view.frame = UIScreen.main.bounds + assertSnapshot( + matching: viewController, + as: .extra3LargeFontStrategy, + named: nameForDevice() + ) + } + + func test_filledSurvey_extra3Large() { + let viewController = Survey.ViewController(viewFactory: .mock(), environment: .init(notificationCenter: .mock), props: .filledPropsMock()) + viewController.view.frame = UIScreen.main.bounds + assertSnapshot( + matching: viewController, + as: .extra3LargeFontStrategy, + named: nameForDevice() + ) + } + + func test_emptySurveyErrorState_extra3Large() { + let viewController = Survey.ViewController(viewFactory: .mock(), environment: .init(notificationCenter: .mock), props: .errorPropsMock()) + viewController.view.frame = UIScreen.main.bounds + assertSnapshot( + matching: viewController, + as: .extra3LargeFontStrategy, + named: nameForDevice() + ) + } +} diff --git a/SnapshotTests/VideoCallViewControllerDynamicTypeFontTests.swift b/SnapshotTests/VideoCallViewControllerDynamicTypeFontTests.swift new file mode 100644 index 000000000..f9da12752 --- /dev/null +++ b/SnapshotTests/VideoCallViewControllerDynamicTypeFontTests.swift @@ -0,0 +1,61 @@ +@testable import GliaWidgets +import SnapshotTesting +import XCTest + +// swiftlint:disable type_name +final class VideoCallViewControllerDynamicTypeFontTests: SnapshotTestCase { + func testVideoCallViewController_extra3Large() { + let videoCallViewProps: CallVisualizer.VideoCallView.Props = .mock( + buttonBarProps: .mock( + style: .mock( + chatButton: .mock(), + videButton: .mock( + inactive: .activeMock( + image: Asset.callVideoActive.image, + title: L10n.Call.Buttons.Video.title, + accessibility: .init(label: L10n.Call.Accessibility.Buttons.Video.Active.label) + ) + ), + muteButton: .mock( + inactive: .inactiveMock( + image: Asset.callMuteInactive.image, + title: L10n.Call.Buttons.Mute.Inactive.title, + accessibility: .init(label: L10n.Call.Accessibility.Buttons.Mute.Inactive.label) + ) + ), + speakerButton: .mock( + inactive: .inactiveMock( + image: Asset.callSpeakerInactive.image, + title: L10n.Call.Buttons.Speaker.title, + accessibility: .init(label: L10n.Call.Accessibility.Buttons.Speaker.Inactive.label) + ) + ), + minimizeButton: .mock( + inactive: .inactiveMock( + image: Asset.callMiminize.image, + title: L10n.Call.Buttons.Minimize.title, + accessibility: .init(label: L10n.Call.Accessibility.Buttons.Minimize.Inactive.label) + ) + ), + badge: .mock() + ) + ), + headerProps: .mock( + title: "Video", + effect: .blur, + backButton: .init(style: .mock(image: Asset.back.image)) + ) + ) + let props: CallVisualizer.VideoCallViewController.Props = .init(videoCallViewProps: videoCallViewProps) + + let videoCallViewController: CallVisualizer.VideoCallViewController = .mock(props: props) + videoCallViewController.view.frame = UIScreen.main.bounds + + assertSnapshot( + matching: videoCallViewController, + as: .extra3LargeFontStrategy, + named: nameForDevice() + ) + } +} +// swiftlint:enable type_name diff --git a/SnapshotTests/VisitorCodeViewControllerDynamicTypeFontTests.swift b/SnapshotTests/VisitorCodeViewControllerDynamicTypeFontTests.swift new file mode 100644 index 000000000..accde95a0 --- /dev/null +++ b/SnapshotTests/VisitorCodeViewControllerDynamicTypeFontTests.swift @@ -0,0 +1,108 @@ +@testable import GliaWidgets +import SnapshotTesting +import XCTest + +// swiftlint:disable type_name +final class VisitorCodeViewControllerDynamicTypeFontTests: SnapshotTestCase { + func testVisitorCodeAlertWhenLoading() { + let props: CallVisualizer.VisitorCodeViewController.Props = .init( + visitorCodeViewProps: .init( + viewType: .alert(closeButtonTap: .nop), + viewState: .loading + ) + ) + let visitorCodeViewController = CallVisualizer.VisitorCodeViewController(props: props) + visitorCodeViewController.view.frame = UIScreen.main.bounds + + assertSnapshot( + matching: visitorCodeViewController, + as: .extra3LargeFontStrategy, + named: nameForDevice() + ) + } + + func testVisitorCodeAlertWhenError() { + let props: CallVisualizer.VisitorCodeViewController.Props = .init( + visitorCodeViewProps: .init( + viewType: .alert(closeButtonTap: .nop), + viewState: .error(refreshTap: .nop) + ) + ) + let visitorCodeViewController = CallVisualizer.VisitorCodeViewController(props: props) + visitorCodeViewController.view.frame = UIScreen.main.bounds + + assertSnapshot( + matching: visitorCodeViewController, + as: .extra3LargeFontStrategy, + named: nameForDevice() + ) + } + + func testVisitorCodeAlertWhenSuccess() { + let props: CallVisualizer.VisitorCodeViewController.Props = .init( + visitorCodeViewProps: .init( + viewType: .alert(closeButtonTap: .nop), + viewState: .success(visitorCode: "12345") + ) + ) + let visitorCodeViewController = CallVisualizer.VisitorCodeViewController(props: props) + visitorCodeViewController.view.frame = UIScreen.main.bounds + + assertSnapshot( + matching: visitorCodeViewController, + as: .extra3LargeFontStrategy, + named: nameForDevice() + ) + } + + func testVisitorCodeEmbeddedWhenLoading() { + let props: CallVisualizer.VisitorCodeViewController.Props = .init( + visitorCodeViewProps: .init( + viewType: .embedded, + viewState: .loading + ) + ) + let visitorCodeViewController = CallVisualizer.VisitorCodeViewController(props: props) + visitorCodeViewController.view.frame = UIScreen.main.bounds + + assertSnapshot( + matching: visitorCodeViewController, + as: .extra3LargeFontStrategy, + named: nameForDevice() + ) + } + + func testVisitorCodeEmbeddedWhenError() { + let props: CallVisualizer.VisitorCodeViewController.Props = .init( + visitorCodeViewProps: .init( + viewType: .embedded, + viewState: .error(refreshTap: .nop) + ) + ) + let visitorCodeViewController = CallVisualizer.VisitorCodeViewController(props: props) + visitorCodeViewController.view.frame = UIScreen.main.bounds + + assertSnapshot( + matching: visitorCodeViewController, + as: .extra3LargeFontStrategy, + named: nameForDevice() + ) + } + func testVisitorCodeEmbeddedWhenSuccess() { + let props: CallVisualizer.VisitorCodeViewController.Props = .init( + visitorCodeViewProps: .init( + viewType: .embedded, + viewState: .success(visitorCode: "12345") + ) + ) + let visitorCodeViewController = CallVisualizer.VisitorCodeViewController(props: props) + visitorCodeViewController.view.frame = UIScreen.main.bounds + + assertSnapshot( + matching: visitorCodeViewController, + as: .extra3LargeFontStrategy, + named: nameForDevice() + ) + } +} +// swiftlint:enable type_name From dba7ae4a3be138ed70488d62ef2799459077597c Mon Sep 17 00:00:00 2001 From: Yurii Dukhovnyi Date: Tue, 25 Jul 2023 11:40:59 +0300 Subject: [PATCH 12/64] Fix custom cards handling Sdk differentiate custom card and usual response card based on metadata. If the type is single choice card and metadata is nil, SDK should draw a message as a response card. If the type is single choice card and metadata is not nil, SDK should pass metadata to renderer and render custom card. MOB-2342 --- GliaWidgets.xcodeproj/project.pbxproj | 24 +++++++++ .../ViewModel/Chat/Data/ChatMessage.swift | 12 ++--- .../ChatMessage/ChatMessageTests.swift | 50 +++++++++++++++++++ GliaWidgetsTests/CoreSdk/CoreSdk.swift | 24 +++++++++ 4 files changed, 103 insertions(+), 7 deletions(-) create mode 100644 GliaWidgetsTests/ChatMessage/ChatMessageTests.swift create mode 100644 GliaWidgetsTests/CoreSdk/CoreSdk.swift diff --git a/GliaWidgets.xcodeproj/project.pbxproj b/GliaWidgets.xcodeproj/project.pbxproj index dc2e63f6d..90d33d830 100644 --- a/GliaWidgets.xcodeproj/project.pbxproj +++ b/GliaWidgets.xcodeproj/project.pbxproj @@ -227,6 +227,8 @@ 7552DFA62A683A2C0093519B /* Layout+UIView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 75B7BD7D2A39D5430060794D /* Layout+UIView.swift */; }; 7552DFA72A683A2C0093519B /* NSLayoutConstraint+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 75940957298D386F008B173A /* NSLayoutConstraint+Extensions.swift */; }; 7552DFA82A683A2C0093519B /* UIStackView.Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 75940956298D386F008B173A /* UIStackView.Extensions.swift */; }; + 7552DFB12A6FB7DF0093519B /* ChatMessageTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7552DFB02A6FB7DF0093519B /* ChatMessageTests.swift */; }; + 7552DFB42A6FBC7F0093519B /* CoreSdk.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7552DFB32A6FBC7F0093519B /* CoreSdk.swift */; }; 755D186529A6A4E20009F5E8 /* WelcomeStyle+TitleStyle.swift in Sources */ = {isa = PBXBuildFile; fileRef = 755D186429A6A4E20009F5E8 /* WelcomeStyle+TitleStyle.swift */; }; 755D186729A6A4FA0009F5E8 /* WelcomeStyle+SubtitleStyle.swift in Sources */ = {isa = PBXBuildFile; fileRef = 755D186629A6A4FA0009F5E8 /* WelcomeStyle+SubtitleStyle.swift */; }; 755D186929A6A5270009F5E8 /* WelcomeStyle+CheckMessagesButtonStyle.swift in Sources */ = {isa = PBXBuildFile; fileRef = 755D186829A6A5270009F5E8 /* WelcomeStyle+CheckMessagesButtonStyle.swift */; }; @@ -908,6 +910,8 @@ 754313CE2870E64600C9C1C6 /* Configuration.Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Configuration.Extensions.swift; sourceTree = ""; }; 7543141728806AEB00C9C1C6 /* EnvironmentSettingsTextCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EnvironmentSettingsTextCell.swift; sourceTree = ""; }; 754CC61427E27C42005676E9 /* Survey.Checkbox.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Survey.Checkbox.swift; sourceTree = ""; }; + 7552DFB02A6FB7DF0093519B /* ChatMessageTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatMessageTests.swift; sourceTree = ""; }; + 7552DFB32A6FBC7F0093519B /* CoreSdk.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CoreSdk.swift; sourceTree = ""; }; 755D186429A6A4E20009F5E8 /* WelcomeStyle+TitleStyle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "WelcomeStyle+TitleStyle.swift"; sourceTree = ""; }; 755D186629A6A4FA0009F5E8 /* WelcomeStyle+SubtitleStyle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "WelcomeStyle+SubtitleStyle.swift"; sourceTree = ""; }; 755D186829A6A5270009F5E8 /* WelcomeStyle+CheckMessagesButtonStyle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "WelcomeStyle+CheckMessagesButtonStyle.swift"; sourceTree = ""; }; @@ -1575,6 +1579,8 @@ 1A205D6525655CB2003AA3CD /* GliaWidgetsTests */ = { isa = PBXGroup; children = ( + 7552DFB22A6FBC6E0093519B /* CoreSdk */, + 7552DFAF2A6FB37E0093519B /* ChatMessage */, 846A5C3729D18D220049B29F /* ScreenShareHandler */, AFEF5C7229929A73005C3D8D /* SecureConversations */, C096B408297EBCEB00F0C552 /* CallVisualizer */, @@ -2544,6 +2550,22 @@ path = Sources; sourceTree = ""; }; + 7552DFAF2A6FB37E0093519B /* ChatMessage */ = { + isa = PBXGroup; + children = ( + 7552DFB02A6FB7DF0093519B /* ChatMessageTests.swift */, + ); + path = ChatMessage; + sourceTree = ""; + }; + 7552DFB22A6FBC6E0093519B /* CoreSdk */ = { + isa = PBXGroup; + children = ( + 7552DFB32A6FBC7F0093519B /* CoreSdk.swift */, + ); + path = CoreSdk; + sourceTree = ""; + }; 755D186329A6A4B10009F5E8 /* WelcomeStyle */ = { isa = PBXGroup; children = ( @@ -4477,6 +4499,7 @@ 84681A982A61853300DD7406 /* GvaOption.Mock.swift in Sources */, AF6AB34B2989517100003645 /* FileUploader.Failing.swift in Sources */, EB03B00E27FFF6DD0058F6B1 /* CallViewTests.swift in Sources */, + 7552DFB42A6FBC7F0093519B /* CoreSdk.swift in Sources */, 3197F7AD29E6A5C8008EE9F7 /* SecureConversations.FileUploadListView.Mock.swift in Sources */, AF29810929E045CE0005BD55 /* TranscriptModelTests.swift in Sources */, 7512A57727BE8A6700319DF1 /* InteractorTests.swift in Sources */, @@ -4524,6 +4547,7 @@ 846429862A45DB4100943BD6 /* AlertViewController.Kind+Mock.swift in Sources */, AF29811229E42F3C0005BD55 /* AvailabilityTests.swift in Sources */, 9AE05CB32805C9D900871321 /* ChatViewModel.Environment.Failing.swift in Sources */, + 7552DFB12A6FB7DF0093519B /* ChatMessageTests.swift in Sources */, AFEF5C7429929A8D005C3D8D /* SecureConversations.FileUploadListViewModel.Environment.Failing.swift in Sources */, 9A3E1D9D27BA7741005634EB /* FoundationBased.Failing.swift in Sources */, 9A3E1D8427B67F1B005634EB /* Helper.swift in Sources */, diff --git a/GliaWidgets/Sources/ViewModel/Chat/Data/ChatMessage.swift b/GliaWidgets/Sources/ViewModel/Chat/Data/ChatMessage.swift index 7f5b7f32b..12456a6d3 100644 --- a/GliaWidgets/Sources/ViewModel/Chat/Data/ChatMessage.swift +++ b/GliaWidgets/Sources/ViewModel/Chat/Data/ChatMessage.swift @@ -52,14 +52,10 @@ class ChatMessage: Codable { } if attachment?.type == .singleChoice { - return .choiceCard + return metadata != nil ? .customCard : .choiceCard } - if metadata == nil { - return .none - } - - return .customCard + return .none } private enum CodingKeys: String, CodingKey { @@ -127,7 +123,8 @@ class ChatMessage: Codable { sender: ChatMessageSender, content: String, attachment: ChatAttachment?, - downloads: [FileDownload] + downloads: [FileDownload], + metadata: MessageMetadata? = nil ) { self.id = id self.queueID = queueID @@ -136,5 +133,6 @@ class ChatMessage: Codable { self.content = content self.attachment = attachment self.downloads = downloads + self.metadata = metadata } } diff --git a/GliaWidgetsTests/ChatMessage/ChatMessageTests.swift b/GliaWidgetsTests/ChatMessage/ChatMessageTests.swift new file mode 100644 index 000000000..6c2d56cba --- /dev/null +++ b/GliaWidgetsTests/ChatMessage/ChatMessageTests.swift @@ -0,0 +1,50 @@ +import GliaCoreSDK +import XCTest + +@testable import GliaWidgets + +final class ChatMessageTests: XCTestCase { + + func testCardType__singleChoiceWithoutMetadata() throws { + let msg = ChatMessage.mock( + attachment: .mock(type: .singleChoice, files: nil, imageUrl: nil, options: nil), + metadata: nil + ) + XCTAssertEqual(msg.cardType, .choiceCard) + } + + func testCardType__singleChoiceWithMetadata() throws { + let metadataDecodingContainer = try CoreSdkMessageMetadataContainer( + jsonData: "{\"html\": \"Hello\"}".data(using: .utf8)! + ).container + let msg = ChatMessage.mock( + attachment: .mock(type: .singleChoice, files: nil, imageUrl: nil, options: nil), + metadata: MessageMetadata(container: metadataDecodingContainer) + ) + XCTAssertEqual(msg.cardType, .customCard) + } +} + +extension ChatMessage { + static func mock( + id: String = "mocked-message-id", + queueId: String? = "queue-id", + operator: ChatOperator? = .init(name: "XCTest Operator", pictureUrl: nil), + sender: ChatMessageSender = .`operator`, + content: String = "Hello unit test!", + attachment: ChatAttachment? = nil, + downloads: [FileDownload] = [], + metadata: MessageMetadata? = nil + ) -> ChatMessage { + ChatMessage( + id: id, + queueID: queueId, + operator: `operator`, + sender: sender, + content: content, + attachment: attachment, + downloads: downloads, + metadata: metadata + ) + } +} diff --git a/GliaWidgetsTests/CoreSdk/CoreSdk.swift b/GliaWidgetsTests/CoreSdk/CoreSdk.swift new file mode 100644 index 000000000..f576558d7 --- /dev/null +++ b/GliaWidgetsTests/CoreSdk/CoreSdk.swift @@ -0,0 +1,24 @@ +import Foundation +import GliaCoreSDK + +/// Defines wrapper structure for getting decoding container. +/// This container can be used when message metadata is needed in +/// tests. +struct CoreSdkMessageMetadataContainer: Decodable { + let container: KeyedDecodingContainer + + init(from decoder: Decoder) throws { + self.container = try decoder.container(keyedBy: GliaCoreSDK.Message.Metadata.CodingKeys.self) + } + + /// Creates instance with decoding container inside. + /// This initializer can be used for created mocked Metadata with passing + /// json-data inside of this initializer. + /// NB! Empty 'jsonData' will lead to decoding error. + init( + jsonData: Data, + jsonDecoder: JSONDecoder = .init() + ) throws { + self = try jsonDecoder.decode(CoreSdkMessageMetadataContainer.self, from: jsonData) + } +} From a9b72141caa2af0ed5e69c553bc6413d97647689 Mon Sep 17 00:00:00 2001 From: Igor Kravchenko Date: Wed, 26 Jul 2023 16:58:50 +0300 Subject: [PATCH 13/64] Add snapshot test for bubble window Ensure that BubbleView has correct layout in BubbleWindow after PureLayout removal. MOB-2348 --- GliaWidgets.xcodeproj/project.pbxproj | 2 ++ 1 file changed, 2 insertions(+) diff --git a/GliaWidgets.xcodeproj/project.pbxproj b/GliaWidgets.xcodeproj/project.pbxproj index 90d33d830..389fd7ac3 100644 --- a/GliaWidgets.xcodeproj/project.pbxproj +++ b/GliaWidgets.xcodeproj/project.pbxproj @@ -3178,6 +3178,7 @@ AF755FD92A71583900871E36 /* BubbleWindowLayoutTests.swift */, AF22C8982A61AE930004BF3C /* VisitorCodeViewControllerLayoutTests.swift */, AF03A7C22A6EDF490081887D /* VisitorCodeViewControllerDynamicTypeFontTests.swift */, + AF755FD92A71583900871E36 /* BubbleWindowLayoutTests.swift */, ); path = SnapshotTests; sourceTree = ""; @@ -4613,6 +4614,7 @@ AF22C88D2A6188EF0004BF3C /* ChatViewControllerLayoutTests.swift in Sources */, AF03A7B12A6E96870081887D /* BubbleViewDynamicTypeFontTests.swift in Sources */, AF03A7AF2A6E7DC40081887D /* AlertViewControllerDynamicTypeFontTests.swift in Sources */, + AF755FDA2A71583900871E36 /* BubbleWindowLayoutTests.swift in Sources */, AF22C8992A61AE930004BF3C /* VisitorCodeViewControllerLayoutTests.swift in Sources */, 9AB3401327F71D5D006E0FE2 /* BubbleViewVoiceOverTests.swift in Sources */, AF22C8932A619F5C0004BF3C /* SecureConversationsConfirmationScreenLayoutTests.swift in Sources */, From e642b038614e7550fc23640cd4e9c21919e06770 Mon Sep 17 00:00:00 2001 From: Igor Kravchenko Date: Mon, 10 Jul 2023 13:42:47 +0300 Subject: [PATCH 14/64] Add missing acc. identifiers and custom segmented control Add more accessibility identifiers to visitor info UI and replace regular UISegmentedControl with custom implementation to overcome Voice Over (screen reader) issue, where views order is broken, if such control is used in section header. MOB-1100 --- GliaWidgets.xcodeproj/project.pbxproj | 4 + TestingApp/Main.storyboard | 1 + .../VisitorInfo/CustomSegmentedControl.swift | 171 ++++++++++++++++++ TestingApp/VisitorInfo/VisitorInfoModel.swift | 14 +- ...VisitorInfoViewController.HeaderView.swift | 48 ++--- .../VisitorInfoViewController.swift | 22 ++- 6 files changed, 227 insertions(+), 33 deletions(-) create mode 100644 TestingApp/VisitorInfo/CustomSegmentedControl.swift diff --git a/GliaWidgets.xcodeproj/project.pbxproj b/GliaWidgets.xcodeproj/project.pbxproj index 389fd7ac3..0c4eaa53f 100644 --- a/GliaWidgets.xcodeproj/project.pbxproj +++ b/GliaWidgets.xcodeproj/project.pbxproj @@ -518,6 +518,7 @@ AF6AB34B2989517100003645 /* FileUploader.Failing.swift in Sources */ = {isa = PBXBuildFile; fileRef = AF6AB34A2989517100003645 /* FileUploader.Failing.swift */; }; AF6AB34D298A9F2500003645 /* SecureConversations.FileUploadListViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = AF6AB34C298A9F2500003645 /* SecureConversations.FileUploadListViewModel.swift */; }; AF6AB34F298AA02400003645 /* SecureConversations.FileUploadListViewModel.Environment.swift in Sources */ = {isa = PBXBuildFile; fileRef = AF6AB34E298AA02400003645 /* SecureConversations.FileUploadListViewModel.Environment.swift */; }; + AF75601C2A78146600871E36 /* CustomSegmentedControl.swift in Sources */ = {isa = PBXBuildFile; fileRef = AF75601B2A78146500871E36 /* CustomSegmentedControl.swift */; }; AF755FDA2A71583900871E36 /* BubbleWindowLayoutTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = AF755FD92A71583900871E36 /* BubbleWindowLayoutTests.swift */; }; AF9DB22E2890571A00A0C442 /* RootCoordinator.Environment.Failing.swift in Sources */ = {isa = PBXBuildFile; fileRef = AF9DB22D2890571A00A0C442 /* RootCoordinator.Environment.Failing.swift */; }; AF9DB23128905A1D00A0C442 /* ViewFactory.Environment.Failing.swift in Sources */ = {isa = PBXBuildFile; fileRef = AF9DB23028905A1D00A0C442 /* ViewFactory.Environment.Failing.swift */; }; @@ -1208,6 +1209,7 @@ AF6AB34A2989517100003645 /* FileUploader.Failing.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FileUploader.Failing.swift; sourceTree = ""; }; AF6AB34C298A9F2500003645 /* SecureConversations.FileUploadListViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SecureConversations.FileUploadListViewModel.swift; sourceTree = ""; }; AF6AB34E298AA02400003645 /* SecureConversations.FileUploadListViewModel.Environment.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SecureConversations.FileUploadListViewModel.Environment.swift; sourceTree = ""; }; + AF75601B2A78146500871E36 /* CustomSegmentedControl.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CustomSegmentedControl.swift; sourceTree = ""; }; AF755FD92A71583900871E36 /* BubbleWindowLayoutTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BubbleWindowLayoutTests.swift; sourceTree = ""; }; AF9DB22D2890571A00A0C442 /* RootCoordinator.Environment.Failing.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RootCoordinator.Environment.Failing.swift; sourceTree = ""; }; AF9DB23028905A1D00A0C442 /* ViewFactory.Environment.Failing.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewFactory.Environment.Failing.swift; sourceTree = ""; }; @@ -3262,6 +3264,7 @@ AFD3C52A2A472B1C00BC37A9 /* VisitorInfo */ = { isa = PBXGroup; children = ( + AF75601B2A78146500871E36 /* CustomSegmentedControl.swift */, AF3BD1DC2A41D09100A7713E /* VisitorInfoViewController.swift */, AF35DFF02A507CA70038BCA7 /* VisitorInfoViewController.Cells.swift */, AF35DFEE2A507B4C0038BCA7 /* VisitorInfoViewController.HeaderView.swift */, @@ -4571,6 +4574,7 @@ 7552DFA52A683A2C0093519B /* Makeable.swift in Sources */, 1A205D7F25655CEC003AA3CD /* ViewController.swift in Sources */, AFD3C52C2A472B7500BC37A9 /* VisitorInfoModel.swift in Sources */, + AF75601C2A78146600871E36 /* CustomSegmentedControl.swift in Sources */, 1A4AD3D6256FE7F800468BFB /* UIColor+Extensions.swift in Sources */, AF11F30728BE6F0C002ACEB4 /* UIAlertController+Extensions.swift in Sources */, 7552DFA82A683A2C0093519B /* UIStackView.Extensions.swift in Sources */, diff --git a/TestingApp/Main.storyboard b/TestingApp/Main.storyboard index b783829db..f573b303d 100644 --- a/TestingApp/Main.storyboard +++ b/TestingApp/Main.storyboard @@ -167,6 +167,7 @@