From 094298a8e4e412937c101ea49187739701d6708c Mon Sep 17 00:00:00 2001 From: Nikita Kaberov Date: Wed, 28 Oct 2020 16:49:08 +0300 Subject: [PATCH] Version 3.33.0 --- Example/Podfile | 9 +- Example/Podfile.lock | 24 +- Example/Tests/AbstractRequestLoopTests.swift | 3 +- ...ift => ChatTableViewControllerTests.swift} | 27 +- Example/Tests/MemoryHistoryStorageTests.swift | 8 +- Example/Tests/MessageHolderTests.swift | 44 +- Example/Tests/MessageImplTests.swift | 116 +- .../Mocks/InternalErrorListenerMock.swift | 4 + Example/Tests/SQLiteHistoryStorageTests.swift | 6 +- Example/Tests/WebimActionsTests.swift | 5 + .../WebimRemoteNotificationImplTests.swift | 97 +- .../project.pbxproj | 286 ++- .../WebimClientLibrary_Example.xcscheme | 14 + .../xcshareddata/IDETemplateMacros.plist | 32 + Example/WebimClientLibrary/AppDelegate.swift | 21 +- .../AppearanceSettings/ColorConstants.swift | 153 -- .../AppearanceSettings/ColourConstants.swift | 151 ++ .../AppearanceSettings/ImageConstants.swift | 70 + .../AppearanceSettings/StringConstants.swift | 165 +- .../Base.lproj/LaunchScreen.xib | 51 - .../Base.lproj/Localizable.strings | 149 +- .../Base.lproj/Main.storyboard | 641 +++--- .../Base.lproj/RatingViewController.xib | 94 - .../ChatTableViewController.swift | 1356 ++++++++++++ .../ChatViewController.swift | 1468 ++++++++----- .../FileViewController.swift | 203 ++ .../ImageViewController.swift | 329 +++ .../Check.imageset/check_Icon-32.png | Bin 500 -> 0 bytes .../Gradient.imageset/Contents.json | 21 + .../Gradient.imageset/gradient-1.jpg | Bin 0 -> 10825 bytes .../Contents.json | 2 +- .../AttachmentButton.imageset/Vector.pdf | Bin 0 -> 3749 bytes .../Icons/Back/Back.imageset/back_Icon-32.png | Bin 710 -> 0 bytes .../Back/Back_dark.imageset/back_Icon-32.png | Bin 710 -> 0 bytes .../Icons/Clip/Clip.imageset/ClipIcon.pdf | Bin 52123 -> 0 bytes .../Clip/Clip_dark.imageset/Clip_dark.pdf | Bin 46261 -> 0 bytes .../Icons/Close/Close.imageset/Contents.json | 15 - .../Close/Close.imageset/close_Icon-32.png | Bin 549 -> 0 bytes .../Close/Close_dark.imageset/Contents.json | 15 - .../Close_dark.imageset/close_Icon-32.png | Bin 549 -> 0 bytes .../CloseButton.imageset/CloseButton-1.pdf | Bin 0 -> 1069 bytes .../CloseButton.imageset}/Contents.json | 2 +- .../Icons/Empty.imageset/Empty.pdf | Bin 44798 -> 0 bytes .../Icons/{Back => FileStatus}/Contents.json | 0 .../Contents.json | 15 + .../FIleDownloadSeccessVisitor.pdf | Bin 0 -> 2307 bytes .../Contents.json | 15 + .../FileDownloadButtonOperator.pdf | Bin 0 -> 1987 bytes .../Contents.json | 15 + .../FileDownloadButtonVisitor.pdf | Bin 0 -> 2485 bytes .../FileDownloadError.imageset/Contents.json | 15 + .../FileDownloadError.pdf | Bin 0 -> 2631 bytes .../Contents.json | 15 + .../FileDownloadSuccessOperator.pdf | Bin 0 -> 2307 bytes .../Contents.json | 15 + .../FileUploadButtonVisitor.pdf | Bin 0 -> 2004 bytes .../ImageDownload.imageset/Contents.json | 15 + .../ImageDownload.imageset/ImageDownload.pdf | Bin 0 -> 5236 bytes .../ActionCopy.imageset/ActionCopy.pdf | Bin 0 -> 4969 bytes .../ActionCopy.imageset}/Contents.json | 2 +- .../ActionDelete.imageset/ActionDelete.pdf | Bin 0 -> 3566 bytes .../ActionDelete.imageset}/Contents.json | 2 +- .../ActionEdit.imageset/ActionEdit.pdf | Bin 0 -> 2060 bytes .../ActionEdit.imageset}/Contents.json | 2 +- .../ActionReply.imageset/ActionReply.pdf | Bin 0 -> 4899 bytes .../ActionReply.imageset/Contents.json | 15 + .../{Clip => MessageActions}/Contents.json | 0 .../{Close => MessageStatus}/Contents.json | 0 .../ReadByOperator.imageset/Contents.json | 15 + .../ReadByOperator.pdf | Bin 0 -> 1108 bytes .../Sent.imageset}/Contents.json | 2 +- .../MessageStatus/Sent.imageset/Sent.pdf | Bin 0 -> 946 bytes .../Contents.json | 12 + .../ReplyCircleToTheLeft.imageset/left.pdf | Bin 0 -> 1839 bytes .../Icons/SendMessage/Contents.json | 6 - .../SendMessage.imageset/Contents.json | 15 - .../SendMessage.imageset/SendMessageIcon.pdf | Bin 53136 -> 0 bytes .../SendMessage_dark.imageset/Contents.json | 15 - .../SendMessage_dark.pdf | Bin 46316 -> 0 bytes .../SendMessageButton.imageset/Contents.json | 15 + .../SendMessageButton.pdf | Bin 0 -> 1795 bytes .../ImagePlaceholder.imageset/Contents.json | 15 + .../ImagePlaceholder.imageset/placeholder.png | Bin 0 -> 1329 bytes .../ScrollToBottom/Contents.json | 6 - .../ScrollToBottom.imageset/Contents.json | 15 - .../ScrollToBottom.pdf | Bin 58455 -> 0 bytes .../Contents.json | 15 - .../ScrollToBottom_dark.pdf | Bin 74796 -> 0 bytes Example/WebimClientLibrary/Info.plist | 20 +- .../LaunchScreenController.swift | 88 + .../Models/ColorScheme.swift | 134 -- .../WebimClientLibrary/Models/Settings.swift | 23 +- .../PopupActionsTableViewCell.swift | 119 ++ .../PopupActionsViewController.swift | 342 +++ .../RatingDialogViewController.swift | 297 +++ .../SettingsTableViewController.swift | 222 +- .../SettingsViewController.swift | 188 +- .../StartViewController.swift | 164 +- .../Utilities/CircleProgressIndicator.swift | 122 ++ .../Utilities/CustomUIButton.swift | 54 + .../CustomUIImage.swift} | 31 +- .../Utilities/CustomUIView.swift | 54 + .../Utilities/Extensions/Message.swift | 56 + .../Extensions/Notification.Name.swift | 55 + .../Utilities/Extensions/String.swift | 5 + .../Utilities/Extensions/UIButton.swift | 70 + .../Utilities/Extensions/UIImage.swift | 66 +- .../Utilities/Extensions/UIImageView.swift | 63 - .../Extensions/UIStackView.swift} | 26 +- .../Utilities/Extensions/UITableView.swift | 2 +- .../Utilities/Extensions/UIView.swift | 95 + .../Extensions/UIViewController.swift | 3 +- .../Utilities/FilePicker.swift | 289 +++ .../Utilities/FlexibleTableViewCell.swift | 1879 +++++++++++++++++ .../MessageTableViewCell.swift | 9 + .../Utilities/MimeType.swift | 24 +- .../Utilities/PopupDialogHandler.swift | 230 -- .../Utilities/SpinningIndicator.swift | 145 ++ .../Utilities/TypingIndicator.swift | 195 ++ .../Utilities/UIAlertHandler.swift | 181 ++ .../Utilities/WebimService.swift | 427 +++- .../en.lproj/LaunchScreen.storyboard | 68 + .../LaunchScreenController.storyboard | 73 + .../WebimClientLibrary/en.lproj/Main.strings | 96 + .../ru-RU.lproj/InfoPlist.strings | 29 + .../ru-RU.lproj/Localizable.strings | 149 +- .../ru-RU.lproj/Main.strings | 92 +- .../ru-RU.lproj/RatingViewController.strings | 9 - LICENSE | 2 +- README.md | 10 +- WebimClientLibrary.podspec | 2 +- .../Backend/AbstractRequestLoop.swift | 2 +- .../Backend/ActionRequestLoop.swift | 20 + .../Backend/DeltaCallback.swift | 2 + .../Backend/DeltaRequestLoop.swift | 2 +- .../ExecIfNotDestroyedHandlerExecutor.swift | 2 +- .../Backend/FAQSQLiteHistoryStorage.swift | 1 + .../Backend/Items/MessageItem.swift | 15 + .../Backend/Items/StickerItem.swift | 56 + .../Backend/MessageHolder.swift | 18 +- .../Backend/MessageToSend.swift | 7 +- .../Backend/SQLiteHistoryStorage.swift | 15 +- .../Backend/Utilities/Extensions/UInt32.swift | 2 +- .../Backend/Utilities/MessageFactories.swift | 19 +- WebimClientLibrary/Backend/WebimActions.swift | 22 + .../Backend/WebimInternalError.swift | 1 + WebimClientLibrary/Backend/WebimRequest.swift | 7 + .../Implementation/MessageImpl.swift | 51 +- .../Implementation/MessageStreamImpl.swift | 9 + .../Implementation/MessageTrackerImpl.swift | 4 +- .../Implementation/WebimSessionImpl.swift | 2 +- WebimClientLibrary/Message.swift | 53 +- WebimClientLibrary/MessageStream.swift | 80 + 153 files changed, 10117 insertions(+), 2319 deletions(-) rename Example/Tests/ExampleTests/{ChatViewControllerTests.swift => ChatTableViewControllerTests.swift} (83%) create mode 100644 Example/WebimClientLibrary.xcworkspace/xcshareddata/IDETemplateMacros.plist delete mode 100644 Example/WebimClientLibrary/AppearanceSettings/ColorConstants.swift create mode 100644 Example/WebimClientLibrary/AppearanceSettings/ColourConstants.swift create mode 100644 Example/WebimClientLibrary/AppearanceSettings/ImageConstants.swift delete mode 100644 Example/WebimClientLibrary/Base.lproj/LaunchScreen.xib delete mode 100755 Example/WebimClientLibrary/Base.lproj/RatingViewController.xib create mode 100644 Example/WebimClientLibrary/ChatTableViewController.swift create mode 100644 Example/WebimClientLibrary/FileViewController.swift create mode 100644 Example/WebimClientLibrary/ImageViewController.swift delete mode 100644 Example/WebimClientLibrary/Images.xcassets/Check.imageset/check_Icon-32.png create mode 100644 Example/WebimClientLibrary/Images.xcassets/Gradient.imageset/Contents.json create mode 100644 Example/WebimClientLibrary/Images.xcassets/Gradient.imageset/gradient-1.jpg rename Example/WebimClientLibrary/Images.xcassets/Icons/{Clip/Clip.imageset => AttachmentButton.imageset}/Contents.json (84%) create mode 100644 Example/WebimClientLibrary/Images.xcassets/Icons/AttachmentButton.imageset/Vector.pdf delete mode 100644 Example/WebimClientLibrary/Images.xcassets/Icons/Back/Back.imageset/back_Icon-32.png delete mode 100644 Example/WebimClientLibrary/Images.xcassets/Icons/Back/Back_dark.imageset/back_Icon-32.png delete mode 100644 Example/WebimClientLibrary/Images.xcassets/Icons/Clip/Clip.imageset/ClipIcon.pdf delete mode 100644 Example/WebimClientLibrary/Images.xcassets/Icons/Clip/Clip_dark.imageset/Clip_dark.pdf delete mode 100644 Example/WebimClientLibrary/Images.xcassets/Icons/Close/Close.imageset/Contents.json delete mode 100644 Example/WebimClientLibrary/Images.xcassets/Icons/Close/Close.imageset/close_Icon-32.png delete mode 100644 Example/WebimClientLibrary/Images.xcassets/Icons/Close/Close_dark.imageset/Contents.json delete mode 100644 Example/WebimClientLibrary/Images.xcassets/Icons/Close/Close_dark.imageset/close_Icon-32.png create mode 100644 Example/WebimClientLibrary/Images.xcassets/Icons/CloseButton.imageset/CloseButton-1.pdf rename Example/WebimClientLibrary/Images.xcassets/{Check.imageset => Icons/CloseButton.imageset}/Contents.json (83%) delete mode 100644 Example/WebimClientLibrary/Images.xcassets/Icons/Empty.imageset/Empty.pdf rename Example/WebimClientLibrary/Images.xcassets/Icons/{Back => FileStatus}/Contents.json (100%) create mode 100644 Example/WebimClientLibrary/Images.xcassets/Icons/FileStatus/FIleDownloadSeccessVisitor.imageset/Contents.json create mode 100644 Example/WebimClientLibrary/Images.xcassets/Icons/FileStatus/FIleDownloadSeccessVisitor.imageset/FIleDownloadSeccessVisitor.pdf create mode 100644 Example/WebimClientLibrary/Images.xcassets/Icons/FileStatus/FileDownloadButtonOperator.imageset/Contents.json create mode 100644 Example/WebimClientLibrary/Images.xcassets/Icons/FileStatus/FileDownloadButtonOperator.imageset/FileDownloadButtonOperator.pdf create mode 100644 Example/WebimClientLibrary/Images.xcassets/Icons/FileStatus/FileDownloadButtonVisitor.imageset/Contents.json create mode 100644 Example/WebimClientLibrary/Images.xcassets/Icons/FileStatus/FileDownloadButtonVisitor.imageset/FileDownloadButtonVisitor.pdf create mode 100644 Example/WebimClientLibrary/Images.xcassets/Icons/FileStatus/FileDownloadError.imageset/Contents.json create mode 100644 Example/WebimClientLibrary/Images.xcassets/Icons/FileStatus/FileDownloadError.imageset/FileDownloadError.pdf create mode 100644 Example/WebimClientLibrary/Images.xcassets/Icons/FileStatus/FileDownloadSuccessOperator.imageset/Contents.json create mode 100644 Example/WebimClientLibrary/Images.xcassets/Icons/FileStatus/FileDownloadSuccessOperator.imageset/FileDownloadSuccessOperator.pdf create mode 100644 Example/WebimClientLibrary/Images.xcassets/Icons/FileStatus/FileUploadButtonVisitor.imageset/Contents.json create mode 100644 Example/WebimClientLibrary/Images.xcassets/Icons/FileStatus/FileUploadButtonVisitor.imageset/FileUploadButtonVisitor.pdf create mode 100644 Example/WebimClientLibrary/Images.xcassets/Icons/ImageDownload.imageset/Contents.json create mode 100644 Example/WebimClientLibrary/Images.xcassets/Icons/ImageDownload.imageset/ImageDownload.pdf create mode 100644 Example/WebimClientLibrary/Images.xcassets/Icons/MessageActions/ActionCopy.imageset/ActionCopy.pdf rename Example/WebimClientLibrary/Images.xcassets/Icons/{Clip/Clip_dark.imageset => MessageActions/ActionCopy.imageset}/Contents.json (84%) create mode 100644 Example/WebimClientLibrary/Images.xcassets/Icons/MessageActions/ActionDelete.imageset/ActionDelete.pdf rename Example/WebimClientLibrary/Images.xcassets/Icons/{Back/Back.imageset => MessageActions/ActionDelete.imageset}/Contents.json (83%) create mode 100644 Example/WebimClientLibrary/Images.xcassets/Icons/MessageActions/ActionEdit.imageset/ActionEdit.pdf rename Example/WebimClientLibrary/Images.xcassets/Icons/{Back/Back_dark.imageset => MessageActions/ActionEdit.imageset}/Contents.json (83%) create mode 100644 Example/WebimClientLibrary/Images.xcassets/Icons/MessageActions/ActionReply.imageset/ActionReply.pdf create mode 100644 Example/WebimClientLibrary/Images.xcassets/Icons/MessageActions/ActionReply.imageset/Contents.json rename Example/WebimClientLibrary/Images.xcassets/Icons/{Clip => MessageActions}/Contents.json (100%) rename Example/WebimClientLibrary/Images.xcassets/Icons/{Close => MessageStatus}/Contents.json (100%) create mode 100644 Example/WebimClientLibrary/Images.xcassets/Icons/MessageStatus/ReadByOperator.imageset/Contents.json create mode 100644 Example/WebimClientLibrary/Images.xcassets/Icons/MessageStatus/ReadByOperator.imageset/ReadByOperator.pdf rename Example/WebimClientLibrary/Images.xcassets/Icons/{Empty.imageset => MessageStatus/Sent.imageset}/Contents.json (86%) create mode 100644 Example/WebimClientLibrary/Images.xcassets/Icons/MessageStatus/Sent.imageset/Sent.pdf create mode 100644 Example/WebimClientLibrary/Images.xcassets/Icons/ReplyCircleToTheLeft.imageset/Contents.json create mode 100644 Example/WebimClientLibrary/Images.xcassets/Icons/ReplyCircleToTheLeft.imageset/left.pdf delete mode 100644 Example/WebimClientLibrary/Images.xcassets/Icons/SendMessage/Contents.json delete mode 100644 Example/WebimClientLibrary/Images.xcassets/Icons/SendMessage/SendMessage.imageset/Contents.json delete mode 100644 Example/WebimClientLibrary/Images.xcassets/Icons/SendMessage/SendMessage.imageset/SendMessageIcon.pdf delete mode 100644 Example/WebimClientLibrary/Images.xcassets/Icons/SendMessage/SendMessage_dark.imageset/Contents.json delete mode 100644 Example/WebimClientLibrary/Images.xcassets/Icons/SendMessage/SendMessage_dark.imageset/SendMessage_dark.pdf create mode 100644 Example/WebimClientLibrary/Images.xcassets/Icons/SendMessageButton.imageset/Contents.json create mode 100644 Example/WebimClientLibrary/Images.xcassets/Icons/SendMessageButton.imageset/SendMessageButton.pdf create mode 100644 Example/WebimClientLibrary/Images.xcassets/ImagePlaceholder.imageset/Contents.json create mode 100644 Example/WebimClientLibrary/Images.xcassets/ImagePlaceholder.imageset/placeholder.png delete mode 100644 Example/WebimClientLibrary/Images.xcassets/ScrollToBottom/Contents.json delete mode 100644 Example/WebimClientLibrary/Images.xcassets/ScrollToBottom/ScrollToBottom.imageset/Contents.json delete mode 100644 Example/WebimClientLibrary/Images.xcassets/ScrollToBottom/ScrollToBottom.imageset/ScrollToBottom.pdf delete mode 100644 Example/WebimClientLibrary/Images.xcassets/ScrollToBottom/ScrollToBottom_dark.imageset/Contents.json delete mode 100644 Example/WebimClientLibrary/Images.xcassets/ScrollToBottom/ScrollToBottom_dark.imageset/ScrollToBottom_dark.pdf create mode 100644 Example/WebimClientLibrary/LaunchScreenController.swift delete mode 100644 Example/WebimClientLibrary/Models/ColorScheme.swift create mode 100644 Example/WebimClientLibrary/PopupActionsTableViewCell.swift create mode 100644 Example/WebimClientLibrary/PopupActionsViewController.swift create mode 100644 Example/WebimClientLibrary/RatingDialogViewController.swift create mode 100644 Example/WebimClientLibrary/Utilities/CircleProgressIndicator.swift create mode 100644 Example/WebimClientLibrary/Utilities/CustomUIButton.swift rename Example/WebimClientLibrary/{AppearanceSettings/ButtonConstants.swift => Utilities/CustomUIImage.swift} (68%) create mode 100644 Example/WebimClientLibrary/Utilities/CustomUIView.swift create mode 100644 Example/WebimClientLibrary/Utilities/Extensions/Message.swift create mode 100644 Example/WebimClientLibrary/Utilities/Extensions/Notification.Name.swift create mode 100644 Example/WebimClientLibrary/Utilities/Extensions/UIButton.swift delete mode 100644 Example/WebimClientLibrary/Utilities/Extensions/UIImageView.swift rename Example/WebimClientLibrary/{RatingViewController.swift => Utilities/Extensions/UIStackView.swift} (66%) create mode 100644 Example/WebimClientLibrary/Utilities/Extensions/UIView.swift create mode 100644 Example/WebimClientLibrary/Utilities/FilePicker.swift create mode 100644 Example/WebimClientLibrary/Utilities/FlexibleTableViewCell.swift rename Example/WebimClientLibrary/{ => Utilities}/MessageTableViewCell.swift (97%) delete mode 100644 Example/WebimClientLibrary/Utilities/PopupDialogHandler.swift create mode 100644 Example/WebimClientLibrary/Utilities/SpinningIndicator.swift create mode 100644 Example/WebimClientLibrary/Utilities/TypingIndicator.swift create mode 100644 Example/WebimClientLibrary/Utilities/UIAlertHandler.swift create mode 100644 Example/WebimClientLibrary/en.lproj/LaunchScreen.storyboard create mode 100644 Example/WebimClientLibrary/en.lproj/LaunchScreenController.storyboard create mode 100644 Example/WebimClientLibrary/en.lproj/Main.strings create mode 100644 Example/WebimClientLibrary/ru-RU.lproj/InfoPlist.strings delete mode 100644 Example/WebimClientLibrary/ru-RU.lproj/RatingViewController.strings create mode 100644 WebimClientLibrary/Backend/Items/StickerItem.swift diff --git a/Example/Podfile b/Example/Podfile index 9db4feb9..7a5dd430 100644 --- a/Example/Podfile +++ b/Example/Podfile @@ -1,4 +1,4 @@ -platform :ios, '9.0' +platform :ios, '10.0' use_frameworks! @@ -12,16 +12,15 @@ target 'WebimClientLibrary_Example' do pod 'SlackTextViewController', :inhibit_warnings => true pod 'SnapKit', :inhibit_warnings => true pod 'SQLite.swift', '0.12.2', :inhibit_warnings => true # WebimClientLibrary dependency – added to inhibit its warnings. - pod 'PopupDialog', '~> 1.0', :inhibit_warnings => true - - + pod 'Nuke', '~> 8.0' + target 'WebimClientLibrary_Tests' do inherit! :search_paths end post_install do |installer| installer.pods_project.targets.each do |target| - if target.name == 'WebimClientLibrary' || target.name == 'SQLite.swift' || target.name == 'Cosmos' + if target.name == 'WebimClientLibrary' || target.name == 'SQLite.swift' || target.name == 'Cosmos' || target.name == 'Nuke' target.build_configurations.each do |config| config.build_settings['SWIFT_VERSION'] = '5.0' end diff --git a/Example/Podfile.lock b/Example/Podfile.lock index e94a68f0..180ae375 100644 --- a/Example/Podfile.lock +++ b/Example/Podfile.lock @@ -2,23 +2,21 @@ PODS: - Cosmos (19.0.3) - Crashlytics (3.14.0): - Fabric (~> 1.10.2) - - DynamicBlurView (3.0.1) - Fabric (1.10.2) - - PopupDialog (1.0.0): - - DynamicBlurView (~> 3.0.1) + - Nuke (8.4.1) - SlackTextViewController (1.9.6) - - SnapKit (4.2.0) + - SnapKit (5.0.1) - SQLite.swift (0.12.2): - SQLite.swift/standard (= 0.12.2) - SQLite.swift/standard (0.12.2) - - WebimClientLibrary (3.32.0): + - WebimClientLibrary (3.33.0): - SQLite.swift (= 0.12.2) DEPENDENCIES: - Cosmos (~> 19.0.3) - Crashlytics - Fabric - - PopupDialog (~> 1.0) + - Nuke (~> 8.0) - SlackTextViewController - SnapKit - SQLite.swift (= 0.12.2) @@ -28,9 +26,8 @@ SPEC REPOS: trunk: - Cosmos - Crashlytics - - DynamicBlurView - Fabric - - PopupDialog + - Nuke - SlackTextViewController - SnapKit - SQLite.swift @@ -42,14 +39,13 @@ EXTERNAL SOURCES: SPEC CHECKSUMS: Cosmos: a6fb17284281fa12cf4c85c2efecd440a215ec44 Crashlytics: 9220f5bc89e7a618df411b4f639389dbfb0e03d2 - DynamicBlurView: b1df5415f9bd31897549e5d7077e5ec120a4d636 Fabric: ea977e3cd9c20425516d3dafd3bf8c941c51223f - PopupDialog: 28f29a1ffe9e1fab32fb258b53d56ca31670e8a2 + Nuke: d780e3507a86b86c589ab3cc5cd302d5456f06fb SlackTextViewController: b854e62c1c156336bc4fd409c6ca79b5773e8f9d - SnapKit: fe8a619752f3f27075cc9a90244d75c6c3f27e2a + SnapKit: 97b92857e3df3a0c71833cce143274bf6ef8e5eb SQLite.swift: d2b4642190917051ce6bd1d49aab565fe794eea3 - WebimClientLibrary: 59d371e8c3cf13a977f56add0bfc27769f2702d0 + WebimClientLibrary: b1b5df11fa7bf8abd4aaef5d407e57d838413c29 -PODFILE CHECKSUM: 6e32648b7078df57752b6b95cd2aa7128e22c8b5 +PODFILE CHECKSUM: 855d312996046cd3434fca3a4cba56d96623a531 -COCOAPODS: 1.9.3 +COCOAPODS: 1.10.0 diff --git a/Example/Tests/AbstractRequestLoopTests.swift b/Example/Tests/AbstractRequestLoopTests.swift index db9e567e..9ea97242 100644 --- a/Example/Tests/AbstractRequestLoopTests.swift +++ b/Example/Tests/AbstractRequestLoopTests.swift @@ -31,7 +31,8 @@ import XCTest class AbstractRequestLoopTests: XCTestCase { // MARK: - Properties - private let abstractRequestLoop = AbstractRequestLoopForTests() + private let abstractRequestLoop = AbstractRequestLoopForTests(completionHandlerExecutor: nil, + internalErrorListener: nil) // MARK: - Tests diff --git a/Example/Tests/ExampleTests/ChatViewControllerTests.swift b/Example/Tests/ExampleTests/ChatTableViewControllerTests.swift similarity index 83% rename from Example/Tests/ExampleTests/ChatViewControllerTests.swift rename to Example/Tests/ExampleTests/ChatTableViewControllerTests.swift index c09e5137..e6da071a 100644 --- a/Example/Tests/ExampleTests/ChatViewControllerTests.swift +++ b/Example/Tests/ExampleTests/ChatTableViewControllerTests.swift @@ -29,10 +29,10 @@ import XCTest @testable import WebimClientLibrary @testable import WebimClientLibrary_Example -class ChatViewControllerTests: XCTestCase { +class ChatTableViewControllerTests: XCTestCase { // MARK: - Properties - var chatViewController: ChatViewController! + var chatTableViewController: ChatTableViewController! // MARK: - Methods override func setUp() { @@ -40,23 +40,24 @@ class ChatViewControllerTests: XCTestCase { let storyboard = UIStoryboard(name: "Main", bundle: nil) - chatViewController = storyboard.instantiateViewController(withIdentifier: "ChatViewController") as? ChatViewController + chatTableViewController = storyboard.instantiateViewController(withIdentifier: "ChatTableViewController") as? ChatTableViewController + } // MARK: - Tests - func testBackgroundViewNotEmpty() { + func testBackgroundViewEmpty() { // When: Table view is empty. - let tableView = chatViewController.tableView! + let tableView = chatTableViewController.tableView! tableView.reloadData() - + // Then: Table view background has the message. let label = tableView.backgroundView as! UILabel XCTAssertEqual(label.attributedText?.string, "Send first message to start chat.") } - func testBackgroundViewEmpty() { + func testBackgroundViewNotEmpty() { // MARK: Set up var messages = [Message]() for index in 0 ... 2 { @@ -68,25 +69,27 @@ class ChatViewControllerTests: XCTestCase { quote: nil, senderAvatarURLString: nil, senderName: "Sender name", + sticker: nil, type: .visitorMessage, + rawData: nil, data: nil, text: "Text", timeInMicrosecond: Int64(index), - attachment: nil, historyMessage: false, internalID: nil, rawText: nil, read: false, messageCanBeEdited: false, - messageCanBeReplied: false) + messageCanBeReplied: false, + messageIsEdited: false) messages.append(message as Message) } // When: Table view is not empty. - chatViewController.set(messages: messages) - chatViewController.tableView?.reloadData() + chatTableViewController.set(messages: messages) + chatTableViewController.tableView?.reloadData() // Then: Table view background view is empty. - XCTAssertNil(chatViewController.tableView!.backgroundView) + XCTAssertNil(chatTableViewController.tableView!.backgroundView) } } diff --git a/Example/Tests/MemoryHistoryStorageTests.swift b/Example/Tests/MemoryHistoryStorageTests.swift index c5d6905a..194310d3 100644 --- a/Example/Tests/MemoryHistoryStorageTests.swift +++ b/Example/Tests/MemoryHistoryStorageTests.swift @@ -47,17 +47,19 @@ class MemoryHistoryStorageTests: XCTestCase { quote: nil, senderAvatarURLString: nil, senderName: "Name", - type: MessageType.operatorMessage, + sticker: nil, + type: .operatorMessage, + rawData: nil, data: nil, text: "Text", timeInMicrosecond: Int64(index), - attachment: nil, historyMessage: true, internalID: String(index), rawText: nil, read: false, messageCanBeEdited: false, - messageCanBeReplied: false)) + messageCanBeReplied: false, + messageIsEdited: false)) } return messages diff --git a/Example/Tests/MessageHolderTests.swift b/Example/Tests/MessageHolderTests.swift index 4768d920..8daba9a9 100644 --- a/Example/Tests/MessageHolderTests.swift +++ b/Example/Tests/MessageHolderTests.swift @@ -63,17 +63,19 @@ class MessageHolderTests: XCTestCase { quote: nil, senderAvatarURLString: MessageImplMockData.avatarURLString.rawValue, senderName: MessageImplMockData.senderName.rawValue, + sticker: nil, type: MessageType.operatorMessage, + rawData: nil, data: nil, text: MessageImplMockData.text.rawValue, timeInMicrosecond: Int64(index), - attachment: nil, historyMessage: true, internalID: String(index), rawText: nil, read: false, messageCanBeEdited: false, - messageCanBeReplied: false)) + messageCanBeReplied: false, + messageIsEdited: false)) } messagesCount = messagesCount + numberOfMessages @@ -93,17 +95,19 @@ class MessageHolderTests: XCTestCase { quote: nil, senderAvatarURLString: MessageImplMockData.avatarURLString.rawValue, senderName: MessageImplMockData.senderName.rawValue, + sticker: nil, type: MessageType.operatorMessage, + rawData: nil, data: nil, text: MessageImplMockData.text.rawValue, timeInMicrosecond: Int64(index), - attachment: nil, historyMessage: false, internalID: String(index), rawText: nil, read: false, messageCanBeEdited: false, - messageCanBeReplied: false)) + messageCanBeReplied: false, + messageIsEdited: false)) } messagesCount = messagesCount + numberOfMessages @@ -123,17 +127,19 @@ class MessageHolderTests: XCTestCase { quote: nil, senderAvatarURLString: message.getSenderAvatarURLString(), senderName: message.getSenderName(), + sticker: nil, type: message.getType(), + rawData: nil, data: message.getData(), text: message.getText(), timeInMicrosecond: message.getTimeInMicrosecond(), - attachment: message.getAttachment(), historyMessage: true, internalID: String(message.getTimeInMicrosecond()), rawText: message.getRawText(), read: message.getRead(), messageCanBeEdited: message.canBeEdited(), - messageCanBeReplied: false) + messageCanBeReplied: false, + messageIsEdited: false) result.append(newMessage) } @@ -151,17 +157,19 @@ class MessageHolderTests: XCTestCase { quote: nil, senderAvatarURLString: MessageImplMockData.avatarURLString.rawValue, senderName: MessageImplMockData.senderName.rawValue, + sticker: nil, type: MessageType.operatorMessage, + rawData: nil, data: nil, text: MessageImplMockData.text.rawValue, timeInMicrosecond: Int64(messagesCount), - attachment: nil, historyMessage: false, internalID: String(messagesCount), rawText: nil, read: false, messageCanBeEdited: false, - messageCanBeReplied: false) + messageCanBeReplied: false, + messageIsEdited: false) } private func newEdited(currentChatMessage: MessageImpl) -> MessageImpl { @@ -173,17 +181,19 @@ class MessageHolderTests: XCTestCase { quote: nil, senderAvatarURLString: currentChatMessage.getSenderAvatarURLString(), senderName: currentChatMessage.getSenderName(), + sticker: nil, type: currentChatMessage.getType(), + rawData: nil, data: nil, text: (currentChatMessage.getText() + " One more thing."), timeInMicrosecond: currentChatMessage.getTimeInMicrosecond(), - attachment: nil, historyMessage: false, internalID: currentChatMessage.getCurrentChatID(), rawText: nil, read: false, messageCanBeEdited: false, - messageCanBeReplied: false) + messageCanBeReplied: false, + messageIsEdited: false) } private func newEdited(historyMessage: MessageImpl) -> MessageImpl { @@ -195,17 +205,19 @@ class MessageHolderTests: XCTestCase { quote: nil, senderAvatarURLString: historyMessage.getSenderAvatarURLString(), senderName: historyMessage.getSenderName(), + sticker: nil, type: historyMessage.getType(), + rawData: nil, data: nil, text: (historyMessage.getText() + " One more thing."), timeInMicrosecond: historyMessage.getTimeInMicrosecond(), - attachment: nil, historyMessage: true, internalID: historyMessage.getHistoryID()?.getDBid(), rawText: nil, read: false, messageCanBeEdited: false, - messageCanBeReplied: false) + messageCanBeReplied: false, + messageIsEdited: false) } private func newMessageHolder(withHistory history: [MessageImpl] = [MessageImpl]()) -> MessageHolder { @@ -414,9 +426,9 @@ class MessageHolderTests: XCTestCase { try messageTracker.getNextMessages(byLimit: 100) { messages in completionHandlerMessages = messages as? [MessageImpl] } - // Then: No messages should be received and no history requests should be performed. + // Then: after emptying the history will be made a one more request. XCTAssertEqual(completionHandlerMessages!, [MessageImpl]()) - XCTAssertEqual((messageHolder.getRemoteHistoryProvider() as! RemoteHistoryProviderForTests).numberOfCalls, 1) + XCTAssertEqual((messageHolder.getRemoteHistoryProvider() as! RemoteHistoryProviderForTests).numberOfCalls, 2) // MARK: Test 4 // When: Resetting 15 messages back and requesting for all messages. @@ -426,7 +438,7 @@ class MessageHolderTests: XCTestCase { } // Then: 15 messages should be received and no history requests should be preformed. XCTAssertEqual(completionHandlerMessages!, (history1 + Array(history2[0 ... 4]))) - XCTAssertEqual((messageHolder.getRemoteHistoryProvider() as! RemoteHistoryProviderForTests).numberOfCalls, 1) + XCTAssertEqual((messageHolder.getRemoteHistoryProvider() as! RemoteHistoryProviderForTests).numberOfCalls, 2) } func testInsertMessagesBetweenOlderHistoryAndCurrentChat() throws { @@ -621,7 +633,7 @@ class MessageHolderTests: XCTestCase { completionHandlerMessages = messages as? [MessageImpl] } // Then: Completion handlers should be called first on received history messages. - XCTAssertEqual(completionHandlerMessages!, history1) + XCTAssertEqual(completionHandlerMessages!, Array(history2[5 ... 9])) } func testRequestAsManyMessagesAsReceivedWithHistoryForCurrentChat() throws { diff --git a/Example/Tests/MessageImplTests.swift b/Example/Tests/MessageImplTests.swift index 9150eab0..5acf5e74 100644 --- a/Example/Tests/MessageImplTests.swift +++ b/Example/Tests/MessageImplTests.swift @@ -41,17 +41,19 @@ class MessageImplTests: XCTestCase { senderAvatarURLString: nil, senderName: "Name", sendStatus: .sent, + sticker: nil, type: .visitorMessage, + rawData: nil, data: nil, text: "Text", timeInMicrosecond: 0, - attachment: nil, historyMessage: false, internalID: nil, rawText: nil, read: false, messageCanBeEdited: false, - messageCanBeReplied: false) + messageCanBeReplied: false, + messageIsEdited: false) let expectedString = """ MessageImpl { serverURLString = http://demo.webim.ru, @@ -85,17 +87,19 @@ MessageImpl { senderAvatarURLString: nil, senderName: "Name", sendStatus: .sent, + sticker: nil, type: .visitorMessage, + rawData: nil, data: nil, text: "Text", timeInMicrosecond: 0, - attachment: nil, historyMessage: false, internalID: nil, rawText: nil, read: false, messageCanBeEdited: false, - messageCanBeReplied: false) + messageCanBeReplied: false, + messageIsEdited: false) XCTAssertNil(message.getSenderAvatarFullURL()) } @@ -110,17 +114,19 @@ MessageImpl { senderAvatarURLString: nil, senderName: "Name", sendStatus: .sent, + sticker: nil, type: .visitorMessage, + rawData: nil, data: nil, text: "Text", timeInMicrosecond: 0, - attachment: nil, historyMessage: false, internalID: nil, rawText: nil, read: false, messageCanBeEdited: false, - messageCanBeReplied: false) + messageCanBeReplied: false, + messageIsEdited: false) XCTAssertEqual(message.getSendStatus(), MessageSendStatus.sent) @@ -136,17 +142,19 @@ MessageImpl { senderAvatarURLString: nil, senderName: "Name", sendStatus: .sent, + sticker: nil, type: .visitorMessage, + rawData: nil, data: nil, text: "Text", timeInMicrosecond: 0, - attachment: nil, historyMessage: false, internalID: nil, rawText: nil, read: false, messageCanBeEdited: false, - messageCanBeReplied: false) + messageCanBeReplied: false, + messageIsEdited: false) let message1 = MessageImpl(serverURLString: "http://demo.webim.ru", id: "id1", @@ -157,17 +165,19 @@ MessageImpl { senderAvatarURLString: nil, senderName: "Name", sendStatus: .sent, + sticker: nil, type: .visitorMessage, + rawData: nil, data: nil, text: "Text", timeInMicrosecond: 0, - attachment: nil, historyMessage: false, internalID: nil, rawText: nil, read: false, messageCanBeEdited: false, - messageCanBeReplied: false) + messageCanBeReplied: false, + messageIsEdited: false) let message2 = MessageImpl(serverURLString: "http://demo.webim.ru", id: "id", keyboard: nil, @@ -177,17 +187,19 @@ MessageImpl { senderAvatarURLString: nil, senderName: "Name1", sendStatus: .sent, + sticker: nil, type: .visitorMessage, + rawData: nil, data: nil, text: "Text", timeInMicrosecond: 0, - attachment: nil, historyMessage: false, internalID: nil, rawText: nil, read: false, messageCanBeEdited: false, - messageCanBeReplied: false) + messageCanBeReplied: false, + messageIsEdited: false) let message3 = MessageImpl(serverURLString: "http://demo.webim.ru", id: "id", keyboard: nil, @@ -197,17 +209,19 @@ MessageImpl { senderAvatarURLString: nil, senderName: "Name", sendStatus: .sent, + sticker: nil, type: .visitorMessage, + rawData: nil, data: nil, text: "Text1", timeInMicrosecond: 0, - attachment: nil, historyMessage: false, internalID: nil, rawText: nil, read: false, messageCanBeEdited: false, - messageCanBeReplied: false) + messageCanBeReplied: false, + messageIsEdited: false) let message4 = MessageImpl(serverURLString: "http://demo.webim.ru", id: "id", keyboard: nil, @@ -217,17 +231,19 @@ MessageImpl { senderAvatarURLString: nil, senderName: "Name", sendStatus: .sent, + sticker: nil, type: .operatorMessage, + rawData: nil, data: nil, text: "Text", timeInMicrosecond: 0, - attachment: nil, historyMessage: false, internalID: nil, rawText: nil, read: false, messageCanBeEdited: false, - messageCanBeReplied: false) + messageCanBeReplied: false, + messageIsEdited: false) let message5 = MessageImpl(serverURLString: "http://demo.webim.ru", id: "id", keyboard: nil, @@ -237,23 +253,47 @@ MessageImpl { senderAvatarURLString: nil, senderName: "Name", sendStatus: .sent, + sticker: nil, type: .visitorMessage, + rawData: nil, data: nil, text: "Text", timeInMicrosecond: 0, - attachment: nil, historyMessage: false, internalID: nil, rawText: nil, read: false, messageCanBeEdited: false, - messageCanBeReplied: false) + messageCanBeReplied: false, + messageIsEdited: false) + let message6 = MessageImpl(serverURLString: "http://demo.webim.ru", + id: "id", + keyboard: nil, + keyboardRequest: nil, + operatorID: nil, + quote: nil, + senderAvatarURLString: nil, + senderName: "Name", + sendStatus: .SENT, + type: .VISITOR, + data: nil, + text: "Text", + timeInMicrosecond: 0, + attachment: nil, + historyMessage: false, + internalID: nil, + rawText: nil, + read: false, + messageCanBeEdited: false, + messageCanBeReplied: false, + messageIsEdited: true) XCTAssertFalse(message.isEqual(to: message1)) XCTAssertFalse(message.isEqual(to: message2)) XCTAssertFalse(message.isEqual(to: message3)) XCTAssertFalse(message.isEqual(to: message4)) XCTAssertTrue(message.isEqual(to: message5)) + XCTAssertFalse(message.isEqual(to: message6)) } // MARK: MessageSource tests @@ -268,17 +308,19 @@ MessageImpl { senderAvatarURLString: nil, senderName: "Name", sendStatus: .sent, + sticker: nil, type: .visitorMessage, + rawData: nil, data: nil, text: "Text", timeInMicrosecond: 0, - attachment: nil, historyMessage: false, internalID: nil, rawText: nil, read: false, messageCanBeEdited: false, - messageCanBeReplied: false) + messageCanBeReplied: false, + messageIsEdited: false) XCTAssertNoThrow(try message.getSource().assertIsCurrentChat()) } @@ -293,17 +335,19 @@ MessageImpl { senderAvatarURLString: nil, senderName: "Name", sendStatus: .sent, + sticker: nil, type: .visitorMessage, + rawData: nil, data: nil, text: "Text", timeInMicrosecond: 0, - attachment: nil, historyMessage: false, internalID: nil, rawText: nil, read: false, messageCanBeEdited: false, - messageCanBeReplied: false) + messageCanBeReplied: false, + messageIsEdited: false) XCTAssertThrowsError(try message.getSource().assertIsHistory()) } @@ -318,17 +362,19 @@ MessageImpl { senderAvatarURLString: nil, senderName: "Name", sendStatus: .sent, + sticker: nil, type: .visitorMessage, + rawData: nil, data: nil, text: "Text", timeInMicrosecond: 0, - attachment: nil, historyMessage: false, internalID: nil, rawText: nil, read: false, messageCanBeEdited: false, - messageCanBeReplied: false) + messageCanBeReplied: false, + messageIsEdited: false) XCTAssertNil(message.getHistoryID()) } @@ -344,17 +390,19 @@ MessageImpl { senderAvatarURLString: nil, senderName: "Name", sendStatus: .sent, + sticker: nil, type: .visitorMessage, + rawData: nil, data: nil, text: "Text", timeInMicrosecond: 0, - attachment: nil, historyMessage: false, internalID: currentChatID, rawText: nil, read: false, messageCanBeEdited: false, - messageCanBeReplied: false) + messageCanBeReplied: false, + messageIsEdited: false) XCTAssertEqual(currentChatID, message.getCurrentChatID()) @@ -372,17 +420,19 @@ MessageImpl { senderAvatarURLString: avatarURLString, senderName: "Name", sendStatus: .sent, + sticker: nil, type: .visitorMessage, + rawData: nil, data: nil, text: "Text", timeInMicrosecond: 0, - attachment: nil, historyMessage: false, internalID: nil, rawText: nil, read: false, messageCanBeEdited: false, - messageCanBeReplied: false) + messageCanBeReplied: false, + messageIsEdited: false) XCTAssertEqual(URL(string: (baseURLString + avatarURLString)), message.getSenderAvatarFullURL()) @@ -395,17 +445,15 @@ class MessageAttachmentTests: XCTestCase { // MARK: - Tests func testInit() { - let messageAttachment = MessageAttachmentImpl(urlString: "/image.jpg", - size: 1, - filename: "image", - contentType: "image/jpeg", - imageInfo: nil) + let messageAttachment = FileInfoImpl(urlString: "/image.jpg", + size: 1, + filename: "image", + contentType: "image/jpeg") XCTAssertEqual(messageAttachment.getContentType(), "image/jpeg") XCTAssertEqual(messageAttachment.getFileName(), "image") - XCTAssertNil(messageAttachment.getImageInfo()) XCTAssertEqual(messageAttachment.getSize(), 1) XCTAssertEqual(messageAttachment.getURL(), diff --git a/Example/Tests/Mocks/InternalErrorListenerMock.swift b/Example/Tests/Mocks/InternalErrorListenerMock.swift index efffa64e..4ac45096 100644 --- a/Example/Tests/Mocks/InternalErrorListenerMock.swift +++ b/Example/Tests/Mocks/InternalErrorListenerMock.swift @@ -28,6 +28,10 @@ import Foundation @testable import WebimClientLibrary final class InternalErrorListenerForTests: InternalErrorListener { + func onNotFaral(error: NotFatalErrorType) { + // No need to do anything when testing + } + func on(error: String) { // No need to do anything when testing. diff --git a/Example/Tests/SQLiteHistoryStorageTests.swift b/Example/Tests/SQLiteHistoryStorageTests.swift index 950cbc5b..fe103c31 100644 --- a/Example/Tests/SQLiteHistoryStorageTests.swift +++ b/Example/Tests/SQLiteHistoryStorageTests.swift @@ -104,6 +104,7 @@ class SQLiteHistoryStorageTests: XCTestCase { quote: nil, senderAvatarURLString: nil, senderName: "Name", + sticker: nil, type: MessageType.operatorMessage, data: nil, text: "Text", @@ -114,7 +115,8 @@ class SQLiteHistoryStorageTests: XCTestCase { rawText: nil, read: true, messageCanBeEdited: false, - messageCanBeReplied: false)) + messageCanBeReplied: false, + messageIsEdited: false)) } return messages @@ -124,7 +126,7 @@ class SQLiteHistoryStorageTests: XCTestCase { func testGetMajorVersion() { XCTAssertEqual(sqLiteHistoryStorage!.getMajorVersion(), - 1) + 4) } func testGetFullHistory() { diff --git a/Example/Tests/WebimActionsTests.swift b/Example/Tests/WebimActionsTests.swift index 94d6773d..2a2dc87f 100644 --- a/Example/Tests/WebimActionsTests.swift +++ b/Example/Tests/WebimActionsTests.swift @@ -374,10 +374,12 @@ class WebimActionsTests: XCTestCase { // Setup. let operatorID = "1" let rating = "2" + let visitorNote = "RateNote" // When: Rating an operator. webimActions?.rateOperatorWith(id: operatorID, rating: Int(rating)!, + visitorNote: visitorNote, completionHandler: nil) // Then: Request parameters should be like this. @@ -387,11 +389,14 @@ class WebimActionsTests: XCTestCase { let expectedParametersDictionary = ["action" : "chat.operator_rate_select", "rate" : rating, + "visitor_note" : visitorNote, "operator_id" : operatorID] as [String : Any] XCTAssertEqual(actionRequestLoop.webimRequest!.getPrimaryData()["action"] as! String, expectedParametersDictionary["action"] as! String) XCTAssertEqual(actionRequestLoop.webimRequest!.getPrimaryData()["rate"] as! String, expectedParametersDictionary["rate"] as! String) + XCTAssertEqual(actionRequestLoop.webimRequest!.getPrimaryData()["visitor_note"] as! String, + expectedParametersDictionary["visitor_note"] as! String) XCTAssertEqual(actionRequestLoop.webimRequest!.getPrimaryData()["operator_id"] as! String, expectedParametersDictionary["operator_id"] as! String) diff --git a/Example/Tests/WebimRemoteNotificationImplTests.swift b/Example/Tests/WebimRemoteNotificationImplTests.swift index a7da208c..201a1590 100644 --- a/Example/Tests/WebimRemoteNotificationImplTests.swift +++ b/Example/Tests/WebimRemoteNotificationImplTests.swift @@ -34,11 +34,17 @@ class WebimRemoteNotificationImplTests: XCTestCase { func testContactRequestNotification() { // Setup. - let notificationDictionary = ["loc-key" : "P.CR"] as [String : Any] - + let notificationDictionary = [ + "aps" : [ + "alert" : [ + "loc-key" : "P.CR" + ] as [String : Any] + ] + ] + // When: Receiving contact request notification. let webimRemoteNotification = WebimRemoteNotificationImpl(jsonDictionary: notificationDictionary) - + // Then: Parameters should be ruturned like this. XCTAssertNil(webimRemoteNotification?.getEvent()) XCTAssertTrue(webimRemoteNotification!.getParameters().isEmpty) @@ -48,8 +54,14 @@ class WebimRemoteNotificationImplTests: XCTestCase { func testOperatorAcceptedNotification() { // Setup. - let notificationDictionary = ["loc-key" : "P.OA", - "loc-args" : ["Operator"]] as [String : Any] + let notificationDictionary = [ + "aps" : [ + "alert" : [ + "loc-key" : "P.OA", + "loc-args" : ["Operator"] + ] as [String : Any] + ] + ] // When: Receiving operator accepted notification. let webimRemoteNotification = WebimRemoteNotificationImpl(jsonDictionary: notificationDictionary) @@ -63,10 +75,15 @@ class WebimRemoteNotificationImplTests: XCTestCase { func testOperatorFileNotification() { // Setup. - let notificationDictionary = ["loc-key" : "P.OF", - "loc-args" : ["Operator", - "File"], - "event" : "add"] as [String : Any] + let notificationDictionary = [ + "aps" : [ + "alert" : [ + "loc-key" : "P.OF", + "loc-args" : ["Operator", "File"], + "event" : "add" + ] as [String : Any] + ] + ] // When: Receiving operator file adding notification. let webimRemoteNotification = WebimRemoteNotificationImpl(jsonDictionary: notificationDictionary) @@ -80,14 +97,19 @@ class WebimRemoteNotificationImplTests: XCTestCase { func testOperatorMessageNotification() { // Setup. - let notificationDictionary = ["loc-key" : "P.OM", - "loc-args" : ["Operator", - "Message"], - "event" : "del"] as [String : Any] - + let notificationDictionary = [ + "aps" : [ + "alert" : [ + "loc-key" : "P.OM", + "loc-args" : ["Operator", "Message"], + "event" : "del" + ] as [String : Any] + ] + ] + // When: Receiving operator message deleting notification. let webimRemoteNotification = WebimRemoteNotificationImpl(jsonDictionary: notificationDictionary) - + // Then: Parameters should be ruturned like this. XCTAssertEqual(webimRemoteNotification?.getEvent(), NotificationEvent.delete) XCTAssertTrue(webimRemoteNotification?.getParameters().count == 2) @@ -97,11 +119,17 @@ class WebimRemoteNotificationImplTests: XCTestCase { func testWidgetNotification() { // Setup. - let notificationDictionary = ["loc-key" : "P.WM"] as [String : Any] - + let notificationDictionary = [ + "aps" : [ + "alert" : [ + "loc-key" : "P.WM" + ] as [String : Any] + ] + ] + // When: Receiving contact request notification. let webimRemoteNotification = WebimRemoteNotificationImpl(jsonDictionary: notificationDictionary) - + // Then: Parameters should be ruturned like this. XCTAssertNil(webimRemoteNotification?.getEvent()) XCTAssertTrue(webimRemoteNotification!.getParameters().isEmpty) @@ -109,9 +137,15 @@ class WebimRemoteNotificationImplTests: XCTestCase { NotificationType.widget) } - func testUnsupportedType() { + func testUnsupportedAps() { // Setup. - let notificationDictionary = ["loc-key" : "NewType"] as [String : Any] + let notificationDictionary = [ + "aps" : [ + "non-alert" : [ + "loc-key" : "P.WM" + ] as [String : Any] + ] + ] // When: Receiving notification of unsupported type. let webimRemoteNotification = WebimRemoteNotificationImpl(jsonDictionary: notificationDictionary) @@ -122,10 +156,15 @@ class WebimRemoteNotificationImplTests: XCTestCase { func testUnsupportedEvent() { // Setup. - let notificationDictionary = ["loc-key" : "P.OM", - "loc-args" : ["Operator", - "Message"], - "event" : "NewEvent"] as [String : Any] + let notificationDictionary = [ + "aps" : [ + "alert" : [ + "loc-key" : "P.OM", + "loc-args" : ["Operator", "Message"], + "event" : "NewEvent" + ] as [String : Any] + ] + ] // When: Receiving notification of unsupported type. let webimRemoteNotification = WebimRemoteNotificationImpl(jsonDictionary: notificationDictionary) @@ -134,11 +173,13 @@ class WebimRemoteNotificationImplTests: XCTestCase { XCTAssertNil(webimRemoteNotification?.getEvent()) } - func testEmptyTypeNotification() { + func testEmptyAlertNotification() { // Setup. - let notificationDictionary = ["loc-args" : ["Operator", - "Message"], - "event" : "del"] as [String : Any] + let notificationDictionary = [ + "aps" : [ + "alert" : [] + ] + ] // When: Receiving notification without type. let webimRemoteNotification = WebimRemoteNotificationImpl(jsonDictionary: notificationDictionary) diff --git a/Example/WebimClientLibrary.xcodeproj/project.pbxproj b/Example/WebimClientLibrary.xcodeproj/project.pbxproj index f0587f60..c1950c61 100644 --- a/Example/WebimClientLibrary.xcodeproj/project.pbxproj +++ b/Example/WebimClientLibrary.xcodeproj/project.pbxproj @@ -8,14 +8,14 @@ /* Begin PBXBuildFile section */ 11EFE7D3231AC33500B64EFA /* FAQ methods documentation.md in Resources */ = {isa = PBXBuildFile; fileRef = 11EFE7D2231AC33500B64EFA /* FAQ methods documentation.md */; }; - 3BD523E036E5E0145C40A893 /* Pods_WebimClientLibrary_Tests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 75BFCD5FB1955EE77D987E3C /* Pods_WebimClientLibrary_Tests.framework */; }; + 38683A7524A4FDCB007DED63 /* UIButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 38683A7424A4FDCB007DED63 /* UIButton.swift */; }; 607FACD61AFB9204008FA782 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 607FACD51AFB9204008FA782 /* AppDelegate.swift */; }; 607FACD81AFB9204008FA782 /* StartViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 607FACD71AFB9204008FA782 /* StartViewController.swift */; }; 607FACDB1AFB9204008FA782 /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 607FACD91AFB9204008FA782 /* Main.storyboard */; }; 607FACDD1AFB9204008FA782 /* Images.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 607FACDC1AFB9204008FA782 /* Images.xcassets */; }; - 607FACE01AFB9204008FA782 /* LaunchScreen.xib in Resources */ = {isa = PBXBuildFile; fileRef = 607FACDE1AFB9204008FA782 /* LaunchScreen.xib */; }; 607FACEC1AFB9204008FA782 /* MessageHolderTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 607FACEB1AFB9204008FA782 /* MessageHolderTests.swift */; }; - BDBBA7998D78E4CE2C982C3E /* Pods_WebimClientLibrary_Example.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D5A6DD52DA6213CB92530809 /* Pods_WebimClientLibrary_Example.framework */; }; + 625B0CFF11CFB24FA44C17A0 /* Pods_WebimClientLibrary_Example.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 85F5395A54F1170A064A76BF /* Pods_WebimClientLibrary_Example.framework */; }; + 754A3410E17BB84F9F78C1E0 /* Pods_WebimClientLibrary_Tests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = FBB18D8A6A1E4DEE621109CB /* Pods_WebimClientLibrary_Tests.framework */; }; CE07E9952049794C00E0A0D3 /* SessionBuilderTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE07E993204972C600E0A0D3 /* SessionBuilderTests.swift */; }; CE0CD75A202338FD00719DBE /* HistoryIDTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE0CD758202338EE00719DBE /* HistoryIDTests.swift */; }; CE0CD75D20234BD800719DBE /* SessionDestroyerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE0CD75B20234BD500719DBE /* SessionDestroyerTests.swift */; }; @@ -40,14 +40,8 @@ CE2845C82021DBE5007B1C25 /* DeltaRequestLoopTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE2845C72021DBE5007B1C25 /* DeltaRequestLoopTests.swift */; }; CE2CD5952035DF6E00997628 /* MemoryHistoryMetaInformationStorageTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE2CD5942035DF6E00997628 /* MemoryHistoryMetaInformationStorageTests.swift */; }; CE2CD5972035F12600997628 /* ChatItemTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE2CD5962035F12600997628 /* ChatItemTests.swift */; }; - CE324E631FDECBD2004BB116 /* RatingViewController.xib in Resources */ = {isa = PBXBuildFile; fileRef = CE324E651FDECBD2004BB116 /* RatingViewController.xib */; }; - CE380424202B0AC2003032D4 /* SettingsTableViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE380423202B0AC2003032D4 /* SettingsTableViewController.swift */; }; - CE380426202B3526003032D4 /* ColorScheme.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE380425202B3526003032D4 /* ColorScheme.swift */; }; - CE44AE651F87CA8E009787E5 /* MessageTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE44AE641F87CA8E009787E5 /* MessageTableViewCell.swift */; }; - CE45EDDD1F9108FA00F56319 /* UIImageView.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE45EDDC1F9108FA00F56319 /* UIImageView.swift */; }; CE45EDE01F95F9F700F56319 /* MimeType.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE45EDDF1F95F95C00F56319 /* MimeType.swift */; }; CE48362C2031DA5E00E18A20 /* MimeTypeTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE48362B2031DA5E00E18A20 /* MimeTypeTests.swift */; }; - CE48362F2032E3F100E18A20 /* PopupDialogHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE48362D2032E0D900E18A20 /* PopupDialogHandler.swift */; }; CE591A431FC2F31E004C95EE /* WebimService.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE591A421FC2F31E004C95EE /* WebimService.swift */; }; CE5A03BA1FDEB756009D320A /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = CE5A03BC1FDEB756009D320A /* Localizable.strings */; }; CE5B7E5C20500A6F00DDA407 /* ProvidedVisitorFieldsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE5B7E5B20500A6F00DDA407 /* ProvidedVisitorFieldsTests.swift */; }; @@ -70,21 +64,44 @@ CE86FAC0203C6FBC00CB9C2D /* WebimErrorImplTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE86FABF203C6FBC00CB9C2D /* WebimErrorImplTests.swift */; }; CE88BCD11FCD7F9A00A4FA2E /* SettingsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE88BCD01FCD7F9A00A4FA2E /* SettingsViewController.swift */; }; CE88BCD31FCD88EA00A4FA2E /* Settings.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE88BCD21FCD88EA00A4FA2E /* Settings.swift */; }; - CE88BCD51FCDA45C00A4FA2E /* ButtonConstants.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE88BCD41FCDA45C00A4FA2E /* ButtonConstants.swift */; }; CE995DC32020908F00E27A3C /* WebimRemoteNotificationImplTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE995DC12020904300E27A3C /* WebimRemoteNotificationImplTests.swift */; }; - CEA832681F9906C0004845F0 /* ColorConstants.swift in Sources */ = {isa = PBXBuildFile; fileRef = CEA832671F9906BF004845F0 /* ColorConstants.swift */; }; CEA8326A1F99079A004845F0 /* StringConstants.swift in Sources */ = {isa = PBXBuildFile; fileRef = CEA832691F99079A004845F0 /* StringConstants.swift */; }; CEA849051FFCE29A006CC417 /* UITableView.swift in Sources */ = {isa = PBXBuildFile; fileRef = CEA849041FFCE29A006CC417 /* UITableView.swift */; }; CEBFB67F2018CB0F00D9E5F6 /* String.swift in Sources */ = {isa = PBXBuildFile; fileRef = CEBFB67E2018CB0F00D9E5F6 /* String.swift */; }; CEBFB6812018E50500D9E5F6 /* UIViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = CEBFB6802018E50500D9E5F6 /* UIViewController.swift */; }; CED3BA08201F7E100071EC23 /* WebimActionsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = CED3BA07201F7E100071EC23 /* WebimActionsTests.swift */; }; CED3BA0B201F87510071EC23 /* InternalErrorListenerMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = CED3BA0A201F87510071EC23 /* InternalErrorListenerMock.swift */; }; - CED72596200E20EF00CD1623 /* ChatViewControllerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = CED72594200E20E500CD1623 /* ChatViewControllerTests.swift */; }; + CED72596200E20EF00CD1623 /* ChatTableViewControllerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = CED72594200E20E500CD1623 /* ChatTableViewControllerTests.swift */; }; CED72599200E5B2200CD1623 /* WebimInternalLoggerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = CED72597200E5B1C00CD1623 /* WebimInternalLoggerTests.swift */; }; CEE382C22035D544006C809E /* WebimTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = CEE382C12035D544006C809E /* WebimTests.swift */; }; - CEF61A391F82A4C700BF7071 /* ChatViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = CEF61A381F82A4C700BF7071 /* ChatViewController.swift */; }; - DDFC632B1F924D41008E1ACC /* UIImage.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDFC632A1F924D41008E1ACC /* UIImage.swift */; }; - DDFC63CB1F93F23E008E1ACC /* RatingViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDFC63CA1F93F23E008E1ACC /* RatingViewController.swift */; }; + DD0B8694234330DA00CB5712 /* ChatViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD0B8693234330DA00CB5712 /* ChatViewController.swift */; }; + DD2B08B7233A24D900BF0283 /* FlexibleTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD2B08B6233A24D900BF0283 /* FlexibleTableViewCell.swift */; }; + DD2F158523336A7600532934 /* ChatTableViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD2F158423336A7600532934 /* ChatTableViewController.swift */; }; + DD2F15872333FB2100532934 /* ImageViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD2F15862333FB2100532934 /* ImageViewController.swift */; }; + DD38105B2341EACA00F44FC6 /* FileViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD38105A2341EACA00F44FC6 /* FileViewController.swift */; }; + DD584C0423715F0600B2EC34 /* TypingIndicator.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD584C0323715F0600B2EC34 /* TypingIndicator.swift */; }; + DD5AA7A5236089AF009680D6 /* CustomUIButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD5AA7A4236089AF009680D6 /* CustomUIButton.swift */; }; + DD5B1ADF2383FCC6005F95A8 /* CustomUIImage.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD5B1ADE2383FCC6005F95A8 /* CustomUIImage.swift */; }; + DD70B9FA235E296100F457A1 /* CircleProgressIndicator.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD70B9F9235E296100F457A1 /* CircleProgressIndicator.swift */; }; + DD71AD05234DAFDC00A32FB6 /* UIAlertHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD71AD04234DAFDC00A32FB6 /* UIAlertHandler.swift */; }; + DD72BE81238E7FB10055F200 /* UIImage.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD72BE80238E7FB10055F200 /* UIImage.swift */; }; + DD7743FF2369F1E8000FAF9A /* RatingDialogViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD7743FE2369F1E8000FAF9A /* RatingDialogViewController.swift */; }; + DD8A8D2D232A3E200011D275 /* LaunchScreenController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD8A8D2C232A3E200011D275 /* LaunchScreenController.swift */; }; + DD8A8D31232F6CF10011D275 /* SettingsTableViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD8A8D30232F6CF10011D275 /* SettingsTableViewController.swift */; }; + DD8A8D35232FE5D30011D275 /* Notification.Name.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD8A8D34232FE5D30011D275 /* Notification.Name.swift */; }; + DD8A8D37233001510011D275 /* UIView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD8A8D36233001510011D275 /* UIView.swift */; }; + DD8F7D0B237AFA23006D9146 /* FilePicker.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD8F7D0A237AFA23006D9146 /* FilePicker.swift */; }; + DD8F7D0D237B3C0E006D9146 /* UIStackView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD8F7D0C237B3C0E006D9146 /* UIStackView.swift */; }; + DD94E0B02345148A009133B4 /* PopupActionsTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD94E0AF2345148A009133B4 /* PopupActionsTableViewCell.swift */; }; + DD976EC323700D1C00412518 /* InfoPlist.strings in Resources */ = {isa = PBXBuildFile; fileRef = DD976EC523700D1C00412518 /* InfoPlist.strings */; }; + DDC478B32366E13C005E6057 /* Message.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDC478B22366E13C005E6057 /* Message.swift */; }; + DDC478B7236717D9005E6057 /* PopupActionsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDC478B6236717D9005E6057 /* PopupActionsViewController.swift */; }; + DDC7924123573CB500AF424F /* LaunchScreenController.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = DDC7924323573CB500AF424F /* LaunchScreenController.storyboard */; }; + DDC7924623573CFF00AF424F /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = DDC7924823573CFF00AF424F /* LaunchScreen.storyboard */; }; + DDC9418E2354F0060005A59C /* SpinningIndicator.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDC9418D2354F0060005A59C /* SpinningIndicator.swift */; }; + DDCC52582356333400D39F8A /* ImageConstants.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDCC52572356333400D39F8A /* ImageConstants.swift */; }; + DDCC525A2356360F00D39F8A /* ColourConstants.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDCC52592356360F00D39F8A /* ColourConstants.swift */; }; + DDDE82CF23830145008DDF61 /* CustomUIView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDDE82CE23830144008DDF61 /* CustomUIView.swift */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -99,21 +116,21 @@ /* Begin PBXFileReference section */ 11EFE7D2231AC33500B64EFA /* FAQ methods documentation.md */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = net.daringfireball.markdown; path = "FAQ methods documentation.md"; sourceTree = ""; }; - 188E01CE8A86B2460C60C156 /* Pods-WebimClientLibrary_Tests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-WebimClientLibrary_Tests.release.xcconfig"; path = "Target Support Files/Pods-WebimClientLibrary_Tests/Pods-WebimClientLibrary_Tests.release.xcconfig"; sourceTree = ""; }; + 1480012CD38CEC0494B440FD /* Pods-WebimClientLibrary_Example.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-WebimClientLibrary_Example.release.xcconfig"; path = "Target Support Files/Pods-WebimClientLibrary_Example/Pods-WebimClientLibrary_Example.release.xcconfig"; sourceTree = ""; }; + 38683A7424A4FDCB007DED63 /* UIButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UIButton.swift; sourceTree = ""; }; 607FACD01AFB9204008FA782 /* WebimClientLibrary_Example.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = WebimClientLibrary_Example.app; sourceTree = BUILT_PRODUCTS_DIR; }; 607FACD41AFB9204008FA782 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 607FACD51AFB9204008FA782 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; 607FACD71AFB9204008FA782 /* StartViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StartViewController.swift; sourceTree = ""; }; 607FACDA1AFB9204008FA782 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; 607FACDC1AFB9204008FA782 /* Images.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Images.xcassets; sourceTree = ""; }; - 607FACDF1AFB9204008FA782 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = Base; path = Base.lproj/LaunchScreen.xib; sourceTree = ""; }; 607FACE51AFB9204008FA782 /* WebimClientLibrary_Tests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = WebimClientLibrary_Tests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; 607FACEA1AFB9204008FA782 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 607FACEB1AFB9204008FA782 /* MessageHolderTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageHolderTests.swift; sourceTree = ""; }; - 75BFCD5FB1955EE77D987E3C /* Pods_WebimClientLibrary_Tests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_WebimClientLibrary_Tests.framework; sourceTree = BUILT_PRODUCTS_DIR; }; - 998795E306696F247C099BA2 /* Pods-WebimClientLibrary_Tests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-WebimClientLibrary_Tests.debug.xcconfig"; path = "Target Support Files/Pods-WebimClientLibrary_Tests/Pods-WebimClientLibrary_Tests.debug.xcconfig"; sourceTree = ""; }; - 9F85297EB410C96E5341BFCC /* Pods-WebimClientLibrary_Example.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-WebimClientLibrary_Example.release.xcconfig"; path = "Target Support Files/Pods-WebimClientLibrary_Example/Pods-WebimClientLibrary_Example.release.xcconfig"; sourceTree = ""; }; + 7767259DD04077F82FEB0E85 /* Pods-WebimClientLibrary_Tests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-WebimClientLibrary_Tests.debug.xcconfig"; path = "Target Support Files/Pods-WebimClientLibrary_Tests/Pods-WebimClientLibrary_Tests.debug.xcconfig"; sourceTree = ""; }; + 85F5395A54F1170A064A76BF /* Pods_WebimClientLibrary_Example.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_WebimClientLibrary_Example.framework; sourceTree = BUILT_PRODUCTS_DIR; }; AB52E1C1E0F009A386DC1C6C /* LICENSE */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text; name = LICENSE; path = ../LICENSE; sourceTree = ""; }; + B8CA89B3A42B8B92B0515245 /* Pods-WebimClientLibrary_Tests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-WebimClientLibrary_Tests.release.xcconfig"; path = "Target Support Files/Pods-WebimClientLibrary_Tests/Pods-WebimClientLibrary_Tests.release.xcconfig"; sourceTree = ""; }; C4CD65CA48F80B5A4AD826B3 /* WebimClientLibrary.podspec */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text; name = WebimClientLibrary.podspec; path = ../WebimClientLibrary.podspec; sourceTree = ""; xcLanguageSpecificationIdentifier = xcode.lang.ruby; }; CE07E993204972C600E0A0D3 /* SessionBuilderTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SessionBuilderTests.swift; sourceTree = ""; }; CE0AD26F1F96477500EA9148 /* WebimClientLibrary_Example.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = WebimClientLibrary_Example.entitlements; sourceTree = ""; }; @@ -141,14 +158,8 @@ CE2845C72021DBE5007B1C25 /* DeltaRequestLoopTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeltaRequestLoopTests.swift; sourceTree = ""; }; CE2CD5942035DF6E00997628 /* MemoryHistoryMetaInformationStorageTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MemoryHistoryMetaInformationStorageTests.swift; sourceTree = ""; }; CE2CD5962035F12600997628 /* ChatItemTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatItemTests.swift; sourceTree = ""; }; - CE324E661FDECBE0004BB116 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = Base; path = Base.lproj/RatingViewController.xib; sourceTree = ""; }; - CE380423202B0AC2003032D4 /* SettingsTableViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsTableViewController.swift; sourceTree = ""; }; - CE380425202B3526003032D4 /* ColorScheme.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ColorScheme.swift; sourceTree = ""; }; - CE44AE641F87CA8E009787E5 /* MessageTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageTableViewCell.swift; sourceTree = ""; }; - CE45EDDC1F9108FA00F56319 /* UIImageView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UIImageView.swift; sourceTree = ""; }; CE45EDDF1F95F95C00F56319 /* MimeType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MimeType.swift; sourceTree = ""; }; CE48362B2031DA5E00E18A20 /* MimeTypeTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MimeTypeTests.swift; sourceTree = ""; }; - CE48362D2032E0D900E18A20 /* PopupDialogHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PopupDialogHandler.swift; sourceTree = ""; }; CE54B2511FC452B9009C05BD /* Index.md */ = {isa = PBXFileReference; lastKnownFileType = net.daringfireball.markdown; path = Index.md; sourceTree = ""; }; CE591A421FC2F31E004C95EE /* WebimService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WebimService.swift; sourceTree = ""; }; CE5A03BB1FDEB756009D320A /* ru-RU */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "ru-RU"; path = "ru-RU.lproj/Localizable.strings"; sourceTree = ""; }; @@ -172,16 +183,12 @@ CE86FABF203C6FBC00CB9C2D /* WebimErrorImplTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WebimErrorImplTests.swift; sourceTree = ""; }; CE88BCD01FCD7F9A00A4FA2E /* SettingsViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = SettingsViewController.swift; path = WebimClientLibrary/SettingsViewController.swift; sourceTree = SOURCE_ROOT; }; CE88BCD21FCD88EA00A4FA2E /* Settings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Settings.swift; sourceTree = ""; }; - CE88BCD41FCDA45C00A4FA2E /* ButtonConstants.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ButtonConstants.swift; sourceTree = ""; }; CE9379091FEAADC80057E270 /* Base */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = Base; path = Base.lproj/Localizable.strings; sourceTree = ""; }; CE995DC12020904300E27A3C /* WebimRemoteNotificationImplTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WebimRemoteNotificationImplTests.swift; sourceTree = ""; }; - CEA832671F9906BF004845F0 /* ColorConstants.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ColorConstants.swift; sourceTree = ""; }; CEA832691F99079A004845F0 /* StringConstants.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StringConstants.swift; sourceTree = ""; }; CEA849041FFCE29A006CC417 /* UITableView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UITableView.swift; sourceTree = ""; }; CEBFB67E2018CB0F00D9E5F6 /* String.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = String.swift; sourceTree = ""; }; CEBFB6802018E50500D9E5F6 /* UIViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UIViewController.swift; sourceTree = ""; }; - CEC27878202DDB760043899C /* ru-RU */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "ru-RU"; path = "ru-RU.lproj/Main.strings"; sourceTree = ""; }; - CEC27879202DDD920043899C /* ru-RU */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "ru-RU"; path = "ru-RU.lproj/RatingViewController.strings"; sourceTree = ""; }; CED02E152031C52E000508C9 /* ChatScreenClassic.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = ChatScreenClassic.png; sourceTree = ""; }; CED02E182031C52E000508C9 /* ChatScreenDark.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = ChatScreenDark.png; sourceTree = ""; }; CED02E242031C5F2000508C9 /* RatingScreenClassic.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = RatingScreenClassic.png; sourceTree = ""; }; @@ -194,15 +201,42 @@ CED02E2B2031C5F3000508C9 /* ImageScreenClassic.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = ImageScreenClassic.png; sourceTree = ""; }; CED3BA07201F7E100071EC23 /* WebimActionsTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WebimActionsTests.swift; sourceTree = ""; }; CED3BA0A201F87510071EC23 /* InternalErrorListenerMock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InternalErrorListenerMock.swift; sourceTree = ""; }; - CED72594200E20E500CD1623 /* ChatViewControllerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatViewControllerTests.swift; sourceTree = ""; }; + CED72594200E20E500CD1623 /* ChatTableViewControllerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatTableViewControllerTests.swift; sourceTree = ""; }; CED72597200E5B1C00CD1623 /* WebimInternalLoggerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WebimInternalLoggerTests.swift; sourceTree = ""; }; CEE382C12035D544006C809E /* WebimTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WebimTests.swift; sourceTree = ""; }; - CEF61A381F82A4C700BF7071 /* ChatViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatViewController.swift; sourceTree = ""; }; - D5A6DD52DA6213CB92530809 /* Pods_WebimClientLibrary_Example.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_WebimClientLibrary_Example.framework; sourceTree = BUILT_PRODUCTS_DIR; }; - DDFC632A1F924D41008E1ACC /* UIImage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UIImage.swift; sourceTree = ""; }; - DDFC63CA1F93F23E008E1ACC /* RatingViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RatingViewController.swift; sourceTree = ""; }; + DD0B8693234330DA00CB5712 /* ChatViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatViewController.swift; sourceTree = ""; }; + DD2B08B6233A24D900BF0283 /* FlexibleTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FlexibleTableViewCell.swift; sourceTree = ""; }; + DD2F158423336A7600532934 /* ChatTableViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatTableViewController.swift; sourceTree = ""; }; + DD2F15862333FB2100532934 /* ImageViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ImageViewController.swift; sourceTree = ""; }; + DD38105A2341EACA00F44FC6 /* FileViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FileViewController.swift; sourceTree = ""; }; + DD584C0323715F0600B2EC34 /* TypingIndicator.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TypingIndicator.swift; sourceTree = ""; }; + DD5AA7A4236089AF009680D6 /* CustomUIButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomUIButton.swift; sourceTree = ""; }; + DD5B1ADE2383FCC6005F95A8 /* CustomUIImage.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CustomUIImage.swift; sourceTree = ""; }; + DD70B9F6235D8E7600F457A1 /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/Main.strings; sourceTree = ""; }; + DD70B9F8235D8E7800F457A1 /* ru-RU */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "ru-RU"; path = "ru-RU.lproj/Main.strings"; sourceTree = ""; }; + DD70B9F9235E296100F457A1 /* CircleProgressIndicator.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CircleProgressIndicator.swift; sourceTree = ""; }; + DD71AD04234DAFDC00A32FB6 /* UIAlertHandler.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UIAlertHandler.swift; sourceTree = ""; }; + DD72BE80238E7FB10055F200 /* UIImage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UIImage.swift; sourceTree = ""; }; + DD7743FE2369F1E8000FAF9A /* RatingDialogViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RatingDialogViewController.swift; sourceTree = ""; }; + DD8A8D2C232A3E200011D275 /* LaunchScreenController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LaunchScreenController.swift; sourceTree = ""; }; + DD8A8D30232F6CF10011D275 /* SettingsTableViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsTableViewController.swift; sourceTree = ""; }; + DD8A8D34232FE5D30011D275 /* Notification.Name.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Notification.Name.swift; sourceTree = ""; }; + DD8A8D36233001510011D275 /* UIView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UIView.swift; sourceTree = ""; }; + DD8F7D0A237AFA23006D9146 /* FilePicker.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FilePicker.swift; sourceTree = ""; }; + DD8F7D0C237B3C0E006D9146 /* UIStackView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UIStackView.swift; sourceTree = ""; }; + DD94E0AF2345148A009133B4 /* PopupActionsTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PopupActionsTableViewCell.swift; sourceTree = ""; }; + DD976EC623700D2000412518 /* ru-RU */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "ru-RU"; path = "ru-RU.lproj/InfoPlist.strings"; sourceTree = ""; }; + DDC478B22366E13C005E6057 /* Message.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Message.swift; sourceTree = ""; }; + DDC478B6236717D9005E6057 /* PopupActionsViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PopupActionsViewController.swift; sourceTree = ""; }; + DDC7924B23573D8700AF424F /* en */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = en; path = en.lproj/LaunchScreen.storyboard; sourceTree = ""; }; + DDC7924C23573D9300AF424F /* en */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = en; path = en.lproj/LaunchScreenController.storyboard; sourceTree = ""; }; + DDC9418D2354F0060005A59C /* SpinningIndicator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SpinningIndicator.swift; sourceTree = ""; }; + DDCC52572356333400D39F8A /* ImageConstants.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageConstants.swift; sourceTree = ""; }; + DDCC52592356360F00D39F8A /* ColourConstants.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ColourConstants.swift; sourceTree = ""; }; + DDDE82CE23830144008DDF61 /* CustomUIView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CustomUIView.swift; sourceTree = ""; }; EB8C258A78F3730770936414 /* README.md */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = net.daringfireball.markdown; name = README.md; path = ../README.md; sourceTree = ""; }; - F0808D307D6955C0CC345203 /* Pods-WebimClientLibrary_Example.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-WebimClientLibrary_Example.debug.xcconfig"; path = "Target Support Files/Pods-WebimClientLibrary_Example/Pods-WebimClientLibrary_Example.debug.xcconfig"; sourceTree = ""; }; + FBB18D8A6A1E4DEE621109CB /* Pods_WebimClientLibrary_Tests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_WebimClientLibrary_Tests.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + FF020338AD26065EB7512BAE /* Pods-WebimClientLibrary_Example.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-WebimClientLibrary_Example.debug.xcconfig"; path = "Target Support Files/Pods-WebimClientLibrary_Example/Pods-WebimClientLibrary_Example.debug.xcconfig"; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -210,7 +244,7 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( - BDBBA7998D78E4CE2C982C3E /* Pods_WebimClientLibrary_Example.framework in Frameworks */, + 625B0CFF11CFB24FA44C17A0 /* Pods_WebimClientLibrary_Example.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -218,7 +252,7 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( - 3BD523E036E5E0145C40A893 /* Pods_WebimClientLibrary_Tests.framework in Frameworks */, + 754A3410E17BB84F9F78C1E0 /* Pods_WebimClientLibrary_Tests.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -228,10 +262,10 @@ 3C3DB6CF3A0FDD8E0CD44FD9 /* Pods */ = { isa = PBXGroup; children = ( - F0808D307D6955C0CC345203 /* Pods-WebimClientLibrary_Example.debug.xcconfig */, - 9F85297EB410C96E5341BFCC /* Pods-WebimClientLibrary_Example.release.xcconfig */, - 998795E306696F247C099BA2 /* Pods-WebimClientLibrary_Tests.debug.xcconfig */, - 188E01CE8A86B2460C60C156 /* Pods-WebimClientLibrary_Tests.release.xcconfig */, + FF020338AD26065EB7512BAE /* Pods-WebimClientLibrary_Example.debug.xcconfig */, + 1480012CD38CEC0494B440FD /* Pods-WebimClientLibrary_Example.release.xcconfig */, + 7767259DD04077F82FEB0E85 /* Pods-WebimClientLibrary_Tests.debug.xcconfig */, + B8CA89B3A42B8B92B0515245 /* Pods-WebimClientLibrary_Tests.release.xcconfig */, ); path = Pods; sourceTree = ""; @@ -246,7 +280,7 @@ 607FACE81AFB9204008FA782 /* Tests */, 607FACD11AFB9204008FA782 /* Products */, 3C3DB6CF3A0FDD8E0CD44FD9 /* Pods */, - D6405400012B6906279A6D61 /* Frameworks */, + D62C6F4259C77B8B65C26706 /* Frameworks */, ); sourceTree = ""; }; @@ -262,19 +296,24 @@ 607FACD21AFB9204008FA782 /* Example for WebimClientLibrary */ = { isa = PBXGroup; children = ( - CEC2787A2031A0110043899C /* Models */, CEA832661F990689004845F0 /* AppearanceSettings */, + CEC2787A2031A0110043899C /* Models */, CE45EDDE1F95F94200F56319 /* Utilities */, 607FACD51AFB9204008FA782 /* AppDelegate.swift */, - CEF61A381F82A4C700BF7071 /* ChatViewController.swift */, - CE44AE641F87CA8E009787E5 /* MessageTableViewCell.swift */, - DDFC63CA1F93F23E008E1ACC /* RatingViewController.swift */, - CE380423202B0AC2003032D4 /* SettingsTableViewController.swift */, + DD0B8693234330DA00CB5712 /* ChatViewController.swift */, + DD2F158423336A7600532934 /* ChatTableViewController.swift */, + DD38105A2341EACA00F44FC6 /* FileViewController.swift */, + DD2F15862333FB2100532934 /* ImageViewController.swift */, + DDC7924823573CFF00AF424F /* LaunchScreen.storyboard */, + DDC7924323573CB500AF424F /* LaunchScreenController.storyboard */, + DD8A8D2C232A3E200011D275 /* LaunchScreenController.swift */, + DD94E0AF2345148A009133B4 /* PopupActionsTableViewCell.swift */, + DDC478B6236717D9005E6057 /* PopupActionsViewController.swift */, + DD7743FE2369F1E8000FAF9A /* RatingDialogViewController.swift */, CE88BCD01FCD7F9A00A4FA2E /* SettingsViewController.swift */, + DD8A8D30232F6CF10011D275 /* SettingsTableViewController.swift */, 607FACD71AFB9204008FA782 /* StartViewController.swift */, - 607FACDE1AFB9204008FA782 /* LaunchScreen.xib */, 607FACD91AFB9204008FA782 /* Main.storyboard */, - CE324E651FDECBD2004BB116 /* RatingViewController.xib */, 607FACDC1AFB9204008FA782 /* Images.xcassets */, 607FACD31AFB9204008FA782 /* Supporting Files */, CE5A03BC1FDEB756009D320A /* Localizable.strings */, @@ -287,6 +326,7 @@ isa = PBXGroup; children = ( 607FACD41AFB9204008FA782 /* Info.plist */, + DD976EC523700D1C00412518 /* InfoPlist.strings */, ); name = "Supporting Files"; sourceTree = ""; @@ -376,7 +416,7 @@ CE2845BF2021B993007B1C25 /* ExampleTests */ = { isa = PBXGroup; children = ( - CED72594200E20E500CD1623 /* ChatViewControllerTests.swift */, + CED72594200E20E500CD1623 /* ChatTableViewControllerTests.swift */, CE2845C02021B9BC007B1C25 /* DecodePercentEscapedLinksIfPresentTests.swift */, CE48362B2031DA5E00E18A20 /* MimeTypeTests.swift */, ); @@ -387,10 +427,14 @@ isa = PBXGroup; children = ( CEBFB67E2018CB0F00D9E5F6 /* String.swift */, - DDFC632A1F924D41008E1ACC /* UIImage.swift */, - CE45EDDC1F9108FA00F56319 /* UIImageView.swift */, + DDC478B22366E13C005E6057 /* Message.swift */, + DD8A8D34232FE5D30011D275 /* Notification.Name.swift */, + DD72BE80238E7FB10055F200 /* UIImage.swift */, CEA849041FFCE29A006CC417 /* UITableView.swift */, + DD8F7D0C237B3C0E006D9146 /* UIStackView.swift */, CEBFB6802018E50500D9E5F6 /* UIViewController.swift */, + DD8A8D36233001510011D275 /* UIView.swift */, + 38683A7424A4FDCB007DED63 /* UIButton.swift */, ); path = Extensions; sourceTree = ""; @@ -399,8 +443,16 @@ isa = PBXGroup; children = ( CE45EDDB1F9108E200F56319 /* Extensions */, + DD70B9F9235E296100F457A1 /* CircleProgressIndicator.swift */, + DD5AA7A4236089AF009680D6 /* CustomUIButton.swift */, + DD5B1ADE2383FCC6005F95A8 /* CustomUIImage.swift */, + DDDE82CE23830144008DDF61 /* CustomUIView.swift */, + DD8F7D0A237AFA23006D9146 /* FilePicker.swift */, + DD2B08B6233A24D900BF0283 /* FlexibleTableViewCell.swift */, CE45EDDF1F95F95C00F56319 /* MimeType.swift */, - CE48362D2032E0D900E18A20 /* PopupDialogHandler.swift */, + DDC9418D2354F0060005A59C /* SpinningIndicator.swift */, + DD584C0323715F0600B2EC34 /* TypingIndicator.swift */, + DD71AD04234DAFDC00A32FB6 /* UIAlertHandler.swift */, CE591A421FC2F31E004C95EE /* WebimService.swift */, ); path = Utilities; @@ -437,9 +489,9 @@ CEA832661F990689004845F0 /* AppearanceSettings */ = { isa = PBXGroup; children = ( - CE88BCD41FCDA45C00A4FA2E /* ButtonConstants.swift */, - CEA832671F9906BF004845F0 /* ColorConstants.swift */, + DDCC52592356360F00D39F8A /* ColourConstants.swift */, CEA832691F99079A004845F0 /* StringConstants.swift */, + DDCC52572356333400D39F8A /* ImageConstants.swift */, ); path = AppearanceSettings; sourceTree = ""; @@ -447,7 +499,6 @@ CEC2787A2031A0110043899C /* Models */ = { isa = PBXGroup; children = ( - CE380425202B3526003032D4 /* ColorScheme.swift */, CE88BCD21FCD88EA00A4FA2E /* Settings.swift */, ); path = Models; @@ -462,11 +513,11 @@ path = Mocks; sourceTree = ""; }; - D6405400012B6906279A6D61 /* Frameworks */ = { + D62C6F4259C77B8B65C26706 /* Frameworks */ = { isa = PBXGroup; children = ( - D5A6DD52DA6213CB92530809 /* Pods_WebimClientLibrary_Example.framework */, - 75BFCD5FB1955EE77D987E3C /* Pods_WebimClientLibrary_Tests.framework */, + 85F5395A54F1170A064A76BF /* Pods_WebimClientLibrary_Example.framework */, + FBB18D8A6A1E4DEE621109CB /* Pods_WebimClientLibrary_Tests.framework */, ); name = Frameworks; sourceTree = ""; @@ -478,13 +529,13 @@ isa = PBXNativeTarget; buildConfigurationList = 607FACEF1AFB9204008FA782 /* Build configuration list for PBXNativeTarget "WebimClientLibrary_Example" */; buildPhases = ( - 97DA4630C02562D10912CF16 /* [CP] Check Pods Manifest.lock */, + A10F31B5F0E29D244BDF000C /* [CP] Check Pods Manifest.lock */, 8CC584B88D331C1B48D729CB /* [Amimono] Create filelist per architecture */, 607FACCC1AFB9204008FA782 /* Sources */, 607FACCD1AFB9204008FA782 /* Frameworks */, 607FACCE1AFB9204008FA782 /* Resources */, CE86FAB4203C3E6700CB9C2D /* ShellScript */, - BC0CA950BB03125C2F6377C1 /* [CP] Embed Pods Frameworks */, + CD51D5F18E3358E8AC393A4A /* [CP] Embed Pods Frameworks */, ); buildRules = ( ); @@ -499,7 +550,7 @@ isa = PBXNativeTarget; buildConfigurationList = 607FACF21AFB9204008FA782 /* Build configuration list for PBXNativeTarget "WebimClientLibrary_Tests" */; buildPhases = ( - 8B15E7718621B61E40699296 /* [CP] Check Pods Manifest.lock */, + 109E14635F799EDBF10172FC /* [CP] Check Pods Manifest.lock */, 607FACE11AFB9204008FA782 /* Sources */, 607FACE21AFB9204008FA782 /* Frameworks */, 607FACE31AFB9204008FA782 /* Resources */, @@ -522,7 +573,7 @@ attributes = { LastSwiftUpdateCheck = 0720; LastUpgradeCheck = 1010; - ORGANIZATIONNAME = CocoaPods; + ORGANIZATIONNAME = Webim; TargetAttributes = { 607FACCF1AFB9204008FA782 = { CreatedOnToolsVersion = 6.3.1; @@ -568,10 +619,11 @@ buildActionMask = 2147483647; files = ( 607FACDB1AFB9204008FA782 /* Main.storyboard in Resources */, + DD976EC323700D1C00412518 /* InfoPlist.strings in Resources */, CE5A03BA1FDEB756009D320A /* Localizable.strings in Resources */, - CE324E631FDECBD2004BB116 /* RatingViewController.xib in Resources */, 11EFE7D3231AC33500B64EFA /* FAQ methods documentation.md in Resources */, - 607FACE01AFB9204008FA782 /* LaunchScreen.xib in Resources */, + DDC7924623573CFF00AF424F /* LaunchScreen.storyboard in Resources */, + DDC7924123573CB500AF424F /* LaunchScreenController.storyboard in Resources */, 607FACDD1AFB9204008FA782 /* Images.xcassets in Resources */, ); runOnlyForDeploymentPostprocessing = 0; @@ -586,7 +638,7 @@ /* End PBXResourcesBuildPhase section */ /* Begin PBXShellScriptBuildPhase section */ - 8B15E7718621B61E40699296 /* [CP] Check Pods Manifest.lock */ = { + 109E14635F799EDBF10172FC /* [CP] Check Pods Manifest.lock */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; files = ( @@ -622,7 +674,7 @@ shellPath = /bin/bash; shellScript = "declare -a DEPENDENCIES=('Cosmos' 'PopupDialog' 'SQLite.swift' 'SlackTextViewController' 'SnapKit' 'WebimClientLibrary');\nIFS=\" \" read -r -a SPLIT <<< \"$ARCHS\"\nfor ARCH in \"${SPLIT[@]}\"; do\n cd \"$OBJROOT/Pods.build\"\n filelist=\"\"\n for dependency in \"${DEPENDENCIES[@]}\"; do\n path=\"${CONFIGURATION}${EFFECTIVE_PLATFORM_NAME}/${dependency}.build/Objects-normal/${ARCH}\"\n if [ -d \"$path\" ]; then\n search_path=\"$path/*.o\"\n for obj_file in $search_path; do\n filelist+=\"${OBJROOT}/Pods.build/${obj_file}\"\n filelist+=$'\\n'\n done\n fi\n done\n filelist=${filelist%$'\\n'}\n echo \"$filelist\" > \"${CONFIGURATION}${EFFECTIVE_PLATFORM_NAME}-${TARGET_NAME}-${ARCH}.objects.filelist\"\ndone\n"; }; - 97DA4630C02562D10912CF16 /* [CP] Check Pods Manifest.lock */ = { + A10F31B5F0E29D244BDF000C /* [CP] Check Pods Manifest.lock */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; files = ( @@ -644,7 +696,7 @@ shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; showEnvVarsInLog = 0; }; - BC0CA950BB03125C2F6377C1 /* [CP] Embed Pods Frameworks */ = { + CD51D5F18E3358E8AC393A4A /* [CP] Embed Pods Frameworks */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; files = ( @@ -652,8 +704,7 @@ inputPaths = ( "${PODS_ROOT}/Target Support Files/Pods-WebimClientLibrary_Example/Pods-WebimClientLibrary_Example-frameworks.sh", "${BUILT_PRODUCTS_DIR}/Cosmos/Cosmos.framework", - "${BUILT_PRODUCTS_DIR}/DynamicBlurView/DynamicBlurView.framework", - "${BUILT_PRODUCTS_DIR}/PopupDialog/PopupDialog.framework", + "${BUILT_PRODUCTS_DIR}/Nuke/Nuke.framework", "${BUILT_PRODUCTS_DIR}/SQLite.swift/SQLite.framework", "${BUILT_PRODUCTS_DIR}/SlackTextViewController/SlackTextViewController.framework", "${BUILT_PRODUCTS_DIR}/SnapKit/SnapKit.framework", @@ -662,8 +713,7 @@ name = "[CP] Embed Pods Frameworks"; outputPaths = ( "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/Cosmos.framework", - "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/DynamicBlurView.framework", - "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/PopupDialog.framework", + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/Nuke.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/SQLite.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/SlackTextViewController.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/SnapKit.framework", @@ -685,7 +735,7 @@ ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; - shellScript = "\"${PODS_ROOT}/Fabric/run\" 80055b71f053ff36cce8743fbac5eecae7df7f28 7b5c5723b733cc6cd3e884c6babc2c58039b5f0100489ed510513c1f04df4945"; + shellScript = "\"${PODS_ROOT}/Fabric/run\" 80055b71f053ff36cce8743fbac5eecae7df7f28 7b5c5723b733cc6cd3e884c6babc2c58039b5f0100489ed510513c1f04df4945\n"; }; /* End PBXShellScriptBuildPhase section */ @@ -694,26 +744,42 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + DD8A8D31232F6CF10011D275 /* SettingsTableViewController.swift in Sources */, CE591A431FC2F31E004C95EE /* WebimService.swift in Sources */, - CE48362F2032E3F100E18A20 /* PopupDialogHandler.swift in Sources */, + DD71AD05234DAFDC00A32FB6 /* UIAlertHandler.swift in Sources */, CEBFB67F2018CB0F00D9E5F6 /* String.swift in Sources */, + DDCC52582356333400D39F8A /* ImageConstants.swift in Sources */, + DD5B1ADF2383FCC6005F95A8 /* CustomUIImage.swift in Sources */, + DD72BE81238E7FB10055F200 /* UIImage.swift in Sources */, + DDDE82CF23830145008DDF61 /* CustomUIView.swift in Sources */, 607FACD81AFB9204008FA782 /* StartViewController.swift in Sources */, CEA849051FFCE29A006CC417 /* UITableView.swift in Sources */, + DD38105B2341EACA00F44FC6 /* FileViewController.swift in Sources */, CE88BCD31FCD88EA00A4FA2E /* Settings.swift in Sources */, - CE380426202B3526003032D4 /* ColorScheme.swift in Sources */, - CE45EDDD1F9108FA00F56319 /* UIImageView.swift in Sources */, - CE44AE651F87CA8E009787E5 /* MessageTableViewCell.swift in Sources */, + DD2F158523336A7600532934 /* ChatTableViewController.swift in Sources */, + DD8F7D0D237B3C0E006D9146 /* UIStackView.swift in Sources */, CE45EDE01F95F9F700F56319 /* MimeType.swift in Sources */, - CE380424202B0AC2003032D4 /* SettingsTableViewController.swift in Sources */, + DD2B08B7233A24D900BF0283 /* FlexibleTableViewCell.swift in Sources */, + DD94E0B02345148A009133B4 /* PopupActionsTableViewCell.swift in Sources */, CEBFB6812018E50500D9E5F6 /* UIViewController.swift in Sources */, - DDFC632B1F924D41008E1ACC /* UIImage.swift in Sources */, - CEF61A391F82A4C700BF7071 /* ChatViewController.swift in Sources */, + DD2F15872333FB2100532934 /* ImageViewController.swift in Sources */, + DD70B9FA235E296100F457A1 /* CircleProgressIndicator.swift in Sources */, + DD8A8D2D232A3E200011D275 /* LaunchScreenController.swift in Sources */, + DDC478B7236717D9005E6057 /* PopupActionsViewController.swift in Sources */, + DD0B8694234330DA00CB5712 /* ChatViewController.swift in Sources */, + DD8A8D37233001510011D275 /* UIView.swift in Sources */, + DD7743FF2369F1E8000FAF9A /* RatingDialogViewController.swift in Sources */, 607FACD61AFB9204008FA782 /* AppDelegate.swift in Sources */, - CE88BCD51FCDA45C00A4FA2E /* ButtonConstants.swift in Sources */, - CEA832681F9906C0004845F0 /* ColorConstants.swift in Sources */, + DD5AA7A5236089AF009680D6 /* CustomUIButton.swift in Sources */, + DDCC525A2356360F00D39F8A /* ColourConstants.swift in Sources */, + DD8A8D35232FE5D30011D275 /* Notification.Name.swift in Sources */, CEA8326A1F99079A004845F0 /* StringConstants.swift in Sources */, - DDFC63CB1F93F23E008E1ACC /* RatingViewController.swift in Sources */, + DDC9418E2354F0060005A59C /* SpinningIndicator.swift in Sources */, + DD584C0423715F0600B2EC34 /* TypingIndicator.swift in Sources */, + DD8F7D0B237AFA23006D9146 /* FilePicker.swift in Sources */, + DDC478B32366E13C005E6057 /* Message.swift in Sources */, CE88BCD11FCD7F9A00A4FA2E /* SettingsViewController.swift in Sources */, + 38683A7524A4FDCB007DED63 /* UIButton.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -727,7 +793,7 @@ CE0CD765202461FE00719DBE /* StringFromHTTPParametersTests.swift in Sources */, CE0CD75A202338FD00719DBE /* HistoryIDTests.swift in Sources */, CE995DC32020908F00E27A3C /* WebimRemoteNotificationImplTests.swift in Sources */, - CED72596200E20EF00CD1623 /* ChatViewControllerTests.swift in Sources */, + CED72596200E20EF00CD1623 /* ChatTableViewControllerTests.swift in Sources */, CE86FA97203AF44700CB9C2D /* VisitorItemTests.swift in Sources */, CE86FAB8203C48DA00CB9C2D /* MemoryHistoryStorageTests.swift in Sources */, CE18FB9C203DB32E0059B9FF /* LocationSettingsHolderTests.swift in Sources */, @@ -789,35 +855,43 @@ isa = PBXVariantGroup; children = ( 607FACDA1AFB9204008FA782 /* Base */, - CEC27878202DDB760043899C /* ru-RU */, + DD70B9F6235D8E7600F457A1 /* en */, + DD70B9F8235D8E7800F457A1 /* ru-RU */, ); name = Main.storyboard; sourceTree = ""; }; - 607FACDE1AFB9204008FA782 /* LaunchScreen.xib */ = { + CE5A03BC1FDEB756009D320A /* Localizable.strings */ = { isa = PBXVariantGroup; children = ( - 607FACDF1AFB9204008FA782 /* Base */, + CE5A03BB1FDEB756009D320A /* ru-RU */, + CE9379091FEAADC80057E270 /* Base */, ); - name = LaunchScreen.xib; + name = Localizable.strings; sourceTree = ""; }; - CE324E651FDECBD2004BB116 /* RatingViewController.xib */ = { + DD976EC523700D1C00412518 /* InfoPlist.strings */ = { isa = PBXVariantGroup; children = ( - CE324E661FDECBE0004BB116 /* Base */, - CEC27879202DDD920043899C /* ru-RU */, + DD976EC623700D2000412518 /* ru-RU */, ); - name = RatingViewController.xib; + name = InfoPlist.strings; sourceTree = ""; }; - CE5A03BC1FDEB756009D320A /* Localizable.strings */ = { + DDC7924323573CB500AF424F /* LaunchScreenController.storyboard */ = { isa = PBXVariantGroup; children = ( - CE5A03BB1FDEB756009D320A /* ru-RU */, - CE9379091FEAADC80057E270 /* Base */, + DDC7924C23573D9300AF424F /* en */, ); - name = Localizable.strings; + name = LaunchScreenController.storyboard; + sourceTree = ""; + }; + DDC7924823573CFF00AF424F /* LaunchScreen.storyboard */ = { + isa = PBXVariantGroup; + children = ( + DDC7924B23573D8700AF424F /* en */, + ); + name = LaunchScreen.storyboard; sourceTree = ""; }; /* End PBXVariantGroup section */ @@ -934,21 +1008,21 @@ }; 607FACF01AFB9204008FA782 /* Debug */ = { isa = XCBuildConfiguration; - baseConfigurationReference = F0808D307D6955C0CC345203 /* Pods-WebimClientLibrary_Example.debug.xcconfig */; + baseConfigurationReference = FF020338AD26065EB7512BAE /* Pods-WebimClientLibrary_Example.debug.xcconfig */; buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CODE_SIGN_ENTITLEMENTS = WebimClientLibrary_Example.entitlements; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 3.32.1.1; + CURRENT_PROJECT_VERSION = 3.33.0; DEVELOPMENT_TEAM = 574GE9X9L7; FRAMEWORK_SEARCH_PATHS = " $(inherited)"; INFOPLIST_FILE = WebimClientLibrary/Info.plist; IPHONEOS_DEPLOYMENT_TARGET = 10.0; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; - MARKETING_VERSION = 3.32.1; + MARKETING_VERSION = 3.33.0; MODULE_NAME = ExampleApp; - OTHER_SWIFT_FLAGS = "$(inherited) \"-D\" \"COCOAPODS\" -Xfrontend -warn-long-expression-type-checking=100 -Xfrontend -warn-long-function-bodies=200 -Onone"; + OTHER_SWIFT_FLAGS = "$(inherited) \"-D\" \"COCOAPODS\" -Xfrontend -warn-long-expression-type-checking=400 -Xfrontend -warn-long-function-bodies=400 -Onone"; PRODUCT_BUNDLE_IDENTIFIER = "ru.webim.Webim-Client"; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE = ""; @@ -961,19 +1035,19 @@ }; 607FACF11AFB9204008FA782 /* Release */ = { isa = XCBuildConfiguration; - baseConfigurationReference = 9F85297EB410C96E5341BFCC /* Pods-WebimClientLibrary_Example.release.xcconfig */; + baseConfigurationReference = 1480012CD38CEC0494B440FD /* Pods-WebimClientLibrary_Example.release.xcconfig */; buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CODE_SIGN_ENTITLEMENTS = WebimClientLibrary_Example.entitlements; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 3.32.1.1; + CURRENT_PROJECT_VERSION = 3.33.0; DEVELOPMENT_TEAM = 574GE9X9L7; FRAMEWORK_SEARCH_PATHS = " $(inherited)"; INFOPLIST_FILE = WebimClientLibrary/Info.plist; IPHONEOS_DEPLOYMENT_TARGET = 10.0; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; - MARKETING_VERSION = 3.32.1; + MARKETING_VERSION = 3.33.0; MODULE_NAME = ExampleApp; PRODUCT_BUNDLE_IDENTIFIER = "ru.webim.Webim-Client"; PRODUCT_NAME = "$(TARGET_NAME)"; @@ -987,7 +1061,7 @@ }; 607FACF31AFB9204008FA782 /* Debug */ = { isa = XCBuildConfiguration; - baseConfigurationReference = 998795E306696F247C099BA2 /* Pods-WebimClientLibrary_Tests.debug.xcconfig */; + baseConfigurationReference = 7767259DD04077F82FEB0E85 /* Pods-WebimClientLibrary_Tests.debug.xcconfig */; buildSettings = { BUNDLE_LOADER = "$(TEST_HOST)"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; @@ -1009,7 +1083,7 @@ }; 607FACF41AFB9204008FA782 /* Release */ = { isa = XCBuildConfiguration; - baseConfigurationReference = 188E01CE8A86B2460C60C156 /* Pods-WebimClientLibrary_Tests.release.xcconfig */; + baseConfigurationReference = B8CA89B3A42B8B92B0515245 /* Pods-WebimClientLibrary_Tests.release.xcconfig */; buildSettings = { BUNDLE_LOADER = "$(TEST_HOST)"; DEVELOPMENT_TEAM = ""; diff --git a/Example/WebimClientLibrary.xcodeproj/xcshareddata/xcschemes/WebimClientLibrary_Example.xcscheme b/Example/WebimClientLibrary.xcodeproj/xcshareddata/xcschemes/WebimClientLibrary_Example.xcscheme index 639a758f..e5093e07 100644 --- a/Example/WebimClientLibrary.xcodeproj/xcshareddata/xcschemes/WebimClientLibrary_Example.xcscheme +++ b/Example/WebimClientLibrary.xcodeproj/xcshareddata/xcschemes/WebimClientLibrary_Example.xcscheme @@ -27,6 +27,15 @@ selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB" selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB" shouldUseLaunchSchemeArgsEnv = "YES"> + + + + @@ -44,6 +53,7 @@ buildConfiguration = "Debug" selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB" selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB" + language = "ru" launchStyle = "0" useCustomWorkingDirectory = "NO" ignoresPersistentStateOnLaunch = "NO" @@ -60,6 +70,10 @@ ReferencedContainer = "container:WebimClientLibrary.xcodeproj"> + + + + + + FILEHEADER + +// ___FILENAME___ +// ___PRODUCTNAME___ +// +// Created by ___FULLUSERNAME___ on ___DATE___. +// ___COPYRIGHT___ +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. +// + + diff --git a/Example/WebimClientLibrary/AppDelegate.swift b/Example/WebimClientLibrary/AppDelegate.swift index dc813004..46977b08 100644 --- a/Example/WebimClientLibrary/AppDelegate.swift +++ b/Example/WebimClientLibrary/AppDelegate.swift @@ -28,6 +28,7 @@ import Crashlytics import Fabric import UIKit import WebimClientLibrary +import UserNotifications @UIApplicationMain class AppDelegate: UIResponder, UIApplicationDelegate { @@ -47,16 +48,20 @@ class AppDelegate: UIResponder, UIApplicationDelegate { Fabric.with([Crashlytics.self]) // Remote notifications configuration - let notificationTypes: UIUserNotificationType = [.alert, + let notificationTypes: UNAuthorizationOptions = [.alert, .badge, .sound] - let remoteNotificationSettings = UIUserNotificationSettings(types: notificationTypes, - categories: nil) - application.registerUserNotificationSettings(remoteNotificationSettings) - application.registerForRemoteNotifications() - application.applicationIconBadgeNumber = 0 - - + UNUserNotificationCenter.current().requestAuthorization(options: notificationTypes) { (granted, error) in + if granted { + //application.registerUserNotificationSettings(remoteNotificationSettings) + DispatchQueue.main.async { + application.registerForRemoteNotifications() + application.applicationIconBadgeNumber = 0 + } + } else { + print(error ?? "Error with remote notification") + } + } return true } diff --git a/Example/WebimClientLibrary/AppearanceSettings/ColorConstants.swift b/Example/WebimClientLibrary/AppearanceSettings/ColorConstants.swift deleted file mode 100644 index 741e950d..00000000 --- a/Example/WebimClientLibrary/AppearanceSettings/ColorConstants.swift +++ /dev/null @@ -1,153 +0,0 @@ -// -// File.swift -// WebimClientLibrary_Example -// -// Created by Nikita Lazarev-Zubov on 19.10.17. -// Copyright © 2017 Webim. All rights reserved. -// -// Permission is hereby granted, free of charge, to any person obtaining a copy -// of this software and associated documentation files (the "Software"), to deal -// in the Software without restriction, including without limitation the rights -// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -// copies of the Software, and to permit persons to whom the Software is -// furnished to do so, subject to the following conditions: -// -// The above copyright notice and this permission notice shall be included in all -// copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -// SOFTWARE. -// - -import UIKit - -// MARK: - Colors - -fileprivate let RED_COLOR = UIColor(red: (192.0 / 255.0), - green: (0.0 / 255.0), - blue: (64.0 / 255.0), - alpha: 1.0) - -// MARK: Classic scheme -fileprivate let BACKGROUND_CELL_LIGHT_COLOR_CLASSIC = UIColor.white -//Background main color light(StartVC) -fileprivate let BACKGROUND_MAIN_COLOR_CLASSIC = UIColor.white -fileprivate let BACKGROUND_SECONDARY_COLOR_CLASSIC = BACKGROUND_MAIN_COLOR_CLASSIC -fileprivate let BACKGROUND_TABLE_VIEW_COLOR_CLASSIC = UIColor.white -//cell field(SettingsVC) -fileprivate let BACKGROUND_TEXT_FIELD_COLOR_CLASSIC = UIColor(red: (239.0 / 255.0), - green: (239.0 / 255.0), - blue: (244.0 / 255.0), - alpha: 1.0) -fileprivate let BUTTON_BORDER_COLOR_CLASSIC = UIColor(red: (67.0 / 255.0), - green: (67.0 / 255.0), - blue: (67.0 / 255.0), - alpha: 1.0) -//startChatButton light(StartVC) -fileprivate let BUTTON_COLOR_CLASSIC = UIColor(red: (11 / 255.0), - green: (180.0 / 255.0), - blue: (168.0 / 255.0), - alpha: 1.0) -//separation_light between light and dark(SettingsVC) -fileprivate let DELIMITER_COLOR_CLASSIC = BACKGROUND_MAIN_COLOR_CLASSIC -//startChatButton_light text(StartVC) -fileprivate let TEXT_BUTTON_COLOR_CLASSIC = BACKGROUND_MAIN_COLOR_CLASSIC -//settingsButton_light text and border(StartVC) -fileprivate let TEXT_BUTTON_TRANSPARENT_COLOR_CLASSIC = BACKGROUND_MAIN_COLOR_DARK -fileprivate let TEXT_BUTTON_TRANSPARENT_HIGHLIGHTED_COLOR_CLASSIC = UIColor.lightGray -//text in separation light(SettingsVC) -fileprivate let TEXT_CELL_LIGHT_COLOR_CLASSIC = BACKGROUND_MAIN_COLOR_DARK -//start_text_main_light color(StartVC) -fileprivate let TEXT_MAIN_COLOR_CLASSIC = BACKGROUND_MAIN_COLOR_DARK -fileprivate let TEXT_NAME_OPERATOR_COLOR_CLASSIC = RED_COLOR -fileprivate let TEXT_NAME_VISITOR_COLOR_CLASSIC = UIColor(red: (0.0 / 255.0), - green: (152.0 / 255.0), - blue: (79.0 / 255.0), - alpha: 1.0) -fileprivate let TEXT_SECONDARY_COLOR_CLASSIC = UIColor.darkGray -//cell text light(SettingsVC) -fileprivate let TEXT_TEXT_FIELD_COLOR_CLASSIC = BACKGROUND_MAIN_COLOR_DARK -fileprivate let TEXT_TEXT_FIELD_ERROR_COLOR_CLASSIC = RED_COLOR -//start_text_tint_main_light color(StartVC) -fileprivate let TEXT_TINT_COLOR_CLASSIC = BUTTON_COLOR_CLASSIC - -// MARK: Dark theme -//(SettingsVC) -fileprivate let BACKGROUND_CELL_LIGHT_COLOR_DARK = BACKGROUND_MAIN_COLOR_DARK -//Background main color dark(StartVC) -fileprivate let BACKGROUND_MAIN_COLOR_DARK = UIColor(red: (47.0 / 255.0), - green: (49.0 / 255.0), - blue: (95.0 / 255.0), - alpha: 1.0) -//top of the screen dark(StartVC) -fileprivate let BACKGROUND_SECONDARY_COLOR_DARK = BACKGROUND_MAIN_COLOR_DARK -//(ChatVC) -fileprivate let BACKGROUND_TABLE_VIEW_COLOR_DARK = BACKGROUND_MAIN_COLOR_DARK -//cell field(SettingsVC) -fileprivate let BACKGROUND_TEXT_FIELD_COLOR_DARK = BACKGROUND_TEXT_FIELD_COLOR_CLASSIC -fileprivate let BUTTON_BORDER_COLOR_DARK = BUTTON_BORDER_COLOR_CLASSIC -//startChatButton light(StartVC) -fileprivate let BUTTON_COLOR_DARK = BUTTON_COLOR_CLASSIC -//separation_dark between light and dark(SettingsVC) -fileprivate let DELIMITER_COLOR_DARK = BACKGROUND_MAIN_COLOR_DARK -//startChatButton_light text(StartVC) -fileprivate let TEXT_BUTTON_COLOR_DARK = UIColor.white -//settingsButton_dark text and border(StartVC) -fileprivate let TEXT_BUTTON_TRANSPARENT_COLOR_DARK = UIColor.white -fileprivate let TEXT_BUTTON_TRANSPARENT_HIGHLIGHTED_COLOR_DARK = UIColor.white -//text in separation light(SettingsVC) -fileprivate let TEXT_CELL_LIGHT_COLOR_DARK = UIColor.white -fileprivate let TEXT_MAIN_COLOR_DARK = UIColor.white -fileprivate let TEXT_NAME_OPERATOR_COLOR_DARK = TEXT_NAME_OPERATOR_COLOR_CLASSIC -fileprivate let TEXT_NAME_VISITOR_COLOR_DARK = TEXT_NAME_VISITOR_COLOR_CLASSIC -fileprivate let TEXT_SECONDARY_COLOR_DARK = UIColor.lightGray -//cell text dark(SettingsVC) -fileprivate let TEXT_TEXT_FIELD_COLOR_DARK = BACKGROUND_MAIN_COLOR_DARK -fileprivate let TEXT_TEXT_FIELD_ERROR_COLOR_DARK = TEXT_TEXT_FIELD_ERROR_COLOR_CLASSIC -//start_text_tint_main_dark color(StartVC) -fileprivate let TEXT_TINT_COLOR_DARK = BUTTON_COLOR_CLASSIC - -// MARK: - Model -let backgroundCellLightColor = SchemeColor(classic: BACKGROUND_CELL_LIGHT_COLOR_CLASSIC, - dark: BACKGROUND_CELL_LIGHT_COLOR_DARK) -let backgroundMainColor = SchemeColor(classic: BACKGROUND_MAIN_COLOR_CLASSIC, - dark: BACKGROUND_MAIN_COLOR_DARK) -let backgroundSecondaryColor = SchemeColor(classic: BACKGROUND_SECONDARY_COLOR_CLASSIC, - dark: BACKGROUND_SECONDARY_COLOR_DARK) -let backgroundTableViewColor = SchemeColor(classic: BACKGROUND_TABLE_VIEW_COLOR_CLASSIC, - dark: BACKGROUND_TABLE_VIEW_COLOR_DARK) -let backgroundTextFieldColor = SchemeColor(classic: BACKGROUND_TEXT_FIELD_COLOR_CLASSIC, - dark: BACKGROUND_TEXT_FIELD_COLOR_DARK) -let buttonBorderColor = SchemeColor(classic: BUTTON_BORDER_COLOR_CLASSIC, - dark: BUTTON_BORDER_COLOR_DARK) -let buttonColor = SchemeColor(classic: BUTTON_COLOR_CLASSIC, - dark: BUTTON_COLOR_DARK) -let delimiterColor = SchemeColor(classic: DELIMITER_COLOR_CLASSIC, - dark: DELIMITER_COLOR_DARK) -let textButtonColor = SchemeColor(classic: TEXT_BUTTON_COLOR_CLASSIC, - dark: TEXT_BUTTON_COLOR_DARK) -let textButtonTransparentColor = SchemeColor(classic: TEXT_BUTTON_TRANSPARENT_COLOR_CLASSIC, - dark: TEXT_BUTTON_TRANSPARENT_COLOR_DARK) -let textButtonTransparentHighlightedColor = SchemeColor(classic: TEXT_BUTTON_TRANSPARENT_HIGHLIGHTED_COLOR_CLASSIC, - dark: TEXT_BUTTON_TRANSPARENT_HIGHLIGHTED_COLOR_DARK) -let textCellLightColor = SchemeColor(classic: TEXT_CELL_LIGHT_COLOR_CLASSIC, - dark: TEXT_CELL_LIGHT_COLOR_DARK) -let textMainColor = SchemeColor(classic: TEXT_MAIN_COLOR_CLASSIC, - dark: TEXT_MAIN_COLOR_DARK) -let textNameOperatorColor = SchemeColor(classic: TEXT_NAME_OPERATOR_COLOR_CLASSIC, - dark: TEXT_NAME_OPERATOR_COLOR_DARK) -let textNameVisitorColor = SchemeColor(classic: TEXT_NAME_VISITOR_COLOR_CLASSIC, - dark: TEXT_NAME_VISITOR_COLOR_DARK) -let textSecondaryColor = SchemeColor(classic: TEXT_SECONDARY_COLOR_CLASSIC, - dark: TEXT_SECONDARY_COLOR_DARK) -let textTextFieldColor = SchemeColor(classic: TEXT_TEXT_FIELD_COLOR_CLASSIC, - dark: TEXT_TEXT_FIELD_COLOR_DARK) -let textTextFieldErrorColor = SchemeColor(classic: TEXT_TEXT_FIELD_ERROR_COLOR_CLASSIC, - dark: TEXT_TEXT_FIELD_ERROR_COLOR_CLASSIC) -let textTintColor = SchemeColor(classic: TEXT_TINT_COLOR_CLASSIC, - dark: TEXT_TINT_COLOR_DARK) diff --git a/Example/WebimClientLibrary/AppearanceSettings/ColourConstants.swift b/Example/WebimClientLibrary/AppearanceSettings/ColourConstants.swift new file mode 100644 index 00000000..7cb9baac --- /dev/null +++ b/Example/WebimClientLibrary/AppearanceSettings/ColourConstants.swift @@ -0,0 +1,151 @@ +// +// ColourConstants.swift +// WebimClientLibrary_Example +// +// Created by Eugene Ilyin on 15.10.2019. +// Copyright © 2019 Webim. All rights reserved. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. +// + +import Foundation +import UIKit + +// MARK: - Colours +fileprivate let CLEAR = #colorLiteral(red: 0, green: 0, blue: 0, alpha: 0) +fileprivate let WHITE = #colorLiteral(red: 0.9999960065, green: 1, blue: 1, alpha: 1) +fileprivate let BLACK = #colorLiteral(red: 0, green: 0, blue: 0, alpha: 1) +fileprivate let LIGHT_GREY = #colorLiteral(red: 0.8823529412, green: 0.8901960784, blue: 0.9176470588, alpha: 1) +fileprivate let GREY = #colorLiteral(red: 0.4901960784, green: 0.4980392157, blue: 0.5843137255, alpha: 1) +fileprivate let TRANSLUCENT_GREY = #colorLiteral(red: 1, green: 1, blue: 1, alpha: 0.5) +fileprivate let WEBIM_CYAN = #colorLiteral(red: 0.08675732464, green: 0.6737991571, blue: 0.8237424493, alpha: 1) +fileprivate let WEBIM_PUPRLE = #colorLiteral(red: 0.1529411765, green: 0.1647058824, blue: 0.3058823529, alpha: 1) +fileprivate let WEBIM_LIGHT_PURPLE = #colorLiteral(red: 0.4117647059, green: 0.4235294118, blue: 0.568627451, alpha: 1) +fileprivate let WEBIM_RED = #colorLiteral(red: 1, green: 0.1491314173, blue: 0, alpha: 1) +fileprivate let WEBIM_GREY = #colorLiteral(red: 0, green: 0, blue: 0, alpha: 0.5) + +// MARK: - Views' colour properties +// FlexibleTableViewCell.swift +let flexibleTableViewCellBackgroundColour = CLEAR +/// System +let messageBodyLabelColourSystem = WEBIM_LIGHT_PURPLE +let messageBackgroundViewColourSystem = LIGHT_GREY +let messageBackgroundViewColourClear = CLEAR +/// Visitor +let messageBodyLabelColourVisitor = WHITE +let messageBackgroundViewColourVisitor = WEBIM_CYAN +let documentFileNameLabelColourVisitor = WHITE +let documentFileDescriptionLabelColourVisitor = LIGHT_GREY +let quoteLineViewColourVisitor = WHITE +let quoteUsernameLabelColourVisitor = WHITE +let quoteBodyLabelColourVisitor = LIGHT_GREY +/// Operator +let messageUsernameLabelColourOperator = WEBIM_CYAN +let messageBodyLabelColourOperator = WEBIM_PUPRLE +let messageBackgroundViewColourOperator = WHITE +let documentFileNameLabelColourOperator = WEBIM_PUPRLE +let documentFileDescriptionLabelColourOperator = GREY +let imageUsernameLabelColourOperator = WHITE +let imageUsernameLabelBackgroundViewColourOperator = WEBIM_GREY +let quoteLineViewColourOperator = WEBIM_CYAN +let quoteUsernameLabelColourOperator = WEBIM_PUPRLE +///Other +let quoteBodyLabelColourOperator = GREY +let dateLabelColour = GREY +let timeLabelColour = GREY +let messageStatusIndicatorColour = WEBIM_PUPRLE.cgColor +let documentFileStatusPercentageIndicatorColour = WEBIM_CYAN.cgColor +let buttonDefaultBackgroundColour = WHITE +let buttonChoosenBackgroundColour = WEBIM_CYAN +let buttonCanceledBackgroundColour = WHITE +let buttonDefaultTitleColour = WEBIM_CYAN +let buttonChoosenTitleColour = WHITE +let buttonCanceledTitleColour = WEBIM_GREY + +// RatingDialogViewController.swift +let ratingDialogOperatorNameLabelColour = WEBIM_PUPRLE +let ratingDialogTitleLabelColour = WEBIM_LIGHT_PURPLE +let ratingDialogBackgroundColour = TRANSLUCENT_GREY +let ratingDialogWhiteBackgroudColour = WHITE + +let cosmosViewFilledColour = WEBIM_CYAN +let cosmosViewFilledBorderColour = WEBIM_CYAN +let cosmosViewEmptyColour = CLEAR +let cosmosViewEmptyBorderColour = GREY + +// PopupActionTableViewCell.swift +let actionColourCommon = WHITE +let actionColourDelete = WEBIM_RED + +// ChatViewController.swift +/// Separator +let bottomBarSeparatorColour = TRANSLUCENT_GREY +/// Bottom bar +let bottomBarBackgroundViewColour = WHITE +let bottomBarQuoteLineViewColour = WEBIM_CYAN +let textInputBackgroundViewBorderColour = LIGHT_GREY.cgColor +/// Bottom bar for edit/reply +let textInputViewPlaceholderLabelTextColour = GREY +let textInputViewPlaceholderLabelBackgroundColour = CLEAR + +// ChatTableViewController.swift +let refreshControlTextColour = WEBIM_PUPRLE +let refreshControlTintColour = WEBIM_PUPRLE + +// ImageViewController.swift +// FileViewController.swift +let topBarTintColourDefault = WEBIM_PUPRLE +let topBarTintColourClear = CLEAR + +// PopupActionViewController.swift +let popupBackgroundColour = CLEAR +let actionsTableViewBackgroundColour = BLACK +let separatorViewBackgroundColour = WHITE +let actionsTableViewCellBackgroundColour = CLEAR + +// SettingsViewController.swift +let backgroundViewColour = WHITE +let saveButtonBackgroundColour = WEBIM_CYAN +let saveButtonTitleColour = WHITE +let saveButtonBorderColour = BLACK.cgColor + +// SettingsTableViewController.swift +let tableViewBackgroundColour = WHITE +let labelTextColour = WEBIM_PUPRLE +let textFieldTextColour = WEBIM_PUPRLE +let textFieldTintColour = WEBIM_PUPRLE +let editViewBackgroundColourEditing = WEBIM_CYAN +let editViewBackgroundColourError = WEBIM_RED +let editViewBackgroundColourDefault = GREY + +// StartViewController.swift +let startViewBackgroundColour = WEBIM_PUPRLE +let navigationBarBarTintColour = WEBIM_PUPRLE +let navigationBarTintColour = WHITE +let welcomeLabelTextColour = WHITE +let welcomeTextViewTextColour = WHITE +let welcomeTextViewForegroundColour = WEBIM_CYAN +let startChatButtonBackgroundColour = WEBIM_CYAN +let startChatButtonBorderColour = WEBIM_CYAN.cgColor +let startChatTitleColour = WHITE +let settingsButtonTitleColour = WHITE +let settingButtonBorderColour = GREY.cgColor + +// UITableView extension +let textMainColour = WEBIM_PUPRLE diff --git a/Example/WebimClientLibrary/AppearanceSettings/ImageConstants.swift b/Example/WebimClientLibrary/AppearanceSettings/ImageConstants.swift new file mode 100644 index 00000000..2ab2d15f --- /dev/null +++ b/Example/WebimClientLibrary/AppearanceSettings/ImageConstants.swift @@ -0,0 +1,70 @@ +// +// ImageConstants.swift +// WebimClientLibrary_Example +// +// Created by Eugene Ilyin on 15.10.2019. +// Copyright © 2019 Webim. All rights reserved. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. +// + +import Foundation +import UIKit + +// ChatViewController.swift +let closeButtonImage = #imageLiteral(resourceName: "CloseButton") +let fileButtonImage = #imageLiteral(resourceName: "AttachmentButton") +let loadingPlaceholderImage = UIImage(named: "ImagePlaceholder")! +let navigationBarTitleImageViewImage = #imageLiteral(resourceName: "LogoWebimNavigationBar_dark") +let scrollButtonImage = #imageLiteral(resourceName: "SendMessageButton").flipImage(.vertically) +let textInputButtonImage = #imageLiteral(resourceName: "SendMessageButton") + +// ChatTableViewController.swift +let documentFileStatusImageViewImage = #imageLiteral(resourceName: "FileDownloadError") +let leadingSwipeActionImage = + UIApplication.shared.userInterfaceLayoutDirection == .rightToLeft ? + #imageLiteral(resourceName: "ReplyCircleToTheLeft") : + #imageLiteral(resourceName: "ReplyCircleToTheLeft").flipImage(.horizontally) +let trailingSwipeActionImage = + UIApplication.shared.userInterfaceLayoutDirection == .rightToLeft ? + #imageLiteral(resourceName: "ReplyCircleToTheLeft").flipImage(.horizontally) : + #imageLiteral(resourceName: "ReplyCircleToTheLeft") + +// ImageViewController.swift +let saveImageButtonImage = #imageLiteral(resourceName: "ImageDownload") + +// StartViewController.swift +let logoImageViewImage = #imageLiteral(resourceName: "LogoWebimNavigationBar_dark") + +// FlexibleTableViewCell.swift +let documentFileStatusButtonDownloadOperator = #imageLiteral(resourceName: "FileDownloadButtonOperator") +let documentFileStatusButtonDownloadVisitor = #imageLiteral(resourceName: "FileDownloadButtonVisitor") +let documentFileStatusButtonDownloadError = #imageLiteral(resourceName: "FileDownloadError") +let documentFileStatusButtonDownloadSuccessOperator = #imageLiteral(resourceName: "FileDownloadSuccessOperator") +let documentFileStatusButtonDownloadSuccessVisitor = #imageLiteral(resourceName: "FIleDownloadSeccessVisitor") +let documentFileStatusButtonUploadVisitor = #imageLiteral(resourceName: "FileUploadButtonVisitor.pdf") +let userAvatarImagePlaceholder = #imageLiteral(resourceName: "HardcodedVisitorAvatar") +let messageStatusImageViewImageSent = #imageLiteral(resourceName: "Sent") +let messageStatusImageViewImageRead = #imageLiteral(resourceName: "ReadByOperator") + +// PopupActionTableViewCell.swift +let replyImage = #imageLiteral(resourceName: "ActionReply") +let copyImage = #imageLiteral(resourceName: "ActionCopy") +let editImage = #imageLiteral(resourceName: "ActionEdit") +let deleteImage = #imageLiteral(resourceName: "ActionDelete").colour(actionColourDelete) diff --git a/Example/WebimClientLibrary/AppearanceSettings/StringConstants.swift b/Example/WebimClientLibrary/AppearanceSettings/StringConstants.swift index eab3ae83..245c0461 100644 --- a/Example/WebimClientLibrary/AppearanceSettings/StringConstants.swift +++ b/Example/WebimClientLibrary/AppearanceSettings/StringConstants.swift @@ -66,29 +66,37 @@ enum LeftButton: String { case accessibilityHint = "ShowsImagePicker" } -enum RateOperatorErrorMessage: String { - case title = "OperatorRatingFailed" +enum NoCurrentOperatorErrorMessage: String { + case title = "NoCurrentOperator" + case buttonTitle = "OK" + case message = "NoAvailableOperator" +} + +enum AlertDialog: String { + case rateSuccessTitle = "RateSuccessTitle" + case rateSuccessMessage = "RateSuccessMessage" case buttonTitle = "OK" case buttonAccessibilityHint = "ClosesRateOperatorError" +} + +enum RateOperatorErrorMessage: String { + case title = "OperatorRatingFailed" - case message = "RateOperatorErrorMessage" + // ErrorMessage text + case rateOperatorNoChat = "RateOperatorNoChat" + case rateOperatorWrongID = "RateOperatorWrongID" + case rateOperatorLongNote = "RateOperatorLongNote" } -enum RatingDialog: String { - case actionButtonAccessibilityHint = "RatesOperator" - case actionButtonTitle = "Rate" - - case cancelButtonAccessibilityHint = "ClosesRatingDialog" - case cancelButtonTitle = "Cancel" +enum SendErrorMessage: String { + case buttonTitle = "OK" + case buttonAccessibilityHint = "ClosesFileError" } enum SendFileErrorMessage: String { case title = "FileSendingFailed" - case buttonTitle = "OK" - case buttonAccessibilityHint = "ClosesSendFileError" - // Error messages case fileSizeExceeded = "FileTooLarge" case fileTypeNotAllowed = "FileTypeNotSupported" @@ -97,6 +105,44 @@ enum SendFileErrorMessage: String { case unauthorized = "FileSengingUnauthorized" } +enum SendMessageErrorMessage: String { + case messageEmpty = "MessageIsEmpty" + case maxMessageLengthExceede = "MaxMessageLengthExceeded" +} + +enum EditMessageErrorMessage: String { + case title = "EditMessageFailed" + + // Error messages + case unknownError = "EditMessageUnknownError" + case notAllowed = "EditingMessagesIsTurnedOffOnTheServer" + case messageEmpty = "EditingMessageIsEmpty" + case messageNotOwned = "MessageNotOwnedByVisitor" + case maxMessageLengthExceede = "MaxMessageLengthExceeded" + case wrongMessageKind = "WrongMessageKind" +} + +enum DeleteMessageErrorMessage: String { + case title = "DeleteMessageFailed" + + // Error messages + case unknownError = "DeleteMessageUnknownError" + case notAllowed = "DeletingMessagesIsTurnedOffOnTheServer" + case messageNotOwned = "MessageNotOwnedByVisitor" + case messageNotFound = "MessageNotFound" +} + +enum SendKeyboardRequestErrorMessage: String { + case title = "SendKeyboardRequestFailed" + + // Error messages + case unknownError = "SendKeyboardRequestUnknownError" + case noChat = "ChatDoesNotExist" + case buttonIDNotSet = "WrongButtonID" + case requestMessageIDNotSet = "WrongMessageID" + case cannotCreateResponse = "ResponseCannotBeCreated" +} + enum SessionCreationErrorDialog: String { case buttonTitle = "OK" case buttonAccessibilityHint = "ClosesSessionError" @@ -131,10 +177,101 @@ enum ShowFileDialog: String { } enum StartView: String { - case welcomeText = "Welcome to the WebimClientLibrary demo app!\n\nTo start a chat tap on the button below.\n\nOperator can answer to your chat at:\nhttps://demo.webim.ru/\nLogin: o@webim.ru\nPassword: password\n\nThis app source code can be found at:\nhttps://github.com/webim/webim-client-sdk-ios" + case welcomeTitle = "WelcomeTitle" + case welcomeText = "WelcomeText" + + case startButtonTitle = "StartChat" + case settingsButtonTitle = "Settings" } enum TableView: String { - case refreshControlText = "LoadingMessages" case emptyTableViewText = "EmptyChat" } + +enum ChatTableView: String { + case refreshControlText = "LoadMessages" +} + +enum FileView: String { + case loadingFileText = "LoadingFile" +} + +enum ChatView: String { + case hardcodedVisitorMessageName = "HardcodedVisitorMessageName" + case editMessageText = "EditMessage" + case textInputPlaceholderText = "InputPlaceholderText" + case navigationBarAccessibilityLabelText = "AccessibilityTextWebimLogo" +} + +enum PopupAction: String { + case reply = "Reply" + case copy = "Copy" + case edit = "Edit" + case delete = "Delete" +} + +enum RatingDialogView: String { + case rateTitleText = "RateOperator" +} + +enum SavingImageDialog: String { + case buttonTitle = "OK" + case saveErrorTitle = "SaveError" + + case saveSuccessTitle = "Saved" + case saveSuccessMessage = "ImageSaved" +} + +enum SavingFileDialog: String { + case buttonTitle = "OK" + case saveErrorTitle = "SaveError" + + case saveSuccessTitle = "Saved" + case saveSuccessMessage = "FileSaved" +} + +enum LoadingFileDialog: String { + case buttonTitle = "OK" + case loadErrorTitle = "LoadError" +} + +enum OperatorStatus: String { + case noOperator = "NoOperator" + case allOperatorsOffline = "OperatorsOffline" + case online = "Online" + case isTyping = "IsTyping" +} + +enum OperatorAvatar: String { + case placeholder = "NoAvatarURL" + case empty = "GhostImage" +} + +enum FilePickerObject: String { + case actionCamera = "Camera" + case actionPhotoLibrary = "PhotoLibrary" + case actionFile = "File" + case actionCancel = "Cancel" + + case cameraNotAvailable = "CameraIsNotAvailable" + case ok = "OK" + + case cameraAccessTitle = "CameraAccessTitle" + case cameraAccessMessage = "CameraAccessMessage" + case cameraAccessOpenSetting = "CameraAccessOpenSettings" + case cameraAccessCancel = "CameraAccessCancel" +} + +enum MessageStatus: String { + case editedMessage = "EditedMessage" +} + +enum FlexibleCellDate: String { + case dateToday = "DateToday" + case dateYesterday = "DateYesterday" +} + +enum UploadingFileDescription: String { + case uploadingFile = "UploadingFile" + case counting = "Counting" +} diff --git a/Example/WebimClientLibrary/Base.lproj/LaunchScreen.xib b/Example/WebimClientLibrary/Base.lproj/LaunchScreen.xib deleted file mode 100644 index 7cac6592..00000000 --- a/Example/WebimClientLibrary/Base.lproj/LaunchScreen.xib +++ /dev/null @@ -1,51 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/Example/WebimClientLibrary/Base.lproj/Localizable.strings b/Example/WebimClientLibrary/Base.lproj/Localizable.strings index ca6dc63c..f7900c35 100644 --- a/Example/WebimClientLibrary/Base.lproj/Localizable.strings +++ b/Example/WebimClientLibrary/Base.lproj/Localizable.strings @@ -34,12 +34,6 @@ // MARK: - StringConstants.swift -// EMPTY_TABLE_VIEW_TEXT -"EmptyChat" = "Send first message to start chat."; - -// REFRESH_CONTROL_TEXT -"LoadingMessages" = "Loading messages..."; - // Avatar "SenderAvatarImage" = "Sender avatar image"; "ShowsRatingDialog" = "Shows rating dialog."; @@ -64,33 +58,67 @@ "ClosesDialog" = "Closes dialog."; // FileMessage -"FileUnavailable" = "File is unavailable."; +"FileUnavailable" = "File is unavailable"; // LeftButton "ChooseFile" = "Choose file"; "ShowsImagePicker" = "Shows image picker to choose an image to send."; -// RateOperatorErrorMessage -"OperatorRatingFailed" = "Operator rating failed"; +// NoCurrentOperatorErrorMessage +"NoCurrentOperator" = "No agents available"; +"OK" = "OK"; +"NoAvailableOperator" = "There is no current agent to rate"; + +// AlertDialog +"RateSuccessTitle" = "Thank you!"; +"RateSuccessMessage" = "You are helping us to become better"; "OK" = "OK"; "ClosesRateOperatorError" = "Closes dialog."; -"RateOperatorErrorMessage" = "Unknown error occured."; -// RatingDialog -"Rate" = "Rate"; -"Cancel" = "Cancel"; -"RatesOperator" = "Rates operator with chosen rating."; -"ClosesRatingDialog" = "Closes rating dialog."; +// RateOperatorErrorMessage +"OperatorRatingFailed" = "Operator rating failed"; +"RateOperatorNoChat" = "This chat does not exist"; +"RateOperatorWrongID" = "This agent not in the current chat"; +"RateOperatorLongNote" = "Note for rate is too long"; // SendFileErrorMessage "FileSendingFailed" = "File sending failed"; "ClosesSendFileError" = "Closes dialog."; "FileTooLarge" = "File is too large."; -"FileTypeNotSupported" = "File type is not supported."; -"FileNotFound" = "Sending files in body is not supported."; -"FileSendingUnknownError" = "Find sending unknown error."; +"FileTypeNotSupported" = "File type is not supported"; +"FileNotFound" = "Sending files in body is not supported"; +"FileSendingUnknownError" = "Find sending unknown error"; "FileSengingUnauthorized" = "Failed to upload file: visitor is not logged in"; +// SendMessageErrorMessage +"SendMessageFailed" = "Message sending failed"; +"MessageIsEmpty" = "Message is empty"; +"MaxMessageLengthExceeded" = "Max message length exceeded"; + +// EditMessageErrorMessage +"EditMessageFailed" = "Message editing failed"; +"EditMessageUnknownError" = "Edit message unknown error"; +"EditingMessagesIsTurnedOffOnTheServer" = "Editing messages is turned off on the server"; +"EditingMessageIsEmpty" = "Editing message is empty"; +"MessageNotOwnedByVisitor" = "Message not owned by visitor"; +"MaxMessageLengthExceeded" = "Max message lenght exceeded"; +"WrongMessageKind" = "Wrong message kind (not text)"; + +// DeleteMessageErrorMessage +"DeleteMessageFailed" = "Message deleting failed"; +"DeleteMessageUnknownError" = "Delete message unknown error"; +"DeletingMessagesIsTurnedOffOnTheServer" = "Deleting messages is turned off on the server"; +"MessageNotOwnedByVisitor" = "Message not owned by visitor"; +"MessageNotFound" = "Message not found"; + +// SendKeyboardRequestErrorMessage +"SendKeyboardRequestFailed" = "Send keyboard request failed"; +"SendKeyboardRequestUnknownError" = "Send keyboard request unknown error"; +"ChatDoesNotExist" = "Chat does not exist"; +"WrongButtonID" = "Wrong button ID in request"; +"WrongMessageID" = "Wrong message ID in request"; +"ResponseCannotBeCreated" = "Response cannot be created for this request"; + // SessionCreationErrorDialog "ClosesSessionError" = "Closes dialog."; "SessionCreationFailed" = "Session creation failed"; @@ -100,11 +128,86 @@ // SettingsErrorDialog "ClosesSettingsError" = "Closes dialog."; "InvalidSettings" = "Invalid account settings"; -"AccountNameEmpty" = "Account name can't be empty."; -"LocationEmpty" = "Location can't be empty."; +"AccountNameEmpty" = "Account name can't be empty"; +"LocationEmpty" = "Location can't be empty"; // ShowFileDialog -"ImageFormatInvalid" = "Image format is not valid."; -"ImageLinkInvalid" = "Image link is not valid."; -"PreviewUnavailable" = "Preview is not available."; +"ImageFormatInvalid" = "Image format is not valid"; +"ImageLinkInvalid" = "Image link is not valid"; +"PreviewUnavailable" = "Preview is not available"; "ClosesFilePreview" = "Closes file preview."; + +// StartView +"WelcomeTitle" = "Welcome to the WebimClientLibrary demo app!"; +"WelcomeText" = "To start a chat tap on the button below.\n\nOperator can answer to your chat at:\nhttps://demo.webim.ru/\nLogin: o@webim.ru\nPassword: password\n\nThis app source code can be found at:\nhttps://github.com/webim/webim-client-sdk-ios"; +"StartChat" = "Start Chat"; +"Settings" = "Settings"; + +// TableView +"EmptyChat" = "Send first message to start chat."; + +// ChatTableView +"LoadMessages" = "Fetching more messages..."; + +// ChatView +"HardcodedVisitorMessageName" = "You"; +"EditMessage" = "Edit Message"; +"InputPlaceholderText" = "Message"; +"AccessibilityTextWebimLogo" = "Webim logo"; + +// FileView +"LoadingFile" = "Loading the file..."; + +// PopupActions +"Reply" = "Reply"; +"Copy" = "Copy"; +"Edit" = "Edit"; +"Delete" = "Delete"; + +// RatingDialogView +"RateOperator" = "Please rate the agent"; + +// SavingImageDialog +"OK" = "ОК"; +"SaveError" = "Save error"; +"Saved" = "Saved!"; +"ImageSaved" = "The image has been saved to your photos"; + +// SavingFileDialog +"OK" = "ОК"; +"SaveError" = "Save error"; +"Saved" = "Saved!"; +"FileSaved" = "The file has been saved to your device in Files App"; + +// LoadingFileDialog +"OK" = "ОК"; +"LoadError" = "Load error"; + +// OperatorStatus +"NoOperator" = "Webim demo-chat"; +"OperatorsOffline" = "No agent"; +"Online" = "Online"; +"IsTyping" = "typing"; + +// FilePicker +"Camera" = "Camera"; +"PhotoLibrary" = "Photo Library"; +"Cancel" = "Cancel"; +"File" = "File"; +"CameraIsNotAvailable" = "Camera is not available"; +"OK" = "OK"; +"CameraAccessTitle" = "Need camera access"; +"CameraAccessMessage" = "Camera access is required to make full use of this app"; +"CameraAccessOpenSettings" = "Open app settings"; +"CameraAccessCancel" = "Cancel"; + +// MessageStatus +"EditedMessage" = "edited"; + +// FlexibleCellDate +"DateToday" = "Today"; +"DateYesterday" = "Yesterday"; + +// UploadingFileDescription +"UploadingFile" = "Uploading file"; +"Counting" = "Counting"; diff --git a/Example/WebimClientLibrary/Base.lproj/Main.storyboard b/Example/WebimClientLibrary/Base.lproj/Main.storyboard index b5b96507..222af808 100644 --- a/Example/WebimClientLibrary/Base.lproj/Main.storyboard +++ b/Example/WebimClientLibrary/Base.lproj/Main.storyboard @@ -1,9 +1,9 @@ - + - + @@ -13,97 +13,171 @@ - + - - - - - - - - Welcome to the WebimClientLibrary demo app! - -To start a chat tap on the button below. - -Operator can answer to your chat at: -https://demo.webim.ru/ -Login: o@webim.ru -Password: password - -This app source code can be found at: -https://github.com/webim/webim-client-sdk-ios - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + To start a chat tap on the button below. + +Operator can answer to your chat at: +https://demo.webim.ru/ +Login: o@webim.ru +Password: password + +This app source code can be found at: +https://github.com/webim/webim-client-sdk-ios + + + + + - + - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - + - + - - + - + @@ -130,14 +204,15 @@ https://github.com/webim/webim-client-sdk-ios - - - - + + + + + @@ -147,264 +222,273 @@ https://github.com/webim/webim-client-sdk-ios - + - - - + + + - - - + - + - - + + - - + + - - - - - + - - - - - + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + - - + - - - - - - + + - - + + - - + + + + + + + + + + - + - - - - - - - - + + + + + + + + + + + + + - + - - + + - - + + - - - + + + + + + + + + + + + + - - - - + + + + + + + + + + - + - - + + - - + + - - - - - - - - - - - - - + + + + - + - - + + - - - - - - - - - - - - - - - - - + + + + + + + + - + - + - + - - - + + + - - - + + + + + + + + + + + + + + + + + + + + + - + + + + + + + + + + + + + + + + + + + + + - + - + - + - + + + @@ -413,8 +497,89 @@ https://github.com/webim/webim-client-sdk-ios + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - + diff --git a/Example/WebimClientLibrary/Base.lproj/RatingViewController.xib b/Example/WebimClientLibrary/Base.lproj/RatingViewController.xib deleted file mode 100755 index 34e260ee..00000000 --- a/Example/WebimClientLibrary/Base.lproj/RatingViewController.xib +++ /dev/null @@ -1,94 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/Example/WebimClientLibrary/ChatTableViewController.swift b/Example/WebimClientLibrary/ChatTableViewController.swift new file mode 100644 index 00000000..9df6421f --- /dev/null +++ b/Example/WebimClientLibrary/ChatTableViewController.swift @@ -0,0 +1,1356 @@ +// +// ChatTableViewController.swift +// WebimClientLibrary_Example +// +// Created by Eugene Ilyin on 19/09/2019. +// Copyright © 2019 Webim. All rights reserved. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. +// + +import AVFoundation +import UIKit +import WebimClientLibrary +import SnapKit +import Nuke + +class ChatTableViewController: UITableViewController { + + // MARK: - Properties + var selectedCellRow: Int? + var scrollButtonIsHidden = true + + // MARK: - Private properties + private let newRefreshControl = UIRefreshControl() + + private var alreadyRatedOperators = [String: Bool]() + private var cellHeights = [IndexPath: CGFloat]() + private var overlayWindow: UIWindow? + private var visibleRows = [IndexPath]() + private var keyboardWindow: UIWindow? { + // The window containing the keyboard always seems to be the last one + return UIApplication.shared.windows.last + } + + private weak var containerNewChatViewController: ChatViewController? + + private lazy var messages = [Message]() + private lazy var alertDialogHandler = UIAlertHandler(delegate: self) + private lazy var webimService = WebimService( + fatalErrorHandlerDelegate: self, + departmentListHandlerDelegate: self + ) + + // MARK: - View Life Cycle + override func prepare(for segue: UIStoryboardSegue, sender: Any?) { + super.prepare(for: segue, sender: sender) + + if let vc = segue.destination as? ChatViewController { + containerNewChatViewController = vc + } + } + + override func viewWillAppear(_ animated: Bool) { + NotificationCenter.default.addObserver( + self, + selector: #selector(addRatedOperator), + name: .shouldRateOperator, + object: nil + ) + + NotificationCenter.default.addObserver( + self, + selector: #selector(showRatingDialog(_:)), + name: .shouldShowRatingDialog, + object: nil + ) + + self.tableView.reloadData() + } + + override func viewDidLoad() { + super.viewDidLoad() + + NotificationCenter.default.addObserver( + self, + selector: #selector(showFile), + name: .shouldShowFile, + object: nil + ) + + NotificationCenter.default.addObserver( + self, + selector: #selector(setVisitorTypingDraft), + name: .shouldSetVisitorTypingDraft, + object: nil + ) + + NotificationCenter.default.addObserver( + self, + selector: #selector(hideOverlayWindow), + name: .shouldHideOverlayWindow, + object: nil + ) + + NotificationCenter.default.addObserver( + self, + selector: #selector(sendKeyboardRequest), + + name: .shouldSendKeyboardRequest, + object: nil + ) + + setupWebimSession() + + registerCells() + + setupRefreshControl() + } + + override func viewDidAppear(_ animated: Bool) { + super.viewDidAppear(animated) + updateCurrentOperatorInfo(to: webimService.getCurrentOperator()) + } + + override func viewWillDisappear(_ animated: Bool) { + if containerNewChatViewController == nil { + stopWebimSession() + + NotificationCenter.default.removeObserver( + self, + name: .shouldRateOperator, + object: nil + ) + + NotificationCenter.default.removeObserver( + self, + name: .shouldShowFile, + object: nil + ) + + NotificationCenter.default.removeObserver( + self, + name: .shouldShowRatingDialog, + object: nil + ) + + NotificationCenter.default.removeObserver( + self, + name: .shouldSetVisitorTypingDraft, + object: nil + ) + + NotificationCenter.default.removeObserver( + self, + name: .shouldHideOverlayWindow, + object: nil + ) + + NotificationCenter.default.removeObserver( + self, + name: .shouldSendKeyboardRequest, + object: nil + ) + } + } + + override func viewWillTransition( + to size: CGSize, + with coordinator: UIViewControllerTransitionCoordinator + ) { + super.viewWillTransition(to: size, with: coordinator) + + // Will not trigger method hidePopupActionsViewController() + // if PopupActionsTableView is not presented + NotificationCenter.default.post( + name: .shouldHidePopupActionsViewController, + object: nil + ) + + // Will not trigger method hideRatingDialogViewController() + // if RatingDialogViewController is not presented + NotificationCenter.default.post( + name: .shouldHideRatingDialogViewController, + object: nil + ) + + + coordinator.animate( + alongsideTransition: { context in + // Save visible rows position + if let visibleRows = self.tableView.indexPathsForVisibleRows { + self.visibleRows = visibleRows + } + context.viewController(forKey: .from) + }, + completion: { _ in + // Scroll to the saved position prior to screen rotate + if let lastVisibleRow = self.visibleRows.last { + self.tableView.scrollToRow( + at: lastVisibleRow, + at: .bottom, + animated: true + ) + } + } + ) + } + + // MARK: - Table view data source + override func numberOfSections(in tableView: UITableView) -> Int { + if messages.count > 0 { + tableView.backgroundView = nil + return 1 + } else { + tableView.emptyTableView( + message: TableView.emptyTableViewText.rawValue.localized + ) + return 0 + } + } + + override func tableView( + _ tableView: UITableView, + numberOfRowsInSection section: Int + ) -> Int { messages.count } + + override func tableView( + _ tableView: UITableView, + cellForRowAt indexPath: IndexPath + ) -> UITableViewCell { + + guard indexPath.row < messages.count else { return UITableViewCell() } + let message = messages[indexPath.row] + + guard let cell = tableView.dequeueReusableCell( + withIdentifier: "FlexibleTableViewCell", + for: indexPath + ) as? FlexibleTableViewCell else { + fatalError("The dequeued cell is not an instance of FlexibleTableViewCell.") + } + + cell.configureTheCell( + forMessage: message, + showFullDate: shouldShowFullDate(forMessageNumber: indexPath.row), + shouldShowOperatorInfo: shouldShowOperatorInfo(forMessageNumber: indexPath.row), + isEdited: message.isEdited() + ) + + cell.selectionStyle = .none + cell.backgroundColor = flexibleTableViewCellBackgroundColour + + if let data = message.getData(), + let attachment = data.getAttachment(), + let contentType = attachment.getFileInfo().getContentType() { + + if isImage(contentType: contentType) { + let gestureRecognizer = UITapGestureRecognizer( + target: self, + action: #selector(showImage) + ) + + if cell.hasImageAsDocument { + cell.documentFileStatusButton.isUserInteractionEnabled = true + cell.documentFileStatusButton.addGestureRecognizer(gestureRecognizer) + } else { + cell.imageImageView.isUserInteractionEnabled = true + cell.imageImageView.addGestureRecognizer(gestureRecognizer) + } + } else if isAcceptableFile(contentType: contentType) { + cell.documentFileStatusButton.isUserInteractionEnabled = true + } else { + cell.documentFileStatusButton.isUserInteractionEnabled = false + } + } + + if cell.userAvatarImageView.image != nil { + let rateOperatorTapGesture = UITapGestureRecognizer( + target: self, + action: #selector(rateOperatorByTappingAvatar) + ) + cell.userAvatarImageView.isUserInteractionEnabled = true + cell.userAvatarImageView.addGestureRecognizer(rateOperatorTapGesture) + } + + let longPressPopupGestureRecognizer = UILongPressGestureRecognizer( + target: self, + action: #selector(showPopoverMenu) + ) + longPressPopupGestureRecognizer.minimumPressDuration = 0.5 + longPressPopupGestureRecognizer.cancelsTouchesInView = false + + + let doubleTapPopupGestureRecognizer = UITapGestureRecognizer( + target: self, + action: #selector(showPopoverMenu) + ) + doubleTapPopupGestureRecognizer.numberOfTapsRequired = 2 + doubleTapPopupGestureRecognizer.cancelsTouchesInView = false + + cell.messageBackgroundView.isUserInteractionEnabled = true + cell.messageBackgroundView.addGestureRecognizer(longPressPopupGestureRecognizer) + cell.messageBackgroundView.addGestureRecognizer(doubleTapPopupGestureRecognizer) + + if selectedCellRow != nil { + cell.alpha = 0 + } + + return cell + } + + @available(iOS 11.0, *) + override func tableView( + _ tableView: UITableView, + trailingSwipeActionsConfigurationForRowAt indexPath: IndexPath + ) -> UISwipeActionsConfiguration? { + + let message = messages[indexPath.row] + + if message.isSystemType() || message.isOperatorType() || !message.canBeReplied() { + return nil + } + + let replyAction = UIContextualAction( + style: .normal, + title: nil, + handler: { (context, view, completionHandler) in + self.selectedCellRow = indexPath.row + let actionsDictionary = ["Action": PopupAction.reply] + NotificationCenter.default.post( + name: .shouldShowQuoteEditBar, + object: nil, + userInfo: actionsDictionary + ) + completionHandler(true) + } + ) + + // Workaround for iOS < 13 + if let cgImageReplyAction = trailingSwipeActionImage.cgImage { + replyAction.image = CustomUIImage( + cgImage: cgImageReplyAction, + scale: UIScreen.main.nativeScale, + orientation: .up + ) + } + replyAction.backgroundColor = tableView.backgroundColor + + return UISwipeActionsConfiguration(actions: [replyAction]) + } + + @available(iOS 11.0, *) + override func tableView( + _ tableView: UITableView, + leadingSwipeActionsConfigurationForRowAt indexPath: IndexPath + ) -> UISwipeActionsConfiguration? { + + let message = messages[indexPath.row] + + if message.isSystemType() || message.isVisitorType() || !message.canBeReplied() { + return nil + } + + let replyAction = UIContextualAction( + style: .normal, + title: nil, + handler: { (context, view, completionHandler) in + self.selectedCellRow = indexPath.row + let actionsDictionary = ["Action": PopupAction.reply] + NotificationCenter.default.post( + name: .shouldShowQuoteEditBar, + object: nil, + userInfo: actionsDictionary + ) + completionHandler(true) + } + ) + + // Workaround for iOS < 13 + if let cgImageReplyAction = leadingSwipeActionImage.cgImage { + replyAction.image = CustomUIImage( + cgImage: cgImageReplyAction, + scale: UIScreen.main.nativeScale, + orientation: .up + ) + } + replyAction.backgroundColor = tableView.backgroundColor + + return UISwipeActionsConfiguration(actions: [replyAction]) + } + + override func tableView( + _ tableView: UITableView, + editingStyleForRowAt indexPath: IndexPath + ) -> UITableViewCell.EditingStyle { .none } + + // Dynamic Cell Sizing + override func tableView( + _ tableView: UITableView, + willDisplay cell: UITableViewCell, + forRowAt indexPath: IndexPath + ) { + cellHeights[indexPath] = cell.frame.size.height + } + + override func tableView( + _ tableView: UITableView, + estimatedHeightForRowAt indexPath: IndexPath + ) -> CGFloat { cellHeights[indexPath] ?? 70.0 } + + override func scrollViewDidScroll(_ scrollView: UIScrollView) { + + let lastCellIndexPath = IndexPath( + row: tableView.numberOfRows(inSection: 0) - 1, + section: 0 + ) + + if tableView.indexPathsForVisibleRows?.contains(lastCellIndexPath) == false { + if scrollButtonIsHidden { + scrollButtonIsHidden = false + NotificationCenter.default.post( + name: .shouldShowScrollButton, + object: nil + ) + } + } else { + if !scrollButtonIsHidden { + scrollButtonIsHidden = true + NotificationCenter.default.post( + name: .shouldHideScrollButton, + object: nil + ) + } + } + + } + + // MARK: - Methods + /// Preparation for the future + internal func set(messages: [Message]) { + self.messages = messages + } + + @objc + private func sendKeyboardRequest(_ notification: Notification) { + guard let buttonInfoDictionary = notification.userInfo as? [String: String], + let messageID = buttonInfoDictionary["Message"], + let buttonID = buttonInfoDictionary["ButtonID"], + buttonInfoDictionary["ButtonTitle"] != nil + else { return } + + if let message = findMessage(withID: messageID), + let button = findButton(inMessage: message, buttonID: buttonID) { + // TODO: Send request + print("Sending keyboard request...") + + webimService.sendKeyboardRequest( + button: button, + message: message, + completionHandler: self + ) + } else { + print("HALT! There isn't such message or button in #function") + } + } + + private func findMessage(withID id: String) -> Message? { + for message in messages { + if message.getID() == id { + return message + } + } + return nil + } + + private func findButton( + inMessage message: Message, + buttonID: String + ) -> KeyboardButton? { + guard let buttonsArrays = message.getKeyboard()?.getButtons() else { return nil } + let buttons = buttonsArrays.flatMap { $0 } + + for button in buttons { + if button.getID() == buttonID { + return button + } + } + return nil + } + /// + + func sendMessage(_ message: String) { + webimService.send(message: message) { [weak self] in + // Delete visitor typing draft after message is sent. + self?.webimService.setVisitorTyping(draft: nil) + } + } + + func sendImage(image: UIImage, imageURL: URL?) { + containerNewChatViewController?.dismissKeyboardNow() + + var imageData = Data() + var imageName = String() + var mimeType = MimeType() + + if let imageURL = imageURL { + mimeType = MimeType(url: imageURL as URL) + imageName = imageURL.lastPathComponent + + let imageExtension = imageURL.pathExtension.lowercased() + + switch imageExtension { + case "jpg", "jpeg": + guard let unwrappedData = image.jpegData(compressionQuality: 1.0) + else { return } + imageData = unwrappedData + + case "heic", "heif": + guard let unwrappedData = image.jpegData(compressionQuality: 0.5) + else { return } + imageData = unwrappedData + + var components = imageName.components(separatedBy: ".") + if components.count > 1 { + components.removeLast() + imageName = components.joined(separator: ".") + } + imageName += ".jpeg" + + default: + guard let unwrappedData = image.pngData() + else { return } + imageData = unwrappedData + } + } else { + guard let unwrappedData = image.jpegData(compressionQuality: 1.0) + else { return } + imageData = unwrappedData + imageName = "photo.jpeg" + } + + webimService.send( + file: imageData, + fileName: imageName, + mimeType: mimeType.value, + completionHandler: self + ) + } + + func sendFile(file: Data, fileURL: URL?) { + if let fileURL = fileURL { + webimService.send( + file: file, + fileName: fileURL.lastPathComponent, + mimeType: MimeType(url: fileURL as URL).value, + completionHandler: self + ) + } else { + let url = URL(fileURLWithPath: "document.pdf") + webimService.send( + file: file, + fileName: url.lastPathComponent, + mimeType: MimeType(url: url).value, + completionHandler: self + ) + } + } + + func getSelectedMessage() -> Message? { + guard let selectedCellRow = selectedCellRow, + selectedCellRow >= 0, + selectedCellRow < messages.count else { return nil } + return messages[selectedCellRow] + } + + func replyToMessage(_ message: String) { + guard let messageToReply = getSelectedMessage() else { return } + webimService.reply( + message: message, + repliedMessage: messageToReply, + completion: { [weak self] in + // Delete visitor typing draft after message is sent. + self?.webimService.setVisitorTyping(draft: nil) + } + ) + } + + func copyMessage() { + guard let messageToCopy = getSelectedMessage() else { return } + UIPasteboard.general.string = messageToCopy.getText() + } + + func editMessage(_ message: String) { + guard let messageToEdit = getSelectedMessage() else { return } + webimService.edit( + message: messageToEdit, + text: message, + completionHandler: self + ) + } + + func deleteMessage() { + guard let messageToDelete = getSelectedMessage() else { return } + webimService.delete( + message: messageToDelete, + completionHandler: self + ) + } + + @objc + func scrollToBottom(animated: Bool) { + if messages.isEmpty { + return + } + + let row = (tableView.numberOfRows(inSection: 0)) - 1 + let bottomMessageIndex = IndexPath(row: row, section: 0) + tableView.scrollToRow(at: bottomMessageIndex, at: .bottom, animated: animated) + } + + @objc + func scrollToTop(animated: Bool) { + if messages.isEmpty { + return + } + + let indexPath = IndexPath(row: 0, section: 0) + self.tableView.scrollToRow(at: indexPath, at: .top, animated: animated) + } + + // MARK - Private methods + private func setupRefreshControl() { + if #available(iOS 10.0, *) { + tableView?.refreshControl = newRefreshControl + } else { + tableView?.addSubview(newRefreshControl) + } + newRefreshControl.layer.zPosition -= 1 + newRefreshControl.addTarget( + self, + action: #selector(requestMessages), + for: .valueChanged + ) + newRefreshControl.tintColor = refreshControlTintColour + let attributes = [NSAttributedString.Key.foregroundColor: refreshControlTextColour] + newRefreshControl.attributedTitle = NSAttributedString( + string: ChatTableView.refreshControlText.rawValue.localized, + attributes: attributes + ) + } + + @objc + private func requestMessages() { + webimService.getNextMessages() { [weak self] messages in + self?.messages.insert(contentsOf: messages, at: 0) + DispatchQueue.main.asyncAfter(deadline: .now() + 1) { + self?.tableView?.reloadData() + self?.newRefreshControl.endRefreshing() + self?.webimService.setChatRead() + } + } + } + + private func shouldShowFullDate(forMessageNumber index: Int) -> Bool { + guard index - 1 >= 0 else { return true } + let currentMessageTime = messages[index].getTime() + let previousMessageTime = messages[index - 1].getTime() + let differenceBetweenDates = Calendar.current.dateComponents( + [.day], + from: previousMessageTime, + to: currentMessageTime + ) + return differenceBetweenDates.day != 0 + } + + private func shouldShowOperatorInfo(forMessageNumber index: Int) -> Bool { + guard messages[index].isOperatorType() else { return false } + guard index + 1 < messages.count else { return true } + + let nextMessage = messages[index + 1] + if nextMessage.isOperatorType() { + return false + } else { + return true + } + } + + @objc + private func showPopoverMenu(_ gestureRecognizer: UIGestureRecognizer) { + self.view.endEditing(true) + var stateToCheck = UIGestureRecognizer.State.ended + + if gestureRecognizer is UILongPressGestureRecognizer { + stateToCheck = UIGestureRecognizer.State.began + } + + if gestureRecognizer.state == stateToCheck { + let touchPoint = gestureRecognizer.location(in: self.tableView) + if let indexPath = self.tableView.indexPathForRow(at: touchPoint) { + + guard let selectedCell = self.tableView.cellForRow(at: indexPath) + as? FlexibleTableViewCell + else { return } + let selectedCellRow = indexPath.row + self.selectedCellRow = selectedCellRow + + let viewController = PopupActionsViewController() + viewController.modalPresentationStyle = .overFullScreen + viewController.cellImageViewImage = selectedCell.takeScreenshot() + + guard let globalYPosition = selectedCell.superview? + .convert(selectedCell.center, to: nil) + else { return } + viewController.cellImageViewCenterYPosition = globalYPosition.y + + guard let cellHeight = cellHeights[indexPath] else { return } + viewController.cellImageViewHeight = cellHeight + + let message = messages[selectedCellRow] + + if message.isOperatorType() { + viewController.originalCellAlignment = .leading + } else if message.isVisitorType() { + viewController.originalCellAlignment = .trailing + } + + if message.canBeReplied() { + viewController.actions.append(.reply) + } + + if message.canBeCopied() { + viewController.actions.append(.copy) + } + + if message.canBeEdited() { + viewController.actions.append(.edit) + + // If image hide show edit action + if let contentType = message.getData()?.getAttachment()?.getFileInfo().getContentType() { + if isImage(contentType: contentType) { + viewController.actions.removeLast() + } + } + viewController.actions.append(.delete) + } + + if viewController.actions.count != 0 { + // Workaround to keep keyboard shown. + /// TODO: Probably there is a better solution to check if the keyboard is shown. + /// Now it is NOT accurate, since this check could fail if something goes wrong (i.e. some windows haven't been hidden) + if UIApplication.shared.windows.count > 2 { + /// More details at: https://github.com/robbajorek/ModalOverlayIOS + guard let overlayFrame = view?.window?.frame else { return } + overlayWindow = UIWindow(frame: overlayFrame) + overlayWindow?.windowLevel = .alert + overlayWindow?.rootViewController = viewController + + showOverlayWindow() + } else { + self.present(viewController, animated: false) + } + } + } + } + } + + @objc + private func hideOverlayWindow(notification: Notification) { + // TODO: Same as the above one. Potentially could fail. + if UIApplication.shared.windows.count > 2 { + keyboardWindow?.isHidden = false + overlayWindow = nil + } + } + + private func showOverlayWindow() { + overlayWindow?.isHidden = false + keyboardWindow?.isHidden = true + } + + @objc + private func showImage(_ recognizer: UIGestureRecognizer) { + guard recognizer.state == .ended else { return } + let tapLocation = recognizer.location(in: tableView) + + guard let tapIndexPath = tableView?.indexPathForRow(at: tapLocation) else { return } + let message = messages[tapIndexPath.row] + guard let url = message.getData()?.getAttachment()?.getFileInfo().getURL(), + let vc = storyboard?.instantiateViewController(withIdentifier: "ImageView") + as? ImageViewController + else { return } + + let request = ImageRequest(url: url) + vc.selectedImageURL = url + vc.selectedImage = ImageCache.shared[request] + + navigationController?.pushViewController(vc, animated: true) + } + + @objc + private func showFile(sender: Notification) { + guard let files = sender.userInfo as? [String: String], + let fileName = files["FullName"] + else { return } + + let documentsPath = FileManager.default.urls( + for: .cachesDirectory, + in: .userDomainMask)[0] + + let destinationURL = documentsPath.appendingPathComponent(fileName) + + guard FileManager.default.fileExists(atPath: destinationURL.path), + let vc = storyboard?.instantiateViewController(withIdentifier: "FileView") + as? FileViewController + else { return } + + vc.fileDestinationURL = destinationURL + navigationController?.pushViewController(vc, animated: true) + } + + @objc + private func showRatingDialog(_ notification: Notification) { + guard let currentOperator = webimService.getCurrentOperator() else { + alertDialogHandler.showNoCurrentOperatorDialog() + return + } + + let operatorInfo = [ + "Name": currentOperator.getName(), + "ID": currentOperator.getID(), + "AvatarURL": currentOperator.getAvatarURL()?.absoluteString + ] + + showRatingDialog(forOperatorInfo: operatorInfo) + } + + private func showRatingDialog(forOperatorInfo info: [String: String?]) { + guard let optionalOperatorName = info["Name"], + let optionalOperatorID = info["ID"], + let operatorName = optionalOperatorName, + let operatorID = optionalOperatorID + else { return } + + let operatorRating = 0 + var operatorAvatarImage = UIImage() + var operatorAvatarImageURL = String() + + if let optionalAvatarURLString = info["AvatarURL"], + let avatarURLString = optionalAvatarURLString, + let avatarURL = URL(string: avatarURLString) { + let request = ImageRequest(url: avatarURL) + + operatorAvatarImageURL = avatarURLString + operatorAvatarImage = ImageCache.shared[request] ?? UIImage() + } else { + operatorAvatarImage = userAvatarImagePlaceholder + } + + let centerOfTheScreen = CGPoint( + x: UIScreen.main.bounds.width / 2, + y: UIScreen.main.bounds.height / 2 + ) + + let vc = RatingDialogViewController() + vc.modalPresentationStyle = .overFullScreen + vc.operatorID = operatorID + vc.viewCenterYPosition = centerOfTheScreen.y + vc.operatorName = operatorName + vc.operatorAvatarImage = operatorAvatarImage + vc.operatorAvatarImageURL = operatorAvatarImageURL + vc.operatorRating = Double(operatorRating) + + // Workaround to keep keyboard shown. + /// TODO: Probably there is a better solution to check if the keyboard is shown. + /// Now it is NOT accurate, since this check could fail if something goes wrong (i.e. some windows haven't been hidden) + if UIApplication.shared.windows.count > 2 { + /// More details at: https://github.com/robbajorek/ModalOverlayIOS + guard let overlayFrame = view?.window?.frame else { return } + overlayWindow = UIWindow(frame: overlayFrame) + overlayWindow?.windowLevel = .alert + overlayWindow?.rootViewController = vc + + showOverlayWindow() + } else { + self.present(vc, animated: false) + } + } + + @objc + func rateOperatorByTappingAvatar(recognizer: UITapGestureRecognizer) { + guard recognizer.state == .ended else { return } + let tapLocation = recognizer.location(in: tableView) + + guard let tapIndexPath = tableView?.indexPathForRow(at: tapLocation) else { return } + let message = messages[tapIndexPath.row] + + let operatorInfo = [ + "Name": message.getSenderName(), + "ID": message.getOperatorID(), + "AvatarURL": message.getSenderAvatarFullURL()?.absoluteString + ] + + showRatingDialog(forOperatorInfo: operatorInfo) + } + + + @objc + private func addRatedOperator(_ notification: Notification) { + guard let ratingInfoDictionary = notification.userInfo + as? [String: Int] + else { return } + + for (id, rating) in ratingInfoDictionary { + rateOperator( + operatorID: id, + rating: rating + ) + } + } + + private func rateOperator(operatorID: String, rating: Int) { + webimService.rateOperator( + withID: operatorID, + byRating: rating, + completionHandler: self + ) + } + + // Webim methods + private func setupWebimSession() { + webimService.createSession() + webimService.startSession() + webimService.setMessageStream() + webimService.setMessageTracker(withMessageListener: self) + webimService.set(operatorTypingListener: self) + webimService.set(currentOperatorChangeListener: self) + webimService.set(chatStateListener: self) + webimService.getLastMessages() { [weak self] messages in + self?.messages.insert(contentsOf: messages, at: 0) + DispatchQueue.main.async() { + self?.tableView?.reloadData() + self?.scrollToBottom(animated: false) + self?.webimService.setChatRead() + } + } + } + + private func stopWebimSession() { + webimService.stopSession() + } + + private func updateCurrentOperatorInfo(to newOperator: Operator?) { + let operatorInfoDictionary: [String: String] + if let currentOperator = newOperator { + let operatorURLString: String + if let avatarURLString = currentOperator.getAvatarURL()?.absoluteString { + operatorURLString = avatarURLString + } else { + operatorURLString = OperatorAvatar.placeholder.rawValue + } + + operatorInfoDictionary = [ + "OperatorName": currentOperator.getName(), + "OperatorAvatarURL": operatorURLString + ] + } else { + operatorInfoDictionary = [ + "OperatorName": OperatorStatus.noOperator.rawValue.localized, + "OperatorAvatarURL": OperatorAvatar.empty.rawValue + ] + } + + NotificationCenter.default.post( + name: .shouldUpdateOperatorInfo, + object: nil, + userInfo: operatorInfoDictionary + ) + } + + @objc + private func setVisitorTypingDraft(_ notification: Notification) { + guard let typingDraftDictionary = notification.userInfo as? [String: String] else { // Not string passed (nil) set draft to nil + webimService.setVisitorTyping(draft: nil) + return + } + guard let draftText = typingDraftDictionary["DraftText"] else { return } + webimService.setVisitorTyping(draft: draftText) + } + + private func registerCells() { + tableView?.register( + FlexibleTableViewCell.self, + forCellReuseIdentifier: "FlexibleTableViewCell" + ) + } +} + +// MARK: - WEBIM: MessageListener +extension ChatTableViewController: MessageListener { + + // MARK: - Methods + + func added(message newMessage: Message, + after previousMessage: Message?) { + var inserted = false + + if let previousMessage = previousMessage { + for (index, message) in messages.enumerated() { + if previousMessage.isEqual(to: message) { + messages.insert(newMessage, at: index) + inserted = true + break + } + } + } + + if !inserted { + messages.append(newMessage) + } + + DispatchQueue.main.async() { + self.tableView?.reloadData() + self.scrollToBottom(animated: true) + } + } + + func removed(message: Message) { + var toUpdate = false + if message.getCurrentChatID() == getSelectedMessage()?.getCurrentChatID() { + NotificationCenter.default.post( + name: .shouldHideQuoteEditBar, + object: nil, + userInfo: nil + ) + } + + for (messageIndex, iteratedMessage) in messages.enumerated() { + if iteratedMessage.getID() == message.getID() { + messages.remove(at: messageIndex) + let indexPath = IndexPath(row: messageIndex, section: 0) + cellHeights.removeValue(forKey: indexPath) + toUpdate = true + + break + } + } + + if toUpdate { + DispatchQueue.main.async() { + self.tableView?.reloadData() + self.scrollToBottom(animated: true) + } + } + } + + func removedAllMessages() { + messages.removeAll() + cellHeights.removeAll() + + DispatchQueue.main.async() { + self.tableView?.reloadData() + } + } + + func changed(message oldVersion: Message, + to newVersion: Message) { + let messagesCountBefore = messages.count + var toUpdate = false + var cellIndexToUpdate = 0 + + for (messageIndex, iteratedMessage) in messages.enumerated() { + if iteratedMessage.getID() == oldVersion.getID() { + messages[messageIndex] = newVersion + toUpdate = true + cellIndexToUpdate = messageIndex + + break + } + } + + if toUpdate { + DispatchQueue.main.async() { + let indexPath = IndexPath(row: cellIndexToUpdate, section: 0) + if self.messages.count != messagesCountBefore || + self.messages.count != self.tableView.numberOfRows(inSection: 0) { + self.tableView.reloadData() + } else { + self.tableView.reloadRows(at: [indexPath], with: .automatic) + } + } + } + + DispatchQueue.main.async() { + self.scrollToBottom(animated: false) + } + } +} + +// MARK: - WEBIM: HelloMessageListener +extension ChatViewController: HelloMessageListener { + func helloMessage(message: String) { + print("Received Hello message: \"\(message)\"") + } +} + +// MARK: - WEBIM: FatalErrorHandler +extension ChatTableViewController: FatalErrorHandlerDelegate { + + // MARK: - Methods + func showErrorDialog(withMessage message: String) { + alertDialogHandler.showCreatingSessionFailureDialog(withMessage: message) + } + +} + +// MARK: - WEBIM: DepartmentListHandlerDelegate +extension ChatTableViewController: DepartmentListHandlerDelegate { + + // MARK: - Methods + func show(departmentList: [Department], action: @escaping (String) -> ()) { + alertDialogHandler.showDepartmentListDialog( + withDepartmentList: departmentList, + action: action + ) + } + +} + +// MARK: - WEBIM: CompletionHandlers +extension ChatTableViewController: SendFileCompletionHandler, + EditMessageCompletionHandler, + DeleteMessageCompletionHandler, + RateOperatorCompletionHandler, + SendKeyboardRequestCompletionHandler { + // MARK: - Methods + + func onSuccess() { + // Workaround needed since operator dialog dismissed after a small delay + DispatchQueue.main.asyncAfter(deadline: .now() + 0.41) { + let title = AlertDialog.rateSuccessTitle.rawValue.localized + let message = AlertDialog.rateSuccessMessage.rawValue.localized + self.alertDialogHandler.showDialog(withMessage: message, title: title) + } + } + + func onSuccess(messageID: String) { + // Ignored. + // Delete visitor typing draft after message is sent. + self.webimService.setVisitorTyping(draft: nil) + } + + // SendFileCompletionHandler + func onFailure(messageID: String, error: SendFileError) { + DispatchQueue.main.async { + var message = SendFileErrorMessage.unknownError.rawValue.localized + switch error { + case .fileSizeExceeded: + message = SendFileErrorMessage.fileSizeExceeded.rawValue.localized + break + case .fileTypeNotAllowed: + message = SendFileErrorMessage.fileTypeNotAllowed.rawValue.localized + break + case .unknown: + message = SendFileErrorMessage.unknownError.rawValue.localized + break + case .uploadedFileNotFound: + message = SendFileErrorMessage.fileNotFound.rawValue.localized + break + case .unauthorized: + message = SendFileErrorMessage.unauthorized.rawValue.localized + } + + self.alertOnFailure( + with: message, + id: messageID, + title: SendFileErrorMessage.title.rawValue.localized + ) + } + } + + // EditMessageCompletionHandler + func onFailure(messageID: String, error: EditMessageError) { + DispatchQueue.main.async { + var message = EditMessageErrorMessage.unknownError.rawValue.localized + switch error { + case .unknown: + message = EditMessageErrorMessage.unknownError.rawValue.localized + case .notAllowed: + message = EditMessageErrorMessage.notAllowed.rawValue.localized + case .messageEmpty: + message = EditMessageErrorMessage.messageEmpty.rawValue.localized + case .messageNotOwned: + message = EditMessageErrorMessage.messageNotOwned.rawValue.localized + case .maxLengthExceeded: + message = EditMessageErrorMessage.maxMessageLengthExceede.rawValue.localized + case .wrongMesageKind: + message = EditMessageErrorMessage.wrongMessageKind.rawValue.localized + } + + self.alertOnFailure( + with: message, + id: messageID, + title: EditMessageErrorMessage.title.rawValue.localized + ) + } + } + + // DeleteMessageCompletionHandler + func onFailure(messageID: String, error: DeleteMessageError) { + DispatchQueue.main.async { + var message = DeleteMessageErrorMessage.unknownError.rawValue.localized + switch error { + case .unknown: + message = DeleteMessageErrorMessage.unknownError.rawValue.localized + case .notAllowed: + message = DeleteMessageErrorMessage.notAllowed.rawValue.localized + case .messageNotOwned: + message = DeleteMessageErrorMessage.messageNotOwned.rawValue.localized + case .messageNotFound: + message = DeleteMessageErrorMessage.messageNotFound.rawValue.localized + } + + self.alertOnFailure( + with: message, + id: messageID, + title: DeleteMessageErrorMessage.title.rawValue.localized + ) + } + } + + // RateOperatorCompletionHandler + func onFailure(error: RateOperatorError) { + DispatchQueue.main.asyncAfter(deadline: .now() + 0.41) { + var message = String() + switch error { + case .noChat: + message = RateOperatorErrorMessage.rateOperatorNoChat.rawValue.localized + case .wrongOperatorId: + message = RateOperatorErrorMessage.rateOperatorWrongID.rawValue.localized + case .noteIsTooLong: + message = RateOperatorErrorMessage.rateOperatorLongNote.rawValue.localized + } + + self.alertDialogHandler.showDialog( + withMessage: message, + title: RateOperatorErrorMessage.title.rawValue.localized + ) + } + } + + // SendKeyboardRequestCompletionHandler + func onFailure(messageID: String, error: KeyboardResponseError) { + DispatchQueue.main.async { + var message = SendKeyboardRequestErrorMessage.unknownError.rawValue.localized + switch error { + case .unknown: + message = SendKeyboardRequestErrorMessage.unknownError.rawValue.localized + case .noChat: + message = SendKeyboardRequestErrorMessage.noChat.rawValue.localized + case .buttonIdNotSet: + message = SendKeyboardRequestErrorMessage.buttonIDNotSet.rawValue.localized + case .requestMessageIdNotSet: + message = SendKeyboardRequestErrorMessage.requestMessageIDNotSet.rawValue.localized + case .canNotCreateResponse: + message = SendKeyboardRequestErrorMessage.cannotCreateResponse.rawValue.localized + } + + let title = SendKeyboardRequestErrorMessage.title.rawValue.localized + + self.alertDialogHandler.showSendFailureDialog( + withMessage: message, + title: title, + action: { [weak self] in + guard self != nil else { return } + + // TODO: Make sure to delete message if needed +// for (index, message) in self.messages.enumerated() { +// if message.getID() == messageID { +// self.messages.remove(at: index) +// DispatchQueue.main.async() { +// self.tableView?.reloadData() +// } +// +// return +// } +// } + } + ) + } + } + + private func alertOnFailure(with message: String, id messageID: String, title: String) { + alertDialogHandler.showSendFailureDialog( + withMessage: message, + title:title, + action: { [weak self] in + guard let self = self else { return } + + for (index, message) in self.messages.enumerated() { + if message.getID() == messageID { + self.messages.remove(at: index) + DispatchQueue.main.async() { + self.tableView?.reloadData() + } + return + } + } + } + ) + } +} + +// MARK: - WEBIM: OperatorTypingListener +extension ChatTableViewController: OperatorTypingListener { + func onOperatorTypingStateChanged(isTyping: Bool) { + guard webimService.getCurrentOperator() != nil else { return } + + if isTyping { + let statusTyping = OperatorStatus.isTyping.rawValue.localized + let operatorStatus = ["Status": statusTyping] + NotificationCenter.default.post( + name: .shouldChangeOperatorStatus, + object: nil, + userInfo: operatorStatus + ) + } else { + let operatorStatus = ["Status": OperatorStatus.online.rawValue.localized] + NotificationCenter.default.post( + name: .shouldChangeOperatorStatus, + object: nil, + userInfo: operatorStatus + ) + } + } +} + +// MARK: - WEBIM: CurrentOperatorChangeListener +extension ChatTableViewController: CurrentOperatorChangeListener { + func changed(operator previousOperator: Operator?, to newOperator: Operator?) { + updateCurrentOperatorInfo(to: newOperator) + } +} + +// MARK: - WEBIM: ChatStateLisneter +extension ChatTableViewController: ChatStateListener { + func changed(state previousState: ChatState, to newState: ChatState) { + if newState == .closedByVisitor || newState == .closed || newState == .closedByOperator { + // TODO: rating operator + } + } +} diff --git a/Example/WebimClientLibrary/ChatViewController.swift b/Example/WebimClientLibrary/ChatViewController.swift index 9e2e2eba..62c3c414 100644 --- a/Example/WebimClientLibrary/ChatViewController.swift +++ b/Example/WebimClientLibrary/ChatViewController.swift @@ -2,8 +2,8 @@ // ChatViewController.swift // WebimClientLibrary_Example // -// Created by Nikita Lazarev-Zubov on 02.10.17. -// Copyright © 2017 Webim. All rights reserved. +// Created by Eugene Ilyin on 01/10/2019. +// Copyright © 2019 Webim. All rights reserved. // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal @@ -24,630 +24,1100 @@ // SOFTWARE. // -import SlackTextViewController +import MobileCoreServices import UIKit +import Nuke import WebimClientLibrary -final class ChatViewController: SLKTextViewController { +class ChatViewController: UIViewController { // MARK: - Properties - private let imagePicker = UIImagePickerController() - private let refreshControl = UIRefreshControl() - private lazy var alreadyRated = [String: Bool]() - private var lastOperatorID: String? - private lazy var messages = [Message]() - private var popupDialogHandler: PopupDialogHandler? - private var scrollToBottomButton: UIButton? - private var webimService: WebimService? + private var alreadyPutTextFromBufferString = false + private var textInputTextViewBufferString: String? + private weak var containerChatTableViewController: ChatTableViewController? - // MARK: - Methods + private lazy var filePicker = FilePicker( + presentationController: self, + delegate: self + ) - override func viewDidLoad() { - super.viewDidLoad() - - webimService = WebimService(fatalErrorHandlerDelegate: self, - departmentListHandlerDelegate: self) - popupDialogHandler = PopupDialogHandler(delegate: self) - - imagePicker.delegate = self - - setupNavigationItem() - setupRefreshControl() - setupSlackTextViewController() - setupWebimSession() - } + // MARK: - Constraints + private let buttonWidthHeight: CGFloat = 20 + private let fileButtonLeadingSpacing: CGFloat = 20 + private let fileButtonTrailingSpacing: CGFloat = 10 + private let textInputBackgroundViewTopBottomSpacing: CGFloat = 8 + + // MARK: - Outletls + @IBOutlet weak var tableViewControllerContainerView: UIView! + @IBOutlet weak var bottomBarBackgroundView: UIView! - override func viewWillLayoutSubviews() { - super.viewWillLayoutSubviews() - - setupTableView() - - scrollToBottomButton?.removeFromSuperview() // Need for redrawing after device is rotated. - setupScrollToBottomButton() - } + // MARK: - Subviews + // Scroll button + lazy var scrollButton: UIButton = { + return createUIButton(type: .system) + }() - // For testing purposes. - func set(messages: [Message]) { - self.messages = messages - } + // Top bar (top navigation bar) + lazy var titleViewOperatorAvatarImageView: UIImageView = { + return createUIImageView(contentMode: .scaleAspectFit) + }() + lazy var titleViewOperatorNameLabel: UILabel = { + return createUILabel(systemFontSize: 15) + }() + lazy var titleViewOperatorStatusLabel: UILabel = { + return createUILabel(systemFontSize: 13, systemFontWeight: .light) + }() + lazy var titleViewTypingIndicator: TypingIndicator = { + let view = TypingIndicator() + view.circleDiameter = 5.0 + view.circleColour = UIColor.white + view.animationDuration = 0.5 + return view + }() - // MARK: SlackTextViewController methods + // Bottom bar + lazy var separatorView: UIView = { + return createUIView() + }() + lazy var fileButton: UIButton = { + return createCustomUIButton(type: .system) + }() + lazy var textInputBackgroundView: UIView = { + return createUIView() + }() + lazy var textInputTextView: UITextView = { + let textView = UITextView() + textView.font = .systemFont(ofSize: 16) + textView.translatesAutoresizingMaskIntoConstraints = false + return textView + }() + lazy var textInputTextViewPlaceholderLabel: UILabel = { + return createUILabel(systemFontSize: 16, numberOfLines: 0) + }() + lazy var textInputButton: UIButton = { + return createUIButton(type: .system) + }() - override func textViewDidChange(_ textView: UITextView) { - webimService!.setVisitorTyping(draft: textView.text) - } + // Bottom bar quote/edit + lazy var bottomBarQuoteBackgroundView: UIView = { + return createUIView() + }() + lazy var bottomBarQuoteLineView: UIView = { + return createUIView() + }() + lazy var bottomBarQuoteAttachmentImageView: UIImageView = { + return createUIImageView(contentMode: .scaleAspectFill) + }() + lazy var bottomBarQuoteUsernameLabel: UILabel = { + return createUILabel(systemFontSize: 16, systemFontWeight: .heavy) + }() + lazy var bottomBarQuoteBodyLabel: UILabel = { + return createUILabel(systemFontSize: 15, systemFontWeight: .light) + }() + lazy var bottomBarQuoteCancelButton: UIButton = { + return createCustomUIButton(type: .system) + }() - override func didPressRightButton(_ sender: Any?) { // Send message button - textView.refreshFirstResponder() + // MARK: - View Life Cycle + override func prepare(for segue: UIStoryboardSegue, sender: Any?) { + super.prepare(for: segue, sender: sender) - if let text = textView.text, - !text.isEmpty { - webimService!.send(message: text) { [weak self] in - self?.textView.text = "" - self?.webimService!.setVisitorTyping(draft: nil) // Delete visitor typing draft after message is sent. - } + if let vc = segue.destination as? ChatTableViewController { + containerChatTableViewController = vc } } - override func didPressLeftButton(_ sender: Any?) { // Send file buton - dismissKeyboard(true) + override func viewWillAppear(_ animated: Bool) { + NotificationCenter.default.addObserver( + self, selector: #selector(keyboardWillChange), + name: UIResponder.keyboardWillChangeFrameNotification, + object: nil + ) + + NotificationCenter.default.addObserver( + self, + selector: #selector(showScrollButton), + name: .shouldShowScrollButton, + object: nil + ) + + NotificationCenter.default.addObserver( + self, + selector: #selector(hideScrollButton), + name: .shouldHideScrollButton, + object: nil) + + NotificationCenter.default.addObserver( + self, + selector: #selector(addQuoteEditBar), + name: .shouldShowQuoteEditBar, + object: nil + ) - imagePicker.allowsEditing = false - imagePicker.sourceType = .photoLibrary - present(imagePicker, - animated: true, - completion: nil) + NotificationCenter.default.addObserver( + self, + selector: #selector(removeQuoteEditBar), + name: .shouldHideQuoteEditBar, + object: nil + ) + + NotificationCenter.default.addObserver( + self, + selector: #selector(copyMessage), + name: .shouldCopyMessage, + object: nil + ) + + NotificationCenter.default.addObserver( + self, + selector: #selector(deleteMessage), + name: .shouldDeleteMessage, + object: nil + ) + + NotificationCenter.default.addObserver( + self, + selector: #selector(updateOperatorStatus), + name: .shouldChangeOperatorStatus, + object: nil + ) + + NotificationCenter.default.addObserver( + self, + selector: #selector(updateOperatorInfo), + name: .shouldUpdateOperatorInfo, + object: nil + ) } - // MARK: UICollectionViewDataSource methods - override func numberOfSections(in tableView: UITableView) -> Int { - if messages.count > 0 { - tableView.backgroundView = nil - - return 1 - } else { - tableView.emptyTableView(message: TableView.emptyTableViewText.rawValue.localized) - - return 0 - } + override func viewDidLoad() { + setupKeyboard() + + setupNavigationBar() + configureSubviews() + + setupScrollButton() } + override func viewWillDisappear(_ animated: Bool) { + NotificationCenter.default.removeObserver( + self, + name: UIResponder.keyboardWillChangeFrameNotification, + object: nil + ) + NotificationCenter.default.removeObserver( + self, + name: .shouldShowQuoteEditBar, + object: nil + ) + NotificationCenter.default.removeObserver( + self, + name: .shouldHideQuoteEditBar, + object: nil + ) + NotificationCenter.default.removeObserver( + self, + name: .shouldShowScrollButton, + object: nil + ) + NotificationCenter.default.removeObserver( + self, + name: .shouldHideScrollButton, + object: nil + ) + NotificationCenter.default.removeObserver( + self, + name: .shouldCopyMessage, + object: nil + ) + NotificationCenter.default.removeObserver( + self, + name: .shouldDeleteMessage, + object: nil + ) + NotificationCenter.default.removeObserver( + self, + name: .shouldChangeOperatorStatus, + object: nil + ) + NotificationCenter.default.removeObserver( + self, + name: .shouldUpdateOperatorInfo, + object: nil + ) + } - // MARK: UITableViewDelegate methods + // MARK: - Methods + @objc + func dismissKeyboardNow() { + view.endEditing(true) + } - override func tableView(_ tableView: UITableView, - numberOfRowsInSection section: Int) -> Int { - return messages.count + // MARK: - Private methods + private func setupKeyboard() { + let tap: UITapGestureRecognizer = UITapGestureRecognizer( + target: self, + action: #selector(dismissKeyboardNow) + ) + view.addGestureRecognizer(tap) } - override func tableView(_ tableView: UITableView, - cellForRowAt indexPath: IndexPath) -> UITableViewCell { - guard let cell = tableView.dequeueReusableCell(withIdentifier: "MessageCell", - for: indexPath) as? MessageTableViewCell else { - fatalError("The dequeued cell is not an instance of MessageTableViewCell.") - } + @objc + private func keyboardWillChange(_ notification: Notification) { + guard let animationDuration = + notification.userInfo?[UIResponder.keyboardAnimationDurationUserInfoKey] + as? TimeInterval, + let keyboardFrame = + notification.userInfo?[UIResponder.keyboardFrameEndUserInfoKey] + as? NSValue + else { return } + let keyboardHeight: CGFloat = view.frame.maxY - keyboardFrame.cgRectValue.minY - let message = messages[indexPath.row] - cell.setContent(withMessage: message) + UIView.animate( + withDuration: animationDuration, + animations: { + self.bottomBarBackgroundView.snp.remakeConstraints { (make) -> Void in + make.leading.trailing.equalToSuperview() + + if keyboardHeight > 0 { // Keyboard is visible + make.bottom.equalToSuperview() + .inset(keyboardHeight) + } else { // Keyboard is hidden + if #available(iOS 11.0, *) { + make.bottom.equalTo(self.view.safeAreaLayoutGuide.snp.bottom) + } else { + make.bottom.equalToSuperview() + } + } + + make.top.equalTo(self.tableViewControllerContainerView.snp.bottom) + make.height.lessThanOrEqualTo(self.view.snp.height).multipliedBy(0.5) + } + + self.view.layoutIfNeeded() + if keyboardHeight > 0 { + self.containerChatTableViewController?.scrollToBottom(animated: false) + } + }, + completion: nil + ) + } + + private func setupNavigationBar() { + setupTitleView() + setupRightBarButtonItem() + } + + private func setupTitleView() { + // TitleView + titleViewOperatorNameLabel.text = OperatorStatus.noOperator.rawValue.localized + titleViewOperatorNameLabel.textColor = .white + titleViewOperatorNameLabel.highlightedTextColor = .lightGray + titleViewOperatorStatusLabel.text = OperatorStatus.allOperatorsOffline.rawValue.localized + titleViewOperatorStatusLabel.textColor = .white + titleViewOperatorStatusLabel.highlightedTextColor = .lightGray - if let operatorID = message.getOperatorID() { - lastOperatorID = operatorID // For using on chat closing. - let gestureRecognizer = UITapGestureRecognizer(target: self, - action: #selector(ChatViewController.rateOperator(_:))) - cell.avatarImageView.addGestureRecognizer(gestureRecognizer) + let customViewForOperatorNameAndStatus = CustomUIView() + customViewForOperatorNameAndStatus.isUserInteractionEnabled = true + customViewForOperatorNameAndStatus.translatesAutoresizingMaskIntoConstraints = false + customViewForOperatorNameAndStatus.addSubview(titleViewOperatorNameLabel) + titleViewOperatorNameLabel.snp.remakeConstraints { (make) -> Void in + make.leading.top.trailing.equalToSuperview() + } + + customViewForOperatorNameAndStatus.addSubview(titleViewOperatorStatusLabel) + titleViewOperatorStatusLabel.snp.remakeConstraints { (make) -> Void in + make.bottom.equalToSuperview() + make.centerX.equalToSuperview() + make.top.equalTo(titleViewOperatorNameLabel.snp.bottom) + .offset(2) } - if (message.getType() == .fileFromOperator) - || (message.getType() == .fileFromVisitor) { - let gestureRecognizer = UITapGestureRecognizer(target: self, - action: #selector(ChatViewController.showFile(_:))) - cell.bodyLabel.addGestureRecognizer(gestureRecognizer) + customViewForOperatorNameAndStatus.addSubview(titleViewTypingIndicator) + titleViewTypingIndicator.snp.remakeConstraints { (make) -> Void in + make.width.equalTo(30.0) + make.height.equalTo(titleViewTypingIndicator.snp.width).multipliedBy(0.5) + make.centerY.equalTo(titleViewOperatorStatusLabel.snp.centerY) + make.trailing.equalTo(titleViewOperatorStatusLabel.snp.leading) + .inset(2) } - return cell + let gestureRecognizer = UILongPressGestureRecognizer( + target: self, + action: #selector(titleViewTapAction) + ) + gestureRecognizer.minimumPressDuration = 0.05 + customViewForOperatorNameAndStatus.addGestureRecognizer(gestureRecognizer) + + navigationItem.titleView = customViewForOperatorNameAndStatus } - // MARK: UIScrollViewDelegate protocol methods - override func scrollViewDidScroll(_ scrollView: UIScrollView) { - if tableView!.contentOffset.y >= (tableView!.contentSize.height - tableView!.frame.size.height - ScrollToBottomButton.visibilityThreshold.rawValue) { - UIView.animate(withDuration: ScrollToBottomButtonAnimation.duration.rawValue, - delay: 0.1, - options: [], - animations: { [weak self] in - self?.scrollToBottomButton?.alpha = 0.0 - } - , completion: nil) - } else { - UIView.animate(withDuration: ScrollToBottomButtonAnimation.duration.rawValue, - delay: 0.1, - options: [], - animations: { [weak self] in - self?.scrollToBottomButton?.alpha = 1.0 - } - , completion: nil) + private func setupRightBarButtonItem() { + // RightBarButtonItem + titleViewOperatorAvatarImageView.image = UIImage() + + let customViewForOperatorAvatar = createUIView() + customViewForOperatorAvatar.addSubview(titleViewOperatorAvatarImageView) + + titleViewOperatorAvatarImageView.snp.remakeConstraints { (make) -> Void in + make.trailing.equalToSuperview() + .inset(-14) + make.width.equalTo(titleViewOperatorAvatarImageView.snp.height) + make.top.bottom.equalToSuperview() + .inset(2) } - } - - // MARK: Private methods - - private func setupTableView() { - tableView?.backgroundColor = backgroundTableViewColor.color() - tableView?.estimatedRowHeight = 64.0 - tableView?.rowHeight = UITableView.automaticDimension - tableView?.separatorStyle = .none - tableView?.register(MessageTableViewCell.self, - forCellReuseIdentifier: "MessageCell") + let customRightBarButtonItem = UIBarButtonItem( + customView: customViewForOperatorAvatar + ) + + navigationItem.rightBarButtonItem = customRightBarButtonItem } - private func setupRefreshControl() { - if #available(iOS 10.0, *) { - tableView?.refreshControl = refreshControl + @objc + private func titleViewTapAction(_ sender: UILongPressGestureRecognizer) { + // Potentially could be anything, but for now only shows rating dialog + if sender.state == .ended { + titleViewOperatorNameLabel.isHighlighted = false + titleViewOperatorStatusLabel.isHighlighted = false + + NotificationCenter.default.post( + name: .shouldShowRatingDialog, + object: nil + ) + } else { - tableView?.addSubview(refreshControl) + titleViewOperatorNameLabel.isHighlighted = true + titleViewOperatorStatusLabel.isHighlighted = true } - refreshControl.addTarget(self, - action: #selector(requestMessages), - for: .valueChanged) - refreshControl.tintColor = textMainColor.color() - refreshControl.attributedTitle = NSAttributedString(string: TableView.refreshControlText.rawValue.localized, - attributes: [.foregroundColor : textMainColor.color()]) - } - - private func setupSlackTextViewController() { - isInverted = false - - leftButton.setImage(#imageLiteral(resourceName: "Clip"), - for: .normal) - leftButton.accessibilityLabel = LeftButton.accessibilityLabel.rawValue.localized - leftButton.accessibilityHint = LeftButton.accessibilityHint.rawValue.localized - - rightButton.setImage(#imageLiteral(resourceName: "SendMessage"), - for: .normal) - rightButton.setTitle(nil, - for: .normal) - - textInputbar.tintColor = textTintColor.color() - textInputbar.backgroundColor = backgroundSecondaryColor.color() - textInputbar.textView.textInputView.layer.backgroundColor = backgroundTextFieldColor.color().cgColor - textInputbar.textView.textColor = textTextFieldColor.color() - textInputbar.textView.tintColor = textTextFieldColor.color() - textInputbar.textView.keyboardAppearance = ColorScheme.shared.keyboardAppearance() - textInputbar.textView.layer.cornerRadius = 15.0 - } - - private func setupNavigationItem() { - setupLeftBarButtonItem() - setupRightBarButtonItem() - setupTitleView() - } - - private func setupLeftBarButtonItem() { - let backButton = UIButton(type: .custom) - backButton.setImage(ColorScheme.shared.backButtonImage(), - for: .normal) - backButton.imageView?.contentMode = .scaleAspectFit - backButton.accessibilityLabel = BackButton.accessibilityLabel.rawValue.localized - backButton.accessibilityHint = BackButton.accessibilityHint.rawValue.localized - backButton.addTarget(self, - action: #selector(onBackButtonClick(sender:)), - for: .touchUpInside) - let leftBarButtonItem = UIBarButtonItem(customView: backButton) - navigationItem.leftBarButtonItem = leftBarButtonItem } @objc - private func onBackButtonClick(sender: UIButton) { - webimService!.stopSession() + private func updateOperatorStatus(sender: Notification) { + guard let operatorStatus = sender.userInfo as? [String: String] else { return } + let status = operatorStatus["Status"] - navigationController?.popViewController(animated: true) - } - - private func setupRightBarButtonItem() { - let closeChatButton = UIButton(type: .custom) - closeChatButton.setImage(ColorScheme.shared.closeChatButtonImage(), - for: .normal) - closeChatButton.imageView?.contentMode = .scaleAspectFit - closeChatButton.accessibilityLabel = CloseChatButton.accessibilityLabel.rawValue.localized - closeChatButton.accessibilityHint = CloseChatButton.accessibilityHint.rawValue.localized - closeChatButton.addTarget(self, - action: #selector(endChat), - for: .touchUpInside) - navigationItem.rightBarButtonItem = UIBarButtonItem(customView: closeChatButton) + DispatchQueue.main.async { + if status == OperatorStatus.isTyping.rawValue.localized { + let offsetX = self.titleViewTypingIndicator.frame.width / 2 + self.titleViewTypingIndicator.addAllAnimations() + self.titleViewOperatorStatusLabel.snp.remakeConstraints { (make) -> Void in + make.bottom.equalToSuperview() + make.centerX.equalToSuperview() + .inset(offsetX) + make.top.equalTo(self.titleViewOperatorNameLabel.snp.bottom) + .offset(2) + } + } else { + self.titleViewTypingIndicator.removeAllAnimations() + self.titleViewOperatorStatusLabel.snp.remakeConstraints { (make) -> Void in + make.bottom.equalToSuperview() + make.centerX.equalToSuperview() + make.top.equalTo(self.titleViewOperatorNameLabel.snp.bottom) + .offset(2) + } + } + self.titleViewOperatorStatusLabel.text = status + } } @objc - private func endChat() { - webimService!.closeChat() + private func updateOperatorInfo(sender: Notification) { + guard let operatorInfo = sender.userInfo as? [String: String] else { return } + let operatorName = operatorInfo["OperatorName"] + let operatorAvatarURL = operatorInfo["OperatorAvatarURL"] - if let operatorID = lastOperatorID { - if alreadyRated[operatorID] != true { // Don't offer to rate an operator if a visitor already did it independently. - if showRatingDialog(forOperator: operatorID) { - return + DispatchQueue.main.async { + self.titleViewOperatorNameLabel.text = operatorName + + if operatorName == OperatorStatus.noOperator.rawValue.localized { + self.titleViewOperatorStatusLabel.text = OperatorStatus.allOperatorsOffline.rawValue.localized + } else { + self.titleViewOperatorStatusLabel.text = OperatorStatus.online.rawValue.localized + } + + if operatorAvatarURL == OperatorAvatar.empty.rawValue { + self.titleViewOperatorAvatarImageView.image = UIImage() + } else if operatorAvatarURL == OperatorAvatar.placeholder.rawValue { + self.titleViewOperatorAvatarImageView.image = userAvatarImagePlaceholder + self.titleViewOperatorAvatarImageView.layer.cornerRadius = self.titleViewOperatorAvatarImageView.bounds.height / 2 + } else { + guard let string = operatorAvatarURL else { return } + guard let url = URL(string: string) else { return } + + let imageDownloadIndicator = CircleProgressIndicator() + imageDownloadIndicator.lineWidth = 1 + imageDownloadIndicator.strokeColor = documentFileStatusPercentageIndicatorColour + imageDownloadIndicator.isUserInteractionEnabled = false + imageDownloadIndicator.isHidden = true + imageDownloadIndicator.translatesAutoresizingMaskIntoConstraints = false + + self.bottomBarQuoteAttachmentImageView.addSubview(imageDownloadIndicator) + imageDownloadIndicator.snp.remakeConstraints { (make) -> Void in + make.edges.equalToSuperview() + .inset(5) } + + let loadingOptions = ImageLoadingOptions( + placeholder: UIImage(), + transition: .fadeIn(duration: 0.5) + ) + let defaultRequestOptions = ImageRequestOptions() + let imageRequest = ImageRequest( + url: url, + processors: [ImageProcessor.Circle()], + priority: .normal, + options: defaultRequestOptions + ) + + Nuke.loadImage( + with: imageRequest, + options: loadingOptions, + into: self.titleViewOperatorAvatarImageView, + progress: { _, completed, total in + DispatchQueue.global(qos: .userInteractive).async { + let progress = Float(completed) / Float(total) + DispatchQueue.main.async { + if imageDownloadIndicator.isHidden { + imageDownloadIndicator.isHidden = false + imageDownloadIndicator.enableRotationAnimation() + } + imageDownloadIndicator.setProgressWithAnimation( + duration: 0.1, + value: progress + ) + } + } + }, + completion: { _ in + DispatchQueue.main.async { + self.bottomBarQuoteAttachmentImageView.image = ImageCache.shared[imageRequest] + imageDownloadIndicator.isHidden = true + } + } + ) } } - - popupDialogHandler?.showChatClosedDialog() } - private func setupTitleView() { - let navigationItemImageView = UIImageView(image: ColorScheme.shared.navigationItemImage()) - navigationItemImageView.contentMode = .scaleAspectFit - navigationItem.titleView = navigationItemImageView - } - - private func setupScrollToBottomButton() { - let xPosition = view.frame.size.width - ScrollToBottomButton.size.rawValue - ScrollToBottomButton.margin.rawValue - let yPosition = UIApplication.shared.statusBarFrame.height + navigationController!.navigationBar.frame.size.height + ScrollToBottomButton.margin.rawValue - scrollToBottomButton = UIButton(frame: CGRect(x: xPosition, - y: yPosition, - width: ScrollToBottomButton.size.rawValue, - height: ScrollToBottomButton.size.rawValue)) - scrollToBottomButton!.setImage(ColorScheme.shared.scrollToBottomButtonImage(), - for: .normal) - scrollToBottomButton!.addTarget(self, - action: #selector(scrollToBottom), - for: .touchUpInside) - scrollToBottomButton!.alpha = 0.0 - view.addSubview(scrollToBottomButton!) - } - - private func setupWebimSession() { - webimService!.createSession() - webimService!.startSession() - - webimService!.setMessageStream() - webimService!.setMessageTracker(withMessageListener: self) - webimService!.setHelloMessageListener(with: self) - webimService!.getLastMessages() { [weak self] messages in - self?.messages.insert(contentsOf: messages, - at: 0) - DispatchQueue.main.async() { - self?.tableView?.reloadData() - self?.scrollToBottom() - self?.webimService?.setChatRead() + private func configureSubviews() { + // tableViewControllerContainerView + view.addSubview(tableViewControllerContainerView) + tableViewControllerContainerView.snp.remakeConstraints { (make) -> Void in + make.leading.top.trailing.equalToSuperview() + } + + // fileButton + bottomBarBackgroundView.addSubview(fileButton) + fileButton.setBackgroundImage(fileButtonImage, + for: .normal) + fileButton.addTarget( + self, + action: #selector(showSendFileMenu), + for: .touchUpInside + ) + fileButton.snp.remakeConstraints { (make) -> Void in + if #available(iOS 11.0, *) { + make.leading.equalTo(self.view.safeAreaLayoutGuide.snp.leading) + .inset(fileButtonLeadingSpacing) + } else { + make.leading.equalToSuperview() + .inset(fileButtonLeadingSpacing) } + + make.bottom.equalToSuperview() + .inset(buttonWidthHeight / 2 + textInputBackgroundViewTopBottomSpacing + 2) + make.height.width.equalTo(buttonWidthHeight) + } + + // textInputTextView + textInputBackgroundView.addSubview(textInputTextView) + textInputTextView.delegate = self + textInputTextView.sizeToFit() + textInputTextView.isScrollEnabled = true + textInputTextView.snp.remakeConstraints { (make) -> Void in + make.top.bottom.leading.equalToSuperview() + .inset(5) + make.height.equalTo( + min(130.5, // MAX value + max(35.5, // MIN value + self.textInputTextView.contentSize.height + ) + ) + ) + } + + // textInputTextViewPlaceholderLabel + textInputBackgroundView.addSubview(textInputTextViewPlaceholderLabel) + textInputTextViewPlaceholderLabel.text = ChatView.textInputPlaceholderText.rawValue.localized + textInputTextViewPlaceholderLabel.textColor = textInputViewPlaceholderLabelTextColour + textInputTextViewPlaceholderLabel.backgroundColor = textInputViewPlaceholderLabelBackgroundColour + textInputTextViewPlaceholderLabel.snp.remakeConstraints { (make) -> Void in + make.leading.equalToSuperview() + .inset(10) + if UIApplication.shared.userInterfaceLayoutDirection == .rightToLeft { + make.trailing.equalToSuperview() + .inset(10+30) + } else { + make.trailing.equalToSuperview() + .inset(10) + } + make.bottom.equalToSuperview() + .inset(12) + } + + //textInputButton + textInputBackgroundView.addSubview(textInputButton) + textInputButton.setBackgroundImage(textInputButtonImage, for: .normal) + textInputButton.addTarget( + self, + action: #selector(sendMessage), + for: .touchUpInside + ) + + textInputButton.snp.remakeConstraints { (make) -> Void in + make.trailing.bottom.equalToSuperview() + .inset(7) + make.leading.equalTo(textInputTextView.snp.trailing) + make.height.width.equalTo(30) + } + + // textInputBackgroundView + bottomBarBackgroundView.addSubview(textInputBackgroundView) + textInputBackgroundView.layer.borderWidth = 1 + textInputBackgroundView.layer.borderColor = textInputBackgroundViewBorderColour + textInputBackgroundView.roundCorners( + [.layerMinXMinYCorner, + .layerMaxXMinYCorner, + .layerMaxXMaxYCorner, + .layerMinXMaxYCorner], + radius: 20 + ) + textInputBackgroundView.snp.remakeConstraints { (make) -> Void in + make.top.equalToSuperview() + .inset(textInputBackgroundViewTopBottomSpacing) + if #available(iOS 11.0, *) { + make.trailing.equalTo(self.view.safeAreaLayoutGuide.snp.trailing) + .inset(20) + } else { + make.trailing.equalToSuperview() + .inset(20) + } + make.bottom.equalToSuperview() + .inset(textInputBackgroundViewTopBottomSpacing) + make.leading.equalTo(fileButton.snp.trailing) + .offset(fileButtonTrailingSpacing) + } + + // bottomBarBackgroundView + view.addSubview(bottomBarBackgroundView) + bottomBarBackgroundView.backgroundColor = bottomBarBackgroundViewColour + bottomBarBackgroundView.snp.remakeConstraints { (make) -> Void in + make.leading.trailing.equalToSuperview() + if #available(iOS 11.0, *) { + make.bottom.equalTo(view.safeAreaLayoutGuide.snp.bottom) + } else { + make.bottom.equalToSuperview() + } + make.top.equalTo(tableViewControllerContainerView.snp.bottom) + make.height.lessThanOrEqualTo(view.snp.height).multipliedBy(0.5) } } @objc - private func requestMessages() { - webimService!.getNextMessages() { [weak self] messages in - self?.messages.insert(contentsOf: messages, - at: 0) - DispatchQueue.main.async() { - self?.tableView?.reloadData() - self?.refreshControl.endRefreshing() - self?.webimService?.setChatRead() + private func sendMessage(_ sender: UIButton) { // Right button pressed + if !textInputTextView.text.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty { + if var text: String = textInputTextView.text { + text = text.trimWhitespacesIn() + if bottomBarQuoteBackgroundView.isDescendant(of: self.view) { + // If bottomBarQuoteBackgroundView is a subview of self.view + // (i.e. is visible and present on the screen) + + if bottomBarQuoteUsernameLabel.text == + ChatView.editMessageText.rawValue.localized { + // Edit mode + if text.trimmingCharacters(in: .whitespacesAndNewlines) != + bottomBarQuoteBodyLabel.text? + .trimmingCharacters(in: .whitespacesAndNewlines) { + containerChatTableViewController?.editMessage(text) + } + } else { + containerChatTableViewController?.replyToMessage(text) + } + removeQuoteEditBar() + } else { + containerChatTableViewController?.sendMessage(text) + } + + if !alreadyPutTextFromBufferString { + textInputTextView.text = "" + // Workaround to trigger textViewDidChange + textInputTextView.replace( + textInputTextView.textRange( + from: textInputTextView.beginningOfDocument, + to: textInputTextView.endOfDocument) ?? UITextRange(), + withText: "" + ) + } else { + alreadyPutTextFromBufferString = false + } + } else { + return } } + containerChatTableViewController?.selectedCellRow = nil } @objc - private func rateOperator(_ recognizer: UITapGestureRecognizer) { - guard recognizer.state == .ended else { - return - } - - let tapLocation = recognizer.location(in: tableView) - if let tapIndexPath = tableView?.indexPathForRow(at: tapLocation) { - let message = messages[tapIndexPath.row] - if let operatorID = message.getOperatorID() { - _ = showRatingDialog(forOperator: operatorID) - } - } + private func showSendFileMenu(_ sender: UIButton) { // Send file button pressed + filePicker.showSendFileMenu(from: sender) } - private func showRatingDialog(forOperator operatorID: String) -> Bool { - guard webimService!.isChatExist() else { - return false - } + @objc + private func addQuoteEditBar(sender: Notification) { + guard let actions = sender.userInfo as? [String: PopupAction] else { return } + let action = actions["Action"] - popupDialogHandler?.showRatingDialog(forOperator: operatorID) { [weak self] rating in - self?.alreadyRated[operatorID] = true - - self?.webimService!.rateOperator(withID: operatorID, - byRating: rating, - completionHandler: self) + guard let message = containerChatTableViewController?.getSelectedMessage() + else { return } + + // Save text from input text view if there was some + if !textInputTextView.text.isEmpty { + textInputTextViewBufferString = textInputTextView.text + alreadyPutTextFromBufferString = false } - return true - } - - @objc - private func showFile(_ recognizer: UITapGestureRecognizer) { - guard recognizer.state == .ended else { - return + // bottomBarQuoteLineView + bottomBarQuoteBackgroundView.addSubview(bottomBarQuoteLineView) + bottomBarQuoteLineView.backgroundColor = bottomBarQuoteLineViewColour + bottomBarQuoteLineView.snp.remakeConstraints { (make) -> Void in + make.height.equalTo(45) + make.width.equalTo(2) + make.top.bottom.equalToSuperview() + .inset(textInputBackgroundViewTopBottomSpacing) + // TODO: Check this + //make.bottom.equalToSuperview() + make.leading.equalToSuperview() + .inset( + fileButtonLeadingSpacing + + fileButtonTrailingSpacing + + buttonWidthHeight + 5 + ) } - let tapLocation = recognizer.location(in: tableView) - guard let tapIndexPath = tableView?.indexPathForRow(at: tapLocation) else { - return + // bottomBarQuoteUsernameLabel + bottomBarQuoteBackgroundView.addSubview(bottomBarQuoteUsernameLabel) + + if action == .reply { + if message.getSenderName() == "Посетитель" { + bottomBarQuoteUsernameLabel.text = ChatView.hardcodedVisitorMessageName.rawValue.localized + } else { + bottomBarQuoteUsernameLabel.text = message.getSenderName() + } + } else { + bottomBarQuoteUsernameLabel.text = ChatView.editMessageText.rawValue.localized + textInputTextView.text = message.getText() + hidePlaceholderIfVisible() } - let message = messages[tapIndexPath.row] - guard let data = message.getData(), - let fileData = data.getAttachment() else { - return + bottomBarQuoteUsernameLabel.snp.remakeConstraints { (make) -> Void in + make.top.equalToSuperview() + .inset(10) + make.leading.equalTo(bottomBarQuoteLineView.snp.trailing) + .offset(10) } - let attachment = fileData.getFileInfo() - guard let contentType = attachment.getContentType(), - let attachmentURL = attachment.getURL() else { - return + // bottomBarQuoteBodyLabel + bottomBarQuoteBackgroundView.addSubview(bottomBarQuoteBodyLabel) + bottomBarQuoteBodyLabel.text = message.getText().replacingOccurrences(of: "\n+", with: " ", options: .regularExpression) + + bottomBarQuoteBodyLabel.snp.remakeConstraints { (make) -> Void in + make.top.equalTo(bottomBarQuoteUsernameLabel.snp.bottom) + .offset(5) + make.leading.equalTo(bottomBarQuoteLineView.snp.trailing) + .offset(10) + make.trailing.equalTo(bottomBarQuoteUsernameLabel.snp.trailing) } - var popupMessage: String? - var image: UIImage? + // bottomBarQuoteCancelButton + bottomBarQuoteBackgroundView.addSubview(bottomBarQuoteCancelButton) + bottomBarQuoteCancelButton.setBackgroundImage( + closeButtonImage, + for: .normal + ) + bottomBarQuoteCancelButton.addTarget( + self, + action: #selector(removeQuoteEditBar), + for: .touchUpInside + ) - if isImage(contentType: contentType) { - let semaphore = DispatchSemaphore(value: 0) - let request = URLRequest(url: attachmentURL) + bottomBarQuoteCancelButton.snp.remakeConstraints { (make) -> Void in + make.trailing.equalToSuperview() + .inset(10) - print("Requesting file: \(attachmentURL.absoluteString)") + make.centerY.equalTo(bottomBarQuoteLineView.snp.centerY) + make.height.width.equalTo(20) - URLSession.shared.dataTask(with: request, - completionHandler: { data, _, _ in - if let data = data { - if let downloadedImage = UIImage(data: data) { - image = downloadedImage - } else { - popupMessage = ShowFileDialog.imageFormatInvalid.rawValue.localized - } - } else { - popupMessage = ShowFileDialog.imageLinkInvalid.rawValue.localized - } - - semaphore.signal() - }).resume() - - _ = semaphore.wait(timeout: .distantFuture) - } else { - popupMessage = ShowFileDialog.notImage.rawValue - } - - popupDialogHandler?.showFileDialog(withMessage: popupMessage, - title: attachment.getFileName(), - image: image) - } - - @objc - private func scrollToBottom() { - if messages.isEmpty { - return + make.leading.equalTo(bottomBarQuoteUsernameLabel.snp.trailing) + .offset(10) } - let bottomMessageIndex = IndexPath(row: (tableView?.numberOfRows(inSection: 0))! - 1, - section: 0) - tableView?.scrollToRow(at: bottomMessageIndex, - at: .bottom, - animated: true) - } - -} + // bottomBarQuoteAttachmentImageView + if let contentType = message.getData()?.getAttachment()?.getFileInfo().getContentType(), + let url = message.getData()?.getAttachment()?.getFileInfo().getURL() { + + bottomBarQuoteBackgroundView.addSubview(bottomBarQuoteAttachmentImageView) + bottomBarQuoteAttachmentImageView.clipsToBounds = true + bottomBarQuoteAttachmentImageView.roundCorners( + [.layerMinXMinYCorner, + .layerMaxXMinYCorner, + .layerMinXMaxYCorner, + .layerMaxXMaxYCorner], + radius: 5 + ) + + bottomBarQuoteAttachmentImageView.snp.remakeConstraints { (make) -> Void in + make.top.equalToSuperview() + .inset(10) + make.leading.equalTo(bottomBarQuoteLineView.snp.trailing) + .offset(10) + make.width.height.equalTo(bottomBarQuoteLineView.snp.height) + } + + if isImage(contentType: contentType) { + let imageDownloadIndicator = CircleProgressIndicator() + imageDownloadIndicator.lineWidth = 1 + imageDownloadIndicator.strokeColor = documentFileStatusPercentageIndicatorColour + imageDownloadIndicator.isUserInteractionEnabled = false + imageDownloadIndicator.isHidden = true + imageDownloadIndicator.translatesAutoresizingMaskIntoConstraints = false + + bottomBarQuoteAttachmentImageView.addSubview(imageDownloadIndicator) + imageDownloadIndicator.snp.remakeConstraints { (make) -> Void in + make.edges.equalToSuperview() + .inset(5) + } + + let request = ImageRequest(url: url) + if let image = ImageCache.shared[request] { + imageDownloadIndicator.isHidden = true + bottomBarQuoteAttachmentImageView.image = image + } else { + bottomBarQuoteAttachmentImageView.image = loadingPlaceholderImage -// MARK: - UIImagePickerControllerDelegate -extension ChatViewController: UIImagePickerControllerDelegate { - - // MARK: - Methods - - func imagePickerController(_ picker: UIImagePickerController, - didFinishPickingMediaWithInfo info: [UIImagePickerController.InfoKey : Any]) { - if let image = info[UIImagePickerController.InfoKey.originalImage] as? UIImage { - if let imageURL = info[UIImagePickerController.InfoKey.referenceURL] as? URL { - let imageData: Data - let mimeType = MimeType(url: imageURL as URL) - let imageName = imageURL.lastPathComponent - let imageExtension = imageURL.pathExtension.lowercased() - if imageExtension == "jpg" || imageExtension == "jpeg" { - imageData = image.jpegData(compressionQuality: 1.0)! + Nuke.ImagePipeline.shared.loadImage( + with: url, + progress: { _, completed, total in + DispatchQueue.global(qos: .userInteractive).async { + let progress = Float(completed) / Float(total) + DispatchQueue.main.async { + if imageDownloadIndicator.isHidden { + imageDownloadIndicator.isHidden = false + imageDownloadIndicator.enableRotationAnimation() + } + imageDownloadIndicator.setProgressWithAnimation( + duration: 0.1, + value: progress + ) + } + } + }, + completion: { _ in + DispatchQueue.main.async { + self.bottomBarQuoteAttachmentImageView.image = ImageCache.shared[request] + imageDownloadIndicator.isHidden = true + } + } + ) + } + } else { + bottomBarQuoteAttachmentImageView.image = nil + } + // bottomBarQuoteUsernameLabel + bottomBarQuoteUsernameLabel.snp.remakeConstraints { (make) -> Void in + make.top.equalToSuperview() + .inset(10) + if bottomBarQuoteAttachmentImageView.image != nil { + make.leading.equalTo(bottomBarQuoteAttachmentImageView.snp.trailing) + .offset(10) } else { - imageData = image.pngData()! + make.leading.equalTo(bottomBarQuoteLineView.snp.trailing) + .offset(10) } - - webimService!.send(file: imageData, - fileName: imageName, - mimeType: mimeType.value, - completionHandler: self) } - } - - dismiss(animated: true, - completion: nil) - } - - func imagePickerControllerDidCancel(_ picker: UIImagePickerController) { - dismiss(animated: true, - completion: nil) - } - -} - -// MARK: - UINavigationControllerDelegate -extension ChatViewController: UINavigationControllerDelegate { - // For image picker. -} - -// MARK: - WEBIM: HelloMessageListener -extension ChatViewController: HelloMessageListener { - func helloMessage(message: String) { - print("Received Hello message: \"\(message)\"") - } -} - -// MARK: - WEBIM: MessageListener -extension ChatViewController: MessageListener { - - // MARK: - Methods - - func added(message newMessage: Message, - after previousMessage: Message?) { - var inserted = false - - if let previousMessage = previousMessage { - for (index, message) in messages.enumerated() { - if previousMessage.isEqual(to: message) { - messages.insert(newMessage, at: index) - inserted = true - - break + + // bottomBarQuoteBodyLabel + bottomBarQuoteBodyLabel.snp.remakeConstraints { (make) -> Void in + make.top.equalTo(bottomBarQuoteUsernameLabel.snp.bottom) + .offset(5) + if bottomBarQuoteAttachmentImageView.image != nil { + make.leading.equalTo(bottomBarQuoteAttachmentImageView.snp.trailing) + .offset(10) + } else { + make.leading.equalTo(bottomBarQuoteLineView.snp.trailing) + .offset(10) } + make.trailing.equalTo(bottomBarQuoteUsernameLabel.snp.trailing) } } + // bottomBarQuoteBackgroundView + bottomBarBackgroundView.addSubview(bottomBarQuoteBackgroundView) - if !inserted { - messages.append(newMessage) + UIView.animate(withDuration: 0.1) { + self.bottomBarQuoteBackgroundView.snp.remakeConstraints { (make) -> Void in + make.top.leading.equalToSuperview() + if #available(iOS 11.0, *) { + make.trailing.equalTo(self.view.safeAreaLayoutGuide.snp.trailing) + .inset(20) + } else { + make.trailing.equalToSuperview() + .inset(20) + } + } + + // textInputBackgroundView + self.textInputBackgroundView.snp.remakeConstraints { (make) -> Void in + make.top.equalTo(self.bottomBarQuoteBackgroundView.snp.bottom) + .offset(self.textInputBackgroundViewTopBottomSpacing) + if #available(iOS 11.0, *) { + make.trailing.equalTo(self.view.safeAreaLayoutGuide.snp.trailing) + .inset(20) + } else { + make.trailing.equalToSuperview() + .inset(20) + } + make.bottom.equalToSuperview() + .inset(self.textInputBackgroundViewTopBottomSpacing) + make.leading.equalTo(self.fileButton.snp.trailing) + .offset(self.fileButtonTrailingSpacing) + } + + self.view.layoutIfNeeded() } - DispatchQueue.main.async() { - self.tableView?.reloadData() - self.scrollToBottom() - } + containerChatTableViewController?.scrollToBottom(animated: true) + textInputTextView.becomeFirstResponder() } - - func removed(message: Message) { - var toUpdate = false + + @objc + private func removeQuoteEditBar() { + guard bottomBarQuoteBackgroundView.isDescendant(of: self.view) else { return } + let typingDraftDictionary = ["DraftText": 123] + NotificationCenter.default.post( + name: .shouldSetVisitorTypingDraft, + object: nil, + userInfo: typingDraftDictionary + ) - for (messageIndex, iteratedMessage) in messages.enumerated() { - if iteratedMessage.getID() == message.getID() { - messages.remove(at: messageIndex) - toUpdate = true + if bottomBarQuoteUsernameLabel.text == ChatView.editMessageText.rawValue.localized { + // Edit mode + bottomBarQuoteUsernameLabel.text = "" + if !textInputTextView.text.isEmpty { - break + let newText: String = textInputTextViewBufferString ?? "" + + if textInputTextViewBufferString != nil { + textInputTextViewBufferString = nil + alreadyPutTextFromBufferString = true + } + + textInputTextView.text = newText + // Workaround to trigger textViewDidChange + textInputTextView.replace( + textInputTextView.textRange( + from: textInputTextView.beginningOfDocument, + to: textInputTextView.endOfDocument) ?? UITextRange(), + withText: newText + ) } } - if toUpdate { - DispatchQueue.main.async() { - self.tableView?.reloadData() - self.scrollToBottom() + bottomBarQuoteBackgroundView.removeFromSuperview() + bottomBarQuoteLineView.removeFromSuperview() + bottomBarQuoteAttachmentImageView.removeFromSuperview() + bottomBarQuoteUsernameLabel.removeFromSuperview() + bottomBarQuoteBodyLabel.removeFromSuperview() + bottomBarQuoteCancelButton.removeFromSuperview() + + UIView.animate(withDuration: 0.1) { + // textInputBackgroundView + self.textInputBackgroundView.snp.remakeConstraints { (make) -> Void in + make.top.equalToSuperview() + .inset(self.textInputBackgroundViewTopBottomSpacing) + if #available(iOS 11.0, *) { + make.trailing.equalTo(self.view.safeAreaLayoutGuide.snp.trailing) + .inset(20) + } else { + make.trailing.equalToSuperview() + .inset(20) + } + make.bottom.equalToSuperview() + .inset(self.textInputBackgroundViewTopBottomSpacing) + make.leading.equalTo(self.fileButton.snp.trailing) + .offset(self.fileButtonTrailingSpacing) } + + self.view.layoutIfNeeded() } } - func removedAllMessages() { - messages.removeAll() - - DispatchQueue.main.async() { - self.tableView?.reloadData() - } + @objc + private func copyMessage(sender: Notification) { + containerChatTableViewController?.copyMessage() + } + + @objc + private func deleteMessage(sender: Notification) { + containerChatTableViewController?.deleteMessage() } - func changed(message oldVersion: Message, - to newVersion: Message) { - var toUpdate = false + private func setupScrollButton() { + scrollButton.setBackgroundImage(scrollButtonImage, for: .normal) + scrollButton.layoutIfNeeded() + scrollButton.subviews.first?.contentMode = .scaleAspectFill + scrollButton.addTarget( + self, + action: #selector(scrollTableView), + for: .touchUpInside + ) + self.view.addSubview(scrollButton) - for (messageIndex, iteratedMessage) in messages.enumerated() { - if iteratedMessage.getID() == oldVersion.getID() { - messages[messageIndex] = newVersion - toUpdate = true - - break + scrollButton.snp.remakeConstraints { (make) -> Void in + if #available(iOS 11.0, *) { + make.trailing.equalTo(self.view.safeAreaLayoutGuide) + .inset(5) + } else { + make.trailing.equalTo(tableViewControllerContainerView) + .inset(5) } + make.bottom.equalTo(tableViewControllerContainerView) + .inset(5) + make.height.equalTo(self.scrollButton.snp.width) + make.width.equalTo(30) } - if toUpdate { - DispatchQueue.main.async() { - self.tableView?.reloadData() - self.scrollToBottom() - } - } + scrollButton.isHidden = true } -} - -// MARK: - SendFileCompletionHandler -extension ChatViewController: SendFileCompletionHandler { + @objc + private func scrollTableView(_ sender: UIButton) { + containerChatTableViewController?.scrollToBottom(animated: true) + } - // MARK: - Methods + @objc + private func showScrollButton(_ sender: Notification) { + scrollButton.fadeIn() + } - func onSuccess(messageID: String) { - // Ignored. + @objc + private func hideScrollButton(_ sender: Notification) { + scrollButton.fadeOut() } - func onFailure(messageID: String, - error: SendFileError) { - DispatchQueue.main.sync { - var message: String? - switch error { - case .fileSizeExceeded: - message = SendFileErrorMessage.fileSizeExceeded.rawValue.localized - - break - case .fileTypeNotAllowed: - message = SendFileErrorMessage.fileTypeNotAllowed.rawValue.localized - - break - case .unknown: - message = SendFileErrorMessage.unknownError.rawValue.localized - - break - case .uploadedFileNotFound: - message = SendFileErrorMessage.fileNotFound.rawValue.localized - - break - case .unauthorized: - message = SendFileErrorMessage.unauthorized.rawValue.localized - - break - } - - popupDialogHandler?.showFileSendFailureDialog(withMessage: message!) { [weak self] in - guard let `self` = self else { - return - } - - for (index, message) in self.messages.enumerated() { - if message.getID() == messageID { - self.messages.remove(at: index) - DispatchQueue.main.async() { - self.tableView?.reloadData() - } - - return - } - } + private func hidePlaceholderIfVisible() { + if !(self.textInputTextViewPlaceholderLabel.alpha == 0.0) { + UIView.animate(withDuration: 0.1) { + self.textInputTextViewPlaceholderLabel.alpha = 0.0 } } } - } +// MARK: - UI methods +extension ChatViewController { + func createUILabel( + textAlignment: NSTextAlignment = .left, + systemFontSize: CGFloat, + systemFontWeight: UIFont.Weight = .regular, + numberOfLines: Int = 1 + ) -> UILabel { + let label = UILabel() + label.textAlignment = textAlignment + label.font = .systemFont(ofSize: systemFontSize, weight: .regular ) + label.numberOfLines = numberOfLines + label.translatesAutoresizingMaskIntoConstraints = false + return label + } -// MARK: - RateOperatorCompletionHandler -extension ChatViewController: RateOperatorCompletionHandler { - - // MARK: - Methods + func createUIView() -> UIView { + let view = UIView() + view.translatesAutoresizingMaskIntoConstraints = false + return view + } - func onSuccess() { - // Ignored. + func createUIImageView( + contentMode: UIView.ContentMode = .scaleAspectFit + ) -> UIImageView { + let imageView = UIImageView() + imageView.contentMode = contentMode + imageView.translatesAutoresizingMaskIntoConstraints = false + return imageView } - func onFailure(error: RateOperatorError) { - popupDialogHandler?.showRatingFailureDialog() + func createUIButton(type: UIButton.ButtonType) -> UIButton { + let button = UIButton(type: type) + button.translatesAutoresizingMaskIntoConstraints = false + return button } + func createCustomUIButton(type: UIButton.ButtonType) -> UIButton { + let button = CustomUIButton(type: type) + button.translatesAutoresizingMaskIntoConstraints = false + return button + } } -// MARK: - FatalErrorHandler -extension ChatViewController: FatalErrorHandlerDelegate { - - // MARK: - Methods - func showErrorDialog(withMessage message: String) { - popupDialogHandler?.showCreatingSessionFailureDialog(withMessage: message) +// MARK: - UITextViewDelegate methods +extension ChatViewController: UITextViewDelegate { + func textViewDidChange(_ textView: UITextView) { + if bottomBarQuoteUsernameLabel.text != ChatView.editMessageText.rawValue.localized { + // If in edit mode, don't setTypingDraft + let typingDraftDictionary = ["DraftText": textView.text] + NotificationCenter.default.post( + name: .shouldSetVisitorTypingDraft, + object: nil, + userInfo: typingDraftDictionary as [AnyHashable : Any] + ) + } + + if textView.text.isEmpty { + UIView.animate(withDuration: 0.1) { + self.textInputTextViewPlaceholderLabel.alpha = 1.0 + } + } else { + UIView.animate(withDuration: 0.1) { + self.textInputTextViewPlaceholderLabel.alpha = 0.0 + } + } + + textInputTextView.snp.remakeConstraints { (make) -> Void in + make.top.bottom.leading.equalToSuperview() + .inset(5) + make.height.equalTo(min(130.5, max(35.5, textView.contentSize.height))) + } } - } -// MARK: - DepartmentListHandlerDelegate -extension ChatViewController: DepartmentListHandlerDelegate { - - // MARK: - Methods - func show(departmentList: [Department], - action: @escaping (String) -> ()) { - popupDialogHandler?.showDepartmentListDialog(withDepartmentList: departmentList, - action: action) +// MARK: - FilePickerDelegate methods +extension ChatViewController: FilePickerDelegate { + func didSelect(image: UIImage?, imageURL: URL?) { + print("didSelect(image: \(String(describing: imageURL?.lastPathComponent)), imageURL: \(String(describing: imageURL)))") + + guard let imageToSend = image else { return } + + containerChatTableViewController?.sendImage( + image: imageToSend, + imageURL: imageURL + ) + } + func didSelect(file: Data?, fileURL: URL?) { + print("didSelect(file: \(fileURL?.lastPathComponent ?? "nil")), fileURL: \(fileURL?.path ?? "nil"))") + + guard let fileToSend = file else { return } + + containerChatTableViewController?.sendFile( + file: fileToSend, + fileURL: fileURL + ) } - } diff --git a/Example/WebimClientLibrary/FileViewController.swift b/Example/WebimClientLibrary/FileViewController.swift new file mode 100644 index 00000000..7a7dd72d --- /dev/null +++ b/Example/WebimClientLibrary/FileViewController.swift @@ -0,0 +1,203 @@ +// +// FileViewController.swift +// WebimClientLibrary_Example +// +// Created by Eugene Ilyin on 30/09/2019. +// Copyright © 2019 Webim. All rights reserved. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. +// + +import UIKit +import WebKit +import SnapKit +import CloudKit + +class FileViewController: UIViewController, WKUIDelegate, WKNavigationDelegate { + + // MARK: - Properties + var fileDestinationURL: URL? + + // MARK: - Private properties + private var contentWebView = WKWebView() + + private lazy var alertDialogHandler = UIAlertHandler(delegate: self) + + // MARK: - Outlets + @IBOutlet weak var contentWebViewContainer: UIView! + @IBOutlet weak var loadingStatusLabel: UILabel! + @IBOutlet weak var loadingStatusIndicator: UIActivityIndicatorView! + + // MARK: - View Life Cycle + override func viewWillAppear(_ animated: Bool) { + super.viewWillAppear(animated) + + setNewTopbar() + } + + override func viewDidLoad() { + super.viewDidLoad() + + setupNavigationItem() + setupLoadingSubiews() + setupContentWebView() + loadData() + } + + override func willMove(toParent parent: UIViewController?) { + setOldTopBar() + } + + // MARK: - WKWebView methods + override func observeValue( + forKeyPath keyPath: String?, + of object: Any?, + change: [NSKeyValueChangeKey : Any]?, + context: UnsafeMutableRawPointer? + ) { + guard keyPath == "estimatedProgress", + contentWebView.estimatedProgress == 1.0 + else { return } + + loadingStatusLabel.isHidden = true + loadingStatusIndicator.stopAnimating() + loadingStatusIndicator.isHidden = true + } + + func webView( + _ webView: WKWebView, + decidePolicyFor navigationAction: WKNavigationAction, + decisionHandler: @escaping (WKNavigationActionPolicy) -> Void + ) { + guard navigationAction.navigationType == .linkActivated, + let url = navigationAction.request.url, + UIApplication.shared.canOpenURL(url) else { + decisionHandler(.allow) + return + } + + UIApplication.shared.open(url) + decisionHandler(.cancel) + } + + // MARK: - Private methods + private func setupNavigationItem() { + /// Files App was presented in iOS 11.0 + guard #available(iOS 11.0, *) else { return } + + let rightButton = UIButton(type: .system) + rightButton.frame = CGRect(x: 0.0, y: 0.0, width: 20.0, height: 20.0) + rightButton.setBackgroundImage(saveImageButtonImage, for: .normal) + rightButton.addTarget( + self, + action: #selector(saveButtonTapped), + for: .touchUpInside + ) + + rightButton.snp.remakeConstraints { (make) -> Void in + make.height.width.equalTo(25.0) + } + + let rightBarButton = UIBarButtonItem(customView: rightButton) + navigationItem.rightBarButtonItem = rightBarButton + } + + private func setupLoadingSubiews() { + loadingStatusLabel.text = FileView.loadingFileText.rawValue.localized + loadingStatusIndicator.startAnimating() + } + + /// Workaround for iOS < 11.0 + private func setupContentWebView() { + contentWebView.navigationDelegate = self + contentWebView.allowsLinkPreview = true + contentWebView.uiDelegate = self + contentWebView.addObserver( + self, + forKeyPath: #keyPath(WKWebView.estimatedProgress), + options: .new, + context: nil + ) + + contentWebViewContainer.addSubview(contentWebView) + contentWebViewContainer.sendSubviewToBack(contentWebView) + + configureConstraints() + } + + private func configureConstraints() { + contentWebView.translatesAutoresizingMaskIntoConstraints = false + contentWebView.snp.makeConstraints { (make) in + make.leading.top.trailing.bottom.equalToSuperview() + } + } + + private func loadData() { + guard let destinationURL = fileDestinationURL else { return } + contentWebView.load(URLRequest(url: destinationURL)) + } + + private func setNewTopbar() { + setTopBar( + isEnabled: false, + isTranslucent: true, + barTintColor: topBarTintColourClear + ) + + } + + private func setOldTopBar() { + setTopBar( + isEnabled: true, + isTranslucent: false, + barTintColor: topBarTintColourDefault + ) + } + + private func setTopBar( + isEnabled: Bool, + isTranslucent: Bool, + barTintColor: UIColor + ) { + navigationController?.interactivePopGestureRecognizer?.isEnabled = isEnabled + navigationController?.navigationBar.isTranslucent = isTranslucent + navigationController?.navigationBar.barTintColor = barTintColor + } + + @objc + @available(iOS 11.0, *) + private func saveButtonTapped(sender: UIBarButtonItem) { + guard let fileDestinationURL = fileDestinationURL, + let documentDirectory = FileManager.default.urls( + for: .documentDirectory, + in: .userDomainMask + ).first else { return } + + let fileName = fileDestinationURL.lastPathComponent + let savingURL = documentDirectory.appendingPathComponent(fileName) + + do { + let data = try Data(contentsOf: fileDestinationURL) + try data.write(to: savingURL) + alertDialogHandler.showFileSavingSuccessDialog() + } catch { + alertDialogHandler.showFileSavingFailureDialog(withError: error) + } + } +} diff --git a/Example/WebimClientLibrary/ImageViewController.swift b/Example/WebimClientLibrary/ImageViewController.swift new file mode 100644 index 00000000..dec04832 --- /dev/null +++ b/Example/WebimClientLibrary/ImageViewController.swift @@ -0,0 +1,329 @@ +// +// ImageViewController.swift +// WebimClientLibrary_Example +// +// Created by Eugene Ilyin on 29/08/2019. +// Copyright © 2019 Webim. All rights reserved. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. +// + +import UIKit +import Nuke + +class ImageViewController: UIViewController { + + // MARK: - Properties + var selectedImage: UIImage? + var selectedImageURL: URL? + + // MARK: - Private properties + private lazy var alertDialogHandler = UIAlertHandler(delegate: self) + private lazy var imageDownloadIndicator: CircleProgressIndicator = { + let indicator = CircleProgressIndicator() + indicator.lineWidth = 1 + indicator.strokeColor = documentFileStatusPercentageIndicatorColour + indicator.isUserInteractionEnabled = false + indicator.isHidden = true + indicator.translatesAutoresizingMaskIntoConstraints = false + return indicator + }() + + // MARK: - Outlets + @IBOutlet var imageView: UIImageView! + + // MARK: - View Life Cycle + override func viewDidAppear(_ animated: Bool) { + DispatchQueue.global(qos: .userInteractive).async { + self.reloadImageIfNeed() + } + } + + override func viewWillAppear(_ animated: Bool) { + super.viewWillAppear(animated) + + setNewTopbar() + } + + override func viewDidLoad() { + super.viewDidLoad() + + setupNavigationItem() + + imageView.isUserInteractionEnabled = true + + if let imageToLoad = selectedImage { + imageView.image = imageToLoad + } + + addGestures() + } + + override func willMove(toParent parent: UIViewController?) { + setOldTopBar() + } + + // MARK: - Private methods + private func setupNavigationItem() { + let rightButton = UIButton(type: .system) + rightButton.frame = CGRect(x: 0.0, y: 0.0, width: 20.0, height: 20.0) + rightButton.setBackgroundImage(saveImageButtonImage, for: .normal) + rightButton.addTarget( + self, + action: #selector(saveButtonTapped), + for: .touchUpInside + ) + + rightButton.snp.remakeConstraints { (make) -> Void in + make.height.width.equalTo(25.0) + } + + let rightBarButton = UIBarButtonItem(customView: rightButton) + navigationItem.rightBarButtonItem = rightBarButton + } + + private func reloadImageIfNeed() { + guard selectedImage == nil, + let url = selectedImageURL else { return } + let request = ImageRequest(url: url) + + if let image = ImageCache.shared[request] { + self.selectedImage = image + DispatchQueue.main.async { + self.imageView.image = image + } + } else { + DispatchQueue.main.async { + self.imageView.image = loadingPlaceholderImage + self.imageView.addSubview(self.imageDownloadIndicator) + self.imageDownloadIndicator.snp.remakeConstraints { (make) -> Void in + make.centerX.equalToSuperview() + make.centerY.equalToSuperview() + make.height.equalTo(45) + make.width.equalTo(45) + } + + Nuke.ImagePipeline.shared.loadImage( + with: url, + progress: { _, completed, total in + DispatchQueue.global(qos: .userInteractive).async { + self.updateImageDownloadProgress( + completed: completed, + total: total + ) + } + }, + completion: { _ in + DispatchQueue.main.async { + self.selectedImage = ImageCache.shared[request] + self.imageView.image = ImageCache.shared[request] + self.imageDownloadIndicator.isHidden = true + } + } + ) + } + } + } + + @objc + private func saveButtonTapped(sender: UIBarButtonItem) { + guard let imageToSave = selectedImage else { return } + + UIImageWriteToSavedPhotosAlbum( + imageToSave, + self, + #selector(image(_:didFinishSavingWithError:contextInfo:)), + nil + ) + } + + @objc + private func image( + _ image: UIImage, + didFinishSavingWithError error: NSError?, + contextInfo: UnsafeRawPointer + ) { + if let error = error { + // Save error + alertDialogHandler.showImageSavingFailureDialog(withError: error) + } else { + alertDialogHandler.showImageSavingSuccessDialog() + } + } + + private func addGestures() { + let pan = UIPanGestureRecognizer( + target: self, + action: #selector(panGesture) + ) + pan.minimumNumberOfTouches = 1 + pan.maximumNumberOfTouches = 1 + imageView.addGestureRecognizer(pan) + + let pinch = UIPinchGestureRecognizer( + target: self, + action: #selector(pinchGesture) + ) + imageView.addGestureRecognizer(pinch) + + let rotate = UIRotationGestureRecognizer( + target: self, + action: #selector(rotateGesture) + ) + imageView.addGestureRecognizer(rotate) + } + + @objc + private func panGesture(sender: UIPanGestureRecognizer) { + if sender.state == .began || sender.state == .changed { + + guard let view = sender.view else { return } + let translation = sender.translation(in: view.superview) + let transform = CGAffineTransform( + translationX: translation.x, + y: translation.y + ) + imageView.transform = transform + + } else { + UIView.animate( + withDuration: 0.5, + animations: { + self.imageView.transform = CGAffineTransform.identity + } + ) + + guard navigationController?.navigationBar.isHidden == false else { return } + let velocity = sender.velocity(in: view) + + guard velocity.y >= 1500 else { return } + navigationController?.popViewController(animated: true) + } + } + + @objc + private func pinchGesture(sender: UIPinchGestureRecognizer) { + if sender.state == .began || sender.state == .changed { + guard let view = sender.view else { return } + + let pinchCenter = CGPoint( + x: sender.location(in: view).x - view.bounds.midX, + y: sender.location(in: view).y - view.bounds.midY + ) + + let transform = view.transform + .translatedBy(x: pinchCenter.x, y: pinchCenter.y) + .scaledBy(x: sender.scale, y: sender.scale) + .translatedBy(x: -pinchCenter.x, y: -pinchCenter.y) + + let currentScale = imageView.frame.size.width / imageView.bounds.size.width + var newScale = currentScale * sender.scale + + if newScale < 1 { + newScale = 1 + let transform = CGAffineTransform(scaleX: newScale, + y: newScale) + imageView.transform = transform + sender.scale = 1 + } else { + view.transform = transform + sender.scale = 1 + } + + } else + if sender.state == .ended || + sender.state == .failed || + sender.state == .cancelled { + + UIView.animate( + withDuration: 0.5, + animations: { + self.imageView.transform = CGAffineTransform.identity + } + ) + } + } + + @objc + private func rotateGesture (sender: UIRotationGestureRecognizer) { + if sender.state == .began || sender.state == .changed { + guard sender.view != nil else { return } + let transform = CGAffineTransform(rotationAngle: sender.rotation) + + imageView.transform = transform + } else + if sender.state == .ended || + sender.state == .failed || + sender.state == .cancelled { + + UIView.animate( + withDuration: 0.5, + animations: { + self.imageView.transform = CGAffineTransform.identity + } + ) + + } + } + + private func setNewTopbar() { + setTopBar( + hidesBarsOnTap: true, + isEnabled: false, + isTranslucent: true, + barTintColor: topBarTintColourClear + ) + + } + + private func setOldTopBar() { + setTopBar( + hidesBarsOnTap: false, + isEnabled: true, + isTranslucent: false, + barTintColor: topBarTintColourDefault + ) + } + + private func setTopBar( + hidesBarsOnTap: Bool, + isEnabled: Bool, + isTranslucent: Bool, + barTintColor: UIColor + ) { + navigationController?.hidesBarsOnTap = hidesBarsOnTap + navigationController?.interactivePopGestureRecognizer?.isEnabled = isEnabled + navigationController?.navigationBar.isTranslucent = isTranslucent + navigationController?.navigationBar.barTintColor = barTintColor + } + + private func updateImageDownloadProgress(completed: Int64, total: Int64) { + let progress = Float(completed) / Float(total) + DispatchQueue.main.async { + if self.imageDownloadIndicator.isHidden { + self.imageDownloadIndicator.isHidden = false + self.imageDownloadIndicator.enableRotationAnimation() + } + self.imageDownloadIndicator.setProgressWithAnimation( + duration: 0.1, + value: progress + ) + } + } +} diff --git a/Example/WebimClientLibrary/Images.xcassets/Check.imageset/check_Icon-32.png b/Example/WebimClientLibrary/Images.xcassets/Check.imageset/check_Icon-32.png deleted file mode 100644 index 917ce63c0f4fe5daf95f7c1475bedf05bfb44837..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 500 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 literal 0 HcmV?d00001 diff --git a/Example/WebimClientLibrary/Images.xcassets/Icons/Clip/Clip.imageset/Contents.json b/Example/WebimClientLibrary/Images.xcassets/Icons/AttachmentButton.imageset/Contents.json similarity index 84% rename from Example/WebimClientLibrary/Images.xcassets/Icons/Clip/Clip.imageset/Contents.json rename to Example/WebimClientLibrary/Images.xcassets/Icons/AttachmentButton.imageset/Contents.json index e6d42a70..5e2d779e 100644 --- a/Example/WebimClientLibrary/Images.xcassets/Icons/Clip/Clip.imageset/Contents.json +++ b/Example/WebimClientLibrary/Images.xcassets/Icons/AttachmentButton.imageset/Contents.json @@ -2,7 +2,7 @@ "images" : [ { "idiom" : "universal", - "filename" : "ClipIcon.pdf" + "filename" : "Vector.pdf" } ], "info" : { diff --git a/Example/WebimClientLibrary/Images.xcassets/Icons/AttachmentButton.imageset/Vector.pdf b/Example/WebimClientLibrary/Images.xcassets/Icons/AttachmentButton.imageset/Vector.pdf new file mode 100644 index 0000000000000000000000000000000000000000..3df6b75ed9cd2e132bf62f30c77757a29912b238 GIT binary patch literal 3749 zcmZvfOOG5y5QOjjEBa!|9O$k0L$Wwv8-xI1*4z+>VLUdA>>bz{km1+!MOM$l*dt5s z&92JIjEsz|e)QtmSD*W(PLrSAy#Mw0X`Y`vnQxxIKYso4xH;~|U*7)PZf3V+r{Da= z7vtZwyMLu^Q}|&2cf0#H$KU3N@s@?t;rOuIyqj*G?f$;o?ry*Sa(?>q`mgO_`g6K@ z?&mj$$uD+<svsmie%~pL|XWsfyqRZOTig%f}?BFSdc{^csC5OWMy3`hZYO_yE z2-uQrJ@r)B6NxQ&SFUXA4&aH;4_5l6TTSj}$*e5|c!Y{{{ek`fAU%UqWh$it9r z7$O$mz#!qvak#B2OU}8rtIB~HhXfU3tz}+<$D;Vpwq(IXt@5Fz;ODrsN_`YTgZ_IJ+H%YhAvQgCk$%oB-HC8FXOP$L2z z3;{g*6ikFSYk1(IfK!bXi_#(jr3EYsc(rUNB@ZrFs|1kG^^|DLg0bTIlqfvfP6FHL zqo`5{$qG^el~JNlqpW2U#YmKMl^?vz6*4t>70;uJnwCzi(7O&K;LxZ69D<%1W&l^F z%>&TVK>cEbJHGK17Q+v52O3UBYL1= z3bgR>j7W@jCYMTm9B8m)wo;SC*J^0C|=FyvnHwn)VG( zR2U&*)%m>2!Mz*Zuqi1uT_OCa)+|H00=9q)DuF@)5fof4o(DH{O#7lAbwB_qAfbX4 zy`N}UIN|X!B*qIEN4dc_UD--#*Z>PfsIZLP9c5KsSTiCb?tyelCQH$z3T&{&>L{Le zOTMsmQ!nfJYh$DrBy;$aN=SEF^{~JWhUaF=KBCH;3xAbYp=)hfClhQ^oGu%Z?4(PA z1+~Khh^xig9!u;!JA9R;s}CKkgc&vvUa179TKB4!lnR8w2}AO%+Uxb#v2B4(0B=x~995j{u8(9Wx+ zlAuP%$ua^SFrREn%eY5)g{MTK-PeZD0LO?^!eDIFd8&l5Dkp}SqUvLy2w3Z5S=NvNgD8Jfi)a7>BL9gwmPM)I(8+W+n`^ zG!{FQLmEOeRr0_i63DDz>|m=NPMHb-C-`bOGVGak-Z)-CwsWEy3Xi4{>x8-3jN>q+ zu0wNOiM*~hc}x_`BGXtm8LTj33|Ha+EA;?T0z3MScU0B#CL8sfMZwG#RWA5&_Bs;` zwE#USKQ^0Z_(9X80A$PcV7*enDoQdgBxt&ETOF+$q61uf2$32KtNsZalP}T?oPAm8 z2A0(6;*t5RbBIi5o33=oW9?#rXA+}~Tq?+atTR&Uo{`WW8U`RL1Z3#ow_r5IN6BqC z(@&a1kl19Zs5_fcm6cZi6$fU2B4_|vM}1&bRDD)|=XG8QlLq&y;cfIT9fygrX6m@q z28_{yC^IUIONaWyEoscbD;pu>1vb*( zmzB!MrnNal46PTOjv@8)$?&b|l4Yn8BG|0`Ol8y`WU>lpMx$6s6rZ?gZ>Qg;*VFy< z zn#%{%w|B1&1g3~@=hMMY_kZEcuLoWn9X|9_KJYY_J>48PZ})H3tvda1j_N9i_Yb?* zlbh>gG#*@roLnql&u1GI8E@_{Hfs1kTSSsRw#AS3LfVhl@jZ0edwkg3z1=-b`jGtQ z8K^uy?BDG^n;+l3`fOlxbGzRkXXS0xxOsW^?~WwwLvizPw5(zdZhG|S`43Px#1ZP1_K>z@;j|==^1poj532;bRa{vGi!vFvd!vV){sAK>D0$WK$K~z{ry_ef+ zQ$ZAl{}&MNL_8yEJ>VON=o2W4qEr+WQBgc0BI1h(1#O!qGikC!l{HnXJ&A?0viPK`v(Jhkw?b)>;rDnmB+)xPV#n8%96iuLB}dN6`%bq8YVO zEw%Bw(MCLZg#Q&LQpYLsf2ap!s#_F&3vZfjRP;?&2oma(WZpvE@0i=)v)Hb}FZhMc ztP&#Br)k;SnCUaG2P^P%`&lJKsLx=Q_PfWtV#mt}#`m&H2$MfYC!Nf^Rr5J#{sX0n z-K-MA+{0hZKYgr{otPZ;g*p%8c zf$~cPBWqYCOkB8+`Q$3_7Sz1AD37gUl`#G1!2axb#vi~r=u**l+_g*H1pfnqyzouv zV$`kdx4>IKd3+PAgsJH3n6qafISCrq{ZEKSx3EeWqpx=IL4Pu$eBc+hu}YXoT~4QC zw-QT;r88oeSYOPp3B0eAmUpsBm`I(^u8REcR1XfZN*JTgcIv`JQEDeq_kO}F>|vEK zMh&}8C|#B>Bh2q(l`w%i<$8mj#)MX)T3W$Wc|h+(s7{~}tae8OZOyubg=4NiOg5(# sYFGzKkUL0|Sfq~W58af9{51pMKg)m*#VkjqPyhe`07*qoM6N<$f{_GB6951J diff --git a/Example/WebimClientLibrary/Images.xcassets/Icons/Back/Back_dark.imageset/back_Icon-32.png b/Example/WebimClientLibrary/Images.xcassets/Icons/Back/Back_dark.imageset/back_Icon-32.png deleted file mode 100644 index 7ffe69b3367cae31932a46f603d105db06fb66c3..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 710 zcmV;%0y+JOP)Px#1ZP1_K>z@;j|==^1poj532;bRa{vGi!vFvd!vV){sAK>D0$WK$K~z{ry_ef+ zQ$ZAl{}&MNL_8yEJ>VON=o2W4qEr+WQBgc0BI1h(1#O!qGikC!l{HnXJ&A?0viPK`v(Jhkw?b)>;rDnmB+)xPV#n8%96iuLB}dN6`%bq8YVO zEw%Bw(MCLZg#Q&LQpYLsf2ap!s#_F&3vZfjRP;?&2oma(WZpvE@0i=)v)Hb}FZhMc ztP&#Br)k;SnCUaG2P^P%`&lJKsLx=Q_PfWtV#mt}#`m&H2$MfYC!Nf^Rr5J#{sX0n z-K-MA+{0hZKYgr{otPZ;g*p%8c zf$~cPBWqYCOkB8+`Q$3_7Sz1AD37gUl`#G1!2axb#vi~r=u**l+_g*H1pfnqyzouv zV$`kdx4>IKd3+PAgsJH3n6qafISCrq{ZEKSx3EeWqpx=IL4Pu$eBc+hu}YXoT~4QC zw-QT;r88oeSYOPp3B0eAmUpsBm`I(^u8REcR1XfZN*JTgcIv`JQEDeq_kO}F>|vEK zMh&}8C|#B>Bh2q(l`w%i<$8mj#)MX)T3W$Wc|h+(s7{~}tae8OZOyubg=4NiOg5(# sYFGzKkUL0|Sfq~W58af9{51pMKg)m*#VkjqPyhe`07*qoM6N<$f{_GB6951J diff --git a/Example/WebimClientLibrary/Images.xcassets/Icons/Clip/Clip.imageset/ClipIcon.pdf b/Example/WebimClientLibrary/Images.xcassets/Icons/Clip/Clip.imageset/ClipIcon.pdf deleted file mode 100644 index 6fafcae780bf809ce4bff9c9f34dbaa0db1ed5f8..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 52123 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 diff --git a/Example/WebimClientLibrary/Images.xcassets/Icons/Close/Close.imageset/Contents.json b/Example/WebimClientLibrary/Images.xcassets/Icons/Close/Close.imageset/Contents.json deleted file mode 100644 index a05197e9..00000000 --- a/Example/WebimClientLibrary/Images.xcassets/Icons/Close/Close.imageset/Contents.json +++ /dev/null @@ -1,15 +0,0 @@ -{ - "images" : [ - { - "idiom" : "universal", - "filename" : "close_Icon-32.png" - } - ], - "info" : { - "version" : 1, - "author" : "xcode" - }, - "properties" : { - "preserves-vector-representation" : true - } -} \ No newline at end of file diff --git a/Example/WebimClientLibrary/Images.xcassets/Icons/Close/Close.imageset/close_Icon-32.png b/Example/WebimClientLibrary/Images.xcassets/Icons/Close/Close.imageset/close_Icon-32.png deleted file mode 100644 index a4f95b2e9115bfade461ed3891dd184699d7c384..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 549 zcmV+=0^0qFP)Px#1ZP1_K>z@;j|==^1poj532;bRa{vGi!vFvd!vV){sAK>D0lG;VfO@L5sU~MA&j9~K`;ec=GKi(u?k&9B- zcltSJl8d zE{VkpE+lGems2`EJSPU{xFlvlWZPk%IGn>0`@7XA4g2U^C2&rw)d1tIVKz()P@nNO z;NzIeLhlL_Z-tNDC9npKdrKwZsh0f{Px#1ZP1_K>z@;j|==^1poj532;bRa{vGi!vFvd!vV){sAK>D0lG;VfO@L5sU~MA&j9~K`;ec=GKi(u?k&9B- zcltSJl8d zE{VkpE+lGems2`EJSPU{xFlvlWZPk%IGn>0`@7XA4g2U^C2&rw)d1tIVKz()P@nNO z;NzIeLhlL_Z-tNDC9npKdrKwZsh0f{_$SZ>+} zs1648Gez-FQnU5ydd{gPgaZTj^A`cQxPZ$`Vw+9al61|qQtu^5LS8z~M)^386l;>x|i1GZ!6z-Vwog~_y15Un3L zRk)JediUIx)$m57cbp22IIqpF_3>9Nxh16a9b8#QM=u6G~DjTtR_jkw027vXmY4W_=1vBNZ7*2 zID`H-G?59+wD*oori-^sV#!YYba)S^%wQFdtZR#^wk-)wyIEmX&b-^(b2zIu=ZD%P gxpN%^-F@McyXv1s5&g?9+OAh+GEQc*Pv5Wq12SFgVE_OC literal 0 HcmV?d00001 diff --git a/Example/WebimClientLibrary/Images.xcassets/Check.imageset/Contents.json b/Example/WebimClientLibrary/Images.xcassets/Icons/CloseButton.imageset/Contents.json similarity index 83% rename from Example/WebimClientLibrary/Images.xcassets/Check.imageset/Contents.json rename to Example/WebimClientLibrary/Images.xcassets/Icons/CloseButton.imageset/Contents.json index 10fc5ba1..1cac978a 100644 --- a/Example/WebimClientLibrary/Images.xcassets/Check.imageset/Contents.json +++ b/Example/WebimClientLibrary/Images.xcassets/Icons/CloseButton.imageset/Contents.json @@ -2,7 +2,7 @@ "images" : [ { "idiom" : "universal", - "filename" : "check_Icon-32.png" + "filename" : "CloseButton-1.pdf" } ], "info" : { diff --git a/Example/WebimClientLibrary/Images.xcassets/Icons/Empty.imageset/Empty.pdf b/Example/WebimClientLibrary/Images.xcassets/Icons/Empty.imageset/Empty.pdf deleted file mode 100644 index f1716049dc44e50615b9fcc2fc5fcee43e8c79f5..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 44798 zcmeEucU%)$*Dy#^5L85pfYLi8p?3(q_g)Plgg^+8LbHG(D4-}Hpnw#WrXWp0ic$oX zCS3tR5EPVVqlmO`hREvfv-`aJywCglegEuTb7$t<)9<-=X3iW|&_YK~5+)_bC^#|M z)yGH=KY@^wP2B09?0Dr7MT3wwHh4TdE zg0gYUd|M18la8x#u$X*QPyE*Hpnnvq>?9}hC01!h;oQufFEEgPlz8D zM^Fw?<3j+Npp=1g5Dekt*+wDys`0_W0Uifa3m$EMJc>t7N?sBPlac08P?CblDM>5J zO7KWS0d1kO5SWrAOhy^_Cky5I(fHKqfiqRSrx6-~zxqe{Q(`zi0F3@;#BiQ3a6j*zIZYL_$zl6d;aB0DK1f zE2B+KeDJ6kuh58u{e@aQV*a}7PC*hGO0R2x*CJ=RlfH3$GR)2$$ZV+J7 zpRx-0B|d-02p9ldAOra=Ac1&O1(uSsHn1Ft{&*XIf2=xr!5Ct(Bw&?-P#$e*e$rGi6AfnU(YDGyXOj26b2BxG8l?T?CI8;U%3jKu% z8S@W3c6N-v!{v9lf8^uuiSY{kTU>r({(*;$H_6WfhrnP7+k3)qOLcp5fMbcjhk~93 zS2q^3y~qO)cmfKHd^NtEK=^eMeli1`3a(z|V2mfxTU}aCe%nRAfPQNAuSDJ`4BDIc zd#WF>e@Eu!kM~0m)r~B4^?yqH0sMD#7(d|Fpbk;;fylz)me#&bMpkIJ796b$w}xvO z!r?j)1xq-=R^L0wQ(q|zp|2EdsAEbpg~MeneX(Y6aVUJdX~6+}u<62u_(R|-a7&OF zXqIp-h`20V$5+qSL)r)n*SFaQ!@VFLa6@1aZK$hL9 zu7OrC(o6>qbp#|h>U-N-=~?-2k8d;l4e?i^zj+Q(1PrxA14iq@;la9AWRu~BR&YyG zE5I~eIAYs+12|}%C4lr>v($qJ8|njYG0=wl0_Fi@XhWSp;*l9z!nFZw0ly#(wJrTk z090*2fDRmOsSUsYj{uBtrod#GS^=u)8Cn^JSla38>q_flkdC^xrUCliS|Ro!mKY1I zZHBf)18>t1Cyb)DRVX@07i$@2W#b!Y9O@`zp>1YsiSv~=4>R-jv~@DD(Sec-4UPOw zjkLYdPTod<>jJHzc0@BfV=a8BEW|>0n<32H2I}NsuZKn0!$LeAuuAq$(%u0GKO(`y zUM|Q{#tP<%^OQw71SohJz{I^|%yqpT5e67TKVyt+sF{zyxVEpHwH-D9<>=?M%`ngd zXC#fn$r<|u7~A^=$YTNm14#rtLO#gf3Jx~}%s150vIN4#(ot6%2nY|*x8AxsTH3nS zrnb6zwpM1|hG?e{w3BY=HbZ|8gJ3;V11ni;Gi^OPBMdS~H_S-KN(b$Yu(l1g$2cmO zXluz>Sev1YbkQN6w!XpWZIg|Bob>cDp1z)Xwla!==3&91=zwj8P;rYeZzF48qN$Co zoP`zE)6w28z{5cghH`*eVV(Ru{So#7cn^IT$wT^oGJl()xDM9H-X1H5aqjjv`#iRyI&U&%yE6ZkypBVE@(S zHpAb`_^;c}R?2_Bk!}0%zn_YqYvG?s`Dar8c*FP`_loVX`5SnD8)ci}e?JvJm&QLUN(X-hS=pYNsQm(D-Fma0O?FWbL=2`58;|87oBIsEC%Jealsa$m}3+bK_V z*f#1ffS*ElFbxm{I}8EifkoNi5jXc2qxcihZ_Q1}2mkZJi# zRzETRm5(VR1hl{!6NdVm$p_W##GP`mx~!rUbi1iSc0fP*gEzi9mns~t~U2P23`Z*r3Qm*Jf>i#00n_t2ez|4sfy84c#b z%@BSl_3oseh@Qyqdf>lOp6*B8WjztyrQKCM(ZG2*4*-3{0}R&zSG7IofRTsY4|?La zP165`%TH^6pR*yOfN3Fhaw_Zx(Kh6dME@?te{cS8F8vFqsd^%nf24O10RdPH5& zcW_=Xe>>tuHa~30sQ@(KbiOMiT_#% z(i>R)C{K0Bj{kN5e{v+gnqU#@b{YM`7kC6X~uKzQ4{Zh09Jm{+N1p_5XU?tR#nkL{3tc3#W zrGC^zY5S9KL>?H#7~@HBRsid$T*>uN}902Z44uIkKG8_Kg?a;6S6 z$adz12L>1hsAz-7`2B_K?J^oF0fgF^ayq98v?&!iQ9 zmX{gC^sWTkb+3@*Edua?cH{UZ8fFSgS_)BTs>y$yvTdDy@Yli+2^J{4w!dG1KMwSP zB1GFC>yNhv%GFRjFtFmz3PM)G83MP~HZ)X}`=Q4#I=Mm&wktAtZ*F+#N32W{YgM=%LKJ^h8W^JQ6VT#hz=?U0~8eLYw<|S$??Fp zOa5FzyFj$<%inqH-zxgUd$PZ}4^Yh2TwOQNOjA%;*xX%H5T&VMq#`J;D58W?cTh96 zlDF2txH+QXGHR|;209LUQdR<3LlaHD03&f-3lTvmy@#tj!pG0d2`9#@CTwo$gtByV z6q8VM)zFsG5_hr1qLnP)^4I2Fi}G%Sx-{HF?wAXo);n^ z26Yxwl0pDw3erLzlG^+PRR^q`o`I&bsv0l9yqzdXN6P_ch?mk+_qTFV(7_6tnmD>h zX?Uue;^^fCEv5Y(z09oi1O&v*%~Zu*t!$0_w4sKoK3Eeee+LY~NyXaH))}I}uPP*? zfJb62jRR3eD1HfBJq0VY2aMj%%wEsfOC6`J<|6c1X90OLoVb&_ zSs>Jvpx{k#J?7v5!#ODmDaj&S{w!d?CHG_g4Y&6N9vHcZ6HIK0Y;IBWQmas_Q!7#M zP{2gV8xm0X39%&rYmNsd3r6_QrMFE=jy^jKDi{bo0tSuy8Ehc>4`DxQ$$sn@SDh0^hDG~*CykLL4FSteQL@>Cq{NVZbwlWJKVFBz1z?fyQa6_<845$P{ z@W9BWuW&=)E%65_2#4{&wl`_W9|xp)VC0SUj{`D1uiOf9c|^=Gz8Jtm zCWtWL4iF^iMDp+t66FyQ1rTh&L?D2o1=Q_I0WZ@aY?~WkFIZp)@RF5+0)>`}fLDN8 zKX4d<3jz{!Kss6(aJr153;@#tEyv){!0xXM%occi1O5R61{mN*@|UAKc`x7wgG3Ps z+j}xWnGvu_&K=l}QGohDDH{;!pfPYmn6$eV34`?nT<(Da41+1if?yfZiiA`#Gx0O$qi18lFq3L`5Ei2&H;14K<@W`PfYiHF!S4e-xcu2 zHhZAz+7E&E1$q3m4IHsS`2~PCt!=vjPs#B}%Yfa^8JXS&Fl8G^c8NA>hT zYBH6)JcuO^G_VJ;8!Ahf_9!~$&sdk{-T z2E+m-?~H*Kf#)C&Xc5?hcJFwCObglsLclA~BCrQJfqL(71}y^5LA${thy~gN$^@~< z`hk{#=O8DtU3x$RH3EGJ;xP(>N3y5Mz9jpKObL#Fc7Q!ucd~9|>7eFhO7a-klN*Qy z+6VR^C-MxEttO9=XAB$xu|WHFW|-_1GA(Ey2nVk~>&WvD_GHaL2zUizf%bttc^1Ja z0PWk6OpcMA7yvZ~wE>}^eLI?gXW%&~m+UvtJ0N#(1e68#pwGc0Svyc3D1}T3_8`Vi z193p>b|MqB4x}bqO&$g910f(5Ile*r$UY&{f<1@>TE{536WQcw0OtsVkR^b;e#S5; z4YXz_+Ca_0D-equ5ukmbE+7oFZ$~fi3_K^xCHoE39MpnrIVcbOk@W(PAl43dGCg@5 zgo4(A)Z~>3+DD#GvejTu#sMuP$1iBtjyFKgjDjE)Xd7q~*n{I_KashCP%t7u9MGbj z=pfG+ctw^8Vu3RT_8=B$5!jP`P1Xp+`RPee_D^r@=n7(jZAUJ6PM#xeZ5Rw(W8j?x zyi=3UVB1X^*fPQP^C-RDz-I@r{XEKUH%37@Ir7NQquh2=P$0v99u>Bm;*Yx+cqGeK z{L$cWGR4m$xzb%riwysH)Y@*0f;u|nk)KDM?WU_shW|Y3ZZ|R%?7{g4d-6Hx8}b%lAV2MHu}E zri@V>@sQO4T=+c(mU#vik!8kS-BH#-TqgX~%_E62X))Zc%qjup#mtvvRnMtTuRV`? zdo!!5w!PD0@k6jwLW zaGgzZF8UA3d{&K2Zpb7G%pKZY_OYLv-8V*--jO)M%y#G5nQ$EUX5=`>Y`5oaqU+vX z-iw|ptW;?NzGvbT9g8W1N~#(aX;#kZ@Xd)pJ0-CzPNI`h*?z37Vn~crX2GH0`@px_&*`66Cbuq>+^y$!zZlN7b^OE< zUesBg^(JqOc~VuKed0-FuE1@-S1FWLO7Z=|RJQs;BW`XrXsuPw|K7^oo04arSt- zb9sxjSH`K*AzTQD#~*#nY51Bj@2i1*wU5VfG=}gX#EUiErB`uREaA?I{{)6*xx6ALT6z&O{%Gmu9nA^T9hI%0grENLJPlw3c_%&*jz9*4^Isgz=Tn;6+{i z!`}~n8@z*j>E_uCrADMu2wb{8>ikMH`HbzW54%_+9tlN@tuWiRh}BpeapB?|dRvv! zut@X_frV0FhT|P?y(pUVF?{%PO7y%uG9kY52ES!t+G~;~U3WH5lJ>oqTZi4Ux9Up$ z)}khzAzW;mC$O8U?H5ATFGP-1By(|`=JedNHIt7&7B zaG=R#BQ3lu>!I1DY{Nr+?t>|QCoI)z?B1lFJG5JYiel^$Rs9=CKn3lVtny8l3s*=L zb0@y~nej36tDVxHb#L&@mJ29%xZ*KM{VbrwtzUpU&M@E+MSI%;v3+cx*y%CUJqWbY zhl3xd#jfalW%R3f|6&&eo563S7Cc8JJ|m`H z>11ZJ=38DJQ4Tipi@N%myj$Psl34*R(zz59pCX zd1YQVi?LJi6<1=(K;u$|XId&^L#Pis54q3Z{Su_!v7jmU`kRne#{Ikaj-}(rx^LIW zc>D2p7gQL!nbqK>d=oSM*dqpHk~HLmUxjJiW4m_t(bEfM*F7Mgzjl=FolCUuvrULjACW+UtNO!Ii<0>pOnQQVG#VTyv z8ViVdO^!wws8NJvplaWpDaMA@u#$x`pG!hzo>XSFsWG?WGLD(}W#{uocKWp?9LqCX z$!M^v?))yZ!qf8cK%AC$*yt5QAEs<2GnY@3!@4ChgQLAEiGJdzWj^?}G>yibG znx1_3ZisF=I-y`Tv+k{IVbKYWn6C!)*#q<==3DZoBJYOpW9wI#JbHHvdaC$t!OQSd z)o1O3S})&tnw%^Z*)GzV@8DhHk&IJg=VoSqZ&Q?JUp4;CawUf#dt$Hs&9qs$6Jty# zx|Ut{A?3~#N7LQRJU7c|eo=NL8dV+iIB1r1$FeI$9c%Z+S+LHfsl8{W0LpEX>QCoN$7Q6P0Um8 zgupk?;AG-k->KvelpW>SaQI-+^lI071j1(iR>qU#u)eC2r-D?o5*&f}YV8SY{(|@| zdhBM%24m@}`S{nRCHMV%*H5lft*?(RDZC8VV!!<%w3(Lgtn{verv{x2@h(3qXfV-aI#|zD^2gj9!9~6b@g`~~i{_Gj+WE3B#LUR^pYgb{n zeHW_~DffGWW6?NcuTJcn$3f+nQqSGlV@Zc0Wt@Id=B<2cv4+B3o!9;py=@xT_3)YG=bh4DUCuZ+|IF?byIW-p?;tGijbnKA)NV zojWtuqy@?7BcEUWx?qx{fu3&a`*6;i%R5-^44P(+Rea>unqjA@yHG4fH*sbozmUBn?f#tO z3so}}mIsk+^Q)9G4)|RS#!GjMHM~0@8!-xN9yV!5sRwV%bqE(|GtSVkIn0_pXSwib zzO(5f_Do)`zizHz9xd#hth_>gvB)hpSLJVz>46%znio+e4QlUuvl+$U6Z_v5$r@SO z80#{l6X|EymPbD-p#|Wt?th;%jBXYOK4jw}p8|3J@{my>VRxGrwYTp z_=mM$+>87c@@`z4ZO84cNjZsVsWhpI9ihGL9M>FZVYQ%HX5(6KQE54!TxEnG;0Yc~ zZ)9!yK%D$~hWE@u4Tp=LsUM9OJfKo$--~J3Q(FR@RF!pg#*8k;iR#%No>_lK&$NzH z5aTnq+`<@IBnEbA4##yOF4~Fui!z=3N9GVGb18|y*F8Km% zy>(S(?iq91`J>~nwCE00hiu8Ew5IyDq~?UMGov%*U5^Ac?A>SdebXHJ4ui+~U8w9oc17ms{rLQ5HY=ryOoUNRO%>JUWCyY^vX34)zVJxOTGT-yFVBY z@XK-P^wOcYjB9vItUhq6<(eDJ%3`g|No?f&*SWjp*QU|0?xdYK6sLDKDy_7naQb)* z{kx%Qqryz7>EtSd>!aMVPASjASowQDSV&{yj00JX-72CiCsI%zDV@Xn-nvpC8QKP( zAG$riI<%40B#pT#DG>{N#}weJFwbMzpc%ZW%hBE!+2-cLw7ST1WKurQBT{v4IwXdk z+Y@$jbTW0;01^??=jrz)x7=4*A-C4N#NS2VfLN~(CYE}%yu;{7d8kTRV+(uG<(r32 z9&Xt0@a5aX!2S7;k93v{ku)^kO&>>??K>}{_yl3ee`Q~asD4=dk@0fL3ts#ORiiYT zZ@WF5m>ha1_cH!mW&FOAwi?u>BhsK~BjH_MuIE1u=59499Nr(~cSoF0E@lu!f(C343-3!a1XZSHrYD5HDF;sf&T)K9epfehM%*d_q z$kM$i#pG`TjE6L=D(;9Ida&P)3l=+=Qu3*h$th>NaW@9eM=7DSYHNBET_+&(R5(vq zO>siY-7fxrr?NEnkX4`uoJCX-=*xYF%68qWymO#FRJ9?$2^vr*l41 z9FVNczMP=nhdvafh{%PRRuTKE zQvJfQM;=7u*LdO-zDGu0WVrYE_8f{3$ot}WqlMs5RARi^taDl^-dhWr+H+9!IU8iQahWeUoVgjyO zXTG94%Y6PFOMaTwo7cY79(yC5qsq9hs|!33=;OGo@RI*Y;9D-HneH|YyexKfH{tgE z!~j9wH4_?|PaaK7eJR9e7OX4-LVL9sMt4_Kd-l=HSGV*~l?PvooZ6^4p0{SN8~aqK zy2L8cnPZ<8#iTNcn~pp0%E8cgs3lHHV(KZ)jo!R+0Wa^&ScRH`Hv;F^aCE`h;+7X%ItmhTTI*a@H*+}1~;KWY%#3{@3>K1Np~z9 z`TEXk#Jy*!i)*)W=1u=JcCd)|birSweT6ozM7a{baAidO(^S`KwGs`#7C*4#SIQ(X6qDJneO#Q zBUE|C->&)U&`Pavf9*a(7v(52qDQ|U`izq}eqK`{K`eruTIg$8teooLm(0SHNfu}W z@9{^W-i}`g5hFSZ4H};hxV3-3OINC)+yy_n(AC(sR;W!ou0gE&5a)81wL$7k?QTXP zM>rcdE4=Hno<_heibuW~U+-Sbw9piR=zr%`%N#S{XVCWjUX^gkfF|*4>$K?pb%UG| zQ-Nt{^~BSOQ7IMmFuO)JOB492-J6pQ1i9ykU67+uPFqt>bk!c%@K4-=|I2Iob}_4v`KukyW= zLmHC`vxMo7uE3uFy~D5tIA7B4Nda=Q2gVJlIT3Ex{cmJR2^mJ>Fc>ctr5-pxgUUAql;YMYoF8 zo1Pz2bHqunP1WDCpANg-&3y8V>RC09$FzJpL$a0-BY; z+1;__({uYKXsvBt?LJibT0!qv?~9(NB9!f|x7r`5AJfvttgXvUECzqbDw$$h-c&5m zQ{-c37&?Ky!r6INRH#k9IG@$5{~?sk^xO5@gKj>lvJcYb@Uu#}#?GIT!yinDQ)=w> z4T-j#1PO{=Or(ong+^<=v4ef!Z2CXB?$<2$uU^)ylZC-ygJ zOx(t@P2SFY-NQUBAW@CsXP(xO+XcNA6m!t) zT`{Uo846vaZ!^Txcz4gq?@}@o_8Jvd_;N+$mQwNV>SQ$iV_TZ)t{LPv@sN)B4E|X9 z?wIo2Rlk?4TgTGavktKgkmM=qGv{wkI#n_wIhp7mc|{8pdRU9R5JNEAuVC)JR;_G} zCD}=x+MGAgZi{TZzxEKSs!$Yg$BF{mlb`Z{(O~$V=t*oHD_bjli!B)euJ8Yh`nmAQB4!lT#e4$`6=**}v-Xx%!}rDP%Kq z&*i;*8+dWzYo)zyP)3KK85d4$@JZw6?nvBG%X`ct`!nFL;;!RI8??>m@THc2)MBY9WUf z(;gZs&PSdTSG7Abx<>O>-rR0qY&x~Nw*F-I+lO-v;qngy;g1<0`E1|6DArfnbga_& z)(m`WLBMxA6)+q&9rD3d7mqKzO73`0Z)jF*;i=D{Y%9)7Klh?--&&%sp zG3DMA)pz_-rE$psqc1!ybV}r14AjxOQT*HM>u09gCw4(@AKll1WuGts#*}lj3MO>W6Mo{`SBL+*&@UgzAw`iuIuB9=5PTy0HxdWu$ENgNX z)KwF_x>GH0RfP0IS<%5UW{hjqbF&kBd2ELVjw-CIaV`N$SfV7K?`Ih85Oz(yB>Z}=Le4r3k+-HW}*MMxl;R}S|sKgn&y0V z#=WV#UGCJT4a~*d%@lgxFT!f)PgXeBQocl|FHC-Bt=?^#sdOG`c{|Whjw|fjIAMGh z7hmlX@anEt(}!NGzFr3R`_yNo&TBT(`*9o}NIjR-N-_E>sD3NxR!qV>m}{uAmigM| zA#B-@i|Va*nJ`}3s_P<9q;%|zVPUHCQCLx{>Z z53Whk&9N1$k9=XsSJ~5TX45T{H13&zZJ)vG}kbQ0cq5Qh1}k_fui%%9`K3>-NxER%t?Ao8WBy*vebpTK9p&z;H>rVrvitS1;5D} z{W89)==l|kQ&~r@nXf(e&B@<$9QL-&@bcQZ$o*V~4eJ-i<}nn9UmeG}whDyv=gpj@ zshkNASN_fOKS4knda=>H?&QZCmswbjY#EoimV#8 z%chPA8K2l=o#&qxWj%h4e{!%^BU$!6Yr%~XX4@MsT!o!;u_$Sk)RU|we7iT(bdJbs zoZ1}(B>#ACcp4O@*&H|-@4;QhJZ}tJdZ&jsYPqOr2K>JjMds?emPg}Ge%tQ|G3<_9 z79QrCXB$hm%>H6D^C9*X&ByG_PmVP#=N}8%K8#=UmrPyswcCn15FVj6#e606aH_-l zZvGl{lyD6h;K#;bwM1RY zm9B|4vWwx)OGa%Su5OX8d0XZCX*zppy0D@mSE>24tADW*GFXTRas zmve=m!9J~e_weiiOt${+aRV)MYst0qR@ILa7;;m13Cj#QLP!0I_AScJyFR|-d+PYx zj#DRl5{5o6CPP2fK+nKJN%I$wuz#>YM}AX(ca!m_MRWy zsC0xK8OO`UPwkZ$b8LJ*5LA#S>>SKYOTW45v2PQSpUpC%rujv~yX3{Yyab7L>aR?P zc`m0TpT_v%sZP*P@hS)l#T@KS95xd!ce*!>Xu2dx8{|_JTX?O0%{RYtna07Z{9yDe z@n`i)e=&I$XGz+|p`y z@s__DH>By1?B>0D4!w0wbLG~lSAAT~mP0z0<6{SA4;lwc+tUZ!%EdxakgTEZ4->Al z!>2lek?(8lu9@HXJdkl9{qD%gy03$0>prFZm(Sv7z~$yk4A!7Y4y-lP4g)^X|cbRdXPf4z87aEQS92KeF~=oJYG?bM=nmaEhIdJ?lYDXb?hHR4ducfX~sE)NhCWT z;pFG2sFP1fAB|ZJ_GoAvahDp6z(1^u^-B6)4rBh7JrZeMGy*E!m0`jk=pp=~4Xd z520TUw%#8e=zSkK(m6NtTwqD}h5n~gUOW?1KeDuUY)PT?xry5tt8$!YR*Qi-~sl`a}~hIrUXmEt~{-Sb9HLe}V3xaXZ6jlZr|m z>1z*Mvs$H#Rn-DjH6*8ueHL1E0z*BRzT-TYYYIHr?mMq&4o;C~hex~uSB70~wl$O= zuQ-EPsYuqo$iTUUpG{8?W@z)SRK0GqU~TJIrkImg%(7n}*(OkTk?p)tCU4N~4)M?W zS4xlXkE*sEk&?p>}*jU67%Z;=SA zq+L#kXXT|w#0Yj23^wf?4^EI`&V-}nw0E^$5gV0H5$4Up6gHV-F)8ixV{O<5`Mg{U^E1E zl1HDn&QtVnNjIn19IwOF6Q5rxGP5m;*wvy$XP9-}{Q27QrRN{#?dP+GA+N@&!tFHQ za(?^F{OCKi^cKbEUHSLiPqR>NrQ4i*y@@dj-0XoghYH_UtT%l2blLawCF3tbho<*F zIt~%2_p48lQre=7X)2WArTnB3`}F9W7Uux^*BOR6;SH|{q92Ms8Dy)zjH-B!UY)TQ zeNVL>JhhLmKrPkim4>FtSuV^S=vJhS=+V;#-Am$%Ts);4^qISsqjYNTMjU4jlFF^S zYSSjPsEoP#iAf?{v?yp(T$Yp(gq#XvTv$OjkxXg7p~Kx!m0fQ(oV)DKFZ5q!9+QpH zBh^tJ@{~X0%hJ)qp5S`)zR63&NvPJ%qRHqI^QOy!o?pf6s&=1Z9f+pCAKmV%%jhCM zOAOJk-Z1Z&eA8|FUbVxlv?L@}`OW@u)$+?md%s;&%SSUKrX;PCe9m2v4?L|dl8n(oVv(r_QE@Bp=pMA!&5oI#X{hLZZq%c2Fh?z6guE ze$2G{Ozd7ypTP~)Yewe=!&7tm=tV|(x&pnWd)57(z14x!!nrs1GWO^h--kgglS=q^ zw|$!1ogUV6LeoU8Upk!2JC3utnf~cm=zyBc*~mtLYajbWdJ-R(YPcZcsSoLQaXUJ| zC8hHp*ya?q7bQ;fVe{~XMONC=%j0P#=Sb5=52d7OqaPE)^o9cabA+T{a$vK4ilf33 zxIejCEN2UTfW<>I9>$BNA6|rsELL@T%Pw$rN4Ou%mC6!KqH>ZKk$EQsb+OA8K0tr_ zv9sev%R`^fy=Mt#tr#*Oy*D;1+5fn6{^Ju!nVqt70oNlDwNM}WuskovbFvA_jdQ-t zZ#l!>boPBD=%}yQmwW9u?kV{8T_x&G^x)H4R(&Q1i0;kW=k;~mhDCeOu`Uj$*e(r+ zuT(ZyJj~%G3E%e>tV?~@eEgkMBy)#B`=B~klYWl1O9su=7}pzkpU@dEZ30j1a@?%c z2Zoz_rl09reJc<9z(B>KfCU{Fza9EtH)(?Qs3!QpxiBIIUn}Y&Lu$~I0udO$lcE1v0%S0OM1t_ z!5Z;Pnx8owZo7);gh<-Kf<-4?U*@Z1#d#?d?z;Fy;=s|UtBKDFOhfYBf_z)w68(g( z7j$;yjr&*1CmDs-TgR?wt)s%SFRX-9nzWAbQE+$Z+Fijqijp|`m#jW0WxaRY-7a#h z!h@kRR#NsEgT0&Er-0X!Z%7*Zf~$NIj^#e6c~j|y!0FK4_*|9)b>P&D}3MbY_esC9lCobGcb8!_@wB;)63@W9gQtl1!0y-e3%bOP1Gf3M44*-1`_5{@B!rk)Cma;5WECcO=~5|% zo;F+M2*vZ4-TSr+cGs$zI9?DfT$eg%tlp@(M0u|Ib%I}Tp+U4Wz5t(f8&zR;=)kDK zY1-cPci}Cu@RV00S9=4w@9)J`-_gIt6WgU)$3aqU^OtzIr-V_a+%n~e;{)0xYM<(_ zx)#+AtY42s-eqo&5(Q{N+|=)}|BdFGaS z;~~0l=#nU2AWm>wEsyBt(q_Nan@T-o!C>W!6ub9LwIe3AvRU&=F)vlHCu7U?vuobI zDLq5kkwz^o-g(@1%@J=lX4fdTc+6?bqn|cDjvB?t1|PG07P zFTo|S8z-m)yY_sn?2T|X(K`LT<#m6TZew($zxMS$hjRsVTFqzR$HdupAxpDbtwrxa zTyp!fO>{e$T9I8;;|)24{CyoKr5GQVKOzKC)a*G|GU~5K9pF#R^z?hj?wK1kt@%X$ zj4v)1T4wt9PO4B=yzySV*Gc{E8Lhr~z_XOSyOYmfhq8EV8iroW6TZYx`HgynlFG~d z!q5tZ7voxr=HT9|rRjs+8%vB+hY9CTd2M!Zd*Ngg*`V<{HQ~+sERz@Ql5bfZTG?av z*xwvWYg8&sb28@JdcUUHr;&`z64Ix~0}canL?5cKiD;T!wG{y)ifxJq)XCG~*B>(YIg4D2?q!a!U4Ii+uf4fn@2ho5Opmo&V_aot*bQMLzfpqery@1o z^8EG`HLRm$dXHu7dg!Xa9RvD_B<@reJNpxGDwgJ1M`gEq|4KHqhnKT`SM#luHt9}m z$r>$9)Q@*>ZSo#U+}iw@r9p%qo1fOD_a1A`a%VU6J*g&{l>u|4%P40E)>!scBMnvL8 zOVe%HR;nTHp~X+tm(jG_BJHkwdseouQC*u{jl9JsedfbUei8eN#q*Gb?uoN<`C>+e zx|M7Ny-%<$4$OmxBybn!FsLHJ#FN1uU7aMG0Yf>9c=n1js)cl4I;spW3DlQ$&usc# zQJsa9Udaxk%jFA^4hzpva&?Kg^_077T>V2l-+q>O2HXImB`(ZuJUakE!cpOKpK-s6Zj*<%{*rw@OmzG#V}J<9bmo_$i!y)wPT z<_2$cWGltp8KZMk2kgB{KUu2Vk50B>r6|9}F$ul;++f=MXd{t2Ml&zC6*lXDahDIa zlBmdw{*ZnpC|CInWdUR9gx zc_#{UT$|3Fue-_3f+=IVfLv}gA!IVWy>Z$*ghf3*F*7vbQXNgffU8k5+U2z-y_WL* zXEj|HXWD&DO!V7Z75o#9($LW$RBv3h3>*~CD=*9)yLK^y#i-%SiVM|&r0WOI^1L}> zB7RnQCWiTLZWY7B*ru{wZ!(Lu&LV3y_QxCJ7+eY7t_(FQS0ID)BhShwV{w9$eUUfE+Pvg+l-`NiyQXIj8*sDEi9NXCF(P;)r}&xdw?~E5mKHrJ zN4{HHQ0*zrx%fC`(n!Sp&0VHKj|)CLIe8Q4=3b>??GR)9nHQeU>g^*zJF$& zR+3Ohv}P1&(0d-{?t7G9Hd5MVR4L{|b_5?%-2HRv_3rmPIiqJKBcdMR zM=7FCp9(2h=s@gZxm8kvy89W1A57>zJ--+e6{O%Nxb`~d@{ME0{o_j+I@-RJi;-plPHGDYY*@g)0zLq2t`ALjUKPv2p=;3pOPXEv96JA&M4duK|Xhf$<6FBV$7Ef%E- zYkfe>WFF56e%3wG?H+1!76+fp-?Mwk$YMNBUffpocFAfS(~Qv-Qv^ra=lZZC**O>2 zxUe09-VMxM_3B!Z(61G72b-$q@DWI3J=;2+F!o*eh465kr(6N0*cU21wf?afk8TB`025^B# z2aL@*`)W@&xA7Mnow$&!he4y`Q*ncZ^HKx464>X!ryORL+9_p}cO9|EMcBeh)B`~P zN;R&)DsG~Dm4?_*ABL9?^%ai>`yEu#ol$66>ean}+=@T?@;>%dCluc;|F=#0kIS@f&_G z!J_1{gW{5&vkXk)#l0L!M~{|Cz@51QJLW8|iQUS$-4%FTOPI7Y?4g%1_0VvmSYl;L z5X~x>x=_@YXxWaqA}bSy@yX4W>mx>oUXt18%l=A#^K}Nsxne?M@s`|82y4Kz<vM`G3t-Lj5rAp>G&*G?>#6>hG z5b>QGU*Bl2EsT|Ys^&)IjPDsXau{?{xQ&xj2?=vNsz!T5gH-s9&)X_Yg^ z)#MJfj6bg3dVi;1@L+b1VN={0qlZ_|77vwDrid|1u)N@Zcuwt|hnU;<+&OiT z$}cwuK3C@zDLg!4t}zqU>E73L+&UcrP0P86U$}B2flB>SMQ?^bTQE+e={*sLK6-0w z!uE0^!*1gh-PYEXm`BA*3y;Ek(QEWqm;{@|(1Ps!i$e*jV|Mq98Ezp_?Ple?+~Oyp zJbq;~222IqSx+K}r)|_1luIaX?Q)_SDW<%e`@B51hT^1gn9^7@^4Y8WuY-;eBw2g5 z0-4KJy)`N&UN%W5_awD#kLk=;k%m_u^U-W<71@6X8*XDhu}h6Qj{2%?)6+cyv~>HA z?S?%$9q~E(Qs9H-&rQpl!Fsb7WQ#c{!x!HX<}w#%7H<2@2RYByWLNM$Ju1F5xzEtc z_WX>o4)u(T`pBkh&TR5n<5lai0}e;!A8D6{8;H}WoL^gcbIn4^!A@jOYQ3@l=CE-R zn~CROJ&JpD?0C78Li@21!N9i(ay7353GYYsk74f5TA}qzy=M%o`;i~i+osL(ot;PS zbAIn!jiuh-B6Da+rY07yP7xGbLj}2Ps@HY(D1*up_i&qG%4?&<$|2_=bAgCnq0C}p z&4Ijo9;C%uw!N|o#&_5CURPE8+2C6r&&21)06Mf=c(=h;aX+%=4Zv7hHi3S+|-jGC>(=bO!QBZjWZ^@tx~ z6^L|oQ8qkqEyqb-(#tLR#blbTK$_>`7CLIx>*`8%X++Ac4arcS(}#syc~(o&-HwyX=>@+@)53@U$JRT==+-rXy6s+V+wRr2ZQHhO zTdQr`wr$(C`L^ACzCYO~IXgKwV_eK+W?a;)S*g_Xj0(}mX7yB3XBXWAI0-A|8=$|s zu+=sJ$qYs=O7$hqS`^EDO4~>8zN5tJkILWP2y8B9zw@-@ivw!Ewt0qxs92C)Ui`}@ zicUrk>-YoY1L86q!yLVIxJIv4!9-$AoA437iNP-b(-UqTb~JjZ1S<4Vytbk2Akcs@ zt0Pg7r+P#ClTvKnb+y&eDGq*|sYe7EmsmfZOBt6+tJgA0u4)@n%_`Z#$=q|5)mWO~ z>}xORR#uxw$-tyPe&8jOaZ=MnEKh+pkXp8$jp5G#Qxm7S^4+tUqAq>)1ROsGM&a28U{4G(5B*nxb?W-` zb8tsQ?Sun=N22u^acQ{!8iKU%j1M%@pfID$0G%2B>DML@i>_us!gpaiPF6O!j54<( zqW^W$ORv-a{j(fyb>Ldxp!mGyDs4d&^Ye@X^)eD(>aWOysnH8g;lfX<5sd~JK9Ft# zP2)8Zb-!qxt9KvkK9V7h&1kF7N0zYz*MjrB-9ZBc^IymXdUOWQJao9KH$EcqQSEXy z`_ng?4XH8C=2z?t^Oa;w?@6jprSak~Bm5mf)FVuoO-(%^u1UF_ZtH|TN1Vr{X~Bab zChs^>RyBvK;8d4c4Bk}nP%A<~pn*j@0ukPepLR=90cglH^pJ_2i2&gRL7Oee!qhLC zYfq;(XNWl?LcIbRlyudmguRFYbv=~eX2(zmC*snCj9dbwgZPnfu~=S9uFwW^q@)8& zBpV5-U3nictY&v4f3Ce!?8}0Pn~%|;M0-)m11=SQD0qO=r{zI~rSWhud(%YMIfO7c zCXDo28S-{0pymHr=9EK!)0}j?x$SguO_IM7F(AE+jMaMioLuhkS(&#xqQYYJ(z$m7 z#*`4p-S!oJR$!(kJS-zu7Vhxlk}4!yc>2oh^Uwdubt8@NlE|WMal#veYAVV=;vj~1 zlDp4{B@R&&V9HeKrg8bW1A{e{4}Rb@2TGAke#&m$iV62;({lPA zbamW25Qq)=MZ>!>h~acbw{|I4Hj5DyMx(cXmfJ%buoQjLmEE9Fq%V-(b9ggkl6E;- zh%JqCWN0m7=P&8yc5u23=@=h$Kg?Hb#`Lwjw383H{T<5yZlHQQyx;UZGME3Caf=>{ zMV=l)u|bM5ayG0K5mL`QWS4ELd1Wz2AC#e(Zn6-4!Co}JTe~}Jjcgjq#M`&Oz=3Z_ zu?=Hia?!irOSFJr+VLvBMAkO4>^$(T66Rhcl@oYqlulRS$jdecsTUH#2HY=zjFS@&kg@K6AJTQplL` z8K&;O*L%FnOwzl$Fb=tT^vH^IB=Pw@$p1ETd;hqi`ewwQ#whGvn&8#>vd}lQSnS=p zFRKVYu`0uQ^uyi&D-AjH{+!wWrBBM(Tt@!Dl_PZpGhkYx0&um?#EaF@&y8f=uaZtISjUz3!%`1~ zekgZE8*>uz1yQxfeltawqSE%>l7{%L#h79Q@%(>|Bl)Er%W@(q48;@f)D@eDzimET z{sW3?R6Rv2HLBAE{MS9FPS*6d#uRIGoET?cdwVIM-r1uw+G;H49_maQQ?~gZqmrbM(H(ZnWa;T7ee`Sb zK5-|(xF4B%e9?%ENA7A7pa6NJgr==)`^u`v#P9BV`YNP~kLM+vt5?KFZ@)*o&zIR4 z4`l7#izArZyYD83Q~1YpQ*!I_%PfM?Jr6Yu&6MM;IRmyf>UCg_LvtUSn(Aia$h6|? z2uTu=lI7UG^=dBLd3d!)Y0bMKG8M@%)UJfuClp!0wduBvctBMa8-V&)cTkdwuw={# zM@F)-nn(~3z$vb4T`PGY`O0#JR*d#2Hr+unoiv07qWn5h%=42Lg>vjy2B&^CQ?rd# zX7%2P5uX^7$ZX6Yw-;L;NOrcp@K$`3n@$p-$9U@W!$g2*mJ9v(d_^EndG}4s0lC*$ za4B3DAXyOQP7K7wPgb1b9c)*kGAHZ4i;-!q9ABZBahr5JJ|y+F+-I6iA`km~kyq-dT>?zEs(!ikXwb4rSX0@w=m z`TM2NNSY2)K!nx@$m7C zvas8HOCI{;)xNnPH-EG=$OExe8wE_VMOt4WPdo1-=`{Y+>$mfOIw4;JAE)>&I>9d% zG%)yl((yN?Fn5O{=^_2XpjZ5k)<{9hTW8P^ZR@rd zc)B3Ar_c6vy{U)H<2sPY<7ppe-!Q%JX3)otAN7)Di}ygW!|SzQnvi_`_%rBy1oY;o zwDI(o4?%tv3UVif0rwS`USJ~YL>n`g0Bv~3xr;C?u2{#!)3#j#y1}Zk0m|7-DCx`T zc7WF+F<7p|`I=;_u-cf5tTc4lZVg9aPWZjSS!XJc?dN2>>7It4JWRR8X9LCD9Ea?q zk-<^x7=an&RxyH{Q${H|nI$lpRFm}YC|9`Dgc@V=@_nF#SM6D85bTlsq!FIAjgwfm zR0W=Y18BFag}K`CY`Wj$rpdGhWRzEw#PZ*4?9XJQXx{#bai}rRw90g0`q3%QksXU= z4ELz(oYb5-LI+R#x@`>ET^Tou0l8ii0g0q=b=n%)MU-Ai3<%TMy>B@ECP_J7W z0*(0LYjjlwbLmHqg|Wk5^B$?TMTkNG$}th)a0n~SDl~o|GCEpCD@agliPE#iLgU9z zPDcFnp^TZ(SJKcye_Dq3U7OC+xyx&aSS?nGh-b$yQfPr}GF4O)yayr*saDhd2hp)@@c`0eyGC+y$81dE0Hv?->;XolL)$<-#owm0pYA->?un{3I5kgfL+l;joZ=qe~WCdKJs@`1vh;%=&Vc zVTFj0g+-3)*2z{%$s_nb?y~hut886eOX9gV|$J)Bfz2@9uy0C;ZzM! z!;YJAZ3JWc?@S=Ri?#S9c|NCs{jY|#lF9oJmi{WSjA};l>xH&E0SCKW)A4!1Op{MC zLQb#i`XHy>iBT~^3dp+Rqn=Agtejxq#`geVg7Q9eMBf@h8k`Xw7Xm1+He3x}(HsNX zqgCXOxEg`V5DCQlR*XkF3ve@rJKjza5cLq34#QNrmsmI6Tp`i|s8_8~+1zMg!eRaG40m+R91FN#_r&cU8?W01afTw zOpkjALXsn2tG_C%(r4_anb&c8`+5{2pwqX#5xU(0@-lwu`+>a8)c^ks3I89f-v8^b z{J)Hq|3}-z$i&9_e;E?~YnEso%AoWF>4XUUK_moo5C#sX9WM-+>MtjGnCPBnfNtFW zUEA5{%(^WGeWXB$BX7yBRCbl3hHtA{IayHVT6=e`NF|`q5>xInym5C#UPIXvdVbdI zn^&G{lBLt{#Y-dQT=LNkZIo(AQ;G^s%J91H+q%71WcADHyT)j=`kSr1yy$pEQd1LW z4)y+Ek)UtBrVD7>)Va#_rNV;aoQVt5DNF2zXWeC8qdj3+YOj0Zl#?HPxVgKyTTSii!gsyO8Aga&7b#lOxM;f^qgeH|nU9~x!H1aiWaW?*Bf2`$Et=aX*Q328zQMJVK?sJcy;mp&OY|QnU zPntb~5(MkTT0z`JTqT<=ZTNtah0VtNu&Th&?ccB5Z|b5R#+~-P(nux4T4hRAoy@R) za~*8haouT z>@Z2Mlf9ir^y)pi&^Pc`-LLzuQ7G{^fCH-sq1*7Dug;>$63(fwN|6?*U7kGr13CVn zrq`Tbu~fl2)AaxoW!Ap=X;wZ_6N@RBIEz_9;B0v^Tu_DdwA#z#yIZ-VPJ$sAz*x2)Y; z+;lBy7p4^4l+5?^mqo=Yt7w69&-$#C5T(e%ZjMK&WuMMme|}P;h;_=2y@DAHGo4f;DX z#4Q$kxHGgSkZLGp`AmeVyq}5Nc}Y6b#k|t6hVeCJR5R5*A>ABnnJn7nLpz;NdMY|) z?M#D&;AD$t6EZVPaBYVER~6g_AE$tnb{2l^&OyXoBSK2RbMV~!D{7eCCnjPl1ex7@ zh#*-P9z)^DzbyA)6HMcZ1Ii$sx96NZ+wb9+bqi4(PbX9ZVJBfO1V`b-dJnK5kRLNc zJqdLkLxlIiazfTS-jfRV?|9@VHL0Ml?7W(Jj?>IU5bVcqT5YUYB|?n(PIfsIb|TfwT-j`a!`Z_XN;Kn#N+zGZ z&Tb}iCVg)wnQ4)-k^T-nOO(g>xPOi>W$*;Xa~InxQw?5O%c-agrEh@}Iac{b8wXuQKhbd%_$WLzC@ z>gYrqK!_@!y}*~gJ`Ys3JYZmqV&9ts?%O$vU0P4W1>dFggE;Ytc>NWL0w?+iy3GI8;zH@gxpV>(_$aY zonY``*4EeH%YvmI;?QF>>>wG>4nX^M)!HjGQATBB^RznV@4Sl5E)zUZ6OzX;dRoXN zrza*Y#5=fgm%pf|q$CnGS02h!7_|;}dX_jX6vSpMFOs1yO$rsBs1QJ+VPGaUaM@^~ z47LNgCq_Kav;Vj-HBkTa?H#6#t!rf{2gbK8@ISukPf-ADfwk(7lk2xm9CXMtClk12 zY}0pv3dr-FOTKwLpzheJyEs$-g^Ij2%|hK7P-^1BB2|xuyi=e;Ebi9S4CEx=F33Oq zUwT5~Av*J8L`i%sG)@)YewnQWBUVzFUz#zp0cn3PhlGqlQfTri)D<(@S zXY*L4#RJ~9@ep^2J0UbTuh6z(Dbej z`D=!O5ak_s!7zekScroLXDr3MbjhRHaoo`f@N95rnFcxCv?A3gHa=n6h1V@}6c{E# zZ9W9Qc~g>T3K+|3!B{v#0N~G1GO}g0y9?zjfzG$x8dXYid-AXmD_~59ofQUwTldQm1^ICA|4&1P76TIwB@ z0k&-enwFpGrj}RD=JcW@oA5L0GI^cZ$eubDJ{EW#kipb?Xk$qOD zb6mJHu>8hX2Gjqkp+X0w=0vfgU7p3G;|4@+*>?%EdEZe$s>ltQBmdG2=H@aeXM4N3 zUMc%*WmDG3=cZ1bTJ!3wmd9piRmw`&=b=$`Zh*enozxcP$|#`Y zK37$$AhW|G^Qc~k(CimpxOQ~l9jUTz&TbNP`{IfUEasH_VFt5#H9rf!=-jfh9v3SDLXfWq%8Oi z#;=FCt#xUEdF_I0S>yCgE(Q<>Ir%s9D^>f%EAeN}&FX6;M-P9*vFyF;54`;yM--=T zoY)X%_oStZsUCWp8TCO2{@h{1!;*gMlFwJ@9m}$ZwDN!D8)Uoh#t{cE)YN;{X8MuK z)8)Z(L^h%ik@F1ZHvl1asj1ZGv+HxznJYhAifN(mM!a{)>$^ZujTPiK%f&C_uBP>0 zp;(VOaqFj9Z6;EEIu-@=9@s8u1R8o)XBWBPYEk!l47d8VKD4o`8t*KH_ktwTYe3;L z70@#W{Ml&?v!L~pU8%j2H@kFWSB@qM-Z~wfBUyo46Rcw}KB%(GO5Pb<>1o|UWg14Z zy~GL{HR+P{2(tD%1HWz~yFPvAsUD$D$7kZ3jq2Y454U?UresCMU@$ z3Tlj}&pvmczyd7HCjU30{hY2`@7dRd?`HND&CAkx^iM1zYagK>O)940Hg$A#xr6CX zb-DuO{=c|n@X}nTiy|LIo|>b%?i*AaWGgtc#m@VvC^{#3bf6qMCkScCCeP5VQ#jtV zr>dJ#)7AP9DlV&-)tWH|=QiV#{MRP))eoX@H3yH1h~&S^{G41YIcL<$l7=56mFhLv z3kg|-(ke}2!%;6V8OLX`kDpTkZ|r-W;RR%xD(^i^A&QfzTAF`Ce6?_4|Gn2(5xG9Z zI7BnD-dSM+qrfDrT($Sh`yO?RF3bE;{-Z~yWU2&ytepJ*T!Af}Lldhs?+7f8Ddk@n zm6`RXWO&41#r3bI#v@hm<-!lV!cS0FKmwjJ#?75RWK&3@i*gU@D9Ki_seLfS$R}pj z8e3N@+ZH!>2n(tLgQyZ8=}B>*78gIU&B5X~OkuB(*PcX8y()?*Zi$%BBBW&WPfb)Z zNR0x9SL4goc;!jyh{g+}Y5@%^1Y0cUN#UwIJzk4nz{Dst-S(aZ zbYkMj+~^RSFf*augu%7(Dra|-i!#%L^=Pr^E2c4ovPIRG=ICxn(odf0HSG-VkLIXM zgquoy%Nq)-vAZnFs?vxJ&DK@8qnk!yq~?&Hp`wU^w-xQ83;uSojA<*oJH8zVb3I`t zQ*=z65j{v}G!0}I>a@dqrRX>6WiVZ(*W=LNW9Z&Akyp=_(L%Oz2$}5%4D#y%Q+;zn z_Argxdb0+_^cOU2Y>R9z`sEcNJ+Lk5f2vAK?qtP7=$x<@`J~1c^TG@%Zb2+Pr4S|! za^L`q!S6ywIm** zMW(`Ivak)b^2ZOs=mgssubBQAEM#g6?U z^^-Iz^#PL}S!y#p6V!l@%0LZAUY5*_6S(0kYf%&eXtyJL%o z=SDH^h+3IjcfM6Yn<1r1c2@pe*IX=1FMr11?Ns?nns~Sfe`eZl6}iU_39|B?WfF-P!%hSbx-Ia@k)7fQ@HNrEb5O zlRY}DjY!0CKUA8kIg7LXlyEn)fmtM!a_xl%?F@`aJl`}K5&j)aa>EJ>Nyac`S%PNS zh$QwrL#NVRm@C>EO4jf(t*V=tlh$h3xZyk4Kf%!>#E`diR#g0Ez`QA`u7Vie$bsvb z1oPJM%(=w0L7hYG=p<9gK(>cl)yq2v4ViDV4-Vvl2dRw>XS%Ic?1W)pqiHIA|BG|C z>uYJ4dk1U-HA&9%fye#^7ok?<#wxy|9EK|Ph&au4Lu>+V`$F=Rkc;hiCXa?YVsDXP z-~lAsuvZPrsfy3%LVQy!;e9>eN6f*d_#8h&+#&cY%*-&v8w}yDS9O2pu*ko*e>5GV zxx&8DEMXB$jwttBdd4>`>h&Mw*$0kr(5vUi!Zmv)-Tb)uMrp&YMSe-Cc3DCkChN+3 zY-IA~nkSMYjoTYi8o+Gze40z!weJJw&Vk6NQ@3bUNP1H)W2-D zV#R(t7Ks)`sIi!14AU9HdV)viSwb)e1nJLAcDd?^Mp<*V1x1=#E6GSeICoUgW!9w3o=# zkuRiR-U77HarCQ4DIYl#_EDos%&-4=i(E}zg&iY5rGI-7(u7kQa`Gspfj(ijZ>Nf@ zbN26B@GP^a95r(65xr(=aP4m_v(GArX);a{$&4~r!ZvsTO-kM@FUw2G5rVf}-eXXM zo>nHy@-rA0yp?4T9KC)4!eRp+OBkJ?bEIL_|DE?LKv}wcV znw0>YOk#QKy9fRL>!t3EuQ7Wc+_Da(C-0e=F-tyVThzoj6?N(%rBE_rvH&bZ?bF$j13JgHu0iaJ~516)B+G)%fx~`^e@hOS{<42NC%q zML6ShFHrq*1M{!;A0kMiJ98~qc>vsQpyslsav17oj6w=oA2*AEx)rcdY^G3h;H^qz zw9BM^#T097pdZ%Mh4bVQxZ5WhwE*Iwb;z^_RP3_?^2*u@UC{3)`9;s(O`+OL=s&$k zq{@am_jg+7W!zUkL3#3+>X3q>lnGR7ew|$Pob8U^YWq@IFFH4G;Dg`g>OyV_0izW2 z0SPeZE)AEVcON*K!bHv6PDmaM+f?bevLZg8+=0PZFplv#OZIP`*aGA}K)fetU)_Hu z_zm;m*Z|BJ8^1M(w8zHJ%nVwG!R$l`77ZZkRB55{_I%siTF+B;%NP|m8s11Oz2)Hg zEf|JJQE5vVFT{~~jgvjrbznpOksGXlyCysatA2 zMIXBYRJw@sl>8v020Y=50MwYaitRo}mHnc5*7z1);M%36 zeeU2xNv%(}}KlfpJjJUX$DJQu7$8lj>3nVl0f~EHYo+Lnwa;9rpb@j=72=89V1tI4EA*tBD3{yL8(xBw z)3TKn*Q<}Ns8&I$R6H;}XyM;{?8m;BpPQ-$v^&$&iK`D z_kRH@$sr3;wuN4dim+rGct%=!6@5X;WLZ&2qfJG#26vL?Ry z_DImYAm%V`C)#tFl9e&(PwU@XPl5sJcCY&+^*Hfj4tdghb3K!s4s|$zyG+XtQM-DB z=%49|bxuOiK9;b>56Z1-Dz6$R!n(>YPMPz@j!RF2>>(B?3bXkA3`$qKCEF`Kuct$C zx4>H`iSwTp_9#|9XQhYS9dwUSwdjcWR^ByIM@>NFq?X`s8s{wPPgK=xKX#|v$$#Iq~bAI66@2*?m?e~h)pi5p~KyGw_V zg2ap8!%n-E->?t(`3h#T{X}@%M-zbpm6Hp_^9E~GMV2DdJr7l+Ll8hne1u}D#1EBF zK9HhY%K0PAYoHtli*n8HK=B7{vobDdu(In5mrzAo;TzLn6t-sj1_=*DXZJ5ZNT=ej z)PEyJupP@~4i#>0vDAWov|Ml=LJ^EB;-&7&V7hm5{jR&lBfcTVH)mz#@i3ttfCP!M zE>CHE-BBB7Msh8&ryLd8IH@o14RcWW4Gc~zs32O&SS{%xna|zCD0}|?%uA?hs{R$O zChodhoh{gu|Uz585bY zeJxRc0o-6?X&eicN1)-mf1q2YEaj%O9iuVw1v(YA3`~5Hwd8|S=)bV^&L!d%YehUt z#u7fnLy(0ct&zWEXE(4%DcpLtCk)tFhOi^GjfG=L<&!zGTVrE)6lAmq{7c~YEG;bu z;#nxzq~~Tjl#}0WuYZ|}_MJgsL6Bc|beC_V7+VW?0(COEbi7qtad|0p<7p+yx|b8vK7Ief z^Gt@#MVoF#)+w}*i9I}SSJJtkXNyw$km3s>fvDa0w9^%6>KxfhnS>q7fGJ~9W1l}& zSXE_>gg&cP-h62!jdM30=J)4F;R4b#mgyyMLEuPaOZ*HEF+HhV`|1ZpWh;TX`)cBL zM=4{-k#A)|3~aWh_&|t#J&${EZUx7|@6ib5@5yKzIQghVA)1@LqIv>V9aAUes9VtA zB$Ez$u0|*(?+ie>bs?j>PwIam_%KiA@Iz>#WXO37%xD)-N>@wYo{hp7^XSBKjoPG% z^Q19I;*cQ(v^5qeJ-e?A=$FD;;;(PBLh7>5eJIujfS3T|={pm#P>at@z!Vp%mXZcDB#jo|r zP4G==C!~J%;#X52zWHWwystDWfdUxm~@t-?>XxX_DTUYL%r|o`HG{-3*kc zrU}mAL~#p`m}G3nvfAUJot{5E-4W7NZheSnJ}D6vSaxIy&Qg9I)1g<2f0^=2RVj*xnru4;p z{>=vN@#$s>xWdeTV}h0xQ$BKxjU%c%wT3)gt!-t!}&xRq2l#9xnzfx<&ox%A(vF zaGqX$Ix#)7esbk88c%3HynDMG{5$@8=#F7{4e`x-JXU*q_wK^DsN$~-^I3l@Wy_J) zNNBP>3(H)biB$vdHI+JI>xBRcy_EJM9eR`>&>Q*_msE~&F;BNMw*O;)9f--R7_pnc zxn_&bss12}ee2d4C`$ls%l|LD((HR(rh{NUGV)e4wcuvyHlQ}Pi*R%wY3t`vsqC;7 z!n@Q^ zf?tZo_Ayx46+`Idsid zN`@~C3P^6*EqmSCv-}IFrF-_9kYbw1&TxLY-Wd9l#u`q++JtXIvu z0Rh1O(pysQ4oeYsdq7k$ue`Rv;H2@3lPUSOiHC>e$ji677|MMo3wY`GHsNyDLJ;|Ds^P)Ag-S z)@kKt(T+6w@wezav7hNL!oP_W>DCwBAkg)F;*a*OV}1mE`gJ!wond~`$yh=f=YfD3 zT~|^5jQa9R9pM`HNevCGuT?*Cj-Jmkyxd^+t|agRe^*HAjE5g|M9s&gBfO^Vmi%|} zzUYIXcOAUWzsCU5OfgN&bc$9N# zc*eacf_9l^`6cvW)99RYMWuOMvnf_|X+{o$9Fih8B|mb`;bD3d$v~R_o*IIK045XH z6*ty^Qwgzb_nrJ`gkLu}))UF#l+bGs2v!3k4sNzcL0TE;al<`p1f#ad75FuG{#^`; z#z{T&z!)MvXD(Uf2-Jai%$a2RF*`6E4MQe{=*>0s=9NhN{)PFFAoqMA2LeRWI2i0i zym5TwkjW;~vBnr7;w9qM#g*~UOP zZ}pIOl|ddT_wV~uC7plnX9A3V1I!)5A(o@D0_0^Mc9CfL627F{R=-*=9$A0H)kL>1QTgv2y&9+H*4E65A~R1~ivA(F$Gq1CC`ea{{GB6CEuU zSz}cZ&FplOSKj&wvhzbaQr8{{+ij=td)U)>ue9yZLLlAyj;i-;1q27;eteI_T3)d4 z2_aX)nVu3iMH}G&T|TySNTgHK&F)uJ^Qt(o@jJy8+eKz0#I=FFoEqxHvC=o_6c%H{BPPQrav;wjod0;w zNCOx(&+xGnBTO`KQR{;{jjgo-az{=7lzPmEl2aptg`qHEfidy12_gF8bjW$}FsXq}`ht;q&UfOW!4Bax2p0b|Qx((%--W!McoZqc5PT&=IfCtwNerjz6h|>H*ZGPApc! zsEMfaKY(-{!9?GG&FT)7SY9aTq!ajsuA23(uRhmKu1*oWQ#z3!&?LRqr`tyT8Z794 zoR{00mOhP~zmmR4j_+Ee$nY01nr?i6`N9w4jM;V^!3bnz!`>8ZgqM1gY~RqH&O|I# zd+kigKbqF(Hr24)9ku?N^JF6eEuR=RRyBwBGNa(XoS>zT&UJ@#lHKvz)RcL1G>`|o z?I3KrE`oWxm2-ZTpdL!`Sc&b;u&}eWHq;W&?p(9|hd-{EGcWOUb#+bbki&0yaPFMM z)L`ABzRH)DXNmRMnmlA+_1oGAs|8+%Z~pQC$~je_YPcJ*dQ@S}!7D#+C&TY{un*45 z79VA!C(+?Osk)*^D|8}m6)tVQMJlW^N}VF-OP0@d|4p7@SiWf#sXTTFR9yMO>AA?C zA7Qi9PKe`nP@KS1DVr(0OA^USA%Wo(C4|O}u2SWH;`QG!!Q68FM?h9n;w%4SzxP8O zA*9b2*C-Hlfs_J^z$eABP|6KNfZ$^~X2d#{a4a0NN^ke@lfak606IBagg%*IG(^*i z77L>$P=l$3ZNwTSBO%wLHO69@EkTDq4YtCf3hF{sp6WI&m#$R}E1pYXB1+S8sL@%} zNE>Kzsu&c8MgZ>hFpB>GGW~D^2}=cALMv|ERC1_7=xE+@nGoo-ZY=)xHkKT$4wz*i znQo5;<41}JQlJL(XCl8By#t{Mq3F~utvQ%NK=&-9Q^UUN79#Y0eP(tMiGVB^VENQNm5XG5PyH^rFvr3WsP)x~<`HA0kRb z6&L|`;ki-&VP$J70T&k$QAU~XZ~>R5b`QL}2ncgeTv_NW_iF>lHQWuZi!wgrhhhhK z;LgB$ke1T6$Rh0&sLD*#5FQuo6w8j-&a3Mw5mvQD{3m7k4pR|k;(zn|y9xjwJxa9& z-PKgNUvh1)0Cl)<4+h9J-kH3*x0qs{F#Y)@%PnQRePYe|w-lyW?MgG@;1d@SWyu7M zG`R&mF&x-0-`KU^9mBDhh|XPad?1`|Ybb;IA%j$sA)DM|k8$0zSH&1+Nsy7Qk-dFV za`0@+v*@$Lb?_qQt+bdZA-+3erG8(mR3gGiRWjgS5MhI#2Rv%jwxp)}K(GZ;IOsfq zLkOE02dQAFUADQ79J8d&xH`|uW3cN}g6)^Zj&M#|kRJg6rljOhfHb638)R7)V7a(S zASDT$ZuB^BG^YlLiw`GZtmok3ig*+&7ca)M-xET>f~!cV-+F;5QK< zQ`?QAgV+m;^jq}e0TZN8#cvWhfzDGV;+#L(>#irP=a6d^hAUKxmgm1Pf&o=&z3(vE zO)wu5iS9UhkW<|x0x)kCR7bf?ZM!GwfwE>1B4qLQB0u(lVYkLZ`|0GO*nF@1_exrV zkr9Q3xr2QzZSfjt`)e@NPTFyvh{mW_-k_$C{Y|Xh?$X$!|a4iNuN4T$JFvcP(YD{8io&idl`_RZM{Q|guh!6zFUgb~pV zgQFqQ={$MGbl)7ZN?gXu9pMR6#1JX?NI5#B#}`ADGyy_1fC4iEc7cK62`7%)f`|Sj z?ko)uS1jDyoYjGH^fhWlObuF?m8LI}&s(x~kld@86xc7PpH!TZ+NmtF<8?3Z%+)>BwpgMa z18LCYj@A>>K3~Bw#+GP4x@4}*gN#nHb4r~`AI%;trJzrr)b{)AfahrdUq}74cfGb| z)&jtLp&f|1NsiCd`XJi{kxp-+<%NtUg61>;LvJdVTq(vUgP@+*?Bek}>LS0LBSYO^ ztdaJYCMX01MgMTYD+ClS7692s`c4xKuuj}b3Y5g{+f)uy3#Wm2()nIOa_E7O{(%{@ zvxy1q79LcwZp9#--5nRFq8tF}_pb@sYRLO1!0gCp;5Yh^s zOc{5uW+V`LA$)N|KVaRW%e|C-3>i1l8&`s_yzL&0>gI{lof_-qN~w2S)qGp05^)() zKmvgmQs9sDIPiuKx=U02fGSmY)B`w<*;y#GKFhp;iQ3HyUws2n5m8&zlauC11{%0_p2_o^1qJz6 zNhtNfP|;3FDp0wQLn)Xs5~UVEpckD1$A?QgsFAK%3&5yYFTK`W4A>53PIUhfjOAw_ z&Sirk4NP&&(UDU95O0#tN+FGP)PhUGd2o!NA!+p<|` z_;-aCP-Kn8J-rfJle~ILo_tt~|_|O%&M^Di=SCqHFC+bLLWi83b&}*eW-12xw_=19_&qkmk z`l3mNcH3}QJeAyJ^;$+4FhJbW+`J^k1f83(a2so-IML^Z{HOojxd)eB@}F-!6ghe~ z*rE&{H#>Imb7U0Uiz2*-92VG)#i_i#TL7#Sy*Hw8(GEsJ z9RF!nEL-pAG{n^lxaX!fXw0N$;#9qP!@4I~tV7&Nja$kTyW!eWcsuZK8BIwM4$zNsN5r)>c^kV%6DMH>z=-?qb624SpTDBS8u7C5@qsrCz zG3STIyc=S#Fsp<1xC=WAF9K!P$l>?BxKfMdb}v_^`ER!041bKY*aQ_BoJ=<# zp)&K`Sep3jCr7h5$w(L9kq#hR?tPUKQHrZ80R@E%PHFjjzka{3-+izC690&%(1{W? zJqlbxu9s)w@UI8fnM&;aA-Ip&xjqEf`3Vy*U~jp5{aLMYPl^k6IiZLwcLQ6U#?9y8x+?QGOo+N|Qb z+Nhi=3S*z4>YPPPd8<~xuy@+?c`YAl<&0Fyc*rGl3)(#O7eQ1ts2*seh7gowbG9OxAh)Mh6FOV@wcxfs#i1w74n(} z0nythYLW1|bnT|91=Su1A<^xtd7&bx>0~W}U)u{ZUhBbx<-bqX3W-%ej!aea=)L^w zM`O|-WX6u#%l?~;K3lV!=Y?xQ_1)&;u$R;PjMvFpD@b$%9I>OB6vT;+bW9BTDAXEt ytXR-)Nrae*LYx15WV|X%%xI`#Kz-qOH~hTDMElKDG~?7A#Ap7&?XDn6cv6w<7C(4HjyBe zs>$=bndkA?lcVd)tCv`LArK^V-@Xfg(^EJ*6U}O#z6w?1lXuPPZgUTsz-jX8H|u7$ z6qC#5*P?Ewug~G)ru{B>lH^ znLg6re_kl=J6T!lVpQ<&8)d4Xq%%IfM(83LOJOP#tuqSf)@q3X1L!Jxt0HyL4pNRF zXQlFCVA9i;-X4$9(ydcfh=|rfOPE%4TNDw<+xe6#A?X5D5v>f4ki?@m(NIF=(B)Vq z*3v6by%i9H@U9i8`o=v~#QAa$((9;p_VS#Q)`atGV!|wShiIM#_>)E1h)*>C+n@g(7 wao;VQ6F6SXPjnos}EOy0lZGs(EtDd literal 0 HcmV?d00001 diff --git a/Example/WebimClientLibrary/Images.xcassets/Icons/FileStatus/FileDownloadButtonOperator.imageset/Contents.json b/Example/WebimClientLibrary/Images.xcassets/Icons/FileStatus/FileDownloadButtonOperator.imageset/Contents.json new file mode 100644 index 00000000..8744094b --- /dev/null +++ b/Example/WebimClientLibrary/Images.xcassets/Icons/FileStatus/FileDownloadButtonOperator.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "FileDownloadButtonOperator.pdf" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + }, + "properties" : { + "preserves-vector-representation" : true + } +} \ No newline at end of file diff --git a/Example/WebimClientLibrary/Images.xcassets/Icons/FileStatus/FileDownloadButtonOperator.imageset/FileDownloadButtonOperator.pdf b/Example/WebimClientLibrary/Images.xcassets/Icons/FileStatus/FileDownloadButtonOperator.imageset/FileDownloadButtonOperator.pdf new file mode 100644 index 0000000000000000000000000000000000000000..2e7c0f13edc30310e5670330a8ec119a50c8a927 GIT binary patch literal 1987 zcma)7O>fgc5WVlOm`kM=sm^}yTB>?zN)ZA?Nx2~o!ExK5B!)Oeg zY1-D3a8C`!0yi&glGH)SCtW6nczL-q)ME-8~tAGv3wiw)K21 zCYSZEWmV7KT+sAp{HtokPoWCgeIy6tG+EEr(o(e+u%k8(;R+wDQxXOPx}>(+2;~OC zM6(uJ7s04921=kVhkc>YK1yXMLz$!!8O`{rA$cMn0C1sUKsw`3BNX85M z6I6-&kqAjSh+qmGtqwTVAp?^vUj*d>G|bRAmyBkXY~|JLld(#?K%N3;mIP&zicfpr zCy65&X({hd5Q}j8N^HcZ|LrwCM`Kkak8TRA(!YBx6w>IZESbV-tF5Iq4!p;dEtHip z7+M{GK^Y}A!Z@^K^n~mc;z4@PwU!P|nxIO!!fKGn5on9jmr5!^h@Nc>^-(L4Ku*t? zQX&)FYj33S2!@c>=?*h4;*Ub1cpG1>AVY}d7=f2DEKl!O3EWs@p>T72VYHK|DnpKM zr(R9$WSUYyN(0_sAG)M zX5AE~l!*vDK$ZaEG#rHJT{)H1HmQ|XxrKS~_EU8j!8~pEqgjC}O=g>5Ozg>rda;~e zG>`O!b?Clq%srYJ7*l-Qo{IA=~gxwGsyH1Q`VxSr7LY3pTS=T3Yyj+~@wVBMCW=C9iC;a4Q`KLw^-E%wN?z*Z}a8Vo`z5aOh7qM}f Ap8x;= literal 0 HcmV?d00001 diff --git a/Example/WebimClientLibrary/Images.xcassets/Icons/FileStatus/FileDownloadButtonVisitor.imageset/Contents.json b/Example/WebimClientLibrary/Images.xcassets/Icons/FileStatus/FileDownloadButtonVisitor.imageset/Contents.json new file mode 100644 index 00000000..5cdcc505 --- /dev/null +++ b/Example/WebimClientLibrary/Images.xcassets/Icons/FileStatus/FileDownloadButtonVisitor.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "FileDownloadButtonVisitor.pdf" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + }, + "properties" : { + "preserves-vector-representation" : true + } +} \ No newline at end of file diff --git a/Example/WebimClientLibrary/Images.xcassets/Icons/FileStatus/FileDownloadButtonVisitor.imageset/FileDownloadButtonVisitor.pdf b/Example/WebimClientLibrary/Images.xcassets/Icons/FileStatus/FileDownloadButtonVisitor.imageset/FileDownloadButtonVisitor.pdf new file mode 100644 index 0000000000000000000000000000000000000000..733e1a18c699a928c6f3688e2057dd014b9d4766 GIT binary patch literal 2485 zcma)8+in{-5Pj!Y%!`4fKq%fXK#+&nO;NN(9i>1Xiax08wXH(Zwpyh~`}I9s?{cVE zKq>>q=x{EFheM98Zg1Yb(lQF6Nzw58AA;!hYkKoW^oP6hRhSlE{n#HKPLE^(u8Y@r zI`*5rSl#r0Z@Yf|-CMf;(0=Pi@t4q2O&?N=@v2&NEo03(ohZm;qVb-(Jx5lzYK+}M zM5(Mv+L4vsI1?1@8PiT`r&3C^I|HM(dMe|i)hUqnQb!Y=M`h5=J~mJ%ymTf-s~XVB z9I{rLP-4#3LUYwlRIt#qPI)ka*1;+pP%N74LYy$`6aFYlZEXfQC8LTlTE%QUc^Pf+ zN|mhB0Xmk5p-u#8eAd~V`FWEJR6}G=7FvO~RVtRfSrvTFprN%48O$9@&D@}nK?t}7 zfb$j@svG6dMi77o2d*6Jrj-mfq3{A_0%WZ9zMeq>mFKOr6OZEl8*vct(ZZG+)-O%G zZ%~lX9tT6lf`T&<*J<%XgMHcmMYB1ex>e)(5@+Z{b>m7k;1Wy5I>8}~vUO>-t^6oH z7CHXseXcyK@%8&`$|F!d<+9DzUN9QbB}t**jX^i*oN!eaKaQvn8my+lBR4196&Vs5 z8&3?Qwyp=MgLQZg7~(Y0@B+uuD(q7;mB(l%T(N3gSgK@kwW}ZJ-8E<>IUBCo3P*63 zR*IT>qP@t%QO&c9*N;?&{(45Qhr_8qoJJ}We*tedM_}X|=$C)~ z>}hqodFTdo+_p>mZp^hV-`%$F`XTb;Y%!l+&N~ndFE$yj_h|j zZnnGrD9WLIe*;xsjKjWvNiVi{FQ2qot%u=6W#5zVs}I|Mdu(F*fweiFrd=81gSfi- I_UCv10mk_Dl>h($ literal 0 HcmV?d00001 diff --git a/Example/WebimClientLibrary/Images.xcassets/Icons/FileStatus/FileDownloadError.imageset/Contents.json b/Example/WebimClientLibrary/Images.xcassets/Icons/FileStatus/FileDownloadError.imageset/Contents.json new file mode 100644 index 00000000..9f9e8ece --- /dev/null +++ b/Example/WebimClientLibrary/Images.xcassets/Icons/FileStatus/FileDownloadError.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "FileDownloadError.pdf" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + }, + "properties" : { + "preserves-vector-representation" : true + } +} \ No newline at end of file diff --git a/Example/WebimClientLibrary/Images.xcassets/Icons/FileStatus/FileDownloadError.imageset/FileDownloadError.pdf b/Example/WebimClientLibrary/Images.xcassets/Icons/FileStatus/FileDownloadError.imageset/FileDownloadError.pdf new file mode 100644 index 0000000000000000000000000000000000000000..d7ffab1c6fe09c16cc6e85c58678969a977996b6 GIT binary patch literal 2631 zcmb7GO>fjN5WV|X%%xI`)Wjc&lSow$-BN@AQMO1N5QmU;+eO(8Y*JME>lx=`#@&qs zO{Hq~*)#L}#$)^F`ts^jMVSx^BsAZD2!OLQI6oI|yYfGU4)OTC+un8eU=UpTS#7s- zn&|?xiFBUm47p76*n^--kON_4 z91d}WO3*aUL=JN-8LI@lFv`~4Xqfq4e0e3dD2AF=MqEzGIvk^+YJ=o+ZBmp;M%qd= zy-FT3&{kWaizzba-2`ZcUb$2Tw2m^PiaZ0##~uV#fNoXH|lUaqc=miR5S`!BW_Rql8irg`V~#3wv$2 zz^pUTom)R+VIJ16#_isVngpUUxw(2U9SnKM@wwuhA9o9F4MqcNW;9I2oEfbJqfC`8 z9%UcKLPeN|=YNaM^a=m|v!N93T0B@X0}4;OMBB{5zgvpBuZWbC%1ZEEIJQE8T~z1< zkHRQbxy&UfY|Jz>I>E-m15H^p2=bLU0BZ!Z!s-Njk!I@1sKn?D#taDrNY6t7w$e0f z2%9Uw-kh+3QjsH(G5kPVS*Auvz>KgO*Ez0=u)&aWTySCi`7@sD{nAi^;D{y7xLhwT zmW>MhO!1Oh^?GQXhPCQtFf!*_4N~P^p|L821bPq_6pFCHz&cU%0PzyeSyATlBWFTxJ$g!8B>eF=SZLP(pSPK0rt{N+F1?9(&A-}uAxKw(Abb&wE@ zuZH-8Tdm8B<`KSfO3KZ5C0pO{xq)B2bZzsnt6U5I8L}MFMYHYPwrjz^K_>9J+#v>d z`4RN_&rb*Ob$RE|F?hQ8IrOB7->p|I67!6gFeP3z4;W012{};vJQ8m8Akr{xQFdkB z+>Ne2drPbW5|2A~DYx15WV|X_)-aw*m(TC65>D$2mzu5Bo2r}*lydPO%}2N0l%K-&928Kksy_- z$(yg|@r);Dmlw|-QWb(LFRnyk)cK~hz^-9uuEl##yjUEqbBQpMc!EJ zBtskqH9Wzw6SqaG9BCsm;f!VBgerGdBbXTH?g})MoCI4Og*`Y*S&B?yq&=5~y*7No zT<1*p)cF}J^Kkt-wEI|8TNIVaow^UEgOJA@?^c}iqg!%oFj`0_v}G#h%xEVVWvVQ( zmVFp2E5bYy|7#4>Tm1d!Kylxx$~j-xTK;>YEXGTD8`5h;zJ{<7wz9SJR!dB+Q4}dk zQ`G@Oh%P!n+5_@V=@2JROlW$00@V&vr>j^KIWIe4Mq^qu9ANE3N|lf_KvhjnMGqwT z;Z3wis6F`{tHe57G$~U@13MpTZz9GUp4Qk2u@R|Z6J4BQ!KT59@P^o+q12?w1n{as z(GysNDtJrQ0waaTSvjv|tjH2YEdjw!A;ScDNtCRt3zMs}NO1y0JOWRb4i+jCyf>rs z2sw#Y*(z=8Ii61tp1OeICdz+b47WsDT+>#XS6JFsht)<1pEsM$FgD)eb$hj5J?VeS zkLl75>naQU&a?Chs}i5ZvK{*GyQUpvTA|AcJ@2>scDo;B+MhFcx!M7fu7KYE`r69H z zNc`Nj*Fwo44)sX?C7yoc{A&RrJr*EG9 E0nfM9T>t<8 literal 0 HcmV?d00001 diff --git a/Example/WebimClientLibrary/Images.xcassets/Icons/FileStatus/FileUploadButtonVisitor.imageset/Contents.json b/Example/WebimClientLibrary/Images.xcassets/Icons/FileStatus/FileUploadButtonVisitor.imageset/Contents.json new file mode 100644 index 00000000..9ab332c1 --- /dev/null +++ b/Example/WebimClientLibrary/Images.xcassets/Icons/FileStatus/FileUploadButtonVisitor.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "FileUploadButtonVisitor.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "preserves-vector-representation" : true + } +} diff --git a/Example/WebimClientLibrary/Images.xcassets/Icons/FileStatus/FileUploadButtonVisitor.imageset/FileUploadButtonVisitor.pdf b/Example/WebimClientLibrary/Images.xcassets/Icons/FileStatus/FileUploadButtonVisitor.imageset/FileUploadButtonVisitor.pdf new file mode 100644 index 0000000000000000000000000000000000000000..c11530cb928d777e066b7b8b4930cc5f299cba6b GIT binary patch literal 2004 zcma)-S#Q)Z5XayBDdwe8i&V$=Ayt*=mLdd*vgNJfA!OZl(QX2p6cs)_W5vZx1>4` zzt&AyzCDA*)%;h}lb-|wu<-*n8OO!SgEBDzlf|Ifl05$0=$n>sJ!$6gEfIoR$-T3X zq43i@ONDWaODL#_G^Eg?GPANw@hKh+c}aWls)Z6t!4?-iF|H_A%vlMWN6>P~nE*pI zZcAqXD@XxNTpF)&qh{0)O_e|qHiL?K<2mDiA(Gl0rKk}_Ef(I9~;COlrl|nPDsvu2`Rf1X6N}d|enz!0O zEJ|2uEgJ`w(HyhDLM_uSGR`d42O1BqLD{Gp?+orSP-;;@af=DV5K^7BxjEwS6krl;p*wyPz*paw; y@6NFuhkdsVM{u}a9qpS^ExT@jSn$~3>T3NbVD{ttTknRk9B!Q;2M2FHUHk>Bc%9h* literal 0 HcmV?d00001 diff --git a/Example/WebimClientLibrary/Images.xcassets/Icons/ImageDownload.imageset/Contents.json b/Example/WebimClientLibrary/Images.xcassets/Icons/ImageDownload.imageset/Contents.json new file mode 100644 index 00000000..c7105d9d --- /dev/null +++ b/Example/WebimClientLibrary/Images.xcassets/Icons/ImageDownload.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "ImageDownload.pdf" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + }, + "properties" : { + "preserves-vector-representation" : true + } +} \ No newline at end of file diff --git a/Example/WebimClientLibrary/Images.xcassets/Icons/ImageDownload.imageset/ImageDownload.pdf b/Example/WebimClientLibrary/Images.xcassets/Icons/ImageDownload.imageset/ImageDownload.pdf new file mode 100644 index 0000000000000000000000000000000000000000..91ff6fe8c6286a4f99f25e180e0e489495eae56b GIT binary patch literal 5236 zcmai&O>Z2>5r*&cE9N4=K6u#G-whZ(Sh9m438K)s1vxOQl}v*o6D}P%`Sp3;?%A1< zWgvuMtlq8as;-Z>s`u5~H{X9{OPMB{-Td^ApQd^K`s?|dZ>IC(JNE_M(-}k5U?eD&wU;l9Y*Xc6-YqG`lzu8?se6@Z0{JXMb{!*S# zA9u}7Z2NFIJx*4a(2B)4n=PR_Z*4}26MTy;&lZ;AQubxGw8W6AnLY*nq~n(sN^zU`lbLJkd|tQ@_oZ>iM1%Q+etWMIhUks4SO4Mn$~L& znyb{qL`s?^yx*|#A(wf4<OiQ4Od%C6ZxPQ`AtmPGBUrN5 zN|h9;3|R;#Q9P1qE=wlMw_)#qQGaRZQH+wJh=TtVA?l+dRp3Y|B_*eZd)3FvB`8R$ zFPXlxNHW6#QiWJaDN<09Lom)N5o=B*(m_(4QT8qszd|uo)wTC7=Bo+Us{}Bqw7Y65 z=Bwmtv9v;=CJ#ipnK})t$;Vg;8enp2m=2oj6-K+4FuY4RP*7Dr+6rx|k;*OOYAKGc zi^RlJVlq)G5wA;f6v=R8#6(t8Ao%cv0#K~V;gsppAy+)N5Y(WH+NJ0yAG*UFqMS;V zmVkEXb#l3ez2)ZA>bVm3ek}^dNPw&s!)&)|Rt>5F;$}pG(+FF^1Q1n3yaYwF)oN*2 z7;=doP;y8%h1%tc3lI`eGNRt0nv&FF8*W69^z5oj4RIMAsPz zFgEVpLCUwuyH;qJ0f`TF2W0Z7Cb+p0LlKAbgr}PbusEtfwAF|SeJuMxnG)-#1;xJ% zRU$|2;z}s3K(K8wiX?O>a@CLoF%m}PC<6FeN7bbe@E%q9HcEo_r;h|#O|+j>IhNLxst@o(H<7InW68nmgc>xV51TFE*H~Uzlk}5{(Xacl1xE91maxn$RNzCyr5JDCmCGHlnq zB*cmob}XaHxZZAht5EnOQbbu0Th zs)@KDnJ5XB3uL4=6jWpAmDVu9BpAA+3%W+mQ`TLssY_H=)LujZ)kf&y5+Fx6t>JDk zJ#Ertt7#KJj`2(Ofs8}~l|`MwGC^fB2BrV!-d8j=K5T_YV$6O=MtRf!GJ0qo0?4ui6fTFexuS7_Dr3m$uGysu zGXoK5%iSt0NHDu9Id)E9YAkX?j*S3D0;-HY+M6-%p?W#O#$!-fS53?-Q{gOy>s61o|y<3Npsl+i4dJPay*ltSjtOAk7E*2!u%wi!n(; zWp02#YIJ+|f!WW09SfcwM~Xvf4x~z^pM=nROb#K?w}u24V*qt~4(Dh!5JqeFT$IZ4 zMCnt=JzI{QjHZ@EQMY}=@J=^t)YneS1jP_^y;C+cR zOp5D+Ru`l6e>HrTDgH5hdWC{%!QxpCc)-MDSyapi%Bad5vK=vaQ42M3o!3gM(3b{d^5jtmG z?SbH7#xwzGB0HVL%y$a*6ws)_SKA`2DlMycTo z6gA#trd<(?KtifW>B-WiJawUNz!*#>8T5JgcRdzhvI54c-EYXR0G<~ZNY<+!J$emp z6~Y6ew}AMcMpPB7ZSgR{tf^H(xi!y*aQ)F?o8H#IC-m5lD_PyzH8Vwo@vo^RgXeK@0d_?=5K+wWgDf4F~l!7+J!J6{dIe)^fiY&Ezabouloit=#M5PN<1 za`*7`VO5p&%jVU=iC>=2?D8<6{`~#_03Qw!e*gdg literal 0 HcmV?d00001 diff --git a/Example/WebimClientLibrary/Images.xcassets/Icons/MessageActions/ActionCopy.imageset/ActionCopy.pdf b/Example/WebimClientLibrary/Images.xcassets/Icons/MessageActions/ActionCopy.imageset/ActionCopy.pdf new file mode 100644 index 0000000000000000000000000000000000000000..e06beef222140913dbcdcefd6ebb3fb15d11352d GIT binary patch literal 4969 zcmb7IO^+Nk5WV-W@Fh}$lpcTEEk!wGLl6Q)*ldR5a;-oAeG+{|g3jMD1x+wYT7FJ7pZFQ@h4hW++{`dwAZThL;|ztXC$^^rRQHyYG8t zoje?O50f!-h&E`gjGqlXXmvj=bHPS%KY(G@bQZF<5D56BqA%4Im#h8f6)bSp>7BL_k24Vx&a;9wBxd z_GF!Lv#Gf&26;Eo#7UMzI)S=39otPGCTuGEiEE3qOLpK|2XWQL%gK>$NQZd~q4LaEr@k$WMFL9_5A4I6tP&irXPVDHQb-`#5OF$61 z+>f9XeO{azAqj2=L))>RdWoEYIV?Fk;yK%18W{mu=jr7LabfOIt;|%p9Th8M5`#SR z0TL(iLlBCJFq)%DamOv0)bbv3Gk}}|ibi#Gj4rE=K)Y$+wM5y@(QC@1tP^5NiU&kO zESS{b5a9t6trAS6Pbypxi6ljrT|^;pNr1Fcgcm4ZzoR`$z#OMoA_t61t`zoVkSX@J z)5G-B|L6>ycPMhENB18(1KI%>E4W9+noz7AL}uu%T!37-kh3!=W++wn=R;>>D>c8I>Do{=JOOj-7@ z@EDP$$wjV=3bXT4;@qHEdKn+9AzQ8M@ z8>G3q2rIRqX}D+p3N}mzZP1`BgxT;s!_&t(NE~E2=qpk2-Yt+CkI5ox(gmK6Xc#IC zLKFp}5@ruONHu)v_W8iU?`eF*wh~BK1TwLZei4N$6om7oA_bz%A{Qdyf{>6Q z2l(rvLhXey@er5k9@cBosgxRKkN8r4thIDm45=QM!mpMx$Rfctfl{P`lwq&3Nia&O zt%HSNOOpWbr&=EnA)#QPf>itnMjGZ4{mhfu6b+PuDN({!hNx|pM_fffB6R}?7LA~x z`I%@PY056Y=}Ndp7D1CKTBS*~V4~0&Bo{h#0Wy;HtVTi zB;t&Tpq^nI2Gl?u$|Y7J?1CUJ{bnuB6#m<<%IHd;NBG-E`%psPMCu0=4`Gq-C-}kM zzpHG1Sa0sOuMVHoFX#iM{A7?Q3P9!O%lO2@*RS{>gTKx7dOUo5+^t7NA8M`{y*fOc z)`!zkp|5Jfl7JwGY?`8Ii*B9XiXL3=B-z4ix3*KH{rWy?WXDboArm& zc0W*rZOY^g1?xkgKn70Emf}2z1-e_J-RmAImYlGI7a}MUxfn{9STdH(E~Evt9mGoF z?DGQL4bjFFnPmY6E13kbQQu>RdNA8z275NU!P&;koT7rhBz9P_TXx!rZ3&Wto0$X@ zvodA2aBal^=!uyGc9+$>EhMf&mt8xwMKw555~S9_-QX<=xtNW&!3QtSG|U&O7@~k?4FuY~FhBi9oyshanfdj6O!ZBuiL6C&L!;5sCw1N{C6|hxDAJ zkTsJa_?e*Q3FbZBV6#guFZHFG$P!Y4oG+|E5{M#XFAHB##W{uqZCJ;Sk)%>e&P7Iu zTqHS|_szlQ4 zbBM%*7()&j9fmNNLSlsBp>oavTQ1b0&pA7;p*L++ih~Cm$)H;V!g&BN{RVJM){*Fd}J9j2FarFL@%cMGsDbsyx!B_$)49eg2)p2R#2 zJG33g{xdkix*>B3y0{mCN-}FIq!zK zbTxg>N_VSor3jL3Nq|u$TeQwvOss_ISg{Bps1?qN)XF5F2ncA$@u7ZEky4Tck}gAG>Q{@RN`#j9#W(@G1Z@i5b`cl34vzueaKVm{sCaT2NnbGf!`Co=x1W)`!E{Xx_CNtGBoR Xj!2?@nQe~eniVsr;mMP4etPpCj|$AA literal 0 HcmV?d00001 diff --git a/Example/WebimClientLibrary/Images.xcassets/Icons/Back/Back.imageset/Contents.json b/Example/WebimClientLibrary/Images.xcassets/Icons/MessageActions/ActionDelete.imageset/Contents.json similarity index 83% rename from Example/WebimClientLibrary/Images.xcassets/Icons/Back/Back.imageset/Contents.json rename to Example/WebimClientLibrary/Images.xcassets/Icons/MessageActions/ActionDelete.imageset/Contents.json index f24c5fd3..f9e79ab2 100644 --- a/Example/WebimClientLibrary/Images.xcassets/Icons/Back/Back.imageset/Contents.json +++ b/Example/WebimClientLibrary/Images.xcassets/Icons/MessageActions/ActionDelete.imageset/Contents.json @@ -2,7 +2,7 @@ "images" : [ { "idiom" : "universal", - "filename" : "back_Icon-32.png" + "filename" : "ActionDelete.pdf" } ], "info" : { diff --git a/Example/WebimClientLibrary/Images.xcassets/Icons/MessageActions/ActionEdit.imageset/ActionEdit.pdf b/Example/WebimClientLibrary/Images.xcassets/Icons/MessageActions/ActionEdit.imageset/ActionEdit.pdf new file mode 100644 index 0000000000000000000000000000000000000000..3441f1215f31bf93b3957c5a288acb41c2546287 GIT binary patch literal 2060 zcmZuy%WhjY5Z&`DxG|6{LUH(*A%P%^*i8|%MO~%4pbJ&Lc2vkxqpK8YzrJTk>FPSp zT?nJYdGXAHSGPCsUXc@H$a=f~^DkrV>(}J>`C&$ z!YH}GsfK7NI}SNYwiI1tpF_38=Oie&SW8Z2hOG^eOOZ3n3?=V!3>0BKd91Evs@|VN zN3k$=Qn;jO*_Glwgsozp0y1MKH6;k-I9AF8grQd9!?9;ZOm(l14|rs{}~RGLn& zmnI=BA!!nmR)JD34JlG8gi>=rn3jqnYoqr_wV(vZ(^E=F9-L5O54yofL5^0`X%y8e zwRmDhIEEN(aG)Dn@Fk7{`>I%k66yu5%v>W7j-242@m;`MK#)*_+M(od-wu;cIe6qy zkxnF#gyKsQW(Av^YZa@fI7uI4mknyo_rFkNJJrQayjS15lBSU zN(K_Nw%Qg2X@b}S%=?VeBaj!NkUA1`0D@nnsdiFx)T;Ha_LXp|-9|0j-h#;D)Rx-7 z*;W0}Nxgm1c{8jxItBtRFPeQMJp7zIVociTpcdz@ell8X^U2`mi2`T=^^8)H4xAIh z#K7E1po-?&8XSXODM}Tir3u(`+buj|G;5}W3*$Z|aYe=8Gon5uIfmv!c3$HGsBhcfmv_um=q7(dv(aRcr=emo+e9;3>FTutab^q41s~ zQv_oP>!f?^5p`X33Cy>`;XGDk-)%4eCqHPe?i)k%6w_tYYt$pEX3saHnmwZKn|?3d zBi~_8YjkgR<~O4o{Is6WyIj|H-|>_}nN~cO!DaO?ei`mItGD~F_Tzl$Pt!l!p3@%v z18)Yt#c~+;Uk>YGwEC7TPw4f2cN})d(dt8T25(mfU=}N5f`0$^@4+r^R}TXWXYY^d zOz+a-r_J4n#5CijJqNzte}OYP1-v*KpL)V#n|bPL*Q?`dyMH*DYV>i+YL>*;!*FlB z71L?ZU&~pC^xjSw`l}t=-33Fhe1l={V|VIz(Id3dGmJ5X-|C5O^|S5qu-a^ggTZ^f zcz**{UX1(4;iY}CxqI2USuFSaqt$yi;ujw_{|&fAd$(7IW4je$otvwxAAWuJKf{5m AX8-^I literal 0 HcmV?d00001 diff --git a/Example/WebimClientLibrary/Images.xcassets/Icons/Back/Back_dark.imageset/Contents.json b/Example/WebimClientLibrary/Images.xcassets/Icons/MessageActions/ActionEdit.imageset/Contents.json similarity index 83% rename from Example/WebimClientLibrary/Images.xcassets/Icons/Back/Back_dark.imageset/Contents.json rename to Example/WebimClientLibrary/Images.xcassets/Icons/MessageActions/ActionEdit.imageset/Contents.json index f24c5fd3..e797cd15 100644 --- a/Example/WebimClientLibrary/Images.xcassets/Icons/Back/Back_dark.imageset/Contents.json +++ b/Example/WebimClientLibrary/Images.xcassets/Icons/MessageActions/ActionEdit.imageset/Contents.json @@ -2,7 +2,7 @@ "images" : [ { "idiom" : "universal", - "filename" : "back_Icon-32.png" + "filename" : "ActionEdit.pdf" } ], "info" : { diff --git a/Example/WebimClientLibrary/Images.xcassets/Icons/MessageActions/ActionReply.imageset/ActionReply.pdf b/Example/WebimClientLibrary/Images.xcassets/Icons/MessageActions/ActionReply.imageset/ActionReply.pdf new file mode 100644 index 0000000000000000000000000000000000000000..4adc0d311548c5a492b36c19b1fbb8cb06a309a1 GIT binary patch literal 4899 zcmZvgTaO$y5QX3SSNJ6oAmQ=-BB4Aa0fYb%79<`J56ff{7VYl9&ISd3Jzu$d-JVPb ziA+wn%T?u5r^@~8^{cNww>nRgRYo0u|8r96i!apk=hNZ-jr?hc{Av8x{xtnP8SQLMHK?p(HqK;qHzn<}sWB_-b#^vn zt8M{n;CE6sXjffIh9x^7~D+ojaTw7{G$k8jHm_jIqx0Vf98VJ^9 z7nRq}8Bc^nT}(+h<#mcRC##AMxngcbhTQeHZr6PFxX!S}k)+9=GgW;~errh1seZ5^ z-r+Y1=>*_mA(M`?A?QG|BMV}zDZn#r-j2?#+S`r!Ri>hwI7mTCzS1&F-f4`P15toc8w zq`-AHB^22T%$Oo&qf;&!k_hHXOqLYr6e#eP!wY-oF`**aN7Itkz3<@YePpBd=IHz; zI4mCzM#?1l?9a{x=Gfq4Zt3gia)$PE=>}Q|od@Uvb?cMZ9cWZbjubfFZi1J~o7JKV zM;NL|U-lH#nYU*wGCBv&z3HkhB(vR78{e`L~AC z(2)Sr7EBPX@Foh3luN5Xg@VTktNF{LpI|PaVbXIzy+^rt4r&}tMBb#q1tb^DO2M!O zx+j`lTTR|_6GaId7=@74w%AGnNPA5w&seFoAod$a!QLC`m9($<=)vC&U@!8s*tgMc z;C3)DfY6_!q(C7Q?jYtl&#j%mW8h#NP1pKhXf_a!xLYbXFzLvMe6bul9@kV5D}%qAdw)-99W$v~KCYAIB=TKF*0ays_=#ar1>R zYxbN0x)PEkSAOLTbtAp8ct(scK+=vE&>_Pq%{n4vbz61wxFL?NYLZ zj7G--k))B-7$RGXEygL*B$1+mh_e)(rS{j@yZb#GL)KRBB~sjlv$P}pLF!xoq@Fnv zFvNfn$9iT+@2eGI!x6(nh?b4VT{b1`922fVd-MVm7)?xWGDSp2x>P~R+@NT_gb4dP zBD%O>L&qwNT8u}5jh5EN9BHK;fiQ77M$o`qLT?dgOkj|Z;T6@YwByXfNRMYON97V@ zS%9)MVETrH`cco`Z{$I;(>?3n&eO%L*&U>0usMU>-`jQd7B(y%WUsg9QT3>cR;_2E zw*~gqgX9g^un)B`+Z^_Re^P#JYVuGs39DV;PSeIK$Gl7S!I%glrPeeGiHF95!jHlw ziEhEAg(*@nh9CxPQPO4916xLlw)&|_Q_)EX*Aj_x2DvfO8V#x}@<{Wc#R$YWC9RE? z#Jo;(g5f47c!E9X>hjD#gu#hQON%RpB9ZruliL!ZqZQJvH0(io=B2_Z>EZ24;G5Jh z^P;rg94K;@cBm`J+OeKeD#Y1}80z#{gXsacnNNs#m`c{UXziLqZZ^``HoG*-UbGiy zNpQQNuoa;*4pZt}1?BR=*o-C)P_m9uMY6#SM(&PseO#jK%Ps1M9iJG zF!D5eWX5sl;tCh5mC0*NDhWb|&Y+(B%g`j^w_N^yUqp7_AKu(tzc_wSzaoEa3Xt3P@?*U$GRvXvksH%*d`jWZ?&@$lzJJ&sPD(zp zTrKF!QH^>W&zP4hI57#%hhlh#JSi5gtk(EzQ$GgL) t>XVx{pU%12T^)~)O5$#b-~Dv+-+>_7=dA08#}-v!2%nxk`|^jc{s$FR*Kq&< literal 0 HcmV?d00001 diff --git a/Example/WebimClientLibrary/Images.xcassets/Icons/MessageActions/ActionReply.imageset/Contents.json b/Example/WebimClientLibrary/Images.xcassets/Icons/MessageActions/ActionReply.imageset/Contents.json new file mode 100644 index 00000000..adc6b8c0 --- /dev/null +++ b/Example/WebimClientLibrary/Images.xcassets/Icons/MessageActions/ActionReply.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "ActionReply.pdf" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + }, + "properties" : { + "preserves-vector-representation" : true + } +} \ No newline at end of file diff --git a/Example/WebimClientLibrary/Images.xcassets/Icons/Clip/Contents.json b/Example/WebimClientLibrary/Images.xcassets/Icons/MessageActions/Contents.json similarity index 100% rename from Example/WebimClientLibrary/Images.xcassets/Icons/Clip/Contents.json rename to Example/WebimClientLibrary/Images.xcassets/Icons/MessageActions/Contents.json diff --git a/Example/WebimClientLibrary/Images.xcassets/Icons/Close/Contents.json b/Example/WebimClientLibrary/Images.xcassets/Icons/MessageStatus/Contents.json similarity index 100% rename from Example/WebimClientLibrary/Images.xcassets/Icons/Close/Contents.json rename to Example/WebimClientLibrary/Images.xcassets/Icons/MessageStatus/Contents.json diff --git a/Example/WebimClientLibrary/Images.xcassets/Icons/MessageStatus/ReadByOperator.imageset/Contents.json b/Example/WebimClientLibrary/Images.xcassets/Icons/MessageStatus/ReadByOperator.imageset/Contents.json new file mode 100644 index 00000000..c03ea607 --- /dev/null +++ b/Example/WebimClientLibrary/Images.xcassets/Icons/MessageStatus/ReadByOperator.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "ReadByOperator.pdf" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + }, + "properties" : { + "preserves-vector-representation" : true + } +} \ No newline at end of file diff --git a/Example/WebimClientLibrary/Images.xcassets/Icons/MessageStatus/ReadByOperator.imageset/ReadByOperator.pdf b/Example/WebimClientLibrary/Images.xcassets/Icons/MessageStatus/ReadByOperator.imageset/ReadByOperator.pdf new file mode 100644 index 0000000000000000000000000000000000000000..7b62dbf9e96dbffe27a9a999495297a2af1f782d GIT binary patch literal 1108 zcmZuxO>fgc5WVlOm`kMg(Di=rTB>?zLJE_`zMeHW+AKVTc1}X+4$eZ z&S!7$Vf{FN+jZm@;gk&zuqn8S(ef-F?J9z?6H=obHCAcPFbIH1q1)Ajb1EfMDF;if z&{8<42xnA@R4@sS8et5#P!dC>m5K>aR9oh_L(EcPmBC_3jY%~Ym;Yld$rIsju6fkP+G!J^nCodjaU2Af(xIccrAS%@ zp}=o0NXJGrZZ2h9IE_MMn9jRg5(b$AsmW&&%rM^cWQ*W{lUJ!ISsJzQevk!x>D%>*g8B_$YWL>Mohg1=A^Qo%ea!9LLh2-o>tFO}w=J zKp0qZ`Xa(F7_Fs-14Jy?d^oAkn8Z$BVcMii4DI|n&nmmXoS5bHZC;eVCE?EAZE!1B sT~qmMxGMJ7r`;r3)ASJL9u}WG7JoesG5i7KZ9h!q93!jMn@-bBp30Y zY`ycZm=ai!BRJn$bAWhZ|5T+do*!|(o}Nkvzad_7+&SP;SAtQoVRJX`u5F{KJOGPQ z#c5Gws)=Yu8!5O_8eLff4|b6y0<%aZN+l8xkVcA0E*0`9V~NZ-?kP{sc#`EPBFU5x zS%(c|hJc)~F?Jv!2;)3TQzmkbI?{}taTUv)< x5N7saL8{!i=3sB}rrO;0+QdcEcnp1a!pH0CpQR?c-^R4Q>q_$sX0zp&r~e~&)f&U42JLe6}%K^2UHRDX#qtK{lu^h>(bqZ9Xv-#Gop2gfcD2v25@l!v)kAH{;{w7Fn+?}t*Jq96!8U@m*XD`+eHjx z@*w_W*sl5~Pz2|+k#+qp)X!{o6aK85Fu%Wo>&NM*renVuB{GN$I2UhM`JohC316WG z&#kx4IcT04MO0E5I+0ta4GLOBAU$-=mZ9Vljkf{}mrBzTo+C82GH3`IBdnHCkflV=$%c_~rK-w6X2};RKy&3ZT1QnT&&U<4jkY9)9KE()0A+53bP5zlO4m4AC@Z{) zHh3YeoRC;lN+R!yH6= z4~}dzR!Aravq-}VEh>}MD5I%^a!iUK*05u7f++>MxZ|wEM5Li+A5s-1!7P@mD2Ha$ zkl9n5yde`5mz9>*7w1$q)^dl&DTIQGNW%(EDh{FOm8T`9u-I}@oW7(5=7Wn~Mnxo= zkn+z7X0iOJ?9V8guzWYPTpV6$GVRA$oLJ)+MEZ8+=|*ilnU0^rVqIUgukfw-mGw&@ zW%ZrLD!pd&(6ukSCUg+T?L5)zcH4(--$9(Ad-zcAh=G})uYZ39m_5|1K!G$=_c_Ns zWpsa7FFHyrW;}=e!Po5ziD5dh2z7@__CRUec3t=Nrd_4bczTz!nk4bM3rj`~$;L~G z|G^rg3@jnDIF*Md+rk`WddN`HG(BdDhTbvLH2RO3p^2SYzpK}quw(Hj?fnh8a@w`e q;S5gKi?h+2*}QFgh;_+mos=mm z(JH)k_iw)4@>%PaXa%<-;f=JesX_Eq*5mhNJrz8VW=4!sGMz|L>1>$FfLs~)Ga3fs zFmEtoO#7*Hg@7q&mMGJSTrx9oaKC<_mNHXU2;7mLAeE`Wm}1sal0u|_KAi~m>(`o! z`(eAVt-AJgR6LjsRiFYK#Su%+n}rpedL6>ltmm#@zf3kwd19?w*7PqXJ-#TFt?*L8 zaJ+AqpH{rvr6TO_+r@$Fx%#A(3WZhWG-W9ptiE#8C-Qgc->+p&vAP&b#(=C9#TXsW zR2Tbp@c>q+YH$sckuDWXk!-YY7ctDJaO%klX(CNk;8lW3AByoRgrX`G7x4&wp9&rq z6(C$bf{H3ptQ!2s=T_7mUHY{IS2byWpxP``)HMh0`gRFrv&m`%DHIA-1*|HO4ua|; z5yIsmJYFRzQJE>lvz}sQJkymWQcp)lWqj#yG8<0BDR9l~6@y4P7Q9?_EBOz#vqAOPrGT5#o+$y~a9zwdmlWG+oJ-QxEl6itEVvKfFGdO4x>`twyU33V)X$(qSV zx^O`~kO7**kn^1n`nCqBZM77rem0S|CK7Z%Ryox)odY8RrdCL$P#y6qtfHsh;|s^L ziA<IZ5Qm0YwEC9J5R8bzy7E{BU%bGeO(uq7{|p-=e3fzs8` zXjJlI8g=&Ps;D$Bdl4OLC>Qg_Jz+Y-jKA~4f*FWnI(uO%L>7)DI?N2Hq$i!BpqBOR zVx}^QT-rx9?t}(n08}_qstaL%Hq?)Y3mAwRB{gvM_evou91LYItW}qGrD}mhI_Amt z(~xpyLu+-3SE?6|0Xy6e5k(NbSZuOH-5PUHED;CgVvAU!7K>$w&?L^-l%c#|DJpxE zqJmnc&*{Ztz9~u@#2l`eIV56G9v*V>yJs z6GoXtZ&gT4R(Y}DRTeWoRgnms|0~EcO6WjQDQG|@4w|GO4Ilyt#r2@G^k%?{LTy$TO?J6b&Xb3IF1byg zRE8u)N6{1}B@97ZRu$40-C;s%E(P;)+Eg}MqbY64g^^N&%@mIcjAcW}Z*!}xGHy<- z)+F>AX(;FpX#jL73)h}C*tL>$iI0$Sh9GLRa@|gcg7!GjqTfl29By7H>4{}CUI(6c zVP@1H_wy-dQW#L7oB(E&hg=?2SRK=b`6WXn!I4IBi=9qVu9%x4nDWLoJSvWBBT20z zmK21OsZ=hL_6YI`vskPK;;Us66HphEOD+Wp;f2_S2)el%88n3cg&ygIFf0v63uye z|0F&`kRzit4hM~g-LYiO7k3K+(UdWo^myVaZ7h`%l(Oj*Cy#i5q5(A#OXX0-WQ15` zpUaA$U>Re|5_cys(Y`ik_1pcmdD<9X%IAurUVzCA}0w5C9t|al&dfQ5L>C z;Z}xe3n%2($TfCaZL#{~-iR?QimAhnq*|TvNf493o8_66VtJq(;?Q^&cOj&%>`jtV z6+_Tw$)$N=u0m5T#>HS1iam%>Z-|=(7|+AyI4PAd=cG#JTQCmHdsYJ4*5z7lBh*2eQmgDh+x0ue@-6Q}>A1?=RpH}0-(iTS;At-y%;ZVkdVym4L z1l*8R>F6Si)2igQ(E*zpVeh27=r1Gfcz)B)o zX%S%w7BeIwmPoC}sEE!J9#UjN5`GO&g!lq@0kLp=gh)mi{9yp6Kb&%Tba)8On{{zP zIcX5;+$EteqC^#*xEm!442K!g9g2}DgC<-y^LRf z;L%J-pEqIIq}j>wn&pHQ$yi7_t^*2d3;PLuieT+wAdO`GqGZ4$j(D{?qR2@K%6Nbf z8xT5QmMH|WsFSdlOxnCuR-|nK+~&3U0{*zxlQy}tMo-DC<_K{jo-mLpR%D0EDYYX3 zBFZnzd~_o%{dZt8YRQO<6r?sq1gY1}e^SdjFae^@g0;*eN>-D_pfD&Yi^ZH_2-@9N zekhdF%M)Q!B3y{+!!j41Hx`D29J$3AK$Rj-kdwk_b;&}N z^0ov@6=Zf%1}7b&kS8w5I~A&YBu}v3SC$Hc@xhbnvH_Vcr_&SMawh(tSY3jLFd~ye zgqFAvj*F*4#OjbH4w%3=1{Eg};2NVs3dxZKKadONkQeCiFj|H~zF0rx^Hwk4k>uhDPpTk_DP5cdH>T5TOJ;cxbNS8DqLMEU36jA2 z;w5=b&NoqR9YauUNplTCkIjz6xK>p-APN^a4qp}+O_M4h)8@5K0a6rV0gsm#RP#y^ zenezNBP9d}dZ!D-lseR)&hyjJpq*mL3Z|Z`AKnbX^N0d4StH7kQWO@m zc%3Ov3<3?D6EH$Ib+7b5DarwLO!7> zNcdK<5{Q*4@O&bGVA_iN`GFixIdym-mBf@Wp|9)`C8-P*AaNv3WjPMA>>*+vq!6|U zISDLnV+g8H2LWbzE6*;Vm1&;Xlh4LA0xYAm@eKlgLPYoxUf%5SX1x-cmk-gHB1U=K zJS&_L7>OF47=jmNl)VI!=RgZtkiy{|mdE@Wp)SDZ1_UAC*I2Vc4j**lC5Jad@WM{H zpa*Fcn3YRl-lxkUmD`!N@R&Ki%*^o#4X!~lQau{W3RPSnF|B0h6hEIw3Ik!B7ZX~_ zt^jZzeEuSb^oe}HhQx8)4{!}h(mtKQ>5IF{(o8mIB??9lDngS&yyVD=Xo5$ASzaGd zGaMo*M5Td_P~L?Lttx3Crb^oBfWt2GYJ5CTQJD}8tx@{0Xwed+lqNNp z<4baWNl`R}oF$nCOh;D~xj`cvoS}cxNQU6w`|6@@s=KwS%mKY4=h65Ohe4BfdyP&n zE${}Us4;Gk*&V7(q0A6ep~`3x7(vc@g719R=Q zgaMw0M2wUpLPdzk;9g)dgGP?hPTBG(62TaPIS}p1ip^PCRa6P$X%&qC&&^8|ic~VG zKoq{HG+hW1Zo7(OSE4Ev<}T$@T&33<%S0_?juX%bWL8055mOZC07DQ%G)8>^(_C8g zl!fvlT4Rjk$!e6eAfz!U#2R}{R?w&NC6ip{M}0Pqu242`mH9ZUHomw@ERLlxMC^0g z1r{Tvl4u1)JSM`Ca)fpnY&=J{RHVeQpu5v}5u z!hsm9S+XH|3zjZ$MXZ6P&8SC7sga;ijq%+vKH?$GB_y22f;k}g91bbSzv083d?A zWY25O(Wr>wkfxI6lqjf73*99uD+G!!qjFj@paM>(N2EyR(2UaQ;V7LxhZ_e=9pe?u z6GM=vO=}WrcU14=0(Uf?iUjpCk)&h;;uSn5S5TPMbrKW=zotJa2j&BX+#Fq z$LuMAm6Ixv7b7-)hEvQMbR1X(Wk)*Q`r$Cs@q8_e*A;`(fG%70+q?JHQyDN!Uwfelz9f(>j^8V3QM3geOsY$_`OP5U_bm5nGP%szmvs zPiXZg-5HV__vVazM3tj*P|FD-haplKJJIu<+#GH!#uRR^FOCO&Xj+)IV&!PoE7ai` zilZi695Z6)fDoB6CZvjGUd$**^rQtio0+tLXTdY+un|NG4BQyQVaR0+ajaU49?xsS zw3yHbO#uTI)fA(;uveOoDla*uUwWEg2%7A|1Qo{6Ksp!ap{O%0;Oc#JfFqRIya$JHm$0P`DOlv^ri;^O{Dqbi7#j#t%Wub)25X5nx zi9-s67A=9`#ZpF@^6DJ8D=)U1bQ+E}tKy{L5OG?Oa52c?qnuz)L-PC)Fmezbr>GW| zk+>>QCRH{bJ5mxnA75p%P-%IQ6V$26BBSXsF=AJmKyO%7inx$;n??FW91nr&1WlIZ zSpi`nEx66^2*txB-&itml=A zVLq42Z$>gv8645Pcrr<3qGU`Zk0*Qt<zDJ=%*CsD2bj4+x{y(>idjnX6o?e?Xhi_e z%8UFs;>y6)ya%l2C0L=9F3}v|1TX|ud9a?(GwXRWOroTN)ByvKC$b!uD{te5k#No; zv4d5EJ{3W<5l=21@@Zo}CBVE{ zYg#}Wfw9F6S`N_MjMHAmU3fs{6$*gndSdZNnH_p$D9@^|h%)iDoTxV^1nL_S0~2pe zoAI~{aYyVHoX`iT0O|mS56c>jPI(~2$wk3z&GALN5>L3qwK$wChkCA{hy+umKpHAw zNe>9=)DmGRATR+l?a=}kLq;G%*2srRJwcn@0>8T;7Ri8jk(LQeTo*e%Xjrw8h-^s} z4(27mcveP}++vuF!RqBSB9_T>TEbwm@N6P`C>U1pbLlLY9&#kl$MpsHUJ(wp*Mn-n z6u_6TE^)eC#&B*P1me-0$ZbW#`-0ArNxp ztuZ{2ieN50B8b@e8FgNxHp~1WE---}mY~#`0zo~XdqrVR#kX?YIxg^s47A7QMxxxb zEKG{RNf6Zo3sg@==9MWCqf?Lvu|T*$4QQ}ZoG%l3nFT6C&;(T2naEqY;X*hYHkite% z$V4WMmWVvcm3U-onJ-CF6h&%K+U7K7c@}-%E+LiW2;xdvfD=gbIc2| z@J1O9N!%Vu>8w^E&mY2kX0tYs#B*Sxhe}EL5W7~*aU&+s%R&=BREnq(4QLaIs|AE7 zk;%#<5keV@d#r@QLbBc$1)UBv=oE=t8t`cm3z(6GW(yJu5hc(rClHUPsC**r<^_cm zEp?EH#*FLBzCb3QbP*h_GfxVFzzXw3DH+3IiEk!IGzlD*SeoOqV@}lXja%{@j#(uG zj(Lo>LVeUE)xM02V~7hed7PI{xqWzCn9qR$k0MU5Adh0Ubc)3=jfy-TizSap^uBV2 zqKhU@Oz#4ZAdNcG2IeihI*vsXG952@+1MhpYR!^HHgpfdp&GZtYIZ~dL?BysXqEAB zQjL>HM(l%2R~MHP=h0f+U%*Hq$*fupWp_TQ#3+BLe$^UP;r@(~jf!xxWimyV3}vad z=-`tj37)lLo;X<619O}xyA2r=UqX~QX~Ksk{05#Z63XidGsi^H4tvDyFl6Yg+d=rV z-lCAfP$QEj%o2jOiD7hFr*-p_@+ilfSDPYoyWFM-sPzreKU+ zjz3f0VCxfCo!CB2z9~jin3L?TpBbFUrJ|2=V!F@ulNQ@F^LA;{qk&`5QjW^G1f? zrJg+6id<(&n2u#J5)D|qS_eV%a4MEBq@BsKk83vzGrpq8oFW`@XEdq~RXkL|x>a`)cs$@5T=b5-ZlZ=O>7Au}3LQ=iiW;WT0V7)dn1RF&> z8|j9Q&ttE5g%V;k(=EHiQW~Bh1eiTNe_Y~ zY1TiCfk;V0lt5)1j>_l@a^z}t63LWuFjVYuTKU;TAg_U;VkDJ?p<)@B6>usDL&X6$ zR9vD#s8~{#-QlsT0PfXaS*n&2f2xJHini5z@%0iLNywIO9b&++)&p`7JHGscMAXe?itTlXAD ztN~BWnMW6wc@ETQXfp&aF_xLQMZB?$%|I$F<`U>Ap4bSZ z92rl*DzwnP9N!yf-MKgu>WJYPj2rRhjfMJ9M@dHHcpy&hz~iU|SK=XsEA3O^VuF_x zX06~o12Ey(+?g0p?GpnJ5=1$?tR8|;hoo4*Vj#$=hM|sPCavUIc$qY1D9=DoROS?DYjg$ZGRdC2QF$`s()uP%R?hrA4jm)FAv zwQQJduKZ+@A^7*llUA8IpyOo)7+6$fHJPNhXexyYb48A#VNzC-ipI0Dgbj?N z&Qc!CseBQpk4pn(4HZ^U$xPZ5igT=%j8P5z#jp%a%mFYl`^?!;BoNn$tof`?NJfRY z)fpvG@aP=IDNOA1GCE};OJ~zcERyy5$&9IxNU6za zDkuXF$lY!$sZhtv1m-Uki$P{A(_X`QtQZa(zjM^+)qo407@n_DUpQ|Jl=-1poS?xg z@x`o8D-ualX4davQ>PJB<Ny5P;u_$~!13?`EtJNREGch7>f^TNz zfttZCGIOj33Ez_TAWk9bN78l-3|0i!+CXf>>tUx|M&zupVeZZ1dGMOp`N=m<*veLe zUf2Lqy~vjVgdzAhd@fV?YRWMLueu|yq9KOhRg}BxA`HPx%z9T%;NrRr!GFNH0XhV6 zH@r;EzA1h2yVNxZ`-b?1Z;rF67rs!=z9)alH_+j`_Tb})Y6J68Oh1$<+9;u+(fX1q zPsSe3guOInO?%=Q@M=00_m%o7JamRSU;8qJu0+k4r_#wZ_&ngUq#8>6osQm9gc4Z7 zW$G&5n6F{J?5HmE;}ca}=BPpHlN#v57xCHD(BBJ&E`pvdn2U$A{m2B6BvVh3NopW^ z9;QZIHzHM!G)aT69}J!tb*WDVEL5sd74~KRx=#lBNlTvirL_Z!Q>Qh002lb2=hEfa z)C=1<&-Eqws%P3#@ML`<_S>rq8Dtv1kmLnyUuD15)q!6+R0rWFUJ#*vTPJOMXxoUb z?}C3zDz>iP`r5XKwl3NF_O?;r`t=Hsy1D{nuLO51wv7fktG2$gZ5$&}V=4_@#I^$A zYG??4fYFcr=%lU^lX7vT{{Yd;fL7!8Ro8t;;YlXxu+IZOu|mLiI5kK^Rzv%+d^Vh= zsY?KVp+&e5c<^afHSLLmPo#>Kek$O}(b+EjEZ|!>J(bA-HBtK#m(eFy)(dGfyj{!& zgi)`rHUpv{0-lIVX-_88FPTmx0@aK>m&|V@8k;Xm_;T>OC$+3!E*JJ!`~5sV4EZE2l{o69xIT;<5c<4KFN3xl)%_b?HF#s#6ZcW|bWxwsBl3HNl{~(H zs`OHPZlwtGVU-vq!UZC}zz-g7)YENLw-H?+R5kci(Vz2C>AH`iz({_*<%;QB|18W*?{T#fb{0IpTvE(Nfg z0Nll(ZaV^S1@|$4dmYs6XCO`F;@Jum(T4q*fkL-D=AtNp3Q&~! zoVy2?+oKoQ?ZF@RT!tVCSDM+neH+Ly1-!L*AOSXjX#BCc2sQ@xc&PtO8g3FHFJ_gN zELcl{Yo&!-fdV<0Lcl+3*h)e#(? zdDVL42F;N+?zywR-h11OY26M;>AOAi=e{$jt8B)-2d=&A)1JR=o3m(dzrA_!)HPps zTyyG~+bzF6_WoSusQ8|OvpZo)4;(rpwOQ3;59e#k>EkW_T6E*&Nv)1g9=PVG7n?M1 z^2n2SFa5iwFcQ~sF9~NeB$bvXV#!1tLQf#l1f57*zy=po1H2Hyh2bfJD-O*;P>ejf(8hP`^@RoV1=?r8tA2>b3aS>ZXtXipO$wH$n4_I zH3m1=S6dzIG5iwR&2R#F9*C&peyT|M5gC;a`zW(gQo+OV3Y6LFV=$B!7GrKN=%FjJ zU)N)N9?_Ux=U`($dCD-LdygK*+5z3E0sS@ib>|R0MN~g$U%gpi=^q~A3W~A5gR4|B zr=rT-jaKUhbV+JBaf#`TksTXiGVo?u_*5%^lcVROJDmVDs+vR!X~O zYMeB#Pz@MZ)3J!*N88Y-(%OCv%`7$^1KzD9vX zXfvv6T9QU#dXTzGmAjYX`tOz3RQ2c;keky8&FSsv(_;{8|OIb+PJN^v1dzqU^jsw&T-coQry8AVJI_rsMCqY-Nd~jzGu*-6b26Sy8c%) zMydkkGXoul5_+Ed>zdBj+;>gCYeY>dnxMT{>j-v`L2Pg>3|4NiXv1)4svD{w9+-TKxFVa*QHVwF$wWcXIK|3#-aaBe4^uCZWa~9F z2+e9;dsk=3$#ggmj2_rG=OVP={dvye_Bmi<4bUnx6M?6PV2xZ?6{e#VC_8&xoWZL= z8B71S8W`AX1hxn)6$oyH<5b#CrQxnDz?LI*h}Wm^xQHPalh6rYv;srSU}E%V!8SGx z*$whD{M7?cw<8cL#2XAk_s*oHlIh7x3`~!$yLYc^th(ZF{oB*r3kY1r1%?j0*WM8LMTeFvAW|f>RGn7S^ia&LI{mn+0rwHXv5O)HiOK z<(ym5bKuq)Faii7e{XJkeCwMq?zi>L!2lYDJedDuo^%w_Xb=?USgBYNPTC9zJbo2k z!NcIG{)(+{1)?w|DtQF2q7oDGDliNH#$%+yY8x;C(-Bpuu7G0>R>f<Q z|E>W!7!GvzHS`;AvC)i?0QiF3SPl8EG3n~Qc@|TBn1aN!!im5%vC4^ z0$5w2fcXjqTZiSC<(K7$ZIR7|HDO(N#(-+UGb{yVf@fF?!(b`ca(y0@2wuZ7P$GDS za@Qll)`c>`6nFgwllvueG;D8)KEMd9U z-Qh2bG>avR6-Zr{;_<)Us$T1I$NL3hpA9HSeqT0P(HRlS*hWfEdwQF^%u%jj|QaN zx;v}|WrH%oGt6gkVyVGYs1dLXl&D@i*dBv-Y@4tY>@j$TrJzLc%wo-Q1j{ra3EOT! zqn=k-5+3#K!fUpVq*4$BVGj7qT15~SVGbUT&1v}MF$eS*;L-5QXO7n0ah%O*_{Et+ zC}h(ceud0I)Xie>mu;J+V9(^I{QKfr3)?e~oDH>uN~33S7d_K-pGc zSs)*14PIUN3-|%80m4lCKmqDF64Y_XwgYR|w|Ma{q{s3s0HQ%oVF`?pLI9*7z|N`{ zw|eD!41AUZcmhm81X_1T#q|<_4}SiJz{UJrcwf&sD|MZ;02fFZmDfv)GxGl(AJF#S z5W6z(ED|`-4k))m#zR4Q&}XbnID^pTv24sc+jirWi?1Okb#xh=L2E=kQg9?f9&>Ij(9?&>ek8yyk7%wDJwC*PUkTJ9AoUb;{#CYdHB10m&2yM?W?cRm=dfFQGnJ3*eB4w zLWY5HQ^zIfsVnIMTl?}7Us;Q;q%DnY8Xs6ws4p1gl(7#O=s~c$xX7mc8wCD&lw65g zLv0p41~o8tA(vootJ5!JAN@9tEYS!qq$%n!hnWcNl)3@ucy#F_~SqxY`;sGregKyw)2?l%)6xa`(v8%X* zna9AW5QBdN4oko?^`)SPa-|+%F~3rYEA_;`A7gd81vE(vZA$~kC>SX+KCsgb^D!Jd zVi7DO0J-NSs+&I=ti$E|SWR{uh|DdIM*h#+m`v_zcou{Zl{tuyR}mZ}hyeZ2?_)3GASMROc~W#5L@F zwr$quzIuJF_XgnX55^1_|FB13uYo>ekU&A7vOR_aKdmuV4chbXwT0CbKo`tapl!zU z0=q%fjZNT{0SF);K<5GejmPc960#%o%A@!~ePLzBC2+oM-JQ>@OQ7vwZ3CnP+6Ue> z%N?KPt$_!CN_}hf{asK0LTzZk=hEr*wJ)8^A_OpiK1bbHYQP6${8UyOFvfpwuxuA2 z1S!>XT>o3&_TO{rOI+!<0rPtOVX?yM`td?6`Cu%ueaU0wxY({;x~0pZ^6z!6F{OX5 z4H02=g9r5;Sg;1vfwr923mzDQ4LMh1vDmi%GsOQpj|&4&51_X5z>L9+bKo7;`2dZ) z!^SvWcwMh87pAiP_RnemyV}r0gyRAFDbULW;hK8QYP8~SjPU<|n8O}}*fi#b)hpId z1eOh~9N=sYWP;u!<4-nV`p=qrCFK8?wSlfMF&x%2XT6X*Nf___LK)G29Wl8OrFv~? zjN64JE^RMhc{mH!St6j#PE^3a?w;VCQ`fc3s|}pA9%%t=tVY8dJGk zZOB*scxPi0pf>@BbzVW8-TLq4hV5JmxyD`s^g*CD@j%JCKKg&W_Gj%g>tph9fg)=xCcLS1wl?ID6UN^=Ydhc>Q)=;8x|2AC1_TYMq zkpYOt%t&CB#v;l_9WUe)Qe?TT|7H98a+0%l3QWulVki^YgRKY{zogzG{}Z2$^XoMb z#%@6j8}tWw`?3xh0chyzdjWFJj(Rq(3@Ft_SAqO6ip?O>fL3EJSeweC0cC`GAz=Js z(0jlmVdru-4qlH`<6qczy(B__1(bse^oi|ZA&{-HO@uXBPGQVm2;>H(!N_Cm6W|m^ z_t`iH!wXDdcxSn|_}K!ieR0}_DI#ENAiD7RJABrCsb}wvUZbjiZ}NgSCF@@f#dwY1 zEmA#x%yBf{aap?`Z#;bWj!N_T0e#vWsyaBuzi!-$)h#A|d*_~OnswjC`|@-0*|GFN z$BI9`Ke0#}EIS zUi{^mnc6QGj(J!!zEf<}6=!}oai;Odg$(GoG)=xdLSu%eJ12b9 zVqn|g1Gi6mLGeNHuZlbJy@sxxUO9TvZpyM8{PG$ z=X*W-%HY(5@CM~?obAneYJ7W6>feZ$?tQ(_65pNf=^4qoCx(2!_2CwC12a0UjpE)Rq+P@Am5(vJZkO7`1Acg`}O;pS3FIw2}ZyEy_s>wU7wDb`R6xX#<^ux zPo?&WPxCv^xJi0lx6dn{ajyOJ(`9en`ugzck()iW{cqdeZl85HchOYE_63Wo&9-~C ztQj{xmU;S?Pue_Mb6th&tB*H-H+)8`CpHf6xTMKRJvzB``-b#0PmTO^pZ6td>0V8< z&n7+fo%~Rf*&)pT@EfjbvhBN{9k}7KyyU4T@*^y!-bbIw>xON8yJGB_!%NQKD+dfL zd!~MO<0gEFQ*d^f^Doh;PMJX+u3zTUh(2w$TYD|>VrWz0`n7j2^p%DVduzzZCEIpM zzI#IV?T$ ziD&xk={G0!(=o%?=&kp>b^oM~f^*;ZOdJ2>^jmj)cI(sQkMG`alG^cm#ck!64>sM| z?fIo*l_I!c^qrozFU;uL`BdL`LhX6jCd2#z^Iv%D=~gW_^Mv}?AK!E+)zpk!czAKz zQC@LZU`3CPiQ=uD4FetMci->%EJW=Izwt`@o}VA-)}!m->L~*Cy3boZ^lNs*i&H1* z@y$M#YuEQui+QhzV zm~H&i!}vG&zWDZyzK?sCZ@fYA<#pvKOGo#9co2-u(MS91YBwD{^6QJWi)K98oHzS$Dm?3x zeO)w5dtF!Er(L3NhZk<{5PJHFG2eDfp8a|7;k#_N?@o?b^1`b}r%pfm?4lor_E9b| z<%WL#@O<0$r@Go!L^{0X!ee`duMUr#o;_q^eDcq{ho{uOKYp?2a5GQyA7(8ca_8JO zANE){F+H<(SE%iK%Z=N!@-UtPAq>&yHU2PW5{>A?BFNl6W@34 zp36?D)H{3Vpj~%~GDARBB+^=ss{5@{(x7!*%{BXj&-CutDL9>Bg&;59M+R^@EUtfdiSL%A-^NWA-M9Vqq=G@wDPfTyJ<&O8|+tK~%S2*it ziOS;62c84}-_YqVRM9UrX}5U5O%qzooBLT@B$fX#O!H}*H60!sI(?J;{y`@M*wU{LeLXgK{h?CDiY}uR zZz+x69&7np&BnJ6)zC`8>%%tGOqgUN_5Dg6rf{C#C;f1Jyl^7*yh%U|eEI1u{Qf!F z?F;UCabdHbwzJm=O56AB>T<)dH#$#z>92h&YL3a<{P4kqwX@rdp1$I=Y1OQI-aR~a z2ygcL8r)0{Kl|gx9h!Et{ofursCx9Znh!sp)xv<*e)`5QuBGQbo9*Gx97^d5k}rOu z&6{^0+y03lXNgU_@s>{0rz`lmHP6j|^|x`i9C?ix{8j$`Ee6-HIeT815I9!7+Jv55 z_tChep(p1|`Q?cYpW(l(>^?i58ri+W*y#gbIaD57llb_X$mX}t=pH@xbc^ZjPTxI! ztZuFE)`(!%&c&yTO_zN2@XM>;3jMW;h^q@@yPbJw%u|Wo&Ml#q?LXojOngGGlLh9P z*DpUdC_nCwRS$L9;WgUU?se(* z^S=9h%gf>;tDe4JkzYCgCvWo?(mPb40n%<0pI!RU%$_%`DZMp+vVPVh9L0<|&wp8X z`_|1%&m~(O73&5ppZe>wL)MCxUwa4fp+v|qKNW?6am&C~C=?(K&j zz6O;(icNgVcP9M7?We-`{FD_)(a2+)Y7ce%owwb4%a-N~UR}6BJz%`+qvcOXRg`+c zMC(ue^=pT2Taa7FeI~VQ=6iRq?73yU`n{Q0lOc0!ZKsFiN=LS=6t$i8!Tno4RJ{CZ z|LJY;xvdrrJ!;r|;EB~ge6sOLdekd3;__>6&5SrzyK8=YS?t@E{l$AdiRxLKXHQoB zxqS7^Zks6Yzy-6bkL-A1b2F!T?t6nj{!!9@Rr*+9>K(y}yNBSDj@!W=)pG2pF4Z1W3CGV|XyLNq5%}Qst)tJQ z+;T`aYHM{?@yG3bpvkx zwv%;0>8%;u%3hAnBcFbMRkJTutrUDCsk&-OUiztel) zb`x}4y#MM3ST7Ue{=51?7%x?3`nNyz}n|5mZv7N_u@BD1%l%+qlcB;^eQ#cHi{)U#|9T-xVzWsQ(@7SM1E3^{Kz^(QWz% ztAsuOICyLs+Ur~!m2B24J)8c#ci)zu_U-H5;o2USt|x=UD#64dGv$wI^h)2iZN59K z?0UHJtgv!<^XaPu<2SEv_To!-&EmZJ*B3S6;mw}<`HS&GlOx~TiJfY<#Mr7Dz4l{l zNwn+14L$tt{r=qXb$4wZaAMBf+}YaudtueTxV-qYwc}*osi~g}Yu=d}s{QbfO;*Rx zRjt0WIZK1~zqSCi9qc*xTTTByZ+*H?!=et-H-`}Gx{ zev%%&_08+=?>3dwd)|XDzKBbg&aUGhNc z{dV$amJ!d=KXiYNPPSSa>A7Zon;p%oJFfm>wjDM+XM4|((ET67(e8-C!USHvGGGm^0@4c`2!cNofe9= zTBse}ab(b#q_;fMrl!TD{>Ps?`)reTqo%JaZWO$I?9durzIp#gx3^5xeh|S&t%xj2 zMMod(>@MB**MxMp**93cEjDq^hJVX#Y&++{cs9(NcWdSTuVzo${Mo7{b1L(*+ut`W zH|AUSd)?BHPnyqtApYT<&2_sv9vZxOpW>#Y>%EqDT&oXkl5L`|U4B4%VtMa(-guzv z`dKmTrgu91eA}%Lk3XCrxqI2TdmLlJTkqRZ^Zf%;W9}E1uKaE4DHV0CbKOl}+_nAK z0sd6t`O^=-u=vjCxw+4(HP3!GG zJY?7C@naq+ZhjSg@4)7ti67$g+uVB7s(}Z#9(?S;nCvG~9mY(`cWW=){>;c5#ovG2=I-^6 zFSQT8GeOR~Tm16%eIw7e+|qo7|JGKYH(Rpdr0%V$Z+H1N{+V>coDc5!`KgM-d+g&z zz4FJO$o6*g`Ht$tMOrp%z~~!(4IPxNeCC_d?q5PbKKk`F%V!i)%@YDvHM%@{d!&E zhxf&QtlV+i6kGE4m993CY@my#!?;~N>85W@Zgt(uPi@*UY{I(9{SBwQ%Q?GtJ=(R) zkMr99DSP7VOEb=Ne?Tc(J$>tv-1U7%`gVRlAiu=$LyIrzo7^KM16Ph%FH%1u_~DgU zd4=QgXND+M_dMf>)INJN@yIW^-Us%!{XEcmBlq>03&srME*^K^7YDTHq416`BBqTm zH{ImNzuEkQptrCU@4CTH_r786!Hv&+^w2iy^bp@YAN6|4v-BSG^YY)5HKL5iy z{Cfdf{N$^>?@aCarP;7eTJDUyZipW(w6HAUzp{9tV~y(J8HYapU}Dq#r`F%F@aD~H zC$v7){)MKG-q-6`_l?u>$3~<+{NeRSsx-5n3vXZ8I{E$NW^V@DBb^^Ra?6BaH!c69 zxuw^_Cw49R{=^i|x?YP$D&Fip`I?2Yn-Aij?^z%ocl%TLJK2`se>0VOzt_?)c0R@Z zVu8`T=z%*=S=-%LZbv@4=;iqx9(rB&Rg;x-r*CaCKt4mgc&w#%>4FMR^pDQexS#g@ zW$~W+23ngpJ5T)bmp42kn(oX`Y<6^7WIFdce9Sx14X<6(-1UlUda$)`%~vy?zcIG^ zp4P91_a}Q?`}&*5d$sP>W7v-O)rr_bWv3-Kw)^yEA$D}FuxW4SzOlAnnzj0o{`s}Y zh^Ex>=a)^IkKH(b{lk(M17m%6>-DF?ay{O9!Z(^vXKuTF|I@n%tBbRDtWFkw*}^~e z_6E&^TYs3?`BU4cTdF2Lu=kVe4=lR*;rC;0-|3`(Xwk$O4{ms*^Q5lNG}TAdC$9~S zKMAsDcbYg*vP}6SmeY)DUt1C*-Td#4y)+R|rm?{`1%UKq7bIK20=?cXf? zeBQb?KXrOxeGyL{xpKUHJ5JbPf%@zpQg zc=s*CclY19`18Zlli`<_OE(OvZD(wGddT)$@Nu3TC;=T%wLQ#af{dc>H=+P&Xt&5i+2cK_nB*OW`2T-$cr z@BOidS`xmGH?Q8W9mM@BvUFq5cb|K~+(RhR?Hm8_dp(;6R<^c&*KO&k{Xd>d2ea?9A}*Z<`m@p$?9`)>UH(7U(2-+W5s zZr-xrpM9tIjypG~e|+JoQ%fi3dUV`>tvEBX+x}lIt#4Gn^M~z`m*%hPu-|omlWnK= zDBl_X%f0*OWiyXXnYZML{=G!)?tg6kr@y}R@y2#94T`tZZC_h6ttu$gh6-;{gAMoi^=wBl_I<_HB`Vp4~U*kDnPne#5W-A7AefEDRGY z3m)6HZQHhO+qP}nwr$(~j&0j#{(EC4X7M84rXzaQi>}Jb?yAZHsU#&?o@@`G#{Fl3 zGWW~895AkO^!i0`Z~+q?!hRWxbjz-ftc4ode*!sgfkt#5ze+BM&xF75KO=${mJPlK z&5mT$VApLhlk?G#`Z|}35>tYzw3)R#tA0y;HPQmHI04SS`_u zg55aW1GOZO*;LNdym4H5@O^MMF)ZA$>|}aqprDHA#r1(Ww4nzvGW7}zA*+SUuD5WZ zy7=RK-Og5aty>9}Z!87e`*u-g6=!{7ba_ek-AS(sW`T!5z32Gz}>`BlN9(8g3` zt34)Esl=%<*xBBD*WXAAQ7TBL&$;nAlvoH1$_HatUxd~Eg!PR>yrTfC1_rC%;O6i8!vS851${2>$$B;!(!xbw*;jkl*Ld`We_#>$PN?MHx4Fv zNAzEC&=d+isDc7CWJ4jStm!Gylp2O=B%+ZFDr75^Khf)0R&8NH3B3aXl)-97-pNeDuZ!Yz*%;PH6o)Rpx8KC|9<48Z%a@(3m!Er zlz%E4Hz=UrE8{Gp4G3y^rEI9@d$TA<(jsj8Yfv~Vs$1QMPElf^k$Ay#gJfZmNxU^= zOcWV*E2T!$!i-c4UV2AtTgymaS&oCHb?{Vd=*u7=1m3E;2%<`XbJNB0T+j1Rseopb8(K_ydS(MvUby0{YB?rNA8iSdB326^dpldYu*a2z^_=j8 zO;ajBOul*{2899(y3m}y-4jU27Yb#gs9s9+ZgTB<5X6-qrN$673yQ}Rc41+t8Cq=| zDy4b6SzfFHSM7Jw1-LQh-SdrSIJxe?zpIEOt5p1y-5lU2)`-C2TvFBxCp<>U;l+XTlJ=gctkUez zR%^n>#|dY~-Epbnnrg-|Y4l`5*TgIyw6;=B@jKWr*MGf|jUE>*ufG9FRCv%^R%`8+ zfKRS1`xGRbO;o<+eMV)?TetDyrDU)gM`jl-E896dVF~U|x63J!34IC5{DJAO&JV|; zUMG!=oG}}X`tu1%b`J`9pOwOu(&C7Sv}KGe{)jptIu z$5Xkk|CGxeF-T=!@T|QkHD+D14cD%whA_jtX7$pTf2_!2gxJ-XUk$(|+oq6qm1Kaj z@0vjI`QEbyJe*=_e0l(IokyXA~1S}z;L2qT7C@R=usk?{&sm&c5ba2BkDp&@zuTV`03h%LR zR$aqsP;BF(Sy%+!7Zyy>l^INaT;PjX+*)TbUb|0Xsx=ZxkwRBevT`XAxKiV# zNim9^E1rGie3ELSls5+(pI#igv`ileEAw`=CQv{J`l2|7`%WEgy|WI!e)@vgnt$nN zeC_XVjg7%SPjM4N9JRBWp102}#RV;oz8C_kn^F3>Z}3k3?(@F==$of){B_;9PyghB z7c1V;+s8fJ0-HBx>eW1_&oQOpeJXji^ua%4p2ilQ{IX$v_&L_Q6L84iOq8v-QjCHh zbsPR(>&39;^@#gg^g-J=v@1Pg*_Za&6x*LLiyF!#%d|h@#O7y=m2%Zi*@_Q$M99Nc zxC0kU&H2WwKAGnWNB^oeouNvZe_03bQ*EqXIP`^#K{1!%3iR5+A(PG1F*GWN-Z3E@ zp-C*_3>)Ac>D%l~D&OWXdrmEK;DgJE=bkol!!z%4ue(m?LsV#k+kAZVW@~hhXD7CR z^4tV~^Xai_2k0unC1MmXaXraz1mq1t;uGW`h#ET?69Gh3LS4dFug*(dumHE=h?MB{ zaHJ}V1<8OtiktaaeASmVU=up(hntWYdFO?&L)P#LOJv9j1QPzlCkrVhosoGt92 z_Gj#{Jn6%twVV41f3vlNbFumvpm&o89X@@sPZnHF+&7$_>YJt&=2o~~qlZhjr)T7Q zUT1gSn&V;L{AG?mqu$W-v@Mfway0d8>A?4NLR@rj$d?@r$*T>5mBpEMK_Bjpg85Rz zQ(p0Rr)J)!|9IQBWcC+&ji=_FChkgt=1skMftSXHnlLA=K1ko)KX%d@TR)nX3B-qm z-bD|wM+rbFocyA~*H6S!6^xzWqP;NEZYNsDXEbsa1g7b@7^JEEm|J2eH!B z^YhHizf*GudI{9>K(@Jto*mcKjz}2$u0sj+djNc`BpG5Hg%PzTRP;UnU(UO1MH*9j3%O*2X( zn+&R2Azsrb$my2&Jx|QOs|X@`pIT0g^`WH!SPlG^cK~Ea#kYs)yF8CQ@YYjgQl2F}ur#w95PIGrnkaeRCfcQErR_E<afvHS8BR?I}@hXa7FX}Ka=^TxV6E-BEjk+-KJv3N9 zZu}?VbU!-|Q?~k>(&ghm2w^4}`obR&2+11xB`eVf-DZ;E6N$PaC>V;bj5zQYgRO$q zM{@bKe--AXK4hS!CW3MhzCLw=vrRu^R#Yo;U0z>wqedD3-(nfr+%1X<%W}FE#*E&V zjSV|Qf%R zX@S$4)&k(NIIox)3_rt)Hb*_bQ^otPzaF<%;gYVTVvkTY5FOs~G2|D4q-YuOOeqCj={|Ym+>pXg1sEee27wXbg$Z*mhA74AHxuCJvnKB0UN21 zgfp`j{JC_@2GT$M&Mc0!7q)ypOZPm=-o+i;&n|q>oe;^GXzL;V^u`h((S$F8$2R!W zD}FbyLc;mY7UKBrUbunAdhe177xU! z>r5x(79X-Pn`&EaEQE4~pKg$(i8YN&f;@=P2lniofh-J?h15fXAqhzoBuD2%8>j`) z%ak{>)>O=Tc(Xx;QPEq#1po?bnBY?DQ9%s(LhxEy-KW_!NY$58wq@G2_;ub zM%Ba23j^`$QSsu@5sXi*;W!IJI_dQ-eK!s%pfm6Hc!A-X_CJo3aUwEukzI&WB}`Im zf{!g8hd>mf#DsXz1x@JHIm;Azb=y3}|KomXdf@OkZr8o)=1-jYb|oo-%g0KbdH1PI zvP{cLEMRnXU+yTPOXEK*kM>)Ks))_*`rbKx$uH$-o&!l_XihBOrYLY&E%SMJ>eIM` zdQ96jYSabv+xV3>d&B1*rC6)uKsI^A)tF)e}n`@As?OEsEzWNn&(c+`VCRn+1|hE-mugbH_CA~aYO*fH!{uk9j%9ljbuGSi`Ly7H(y*j_oyf&t4HFWSdz=4Y&&n|^&3n_nYXa_O(PYvB)`lK(zkKD@=ve&}iQ zw1V?<_`Aao4eYKkPQ$DTtjh1h{u^FE*FZJN9zSS<+hzm4Ps4v?d^aV#x zq#~93zNEJL1$Jw|dPj9tuPc~pvNmQ{Lai%Q*}XeYpMaL2{f|kq|(S(ScKcF1&nD*{@G~2eEMCOB4ZIpDFMX zxSk;y2>ni@;Nm-Fpbloog}T)1_1B_rUn}|1YE~I}9V^N<4C4s64N~9E*?vN0XaMZ$ z5WCvjU-^=gp21SoY{3UZ%%Tsa#|S#)+Q)8YX|8WRYh_7z#x=d!isl2tU|_9Fzzr}g?f1ZmtUKv zhoAPiT4S$ifI$y-aivs~MZ=J$?LV$2iHbB2hTTF!JrhGLb*}PDjJg&=?l5Yt{kTl% zRj@2I>{w=mE`D4DcLLQc!=9SbZ-Uop!G!A-TzDXw4J*P)BSJMrIk@>vWkA0?%k8#u zdzMp`@!h>R@9TfNNuJk?!uQ96$N>r&{kh*hT$m8DS+{uvDt3F3uSB@WIFBUsUJnf& z{q@%!+h;GyZbOZ?Qf}O2IP?Lv?9=TEJp2?9)6U%lQSl=>CZ6UU5;exFCdT;3GbV(d zo|4Zy@RNS!OQg?sL<0GFcwEAzOtN(71 z{4_IswT|V00pBPfpz_?b11b&5)1(V~L>sA@X>B6HoN2V(t z)X_ovmy?z|A4~7sRU&MjN;WCt=wT`Ow=^^V4RtEJhgvXxQ_U_ZmKvp}I#d${N5$YA z?lTiaa@o=l)CI0H?iwG3o$p3JsB@fDGcKKdNQo6puUJEy#C?$=+{x0-4RTU(6joDg zZ?a8uwYvAkW0&S#qDh8-&paKcC6u5JexV`3;PZShEbRV{^lk+xErys$@_o<7L{Xaq zqnl;uj(Ce7sE`n$gHR#$C{B^{;egvka6eVugbA9i=_7;qrc~N0iex*}DPXMOhuoWF z;BLn4=Z4ahs_EjouuH=!`RI2r@8ptQS0k0)M^zwjRL{jq zPRlO_E0!V0MFS#pc8=CZ)ONRl5xg^IKU>TIW<8QS#d z(25dsG%cWo5|7P(xUjN?JibH9%sVPG$$|G_H3<=>EejeTCVS`&4>dZvrsfAWFA7hK zQPcO94kNj8FleB~ws7lIT|Q&V$C`AK9>yO-&$NtqpWMzW%CM)7-mVX9w@~Fu+NJP7A~%LFJ!}P zUMh`j}F!EGLvjs^*T{i>;zR4bBH`_i=O)Hxs5z4K||U~Nq~zgJtn zg4_@~pGAuqF)$m7#lbhtUX#}rj z>U8Cx>kPx1!+qwkyphJGBwb-AATj(gFb)G)JOp^0HHHe=rTg@2$9k^KxU9Z2nN08+y6C>?Gh$8VLn>XNn4BMJI=LclvdlGF^WRLz{Rt_^_i- zBgM{~bNlxA;jr(Xr7Uo6^8V~zDRJ&e-_fgCy_d5B$90@%EceaBd~W$$9&or z{WzSgX^v%*CB;RXPJdO~j^wEg8$?kJg~^h*?$XIC2l>;^Rz?=({AArzipR-!Ega{; za-^o1wtQ#jpoTfOtlq8E1D29%bbopLLUQ7D$~==cb&9;s0-U~aqbBPYMw)W8xaFYo zp3y&&Ukh$k{he~8pr{;K1^1>0+yL*U5x2t=y_m1~%CKci5M9H?5MR6R14zjo&{xg6c}-Y;W*V93*1* z$C~tQ0L1`$-WhD1HX8(=>1E4`)lgOX$<%~D-g^^&TB|V#u2g=H5QukvT#ZBQ z^InjuE4qZ=H^OyFqxX~}&P!S4B z8yKwpPeYwJ;*%Ji$M2}vCH*{B*@{PVqy!G&1p@ZOgGiY?y3ucl{d|@=kHvqLV&2m$ zOid*b+oVTwY+$(!SpoOy5>kvw=mGK-XM98Y6ofjBf?j1-{b^nE*9qe6MriuZTPp$Z zI+Boz8H6*}zvJNMNsG2EFQmrI*<8d#fjd9t-#q7ssD@eNWlxd+0P%(_ET0;`EAhV} zeW^r5PnTC1)*f9C9qrWdi_fSmXxKkwaO=la^Slq4T1z!)&?GVirPqxH6FLxs)rKbI6A@q_!(0$Rg_9~gK!SvR$(T@q_ z_Wk7E&_Ut^m6arn0i6&SMirbbLOyn)EbiYyjg>(uKbEm;Oe zDE_oZ=O@r}<&{@M%o;YAlrfaQ_s2KeNE&wja?#x2V%z>E2`S5D0AvNqi`4yu!CuGW zhq0vx!CbTI^X@Bj{2^Q>R2meL!@UK+jij-D!+cfKfV%ND;?P;S5GcUb7#ui-a09HT zMr9!!O&9W*&rpkr|0Vdq2`E1~uP5*VFkgZ1UuhB`Y9NB?NSf~gGXOXM+(b~lT64l{ zxpjSy%G-S`=1ucm5~UnZ{(UN;>?KrKFV!Nc>*jz~Lnm?-CMY>RLY988S1TV)>uWG@ zpfLb_-kh=3HnZ}>Q7L>fWgj6g4R0eO3H{DqqErB!Cy+`^8idx~Ru-|HFuxr1hc6QS5hfZc4Ybv@d6^8%1%ceVmyEpK0WL4YC{q@o1sLdBkri~;rY@Jh~Ow6U?@!mLI<-trpzTrb%zRNKg8tjASr zvI77ei4yf>(1P03+!Dw`m4s{=&TH`DZW<<~%%co49>J(q6UH&1F6Cvz#>w{@inI|Re*{Yp$0NzbI$CZrzN8^m2XISCY($}m{W^O>OL-> z8oZdRJ~{zA3OuDw%Y=4Lkh~uNK4H?618$HoI9fzq32|m4*Hn=-Ft(-l0YO2)H%w77 zighIl>i+BhPk{Z&a5DA()aU$P`QraqY54!zBgDwS#Pa{Ec1lFns9$MqN}>1$@q`FS z5G0ZWv@HSAA_NnlDo}V(rpxoFZ3cnu{>>b7A)Au{znk+>x!hcBdf)UZ`R<~~#a(9V zEzWJ>mO3qIsLjrT#Oe$^3$(feAz{PDUJjR z><61hSiTb9>kk+R|hO!A5?J}=IP%mXiTSv9%OI5`Ban$ z)8uic35Yfh3$<|{0Z9fXzb9+(`n4S+07(5`feRvhn%f`+3r-kIj0yHyXt&|GwH}t3 zf&h(T&afqff&&sz-*xnGks*jE0w#wG78plEV{aI3uTtTDNafk>D=4u4RX8ock@B$0ysgK&+E^ zX#AMya8gZQv7zLgD9~H#QXLCUqea9NfJ^j2H#Q|?#N~Qgn+F3X0G<^UF7IhrW_&g) zBEuDhY@M_u-Iytj4^KlK$>M;A#)SoLT+9#hk-BSA$;LH8?AaY8k*k0C{bjAe$iUkLQ@!|f0Pm4=e#6=$%PmJLb>bZA%N8Cwh~DeELO zC@v^YC5Ry1JY9yDO;f4^LnfSB>a1Ihk7Rc`V#w40rx+;nX1b#R4;GkNjRNkj%b^;> zI7!`E%fJUk3PB%mF-@kAElVLi=Bv{^6PjaGGqJ{WVa4kYla60dV?xrtbbpRnKFc(F zY^h?sWo>@!agpmz1=TwH$*Kw`XnxHtyDjnRy!hK$yk}*SMMrcwr>vC5*!R5T#3~F2 zKT_Gxhj?o|R&Ebzz|)R#={CP_Tk^0Y`lLCjW&(iQ6}`iSbF)&_UhJ^g5?B78q`{;0 z$>QrxZ3gBERJy#rq}X2Da*`lxRsPjKxY2V5u8gzar&uk2g<}19ri#TwS14KgH7r5HcE zDD{4B=B!1(kqQE%pDnzl*sRlioO&xLziP1j?Y&;xW>eZnZg%54f6a<3oQO{r?a#Rf z?u5oAE8{S0BoSV^u0Qe|>Rs#!#3idF#j1 zt1{b5P$AVW6IkG!uI<~j^6R%wNew?(^LlTn*8v_~@(j980{wb`uS42jH0kaeQMI*xnM)Tq6q{$P z!7}EgfF~VF_)6Udy&@j(WtWDgUAIX<5gUs)sk#+%tVmE@;j# zp?;Jq{Q|oEgq~uS&Hu$K%&Gsvc8HhOLs#s^Eq?S6KaDXWmW6P)${L#3y!bb8fq*oT>X>C?;aW>On3ea&RQLDJC)r-VhB;w*>zoJg`xXBn^Rl?9m~(yH#VM74mxao8rM` zrOB{{!;eZTb~eXL2viVEDXc!-(9)<|6SC!hQH4ar3y{<^x!4cL=?78!ad4Leehuh0 zq;td9Lg7gS3)3$CCWBn3fZ=m{9;pJFQ+|GPyd{8`tf8A}moiJ*vrQVPUwQj!zgKz# z4OkA9pF+MPgn){4mnWC0rTHk9zisjI$lZ?4=?n<}x9zpZX(tn>&63&+yb#=AB1dM^ z5Y911E!GVRdg(KG&9of3K7s0F>0ANA;v627(c|-1ri`Hhl((CQg@x0p7tHoD{FE=6 z?m4@sQ6R4NR^Lq}2w7<}GI}Y!;55cjPvvzwjpjj#ddjg$@8}UGX29AVI5_LbQSE2Xcd7QzUJL&s&oYZRtAm5-~1zd^= ziP?x025U3s+6@7D3!p*O?NO-pH}tbc$)q%IWcld9aGpCsOVah7ubQB-5VR7rxzwf z{?TrVH^dPRVq&ZHPwYcQ#utqSs|UMlf?m$us!1;WF?Fd^kJmw*pd=kDs|045OK1R4 zRcbQviR4`2cA*9}w&Ux@40|}o(N~Nf4`x8=Z@NryQHiy^In34bkPU&W$zCN*%sd6uU9&_(521Q5K7Ods)B zE-?rRb?7I!;n(wrh0&=92<>u;l^%G=79GS{;hbjs(Lb_bZP1_k3Ul@t0z&2TZLe1c zH`Dtr(1JO(z(-2OCm?A2liuVu083p&q_B@WFtBa`EyG;>kUXi$^n%O)_GyTdDNC6P zBL>#O#GgKXWkSMF7|=9oiS5+WEgU9v2G}4G3`G)wD+~^tP431oEco0g`GmBen7d7e z$_9U{S8#z7LK#T+b-X^2*Rv1OjtKw`n?Ap<5P*;6v}^RpN%Dzy+iy{QQLOWdkb{F4 z32oCN5Ikkm@+eJ)hx(J%w*kv~<s(Zk+a1!5ZWvX-97vQxIR4LCFiS`xI5K3wtEbGk+fH*{!4Vf^WUg;={2l{ z)(?n+AQ~eH#gIml7*$khDx!#15+3}{Z*nuXh-=?`^znPXru4pM?Y`6{r_lTe({?6e zxYZ#F$1tQD+*cO(XeJ6PdCU2z0`my~`q=QZb{8DO+b(`ET~*;6d`{78ayd!Ah~i&0 zL5cdl(ETo}`kyioRvNqi=&<5$Bv;gbgYgZdcTRoPru!!5(}PKTl%4B}jm$@^4W68C zCKHVL(ZQ=n0Xvs+@c3%t5RC0l!sc|91w@C3{j5UUmpDHGd+EIda~jwR#ir0F{p(qY zW%wu-oh4}lA;?ZC%0HZJfy^kdiR|CfGpG~&N27QfuWBPzPRp%X)*F*0ep~gO-^bM5Hzax^KLwR1=r(EVM*S& zSwq9+1|CW7RSXGUT(}=Y3Lka#Yp5Q3otA6IA{0K}=-n!dZkZdA;&f1`qRYmVyve(l zwO)QQQFoEo$E96FPSgMIYPBd=eKM^Rn3Lme&OD!+)2F=Q z?L}C7uVoN7+F1-l+MSB^ERY*-w1@?p_2*I!DFf9?ipI;Uj)7ATb=CE7$9B6Btpxh8 zqfCEBBKjSu>FrkdF1*(jUw4@TQRmVQTBc zDrvsE7v4fIr0Xze?BDFTk;pcRjN(p0>EyD zpyXAl5}nL7y3Hb4>pZA=)Yq#HKfPrQQx&97I*PTUy+Hq^n6|zkWQtAaB0{x2rVuAv zz_yc;#+b}Sz~2Pd`{JCsI=P_;)HBvWe5WZu^{#lJCy@$`e@5P9ClR7+*9OX2wy>}Q z4^vSW0t$hlTkP48il`|9^1ji^ z-2He$z1CI@vr6OpD`TocZjBu($39K?A$v0}*B(C#^0VOM;Ay??(^?Vt;^naWrel-r zeKsL%VAH8Q*7PbHlg&slat*B{E1EPPPB%9oOpCz_3dVcl9$0SBzS9rvXl*G zPGym_L0 z?>$$4kiXlS`Yna!N|yX%bAgH946P@>oQGNO^(rIwv?0+wz)0&IMt^o)#35UljOa~P zKzcKD|2vphhYhUKokLFD)qqMn({+^*&+oZp{UQc%x0bj_7r@CcHrMbKlTF?L+<(2y z?e@v`QdkMDu>LaFr^L+@DsL{FogfJ*f)CDiI7tCR?T6z&Q`B278{)_a&9A~W62M`u zM5$A}hwyV!lxbTIziAPWLSf*1L9WgfW5t7vo_0JGW8+0_Jp!!U~-UU28UN0RzWOH>1k zrSaLPJ*|?+$M<=608%kVlShfm4AH1}9LR5w_j!gU>sNXTv>vmTQ6vO3b--P_m)!26cNRZh6M@-*^aI$p}38 zFDctE&2$CO*^bjLHO3fb+Ah>2ORjZgun!+4Db)0(=05%s2wWClXQX@%k{rp1nUALQ zer|hw4aKYCPG)N6^H3tC%HZxoNULinzCSrfvP_p8=d1ntj+8uFu9N)C$z8gU&bb>~ z2CmD8!3#owBjYc*`wz$1I||Mk>80_eb-_ui^NFy9MD{^Sekb>{l*A#WH$k_8PS--| zCN~y^Eo)a1+;5hUZ$^ofL1=@rpASnlCmL+A+{8_&&U9jGZq?>(Um_h9!oBJN$bsoOA51pXq}&U=5Dn$aed*2KB-@MUCd7j1IYGj zr1C#mIT2}gFujxcqSDKK#l#(dm1%Mq6WX~!hTH`9^*FOblhRHNjV)tC`>!b@vCnAN2z7|_mErLT- zZYPJ`es+yNf1I{Z1>%8AffRi^0(Lt2F%)^n+)QJ>M*fKREV zaRc4?pGV*IY#TByKJ0qn9}26}l#YfkpVnbNJZ5g5jo_e#QAJ{);=?tuTOhNh1* zZT$fdFNh?@%bZ>z!Pxbj`q%8qfNkq~q(yM^!n&EFv7vB5I)!3PUN`2D`vXqX>-?3@-HumMKGq1}v>JX$N&x)V?oFq*meli|| zQ4W+aelEH}+s3pd$Ao^(Pqu3{9no?NBb~_c=R~a+@1BJ5CebHD>$eOe*-osbrYwPCG5gT8SEm+s-SDEYTRH>Y_)qoL+L?Yl*; z$);I7VCvYR@i98j`%K<%t6Rl?r-GL_JCkYYmGz#kd%sQ*-`VPD2DiXZtimQ{*dH&Y#qKLr2Xz(HXV`5}MljXuFnQ`> z6!+E;Z()#gTr^w1>SVX48WTnQqTjLyAZJiCOx?h)*sn)hT~Spvz$CpyXCQM@iOX4> zwJF+!y-MsjT3HCbE#I?!OEIcDFzWb6J1Pfkd*xWSKHtg0=BB7SV16|BVdo1DIUG=>W`t%p;DW^%Ia7aS zw@Qt&Yy<{dZ(|XCgbfSJ2o9J_0Q!eN;dEq!@>i1qeljm51A67DB@x(FQ$Q@9S2Kci zyGu$78uNj$2tVv4?@6aX3P}tR=xC{}vFfZJtmne>OgHKmQt*+ZGc{3sX-9O!csHc4wO)NG8M=V_e%g&YZ zmUcDbFQPIC#u&lHZ70O8bl`$$2QY3n43RupZ2BKa>DinGXZFJ-P{P0&tB1Nl=NCYn zF!etq@CVsIGyOpJNQ_FQbqP3I~#|7m)kavI;LWChl$PSacYYVMTqMHJ9-=HIoIkvbg&9z9g_v)6geQDpiW4=^O69@LFcy0 zIM@+OP09jrpK)|_1Kljh(5BL`uw^9lu56kF6Ac3Q-yl8?2UFt(gg}CMz(Hb|J;b_G z;0P2vVC=A`0YeHk0m5Up$4v6*6dSMbdYz#cZxqzKd$b+EuR&KsG_2iO<{tITe}&9{ z#F1X$tLgJ-br__41DXvVQ4Lt^20~qH?79&}b|jR3@Np zd;;ovprVt1<2da;_owr<1K__6sXI1JbhZ3@{Ad0a;v<7(PdtA-`1DLylO=2)dulo{ zZhOfeuk!Ek*)?TPA6o$&n6zWa_s-{je`3Yl^+E7e6aqf)EP|TeNG$KABkc-qS>1Ia z;D^}jF_ub#d50zR=%YO~*b3-kqh;FMAjC{jLI_!|O z@>2xn*+p~ue+hb}+Lg@G&yXGv#L5yA6Fi_0$we@D0uWIF1XK_c_iub(wRlKaX`ha9 z{!Lt!a;~nh-EJ2j8FL3sa~~8TA z$)+jO$L*YS%+VtqHUW1IV^4>H%rQ{&rmi1(%tWnB>C>{x=Pzl@HR(L&G;HnXiX9L> zdO}B7zGDquTDJZGw8yn=hR}5;Z3xNDIkXkkGnBxaXHypd!)O`F(`6G7 z|E=msLH7Xrqrn0Hz*8>1@Ic(Dy6S4wVz_f#J^_Yz#+U&Ib(KqQ_HbntOh7q^Bkplz zsx6{rFhd+ef!0(m@MqbKAY?ss%IFp+frz~1u{V3)4IoAfP))GV?M;&T-An1clFr@( zAwri^S^AWuezb_I>phdlK6UV5P!Dv6d1?8z8Z~d{cYoAc$Y4Qtq+Gjh?*-rjwP0Mh z8?_131ThxP!?i#tBQ3I+;--6ToKL(Lc>RtSrCPZ+dF%F2wJzqUL{w0c@exM=E5^#t zkf0(@6l|pjsePhKn_z^nIb=7NGzATB%Rrg@;kVoyiOdyPWuAWU%S~c=5_oBBy;I=A zFZKH3ay}vAcYE<`Rm@8pzwFEjebUDl&|g`khvnvJAQ<~psCM0KrW(E=sFxjyf|yjC zFp@-J1htv0|4Kp`m&(wr?=>v2k?thqj=hEcpT4d<8p^hfYocUJN!A$TMT%L>A|htS z62e#_gQhVuVTLgxQcT&4$i9yydx(&tp=5bw*N2oP6Dn)!rC0hMZ(r~EKE3C=|GJ;w zxvt-HU)OV;^W5h?*SUZDoZ%uB_2cHffz{AlX7A`8%K~%>XRw)!I7n?u&$Mu-=*+6v zOg@$Ee9?F{&NS(5YeX6|D5TPL-U-~6JGAWR_<7+hEUbmp`5})tEqdLH_{!3=Ou22Y zn1Edb)$OF;=-He(%00@Sq-VTq1B+l(2bVAX=`-t6D&!A!CGZb5_}9 z1+?hW2bL#hm zB2*y7m6t-Dm_3s^X4(3ncsM^?zeKRe;?Ki8TsHH64sj=~2ps0Nd;m_3?EJvZhY>tO zuCaoKe;PZ`%edd_7SLNRHP3%?*Z|4pBrSGm&F<+qr&Z2`9Ts*u;R`49(E0G-SF#^F ziBhXb)j3qv36|oUrIH(`i{aLxa3AT7OLdrZt;%&p-?Cu8%XXqu4$Iika}BrTYfa

^EFU?sqkt zaC`mDRb)!GS;9MpH@@eygNBlqQLomT&``A32OqUn$@Vw94TLH8>PPktNM^w*ga42U z%Gp6E5&+qpKLkIJbcygqY_^wwe@JibBF=OcRMjLrj>|Q<*pur>V&0jHG`Gg1nVWl#NW+CdG9DWH3ecQcSEPRCF8$e0!aqB z(2Hcsdy4sM8ozqVOau@!z_R-v1y;pJKiiXjrQ(_w^V-Lzz_s7@`s=>MnZG|DA$oE( z)@sgBbmPpCZVN8QF57);hN3Z`$yWhvZ_PY;|78^N+@6vIu$Ug? zx2t7bL!0eIvZZUD_{h#k(9-aDiA9eW$C%t}-(|@fIU=7NzA{SZlw7Vc7n1(+Fgf79 z-XDoNHE)Aj>@~j(luuV%&fF#y9$`K?Dw=96oII0##A)*pck?%nb?C|~_l(?_Owu)P z!TCi`bO+S_`{KpY8~t^6Pg;Axh~JDtz9_yw7n}KEkJ*Nyl+YpjKRxl3{yK@~OVio8 ztV|45i0un%u?EwkmuiB&-2Ay0B*csUM*zvf+*c~`WDAbfB zH~EO5Z~eQC`p1u+2^0({rs0!`|9y*lPi4gUTjG11imyVPR-gLju~(n=ami>-#Gi4$ zBDcZ!_S7{@7xoFOL!J;nA5xH8=~K4&xdU0#Sr6Y*d7Dtqc!W ze=mEHs$e0Zr};e+^6=ofCl9`n0)5O}c&dA!alEk;x&2pJwqy3VtC%mZJJGGycUF6y99s%!f&a6~>D?RA>yj!%j#(S>mn1L@ z=e(z41f$foufJJ_kruSuCxCOi*B#E3jZ?DN??-QWa&n;kriz-^LU;nA-w}tOrFaLE5Es#I6q1_680w3`$hPq zvWU80dW2Hoe7G3jmBC{0P6{1U|Sh5sz0`kTf)^tYhfx+ce^>>b_jqHxf>E5|V zxVFQYopw_A1#&HpdExmp30@gYt4@!4J$X*NFJ}g;CF+CjF&jRhuHo@Moj6IXf&VMltfJUGD^;G=K~Wd3ibueXr2JJ#C9CqVea%5&}{Ic7j>7ETXO!~_~TLK&V16;-2>QD8C9kAwT#s2 z*tevJ3HWN9wmr~$lc+hp_`4syNu(S1hGBOUsha5CKL>_c1z1Tk@7%ObNMzbelPD&(s;U<=YN5*{7 zQF-O|EuTVPcli)(Gvs9Gs`);-3Z2oI$H6EkV0hZ-*@1 zQXB7zOEoiUkoB|Md}7g8RL)a&(@`;Fs;AaR(F#%F@`afgcaKvFN}UN!I-BSZq7=Mr zC^}G<-1-tcOV1(QGVAH60~%|mc=S?!ZLCFtA>ggPS_cF$!8>jlY(2uD|G~Aj)iHE3 zkwKwRf$RIp%Gl#D2%wSxg`%NIK!_Ql3V|y@AWAAftY~y)J;0v~HumuF^qDb z!GHlDgc=-7P&OlyC{zZ`$Bm$(qHadpk_7laoECJNi!X`%lhguh0QR7qrxWRy!2llw zqM~k1VR(?0t$oR!J*-3GzMw_#UM3sI4BOOqm9;w00{+@R{*UH4HOKi2dJyV m5rA|MTwfCqM#1Pf0@dTgAkrCs)eZuN&;W#o6mjMTg8v49y#hu6 diff --git a/Example/WebimClientLibrary/Images.xcassets/Icons/SendMessage/SendMessage_dark.imageset/Contents.json b/Example/WebimClientLibrary/Images.xcassets/Icons/SendMessage/SendMessage_dark.imageset/Contents.json deleted file mode 100644 index 68ae080c..00000000 --- a/Example/WebimClientLibrary/Images.xcassets/Icons/SendMessage/SendMessage_dark.imageset/Contents.json +++ /dev/null @@ -1,15 +0,0 @@ -{ - "images" : [ - { - "idiom" : "universal", - "filename" : "SendMessage_dark.pdf" - } - ], - "info" : { - "version" : 1, - "author" : "xcode" - }, - "properties" : { - "preserves-vector-representation" : true - } -} \ No newline at end of file diff --git a/Example/WebimClientLibrary/Images.xcassets/Icons/SendMessage/SendMessage_dark.imageset/SendMessage_dark.pdf b/Example/WebimClientLibrary/Images.xcassets/Icons/SendMessage/SendMessage_dark.imageset/SendMessage_dark.pdf deleted file mode 100644 index 4e8ba63b1add5f3fcbc68818b97f33e68f0605e2..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 46316 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 literal 0 HcmV?d00001 diff --git a/Example/WebimClientLibrary/Images.xcassets/ImagePlaceholder.imageset/Contents.json b/Example/WebimClientLibrary/Images.xcassets/ImagePlaceholder.imageset/Contents.json new file mode 100644 index 00000000..5a9ffb54 --- /dev/null +++ b/Example/WebimClientLibrary/Images.xcassets/ImagePlaceholder.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "placeholder.png", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "preserves-vector-representation" : true + } +} diff --git a/Example/WebimClientLibrary/Images.xcassets/ImagePlaceholder.imageset/placeholder.png b/Example/WebimClientLibrary/Images.xcassets/ImagePlaceholder.imageset/placeholder.png new file mode 100644 index 0000000000000000000000000000000000000000..26a0a013c018a64ea5ce131439602ed969dc2015 GIT binary patch literal 1329 zcmeAS@N?(olHy`uVBq!ia0vp^(?FPm4M^HB7Cr(}oCO|{#S9GG!XV7ZFl&wkP>``W z$lZxy-8q?;Kn_c~qpu?a!^VE@KZ&eB?lDgn$B+ufw|6HvoModu9@emMGp_A~VW0j9etajebdv9lF7t=FIrO@0w_WIkhx%_)LviIBXSU%lV zEd0!_%$V<=J*94(ufM%#hseHtx{Lcy$y~lz^>JH~-3O&LhabN9kh%T7wY))PJ63Px zf8)BnLzaK8?w$45q_2l9zg+nE;sIW^+Uwri!`{44tG7t0p1Jz#HQ9ch7i{v=_Wqo_ z`*z9aUr)BZl)nGW_x<+4rCEDHEPXXb@Wf0uVWvcZtsN{9PpHpkw3&tHU2XSG@_`BYk~g&byU zGJh?L@|d2p@9O)jvl(xATy>c|U$jf%@Fw$;Is3ZKESr4F{LIZgt4=*#-{JcyStKUn z`N?0VVyAL~-Wr+u9L`EMk=Z4|Dthhu$tp?d&SQ%|ng3n(bmKD)ND|qgD&SrV=oObhDTuN&;0gW{~baPj*V6<=k zVX>6{<3ApKv0I}x`KwXn4A#$U&u)v2$UXdJ>h;8{GuKSt7^t1kd5BMcOQ-U+wX*9E zud5DQ`?g8& z9Mkq0Z|>)rS%2SmuORETVA#6P)B2B2J?^Goy-;Gin)r%cPn&C6N?oVw&R-T%d}Myp z2Cpf(KTQ%}r|y@3_VU)v6Rllfkt?PzzF%IE_W%F=fX7Ln;u>`wZ!F)pZF$WnCo6a9 zTU@rvVXm&xcA+Npm(((kZ6ps>4Oi`Kvy!)Y8$L-`^(~<~%*xuJn0u zsEXYBbDR6FHs$_&^R4~Oy$ut>FV0q2Gp+8ffAjtC*-hSw-Y!%7F*0EO^^I>FrC;Y) z?D^*Z>~r$oy}a7h%bL{Vxg?eKT0vR0LNIJi|CV!?KR=Lu(<`2M^X&m&exsHn-&B_i z+wYrlp!?6;xZ>)L{k4KYYtFO0EfF$*@qN$DFW;U?C7WL;-CbzcbKl+Po+7=oJz7cg apFyMSPIQFsiD+O6$KdJe=d#Wzp$Pyw7fZbW literal 0 HcmV?d00001 diff --git a/Example/WebimClientLibrary/Images.xcassets/ScrollToBottom/Contents.json b/Example/WebimClientLibrary/Images.xcassets/ScrollToBottom/Contents.json deleted file mode 100644 index da4a164c..00000000 --- a/Example/WebimClientLibrary/Images.xcassets/ScrollToBottom/Contents.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "info" : { - "version" : 1, - "author" : "xcode" - } -} \ No newline at end of file diff --git a/Example/WebimClientLibrary/Images.xcassets/ScrollToBottom/ScrollToBottom.imageset/Contents.json b/Example/WebimClientLibrary/Images.xcassets/ScrollToBottom/ScrollToBottom.imageset/Contents.json deleted file mode 100644 index 694526fd..00000000 --- a/Example/WebimClientLibrary/Images.xcassets/ScrollToBottom/ScrollToBottom.imageset/Contents.json +++ /dev/null @@ -1,15 +0,0 @@ -{ - "images" : [ - { - "idiom" : "universal", - "filename" : "ScrollToBottom.pdf" - } - ], - "info" : { - "version" : 1, - "author" : "xcode" - }, - "properties" : { - "preserves-vector-representation" : true - } -} \ No newline at end of file diff --git a/Example/WebimClientLibrary/Images.xcassets/ScrollToBottom/ScrollToBottom.imageset/ScrollToBottom.pdf b/Example/WebimClientLibrary/Images.xcassets/ScrollToBottom/ScrollToBottom.imageset/ScrollToBottom.pdf deleted file mode 100644 index 357d8d1869f8d192febd872e9d5501ee5ee379e5..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 58455 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 + ITSAppUsesNonExemptEncryption + LSRequiresIPhoneOS + LSSupportsOpeningDocumentsInPlace + + NSCameraUsageDescription + This app wants to take pictures. + NSPhotoLibraryAddUsageDescription + This app wants so save images to your photo library. NSPhotoLibraryUsageDescription - This app requires access to the photo library. + This app wants so get access images to your photo library. + UIFileSharingEnabled + UILaunchStoryboardName LaunchScreen UIMainStoryboardFile - Main + LaunchScreenController UIRequiredDeviceCapabilities armv7 UIRequiresFullScreen + UIStatusBarHidden + + UIStatusBarStyle + UIStatusBarStyleLightContent UISupportedInterfaceOrientations UIInterfaceOrientationPortrait @@ -64,6 +78,8 @@ UIInterfaceOrientationLandscapeRight UIInterfaceOrientationPortraitUpsideDown + UIUserInterfaceStyle + Light UIViewControllerBasedStatusBarAppearance diff --git a/Example/WebimClientLibrary/LaunchScreenController.swift b/Example/WebimClientLibrary/LaunchScreenController.swift new file mode 100644 index 00000000..5fe3822a --- /dev/null +++ b/Example/WebimClientLibrary/LaunchScreenController.swift @@ -0,0 +1,88 @@ +// +// LaunchScreenController.swift +// WebimClientLibrary_Example +// +// Created by Eugene Ilyin on 12/09/2019. +// Copyright © 2019 Webim. All rights reserved. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. +// + +import UIKit + +// MARK: - +class LaunchScreenController: UIViewController { + + // MARK: - Outlets + @IBOutlet weak var progressBarView: UIProgressView! + @IBOutlet weak var bottomTextLabel: UILabel! + @IBOutlet weak var webimLogoImageView: UIImageView! + + // MARK: - Properties + private let progress = Progress(totalUnitCount: 100) + private var timer = Timer() + + // MARK: - View Life Cycle + override func viewDidAppear(_ animated: Bool) { + super.viewDidAppear(animated) + + timer = Timer.scheduledTimer( + timeInterval: 0.02, + target: self, + selector: #selector(updateProgressBar), + userInfo: nil, + repeats: true + ) + + animateView() + } + + // MARK: - Private methods + @objc + private func updateProgressBar(timer: Timer) { + guard !self.progress.isFinished else { + timer.invalidate() + return + } + + self.progress.completedUnitCount += 1 + self.progressBarView.setProgress( + Float(self.progress.fractionCompleted), + animated: true + ) + } + + private func animateView() { + UIView.animate( + withDuration: 1, + delay: 2.0, + animations: { + self.progressBarView.alpha = 0 + self.webimLogoImageView.alpha = 0 + self.bottomTextLabel.alpha = 0 + }, + completion: { _ in + let sb = UIStoryboard(name: "Main", bundle: nil) + let vc = sb.instantiateInitialViewController() + UIApplication.shared.keyWindow?.rootViewController = vc + } + ) + } + +} diff --git a/Example/WebimClientLibrary/Models/ColorScheme.swift b/Example/WebimClientLibrary/Models/ColorScheme.swift deleted file mode 100644 index ef548081..00000000 --- a/Example/WebimClientLibrary/Models/ColorScheme.swift +++ /dev/null @@ -1,134 +0,0 @@ -// -// ColorScheme.swift -// WebimClientLibrary_Example -// -// Created by Nikita Lazarev-Zubov on 07.02.18. -// Copyright © 2018 Webim. All rights reserved. -// -// Permission is hereby granted, free of charge, to any person obtaining a copy -// of this software and associated documentation files (the "Software"), to deal -// in the Software without restriction, including without limitation the rights -// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -// copies of the Software, and to permit persons to whom the Software is -// furnished to do so, subject to the following conditions: -// -// The above copyright notice and this permission notice shall be included in all -// copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -// SOFTWARE. -// - -import UIKit - -final class ColorScheme { - - // MARK: - Properties - static let shared = ColorScheme() - var schemeType: SchemeType - - // MARK: - Initialization - private init() { - if let settings = UserDefaults.standard.object(forKey: USER_DEFAULTS_NAME) as? [String : String] { - if let rawValue = settings[UserDefaultsKey.colorScheme.rawValue], - let option = SchemeType(rawValue: rawValue) { - self.schemeType = option - } else { - self.schemeType = .light - } - } else { - self.schemeType = .light - } - } - - // MARK: - Methods - - func navigationItemImage() -> UIImage { - switch schemeType { - case .light: - return #imageLiteral(resourceName: "LogoWebimNavigationBar") - case .dark: - return #imageLiteral(resourceName: "LogoWebimNavigationBar_dark") - } - } - - func backButtonImage() -> UIImage { - switch schemeType { - case .light: - return #imageLiteral(resourceName: "Back") - case .dark: - return #imageLiteral(resourceName: "Back_dark") - } - } - - func closeChatButtonImage() -> UIImage { - switch schemeType { - case .light: - return #imageLiteral(resourceName: "Close") - case .dark: - return #imageLiteral(resourceName: "Close_dark") - } - } - - func scrollToBottomButtonImage() -> UIImage { - switch schemeType { - case .light: - return #imageLiteral(resourceName: "ScrollToBottom") - case .dark: - return #imageLiteral(resourceName: "ScrollToBottom_dark") - } - } - - func keyboardAppearance() -> UIKeyboardAppearance { - switch schemeType { - case .light: - return .light - case .dark: - return .dark - } - } - - // MARK: - - enum SchemeType: String { - case light = "classic" - case dark = "dark" - } - -} - -// MARK: - -struct SchemeColor { - - // MARK: - Properties - private let classic: UIColor - private let dark: UIColor - - // MARK: - Initialization - init(classic: UIColor, - dark: UIColor) { - self.classic = classic - self.dark = dark - } - - // MARK: - Methods - - func color() -> UIColor { - return colorWith(scheme: ColorScheme.shared.schemeType) - } - - // MARK: Private methods - private func colorWith(scheme: ColorScheme.SchemeType) -> UIColor { - switch scheme { - case .light: - return classic - case .dark: - return dark - } - } - -} diff --git a/Example/WebimClientLibrary/Models/Settings.swift b/Example/WebimClientLibrary/Models/Settings.swift index 4886aa0f..8a9f289e 100644 --- a/Example/WebimClientLibrary/Models/Settings.swift +++ b/Example/WebimClientLibrary/Models/Settings.swift @@ -30,7 +30,6 @@ import Foundation let USER_DEFAULTS_NAME = "settings" enum UserDefaultsKey: String { case accountName = "account_name" - case colorScheme = "color_scheme" case location = "location" case pageTitle = "page_title" } @@ -53,10 +52,14 @@ final class Settings { // MARK: - Initialization private init() { - if let settings = UserDefaults.standard.object(forKey: USER_DEFAULTS_NAME) as? [String : String] { - self.accountName = settings[UserDefaultsKey.accountName.rawValue] ?? DefaultSettings.accountName.rawValue - self.location = settings[UserDefaultsKey.location.rawValue] ?? DefaultSettings.location.rawValue - self.pageTitle = settings[UserDefaultsKey.pageTitle.rawValue] ?? DefaultSettings.pageTitle.rawValue + if let settings = UserDefaults.standard.object(forKey: USER_DEFAULTS_NAME) + as? [String : String] { + self.accountName = settings[UserDefaultsKey.accountName.rawValue] ?? + DefaultSettings.accountName.rawValue + self.location = settings[UserDefaultsKey.location.rawValue] ?? + DefaultSettings.location.rawValue + self.pageTitle = settings[UserDefaultsKey.pageTitle.rawValue] ?? + DefaultSettings.pageTitle.rawValue } else { self.accountName = DefaultSettings.accountName.rawValue self.location = DefaultSettings.location.rawValue @@ -66,10 +69,12 @@ final class Settings { // MARK: - Methods func save() { - let settings = [UserDefaultsKey.accountName.rawValue : accountName, - UserDefaultsKey.location.rawValue : location, - UserDefaultsKey.pageTitle.rawValue : pageTitle, - UserDefaultsKey.colorScheme.rawValue : ColorScheme.shared.schemeType.rawValue] + let settings = [ + UserDefaultsKey.accountName.rawValue : accountName, + UserDefaultsKey.location.rawValue : location, + UserDefaultsKey.pageTitle.rawValue : pageTitle + ] + UserDefaults.standard.set(settings, forKey: USER_DEFAULTS_NAME) } diff --git a/Example/WebimClientLibrary/PopupActionsTableViewCell.swift b/Example/WebimClientLibrary/PopupActionsTableViewCell.swift new file mode 100644 index 00000000..813e845d --- /dev/null +++ b/Example/WebimClientLibrary/PopupActionsTableViewCell.swift @@ -0,0 +1,119 @@ +// +// PopoverActionTableViewCell.swift +// WebimClientLibrary_Example +// +// Created by Eugene Ilyin on 02/10/2019. +// Copyright © 2019 Webim. All rights reserved. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. +// + +import UIKit + +class PopupActionsTableViewCell: UITableViewCell { + + // MARK: - Subviews + private lazy var actionNameLabel: UILabel = { + let label = UILabel() + label.numberOfLines = 1 + label.font = .systemFont(ofSize: 17) + label.translatesAutoresizingMaskIntoConstraints = false + return label + }() + private lazy var actionImageView: UIImageView = { + let imageView = UIImageView() + imageView.contentMode = .scaleAspectFit + imageView.translatesAutoresizingMaskIntoConstraints = false + return imageView + }() + + // MARK: - Methods + func setupCell(forAction action: PopupAction) { + self.addSubview(actionNameLabel) + self.addSubview(actionImageView) + + setupConstraints() + + actionNameLabel.textColor = .white + + switch action { + case .reply: + fillCell( + actionText: action.rawValue.localized, + actionImage: replyImage + ) + case .copy: + fillCell( + actionText: action.rawValue.localized, + actionImage: copyImage + ) + case .edit: + fillCell( + actionText: action.rawValue.localized, + actionImage: editImage + ) + case .delete: + actionNameLabel.textColor = actionColourDelete + fillCell( + actionText: action.rawValue.localized, + actionImage: deleteImage + ) + } + } + + // MARK: - Private methods + private func setupConstraints() { + actionNameLabel.snp.remakeConstraints { (make) in + make.centerY.equalToSuperview() + // For some reason this layout only works for iOS 13+ only, not iOS 11+ as supposed to + if #available(iOS 13.0, *) { + make.leading.equalTo(self.safeAreaLayoutGuide.snp.leading) + .inset(10) + } else { + make.leading.equalToSuperview() + .inset(10) + } + } + + actionImageView.snp.remakeConstraints { (make) in + // For some reason this layout only works for iOS 13+ only, not iOS 11+ as supposed to + if #available(iOS 13.0, *) { + make.trailing.equalTo(self.safeAreaLayoutGuide) + .inset(10) + make.top.bottom.equalTo(self.safeAreaLayoutGuide) + .inset(10) + make.leading.equalTo(actionNameLabel.snp.trailing) + .offset(10) + } else { + make.trailing.bottom.equalToSuperview() + .inset(10) + make.top.bottom.equalToSuperview() + .inset(10) + } + make.centerY.equalTo(actionNameLabel.snp.centerY) + make.width.equalTo(actionImageView.snp.height) + } + } + + private func fillCell(actionText: String, actionImage: UIImage) { + actionNameLabel.text = actionText + actionImageView.image = actionImage + actionImageView.tintColor = nil + } +} diff --git a/Example/WebimClientLibrary/PopupActionsViewController.swift b/Example/WebimClientLibrary/PopupActionsViewController.swift new file mode 100644 index 00000000..4e0b8911 --- /dev/null +++ b/Example/WebimClientLibrary/PopupActionsViewController.swift @@ -0,0 +1,342 @@ +// +// NewPopupActionsViewController.swift +// WebimClientLibrary_Example +// +// Created by Eugene Ilyin on 28.10.2019. +// Copyright © 2019 Webim. All rights reserved. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. +// + +import AVFoundation +import UIKit + +class PopupActionsViewController: UIViewController { + // MARK: - ActionsTableView positions + private enum ActionsTableViewPosition { + case top, bottom + } + + // MARK: - Size constants + // HINT: Look for same properties in FlexibleTableViewCell.swift + fileprivate let CELL_SPACING_DEFAULT: CGFloat = 10.0 + fileprivate let USERAVATARIMAGEVIEW_WIDTH: CGFloat = 40.0 + + // MARK: - Properties + enum OriginalCellAlignment { + case leading, center, trailing + } + var cellImageViewImage = UIImage() + var cellImageViewHeight = CGFloat() + var cellImageViewCenterYPosition = CGFloat() + var actions = [PopupAction]() + var originalCellAlignment: OriginalCellAlignment = .center + + // MARK: - Private properties + private var actionsStates = [PopupAction: Bool]() + private var actionsTableViewCenterYPosition = CGFloat() + private var actionsTableViewContentHeight = CGFloat() + + // MARK: - Subviews + lazy var blurBackground: UIVisualEffectView = { + let blurEffect = UIBlurEffect(style: .dark) + let blurEffectView = UIVisualEffectView(effect: blurEffect) + blurEffectView.frame = view.bounds + blurEffectView.autoresizingMask = [.flexibleWidth, .flexibleHeight] + return blurEffectView + }() + lazy var cellImageView: UIImageView = { + let imageView = UIImageView() + imageView.contentMode = .scaleAspectFit + return imageView + }() + lazy var actionsTableView: UITableView = { + return UITableView() + }() + + // MARK: - View Life Cycle + override func viewDidLoad() { + super.viewDidLoad() + AudioServicesPlaySystemSound(1519) // Actuate "Peek" feedback (weak boom) + NotificationCenter.default.addObserver( + self, + selector: #selector(hidePopupActionsViewController), + name: .shouldHidePopupActionsViewController, + object: nil + ) + + view.backgroundColor = popupBackgroundColour + + for action in actions { + actionsStates[action] = false + } + + setupSubviews() + } + + override func viewDidAppear(_ animated: Bool) { + super.viewDidAppear(animated) + NotificationCenter.default.post( + name: .shouldHideQuoteEditBar, + object: nil, + userInfo: nil + ) + findAvailableSpace() + } + + override func viewWillDisappear(_ animated: Bool) { + NotificationCenter.default.removeObserver( + self, + name: .shouldHidePopupActionsViewController, + object: nil + ) + + if let index = self.actionsTableView.indexPathForSelectedRow { + self.actionsTableView.deselectRow(at: index, animated: true) + } + } + + // MARK: - Methods + func findAvailableSpace() { + let yValueImageViewTopEdge = cellImageViewCenterYPosition - cellImageViewHeight / 2 + let yValueImageViewBottomEdge = cellImageViewCenterYPosition + cellImageViewHeight / 2 + var yValueTopScreen = CGFloat() + var yValueBottomScreen = CGFloat() + if #available(iOS 11.0, *) { + yValueTopScreen = self.view.safeAreaLayoutGuide.layoutFrame.minY + yValueBottomScreen = self.view.safeAreaLayoutGuide.layoutFrame.maxY + } else { + yValueTopScreen = self.view.frame.minY + yValueBottomScreen = self.view.frame.maxY + } + + let topSpace = yValueImageViewTopEdge - yValueTopScreen + let bottomSpace = yValueBottomScreen - yValueImageViewBottomEdge + + if bottomSpace > 0 && bottomSpace > actionsTableViewContentHeight { + // There is space on the bottom + positionActionsTableView(on: .bottom) + } else if topSpace > 0 && topSpace > actionsTableViewContentHeight { + // There is space on the top + positionActionsTableView(on: .top) + } else { + // Content out of bounds + var neededBottomSpaceToAdd = CGFloat() + neededBottomSpaceToAdd = actionsTableViewContentHeight - bottomSpace + cellImageViewCenterYPosition -= neededBottomSpaceToAdd + positionCellImageView(withAnimationDuration: 0.5) + positionActionsTableView(on: .bottom) + } + } + + // MARK: - Private methods + private func setupSubviews() { + setupBackground() + + setupCellImageView() + setupActionsTableView() + } + + private func setupBackground() { + view.addSubview(blurBackground) + let tapGestureRecognizer = UITapGestureRecognizer( + target: self, + action: #selector(hidePopupActionsViewController) + ) + tapGestureRecognizer.cancelsTouchesInView = false + tapGestureRecognizer.delegate = self + view.addGestureRecognizer(tapGestureRecognizer) + } + + private func setupActionsTableView() { + actionsTableView.delegate = self + actionsTableView.dataSource = self + + actionsTableView.backgroundColor = actionsTableViewBackgroundColour + actionsTableView.layer.cornerRadius = 20 + actionsTableView.clipsToBounds = true + actionsTableView.isScrollEnabled = false + + actionsTableView.rowHeight = 40.0 + actionsTableView.separatorStyle = .none + + actionsTableView.register( + PopupActionsTableViewCell.self, + forCellReuseIdentifier: "PopupActionsTableViewCell" + ) + + actionsTableViewContentHeight = actionsTableView.rowHeight * CGFloat(actions.count) + + view.addSubview(actionsTableView) + } + + @objc + private func hidePopupActionsViewController() { + dismiss(animated: false) + hideOverlayWindowIfPresented() + } + + private func hideOverlayWindowIfPresented() { + // Will not trigger hideOverlayWindow() if there is no keyboard shown. Could fail. For more check todo in ChatTableViewController.swift + NotificationCenter.default.post( + name: .shouldHideOverlayWindow, + object: nil + ) + } + + private func setupCellImageView() { + cellImageView.image = cellImageViewImage + cellImageView.translatesAutoresizingMaskIntoConstraints = false + view.addSubview(cellImageView) + cellImageView.snp.remakeConstraints { (make) in + make.leading.trailing.equalToSuperview() + make.centerY.equalTo(cellImageViewCenterYPosition) + } + } + + private func positionCellImageView() { + cellImageView.snp.remakeConstraints { (make) in + make.leading.trailing.equalToSuperview() + make.centerY.equalTo(cellImageViewCenterYPosition) + } + self.view.layoutIfNeeded() + } + + private func positionCellImageView(withAnimationDuration: TimeInterval) { + UIView.animate(withDuration: withAnimationDuration) { + self.cellImageView.snp.remakeConstraints { (make) in + make.leading.trailing.equalToSuperview() + make.centerY.equalTo(self.cellImageViewCenterYPosition) + } + self.view.layoutIfNeeded() + } + } + + private func positionActionsTableView(on position: ActionsTableViewPosition) { + var actionsTableViewCenterYPosition: CGFloat = 0 + switch position { + case .top: + actionsTableViewCenterYPosition = + cellImageViewCenterYPosition - + cellImageViewHeight / 2 - + actionsTableViewContentHeight / 2 + + case .bottom: + actionsTableViewCenterYPosition = + cellImageViewCenterYPosition + + cellImageViewHeight / 2 + + actionsTableViewContentHeight / 2 + } + + actionsTableView.snp.remakeConstraints { (make) -> Void in + make.centerY.equalTo(actionsTableViewCenterYPosition) + + switch originalCellAlignment { + case .center: + make.centerX.equalToSuperview() + + case .leading: + if #available(iOS 11.0, *) { + make.leading.equalTo(self.view.safeAreaLayoutGuide) + .inset(2 * CELL_SPACING_DEFAULT + USERAVATARIMAGEVIEW_WIDTH) + } else { + make.leading.equalToSuperview() + .inset(2 * CELL_SPACING_DEFAULT + USERAVATARIMAGEVIEW_WIDTH) + } + + case .trailing: + if #available(iOS 11.0, *) { + make.trailing.equalTo(self.view.safeAreaLayoutGuide) + .inset(10) + } else { + make.trailing.equalToSuperview() + .inset(10) + } + } + + make.height.equalTo(actionsTableViewContentHeight) + make.width.equalTo(200.0) + } + } +} + +// MARK: - TableViewMethods +extension PopupActionsViewController: UITableViewDelegate, UITableViewDataSource { + + func numberOfSections(in tableView: UITableView) -> Int { 1 } + + func tableView( + _ tableView: UITableView, + numberOfRowsInSection section: Int + ) -> Int { actions.count } + + func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { + let actionsDictionary = ["Action": actions[indexPath.row]] + + switch actions[indexPath.row] { + + case .reply: + NotificationCenter.default.post( + name: .shouldShowQuoteEditBar, + object: nil, + userInfo: actionsDictionary + ) + case .copy: + NotificationCenter.default.post( + name: .shouldCopyMessage, + object: nil + ) + case .edit: + NotificationCenter.default.post( + name: .shouldShowQuoteEditBar, + object: nil, + userInfo: actionsDictionary + ) + case .delete: + NotificationCenter.default.post( + name: .shouldDeleteMessage, + object: nil + ) + } + + hidePopupActionsViewController() + } + + func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { + guard let cell = tableView.dequeueReusableCell( + withIdentifier: "PopupActionsTableViewCell", + for: indexPath) as? PopupActionsTableViewCell + else { + fatalError("The dequeued cell is not an instance of PopupActionsTableViewCell.") + } + + cell.backgroundColor = actionsTableViewCellBackgroundColour + cell.setupCell(forAction: actions[indexPath.row]) + return cell + } +} + +extension PopupActionsViewController: UIGestureRecognizerDelegate { + + func gestureRecognizer( + _ gestureRecognizer: UIGestureRecognizer, + shouldReceive touch: UITouch + ) -> Bool { !(touch.view?.isDescendant(of: self.actionsTableView) ?? false) } + +} diff --git a/Example/WebimClientLibrary/RatingDialogViewController.swift b/Example/WebimClientLibrary/RatingDialogViewController.swift new file mode 100644 index 00000000..44e5aef5 --- /dev/null +++ b/Example/WebimClientLibrary/RatingDialogViewController.swift @@ -0,0 +1,297 @@ +// +// RatingDialogViewController.swift +// WebimClientLibrary_Example +// +// Created by Eugene Ilyin on 09.10.2019. +// Copyright © 2019 Webim. All rights reserved. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. +// + +import AVFoundation +import Cosmos +import UIKit +import Nuke + +class RatingDialogViewController: UIViewController { + + // MARK: - Properties + var operatorID = String() + var operatorName = String() + var operatorAvatarImage = UIImage() + var operatorAvatarImageURL = String() + var operatorRating = 0.0 + var viewCenterYPosition = CGFloat() + + // MARK: - Subviews + lazy var blurBackground: UIVisualEffectView = { + let blurEffect = UIBlurEffect(style: .dark) + let blurEffectView = UIVisualEffectView(effect: blurEffect) + blurEffectView.frame = view.bounds + blurEffectView.autoresizingMask = [.flexibleWidth, .flexibleHeight] + return blurEffectView + }() + lazy var whiteBackground: UIView = { + let view = UIView() + view.translatesAutoresizingMaskIntoConstraints = false + view.layer.cornerRadius = 20.0 + view.backgroundColor = ratingDialogWhiteBackgroudColour + return view + }() + lazy var operatorAvatarImageView: UIImageView = { + let imageView = UIImageView() + imageView.translatesAutoresizingMaskIntoConstraints = false + imageView.contentMode = .scaleAspectFit + imageView.clipsToBounds = true + imageView.layer.cornerRadius = 20 + return imageView + }() + lazy var operatorNameLabel: UILabel = { + let label = UILabel() + label.translatesAutoresizingMaskIntoConstraints = false + label.numberOfLines = 1 + label.textAlignment = .center + label.textColor = ratingDialogOperatorNameLabelColour + return label + }() + lazy var titleLabel: UILabel = { + let label = UILabel() + label.translatesAutoresizingMaskIntoConstraints = false + label.numberOfLines = 0 + label.textAlignment = .center + label.font = .systemFont(ofSize: 17) + label.textColor = ratingDialogTitleLabelColour + return label + }() + + lazy var cosmosRatingView: CosmosView = { + let cosmosView = CosmosView() + cosmosView.translatesAutoresizingMaskIntoConstraints = false + cosmosView.settings.fillMode = .full + cosmosView.settings.starSize = 30 + cosmosView.settings.filledColor = cosmosViewFilledColour + cosmosView.settings.filledBorderColor = cosmosViewFilledBorderColour + cosmosView.settings.emptyColor = cosmosViewEmptyColour + cosmosView.settings.emptyBorderColor = cosmosViewEmptyBorderColour + cosmosView.settings.emptyBorderWidth = 2 + + return cosmosView + }() + + lazy var imageDownloadIndicator: CircleProgressIndicator = { + let downloadIndicator = CircleProgressIndicator() + downloadIndicator.lineWidth = 1 + downloadIndicator.strokeColor = documentFileStatusPercentageIndicatorColour + downloadIndicator.isUserInteractionEnabled = false + downloadIndicator.isHidden = true + downloadIndicator.translatesAutoresizingMaskIntoConstraints = false + + return downloadIndicator + }() + + // MARK: - View Life Cycle + override func viewDidLoad() { + super.viewDidLoad() + AudioServicesPlaySystemSound(1519) // Actuate "Peek" feedback (weak boom) + NotificationCenter.default.addObserver( + self, + selector: #selector(hideRatingDialogViewController), + name: .shouldHideRatingDialogViewController, + object: nil + ) + + self.view.backgroundColor = ratingDialogBackgroundColour + + setupSubviews() + } + + override func viewDidAppear(_ animated: Bool) { + super.viewDidAppear(animated) + operatorAvatarImageView.roundCorners( + [.layerMaxXMaxYCorner, + .layerMaxXMinYCorner, + .layerMinXMaxYCorner, + .layerMinXMinYCorner], + radius: operatorAvatarImageView.frame.height / 2 + ) + downloadOperatorAvatarIfNeeded() + } + + override func viewWillDisappear(_ animated: Bool) { + NotificationCenter.default.removeObserver( + self, + name: .shouldHideRatingDialogViewController, + object: nil + ) + } + + // MARK: - Private methods + private func setupSubviews() { + setupBackground() + + setupOperatorAvatarImageView() + setupOperatorNameLabel() + + setupCosmosRatingView() + setupTitleLabel() + } + + private func setupBackground() { + view.addSubview(blurBackground) + let tapGestureRecognizer = UITapGestureRecognizer( + target: self, + action: #selector(hideRatingDialogViewController) + ) + tapGestureRecognizer.cancelsTouchesInView = false + tapGestureRecognizer.delegate = self + view.addGestureRecognizer(tapGestureRecognizer) + + view.addSubview(whiteBackground) + whiteBackground.snp.remakeConstraints { (make) -> Void in + make.centerX.equalToSuperview() + make.centerY.equalTo(viewCenterYPosition) + make.height.width.equalTo(200) + } + } + + @objc + private func hideRatingDialogViewController() { + dismiss(animated: false) + + // Will not trigger hideOverlayWindow() if there is no keyboard shown. Could fail. For more check todo in ChatTableViewController.swift + NotificationCenter.default.post( + name: .shouldHideOverlayWindow, + object: nil + ) + } + + private func downloadOperatorAvatarIfNeeded() { + if let avatarURL = URL(string: operatorAvatarImageURL) { + let request = ImageRequest(url: avatarURL) + if let image = ImageCache.shared[request] { + imageDownloadIndicator.isHidden = true + operatorAvatarImageView.image = image + } else { + operatorAvatarImageView.image = loadingPlaceholderImage + + Nuke.ImagePipeline.shared.loadImage( + with: avatarURL, + progress: { _, completed, total in + DispatchQueue.global(qos: .userInteractive).async { + let progress = Float(completed) / Float(total) + DispatchQueue.main.async { + if self.imageDownloadIndicator.isHidden { + self.imageDownloadIndicator.isHidden = false + self.imageDownloadIndicator.enableRotationAnimation() + } + self.imageDownloadIndicator.setProgressWithAnimation( + duration: 0.1, + value: progress + ) + } + } + }, + completion: { _ in + DispatchQueue.main.async { + self.operatorAvatarImageView.image = ImageCache.shared[request] + self.imageDownloadIndicator.isHidden = true + } + } + ) + } + } + } + + private func setupOperatorAvatarImageView() { + operatorAvatarImageView.image = operatorAvatarImage + + operatorAvatarImageView.addSubview(imageDownloadIndicator) + imageDownloadIndicator.snp.remakeConstraints { (make) -> Void in + make.edges.equalToSuperview() + .inset(5) + } + + whiteBackground.addSubview(operatorAvatarImageView) + operatorAvatarImageView.snp.remakeConstraints { (make) -> Void in + make.width.height.equalTo(50) + make.centerX.equalToSuperview() + make.top.equalToSuperview() + .inset(10) + } + } + + private func setupOperatorNameLabel() { + operatorNameLabel.text = operatorName + + whiteBackground.addSubview(operatorNameLabel) + operatorNameLabel.snp.remakeConstraints { (make) -> Void in + make.centerX.equalToSuperview() + make.top.equalTo(operatorAvatarImageView.snp.bottom) + .offset(10) + } + } + + private func setupCosmosRatingView() { + cosmosRatingView.rating = operatorRating + cosmosRatingView.didTouchCosmos = { _ in + AudioServicesPlaySystemSound(1519) // Actuate "Peek" feedback (weak boom) + } + cosmosRatingView.didFinishTouchingCosmos = { (rating) -> Void in + let rating = Int(rating) + let dictionaryToPost = [self.operatorID: rating] + NotificationCenter.default.post( + name: .shouldRateOperator, + object: nil, + userInfo: dictionaryToPost + ) + + DispatchQueue.main.asyncAfter(deadline: .now() + 0.4) { + self.hideRatingDialogViewController() + } + } + + whiteBackground.addSubview(cosmosRatingView) + cosmosRatingView.snp.remakeConstraints { (make) -> Void in + make.bottom.equalToSuperview() + .inset(10) + make.centerX.equalToSuperview() + } + } + + private func setupTitleLabel() { + titleLabel.text = RatingDialogView.rateTitleText.rawValue.localized + + whiteBackground.addSubview(titleLabel) + titleLabel.snp.remakeConstraints { (make) -> Void in + make.leading.trailing.equalToSuperview() + .inset(10) + make.bottom.equalTo(cosmosRatingView.snp.top) + .offset(-10) + } + } +} + +extension RatingDialogViewController: UIGestureRecognizerDelegate { + + func gestureRecognizer( + _ gestureRecognizer: UIGestureRecognizer, + shouldReceive touch: UITouch + ) -> Bool { !(touch.view?.isDescendant(of: self.whiteBackground) ?? false) } + +} diff --git a/Example/WebimClientLibrary/SettingsTableViewController.swift b/Example/WebimClientLibrary/SettingsTableViewController.swift index 649da754..b833589f 100644 --- a/Example/WebimClientLibrary/SettingsTableViewController.swift +++ b/Example/WebimClientLibrary/SettingsTableViewController.swift @@ -1,9 +1,9 @@ // -// SettingsTableViewController.swift +// SettingsTableView.swift // WebimClientLibrary_Example // -// Created by Nikita Lazarev-Zubov on 07.02.18. -// Copyright © 2018 Webim. All rights reserved. +// Created by Eugene Ilyin on 16/09/2019. +// Copyright © 2019 Webim. All rights reserved. // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal @@ -26,14 +26,12 @@ import UIKit -final class SettingsTableViewController: UITableViewController { - +class SettingsTableViewController: UITableViewController { + // MARK: - Properties - weak var delegate: SettingsViewController? - // MARK: Outlets - + // MARK: - Outlets // Text fields @IBOutlet weak var accountNameTextField: UITextField! @IBOutlet weak var locationTextField: UITextField! @@ -43,31 +41,15 @@ final class SettingsTableViewController: UITableViewController { @IBOutlet weak var accountNameHintLabel: UILabel! @IBOutlet weak var locationHintLabel: UILabel! - // Text - @IBOutlet var textFieldLabels: [UILabel]! - - // Color scheme - @IBOutlet weak var classicCheckboxImageView: UIImageView! // Row 0 - @IBOutlet weak var darkCheckboxImageView: UIImageView! // Row 2 - @IBOutlet var colorSchemeNames: [UILabel]! - - // Table cells - @IBOutlet weak var accountSettingsCell: UITableViewCell! - @IBOutlet var colorThemeCells: [UITableViewCell]! - @IBOutlet weak var delimiter: UIView! - - // Status bar style - override var preferredStatusBarStyle : UIStatusBarStyle { - switch ColorScheme.shared.schemeType { - case .light: - return .default - case .dark: - return .lightContent - } - } - - // MARK: - Methods + // Editing/error views + @IBOutlet weak var accountNameView: UIView! + @IBOutlet weak var locationView: UIView! + @IBOutlet weak var pageTitleView: UIView! + + // Labels + @IBOutlet weak var accountTitlelabel: UILabel! + // MARK: - View Life Cycle override func viewDidLoad() { super.viewDidLoad() @@ -75,131 +57,115 @@ final class SettingsTableViewController: UITableViewController { locationTextField.text = Settings.shared.location pageTitleTextField.text = Settings.shared.pageTitle - setupColorSchemeCheckboxes() - setupColorScheme() + accountNameTextField.placeholder = Settings.shared.accountName + locationTextField.placeholder = Settings.shared.location - for hintLabel in [accountNameHintLabel, locationHintLabel] { - hintLabel!.alpha = 0.0 - hintLabel!.textColor = textTextFieldErrorColor.color() - } - for textField in [accountNameTextField, - locationTextField] { - textField!.addTarget(self, - action: #selector(textDidChange), - for: .editingChanged) - textField!.layer.cornerRadius = 5.0 - textField!.layer.borderColor = textTextFieldErrorColor.color().cgColor - textField!.delegate = self - } - pageTitleTextField.delegate = self - } - - // MARK: UITableViewDelegate protocol methods - - override func tableView(_ tableView: UITableView, - willDisplayHeaderView view: UIView, - forSection section: Int) { - view.tintColor = backgroundMainColor.color() - } - - override func tableView(_ tableView: UITableView, - didSelectRowAt indexPath: IndexPath) { - guard indexPath.section == 1 else { // Color theme section - return + for hintLabel in [ + accountNameHintLabel, + locationHintLabel + ] { + guard let hintLabel = hintLabel else { continue } + hintLabel.alpha = 0.0 } - var selectedColorScheme: ColorScheme.SchemeType? - switch indexPath.row { - case 0: - selectedColorScheme = .light + for textField in [ + accountNameTextField, + locationTextField, + pageTitleTextField + ] { + guard let textField = textField else { continue } - break - case 2: - selectedColorScheme = .dark - - break - default: - return - } - - if ColorScheme.shared.schemeType != selectedColorScheme { - ColorScheme.shared.schemeType = selectedColorScheme! - UIView.animate(withDuration: 0.2) { - self.setupColorSchemeCheckboxes() - self.setupColorScheme() - } + textField.addTarget( + self, + action: #selector(startedTyping), + for: .editingDidBegin + ) + textField.addTarget( + self, + action: #selector(stoppedTyping), + for: .editingDidEnd + ) + textField.delegate = self } } - // MARK: Private mwethods + // MARK: - Methods + @objc + func scrollToBottom(animated: Bool) { + let row = (tableView.numberOfRows(inSection: 0)) - 1 + let bottomMessageIndex = IndexPath(row: row, section: 0) + tableView.scrollToRow(at: bottomMessageIndex, at: .bottom, animated: animated) + } - private func setupColorSchemeCheckboxes() { - switch ColorScheme.shared.schemeType { - case .light: - classicCheckboxImageView.alpha = 1.0 - darkCheckboxImageView.alpha = 0.0 - case .dark: - classicCheckboxImageView.alpha = 0.0 - darkCheckboxImageView.alpha = 1.0 - } + @objc + func scrollToTop(animated: Bool) { + let indexPath = IndexPath(row: 0, section: 0) + self.tableView.scrollToRow(at: indexPath, at: .top, animated: animated) } - private func setupColorScheme() { - delegate?.setupColorScheme() - - tableView.backgroundColor = backgroundMainColor.color() - accountSettingsCell.backgroundColor = backgroundMainColor.color() - for cell in colorThemeCells { - cell.backgroundColor = backgroundCellLightColor.color() - } - - for label in textFieldLabels { - label.textColor = textMainColor.color() - } - for label in colorSchemeNames { - label.textColor = textCellLightColor.color() - } + // MARK: - Private Methods + @objc + private func startedTyping(textField: UITextField) { + var hintLabel: UILabel? + var editView: UIView? - for textField in [accountNameTextField, - locationTextField, - pageTitleTextField] { - textField!.backgroundColor = backgroundTextFieldColor.color() - textField!.textColor = textTextFieldColor.color() - textField!.tintColor = textTextFieldColor.color() - textField!.keyboardAppearance = ColorScheme.shared.keyboardAppearance() + if textField == accountNameTextField { + editView = accountNameView + hintLabel = accountNameHintLabel + } else if textField == locationTextField { + editView = locationView + hintLabel = locationHintLabel + } else { + editView = pageTitleView } - delimiter.backgroundColor = delimiterColor.color() + UIView.animate( + withDuration: 0.2, + animations: { + hintLabel?.alpha = 0.0 + editView!.backgroundColor = editViewBackgroundColourEditing + } + ) } - + @objc - private func textDidChange(textField: UITextField) { + private func stoppedTyping(textField: UITextField) { var hintLabel: UILabel? + var editView: UIView? + if textField == accountNameTextField { + editView = accountNameView hintLabel = accountNameHintLabel - } else { + } else if textField == locationTextField { + editView = locationView hintLabel = locationHintLabel + } else { + editView = pageTitleView + if let text = textField.text?.trimWhitespacesIn(), text.isEmpty { + textField.text = "iOS demo app" + } } - UIView.animate(withDuration: 0.2) { - if (textField.text == nil) - || textField.text!.isEmpty { - textField.layer.borderWidth = 1.0 - hintLabel!.alpha = 1.0 - } else { - textField.layer.borderWidth = 0.0 - hintLabel!.alpha = 0.0 + UIView.animate( + withDuration: 0.2, + animations: { + if let text = textField.text?.trimmingCharacters(in: .whitespacesAndNewlines), + text.isEmpty { + hintLabel?.alpha = 1.0 + editView!.backgroundColor = editViewBackgroundColourError + } else { + hintLabel?.alpha = 0.0 + editView!.backgroundColor = editViewBackgroundColourDefault + } } - } + ) } - } extension SettingsTableViewController: UITextFieldDelegate { func textFieldShouldReturn(_ textField: UITextField) -> Bool { textField.resignFirstResponder() - return true } diff --git a/Example/WebimClientLibrary/SettingsViewController.swift b/Example/WebimClientLibrary/SettingsViewController.swift index 129cd412..5c7088c8 100644 --- a/Example/WebimClientLibrary/SettingsViewController.swift +++ b/Example/WebimClientLibrary/SettingsViewController.swift @@ -24,26 +24,39 @@ // SOFTWARE. // -import PopupDialog import UIKit final class SettingsViewController: UIViewController { - // MARK: - Properties - private var popupDialogHandler: PopupDialogHandler? + // MARK: - Private Properties private var settingsTableViewController: SettingsTableViewController? + private var rotationDuration: TimeInterval = 0.0 - // MARK: Outlets + private lazy var alertDialogHandler = UIAlertHandler(delegate: self) + + // MARK: - Outlets @IBOutlet weak var saveButton: UIButton! + @IBOutlet weak var bottomConstraint: NSLayoutConstraint! + // MARK: - View Life Cycle + override func prepare(for segue: UIStoryboardSegue, + sender: Any?) { + if let viewController = segue.destination as? SettingsTableViewController, + segue.identifier == "EmbedSettingsTable" { + settingsTableViewController = viewController + + viewController.delegate = self + } + } - // MARK: - Methods + override func shouldPerformSegue( + withIdentifier identifier: String, + sender: Any? + ) -> Bool { identifier == "EmbedSettingsTable" || settingsValidated() } override func viewDidLoad() { super.viewDidLoad() - popupDialogHandler = PopupDialogHandler(delegate: self) - setupNavigationItem() setupSaveButton() @@ -56,70 +69,59 @@ final class SettingsViewController: UIViewController { setupColorScheme() } - func setupColorScheme() { - view.backgroundColor = backgroundMainColor.color() - - navigationController?.navigationBar.barTintColor = backgroundSecondaryColor.color() - setupVisibleNavigationItemsElements() + override func viewWillAppear(_ animated: Bool) { + super.viewWillAppear(animated) - saveButton.backgroundColor = buttonColor.color() - saveButton.setTitleColor(textButtonColor.color(), - for: .normal) + NotificationCenter.default.addObserver( + self, + selector: #selector(keyboardWillChange), + name: UIResponder.keyboardWillChangeFrameNotification, + object: nil + ) } - // MARK: Navigation + override func viewWillDisappear(_ animated: Bool) { + NotificationCenter.default.removeObserver( + self, + name: UIResponder.keyboardWillChangeFrameNotification, + object: nil + ) + } - override func prepare(for segue: UIStoryboardSegue, - sender: Any?) { - if let viewController = segue.destination as? SettingsTableViewController, - segue.identifier == "EmbedSettingsTable" { - settingsTableViewController = viewController - - viewController.delegate = self - } + override func viewWillTransition( + to size: CGSize, + with coordinator: UIViewControllerTransitionCoordinator + ) { + view.endEditing(true) + super.viewWillTransition(to: size, with: coordinator) + coordinator.animate( + alongsideTransition: { context in + self.rotationDuration = context.transitionDuration + } + ) } - override func shouldPerformSegue(withIdentifier identifier: String, - sender: Any?) -> Bool { - if identifier != "EmbedSettingsTable", - !settingsValidated() { - return false - } + // MARK: - Methods + func setupColorScheme() { + view.backgroundColor = backgroundViewColour - return true + saveButton.backgroundColor = saveButtonBackgroundColour + saveButton.setTitleColor(saveButtonTitleColour, for: .normal) } - // MARK: Private methods - + // MARK: - Private methods private func setupNavigationItem() { - setupVisibleNavigationItemsElements() + navigationItem.setHidesBackButton(true, animated: false) - // Need for title view to be centered. - let emptyBarButton = UIButton(type: .custom) - emptyBarButton.setImage(#imageLiteral(resourceName: "Empty"), - for: .normal) - emptyBarButton.isUserInteractionEnabled = false - navigationItem.rightBarButtonItem = UIBarButtonItem(customView: emptyBarButton) - } - - private func setupVisibleNavigationItemsElements() { - let backButton = UIButton(type: .custom) - backButton.setImage(ColorScheme.shared.backButtonImage(), - for: .normal) - backButton.imageView?.contentMode = .scaleAspectFit - backButton.accessibilityLabel = BackButton.accessibilityLabel.rawValue.localized - backButton.accessibilityHint = BackButton.accessibilityHint.rawValue.localized - backButton.addTarget(self, - action: #selector(onBackButtonClick(sender:)), - for: .touchUpInside) - let leftBarButtonItem = UIBarButtonItem(customView: backButton) - self.navigationItem.leftBarButtonItem = leftBarButtonItem + let imageView = UIImageView() + imageView.contentMode = .scaleAspectFit + imageView.image = navigationBarTitleImageViewImage + imageView.accessibilityLabel = ChatView.navigationBarAccessibilityLabelText.rawValue.localized + imageView.accessibilityTraits = .header - let navigationItemImageView = UIImageView(image: ColorScheme.shared.navigationItemImage()) - navigationItemImageView.contentMode = .scaleAspectFit - navigationItem.titleView = navigationItemImageView + navigationItem.titleView = imageView } - + @objc private func onBackButtonClick(sender: UIButton) { if settingsValidated() { @@ -127,17 +129,59 @@ final class SettingsViewController: UIViewController { } } + @objc + private func keyboardWillChange(_ notification: Notification) { + guard let keyboardDurationEmergenceAnimation = notification.userInfo?[UIResponder.keyboardAnimationDurationUserInfoKey] + as? TimeInterval, + let keyboardFrame = notification.userInfo?[UIResponder.keyboardFrameEndUserInfoKey] + as? NSValue + else { return } + + var animationDuration: TimeInterval = keyboardDurationEmergenceAnimation + var keyboardHeight: CGFloat = view.frame.maxY - keyboardFrame.cgRectValue.minY + + if animationDuration == 0 { + if keyboardHeight == 0 { + return + } + keyboardHeight = 0 + animationDuration = rotationDuration + } + + UIView.animate( + withDuration: animationDuration, + animations: { + if keyboardHeight > 0 { + // Keyboard is visible + self.bottomConstraint.constant = 8.0 + keyboardHeight + } else { + // Keyboard is hidden + self.bottomConstraint.constant = 8.0 + } + + self.view.layoutIfNeeded() + if keyboardHeight > 0 { + self.settingsTableViewController?.scrollToBottom(animated: false) + } + } + ) + } + private func settingsValidated() -> Bool { - guard let accountName = settingsTableViewController?.accountNameTextField.text, + guard let accountName = settingsTableViewController?.accountNameTextField.text? + .trimmingCharacters(in: .whitespacesAndNewlines), !accountName.isEmpty else { - popupDialogHandler?.showSettingsAlertDialog(withMessage: SettingsErrorDialog.wrongAccountName.rawValue.localized) - + alertDialogHandler.showSettingsAlertDialog( + withMessage: SettingsErrorDialog.wrongAccountName.rawValue.localized + ) return false } - guard let location = settingsTableViewController?.locationTextField.text, + guard let location = settingsTableViewController?.locationTextField.text? + .trimmingCharacters(in: .whitespacesAndNewlines), !location.isEmpty else { - popupDialogHandler?.showSettingsAlertDialog(withMessage: SettingsErrorDialog.wrongLocation.rawValue.localized) - + alertDialogHandler.showSettingsAlertDialog( + withMessage: SettingsErrorDialog.wrongLocation.rawValue.localized + ) return false } @@ -148,23 +192,23 @@ final class SettingsViewController: UIViewController { return true } - private func set(accountName: String, - location: String, - pageTitle: String?) { + private func set( + accountName: String, + location: String, + pageTitle: String? + ) { Settings.shared.accountName = accountName Settings.shared.location = location if let pageTitle = pageTitle, !pageTitle.isEmpty { Settings.shared.pageTitle = pageTitle } - Settings.shared.save() } private func setupSaveButton() { - saveButton.layer.borderWidth = Button.borderWidth.rawValue - saveButton.layer.borderColor = buttonBorderColor.color().cgColor - saveButton.layer.cornerRadius = Button.cornerRadius.rawValue + saveButton.layer.borderWidth = 0 + saveButton.layer.borderColor = saveButtonBorderColour + saveButton.layer.cornerRadius = 8.0 } - } diff --git a/Example/WebimClientLibrary/StartViewController.swift b/Example/WebimClientLibrary/StartViewController.swift index e0b97aa3..bc8a0cbf 100644 --- a/Example/WebimClientLibrary/StartViewController.swift +++ b/Example/WebimClientLibrary/StartViewController.swift @@ -25,78 +25,164 @@ // import UIKit +import WebimClientLibrary final class StartViewController: UIViewController { - // MARK: - Properties - // MARK: Outlets + // MARK: - Private Properties + private var unreadMessageCounter: Int = 0 + + private lazy var alertDialogHandler = UIAlertHandler(delegate: self) + private lazy var webimService = WebimService( + fatalErrorHandlerDelegate: self, + departmentListHandlerDelegate: self + ) + + // MARK: - Outlets @IBOutlet weak var startChatButton: UIButton! @IBOutlet weak var settingsButton: UIButton! @IBOutlet weak var welcomeTextView: UITextView! + @IBOutlet weak var welcomeLabel: UILabel! + @IBOutlet weak var logoImageView: UIImageView! + @IBOutlet weak var unreadMessageCounterLabel: UILabel! + @IBOutlet weak var unreadMessageCounterActivity: UIActivityIndicatorView! - // MARK: - Methods + // MARK: - View Life Cycle + override func viewWillAppear(_ animated: Bool) { + super.viewWillAppear(animated) + + // Workaround for displaying correctly the position of the text inside weclomeTextView + DispatchQueue.main.async { + // Workaround for localization + self.welcomeTextView.text = StartView.welcomeText.rawValue.localized + self.welcomeTextView.scrollRangeToVisible(NSRange(location: 0,length: 0)) + } + + setupColorScheme() + + startWebimSession() + } override func viewDidLoad() { super.viewDidLoad() setupStartChatButton() setupSettingsButton() - - // Xcode does not localize UITextView text automatically. - welcomeTextView.text = NSLocalizedString(StartView.welcomeText.rawValue, - tableName: "Main", - bundle: .main, - value: "", - comment: "") } - override func viewWillAppear(_ animated: Bool) { - super.viewWillAppear(animated) - - setupColorScheme() - setupNavigationItem() + override func viewWillDisappear(_ animated: Bool) { + stopWebimSession() } @IBAction func unwindFromSettings(_: UIStoryboardSegue) { // No need to do anything. } - // MARK: Private methods - + // MARK: - Private methods private func setupStartChatButton() { - startChatButton.layer.cornerRadius = Button.cornerRadius.rawValue - startChatButton.layer.borderWidth = Button.borderWidth.rawValue - startChatButton.layer.borderColor = buttonBorderColor.color().cgColor + startChatButton.layer.cornerRadius = 8.0 + startChatButton.layer.borderWidth = 1.0 + startChatButton.layer.borderColor = startChatButtonBorderColour } private func setupSettingsButton() { - settingsButton.layer.cornerRadius = TransparentButton.cornerRadius.rawValue - settingsButton.layer.borderWidth = TransparentButton.borderWidth.rawValue + settingsButton.layer.cornerRadius = 8.0 + settingsButton.layer.borderWidth = 1.0 } private func setupColorScheme() { - view.backgroundColor = backgroundMainColor.color() - navigationController?.navigationBar.barTintColor = backgroundSecondaryColor.color() + view.backgroundColor = startViewBackgroundColour + + // Fixing 'shadow' on top of the main colour + navigationController?.navigationBar.barStyle = .black + navigationController?.navigationBar.setBackgroundImage(UIImage(), for: .default) + navigationController?.navigationBar.shadowImage = UIImage() + navigationController?.navigationBar.barTintColor = navigationBarBarTintColour - welcomeTextView.backgroundColor = backgroundMainColor.color() - welcomeTextView.textColor = textMainColor.color() - welcomeTextView.tintColor = textTintColor.color() + navigationController?.navigationBar.tintColor = navigationBarTintColour + + logoImageView.image = logoImageViewImage + + welcomeLabel.textColor = welcomeLabelTextColour + + welcomeTextView.textColor = welcomeTextViewTextColour + welcomeTextView.linkTextAttributes = [ + NSAttributedString.Key.foregroundColor: welcomeTextViewForegroundColour + ] + + startChatButton.backgroundColor = startChatButtonBackgroundColour + startChatButton.setTitleColor(startChatTitleColour, for: .normal) - startChatButton.backgroundColor = buttonColor.color() - startChatButton.setTitleColor(textButtonColor.color(), - for: .normal) + settingsButton.setTitleColor(settingsButtonTitleColour, for: .normal) - settingsButton.setTitleColor(textButtonTransparentColor.color(), - for: .normal) - settingsButton.setTitleColor(textButtonTransparentHighlightedColor.color(), - for: .highlighted) - settingsButton.layer.borderColor = textButtonTransparentColor.color().cgColor + settingsButton.layer.borderColor = settingButtonBorderColour + } + + private func updateMessageCounter() { + DispatchQueue.main.async { + self.unreadMessageCounterLabel.text = "\(self.unreadMessageCounter)" + self.unreadMessageCounterActivity.stopAnimating() + self.unreadMessageCounterLabel.fadeTransition(0.2) + self.unreadMessageCounterLabel.text = "\(self.unreadMessageCounter)" + } } - private func setupNavigationItem() { - let navigationItemImageView = UIImageView(image: ColorScheme.shared.navigationItemImage()) - navigationItemImageView.contentMode = .scaleAspectFit - navigationItem.titleView = navigationItemImageView + private func startWebimSession() { + webimService.createSession() + webimService.startSession() + webimService.setMessageStream() + webimService.set(unreadByVisitorMessageCountChangeListener: self) + + unreadMessageCounter = webimService.getUnreadMessagesByVisitor() + updateMessageCounter() + } + + private func stopWebimSession() { + webimService.stopSession() + + unreadMessageCounter = 0 + unreadMessageCounterLabel.text = nil + unreadMessageCounterActivity.startAnimating() + } +} + +// MARK: - WEBIM: MessageListener +extension StartViewController: UnreadByVisitorMessageCountChangeListener { + + // MARK: - Methods + func changedUnreadByVisitorMessageCountTo(newValue: Int) { + if unreadMessageCounter == 0 { + DispatchQueue.main.async { + self.unreadMessageCounterActivity.stopAnimating() + } + } + unreadMessageCounter = newValue + updateMessageCounter() + } + +} + +// MARK: - FatalErrorHandler +extension StartViewController: FatalErrorHandlerDelegate { + + // MARK: - Methods + func showErrorDialog(withMessage message: String) { + alertDialogHandler.showCreatingSessionFailureDialog(withMessage: message) + } + +} + +// MARK: - DepartmentListHandlerDelegate +extension StartViewController: DepartmentListHandlerDelegate { + + // MARK: - Methods + func show(departmentList: [Department], action: @escaping (String) -> ()) { + + // Due to the fact that it is the start screen, there is no need in diplaying departmentlist +// alertDialogHandler.showDepartmentListDialog( +// withDepartmentList: departmentList, +// action: action +// ) } } diff --git a/Example/WebimClientLibrary/Utilities/CircleProgressIndicator.swift b/Example/WebimClientLibrary/Utilities/CircleProgressIndicator.swift new file mode 100644 index 00000000..72247d2a --- /dev/null +++ b/Example/WebimClientLibrary/Utilities/CircleProgressIndicator.swift @@ -0,0 +1,122 @@ +// +// CircleProgressIndicator.swift +// WebimClientLibrary_Example +// +// Created by Eugene Ilyin on 21.10.2019. +// Copyright © 2019 Webim. All rights reserved. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. +// + +import Foundation +import UIKit + +class CircleProgressIndicator: UIView { + + // MARK: - Properties + var lineWidth: CGFloat = 2 { + didSet { + circleLayer.lineWidth = lineWidth + setNeedsLayout() + } + } + var strokeColor: CGColor = UIColor.red.cgColor { + didSet { + circleLayer.strokeColor = strokeColor + setNeedsLayout() + } + } + + // MARK: - Private properties + private var startValue: Float = 0 + private let backgrondCircleLayer = CAShapeLayer() + private let circleLayer = CAShapeLayer() + private let rotationAnimation: CAAnimation = { + let animation = CABasicAnimation(keyPath: "transform.rotation.z") + animation.fromValue = 0 + animation.toValue = Double.pi * 2 + animation.duration = 4 + animation.repeatCount = .infinity + + return animation + }() + + // MARK: - Methods + override init(frame: CGRect) { + super.init(frame: frame) + setup() + } + + required init?(coder: NSCoder) { + super.init(coder: coder) + setup() + } + + override func layoutSubviews() { + super.layoutSubviews() + + let center = CGPoint(x: bounds.midX, y: bounds.midY) + let radius = min(bounds.width, bounds.height) / 2 - circleLayer.lineWidth / 2 + + let startAngle = CGFloat(-Double.pi/2) // -90° + let endAngle = startAngle + CGFloat(Double.pi * 2) + let path = UIBezierPath( + arcCenter: CGPoint.zero, + radius: radius, + startAngle: startAngle, + endAngle: endAngle, + clockwise: true + ) + + backgrondCircleLayer.position = center + backgrondCircleLayer.path = path.cgPath + + circleLayer.position = center + circleLayer.path = path.cgPath + } + + func enableRotationAnimation() { + circleLayer.add(rotationAnimation, forKey: "rotation") + } + + func setProgressWithAnimation(duration: TimeInterval, value: Float) { + let animation = CABasicAnimation(keyPath: "strokeEnd") + animation.duration = duration + animation.fromValue = startValue + startValue = value + animation.toValue = value + animation.timingFunction = CAMediaTimingFunction(name: .linear) + circleLayer.strokeEnd = CGFloat(value) + + circleLayer.add(animation, forKey: "strokeEnd") + } + + // MARK: - Private methods + private func setup() { + backgrondCircleLayer.lineWidth = lineWidth + backgrondCircleLayer.fillColor = nil + backgrondCircleLayer.strokeColor = #colorLiteral(red: 0.8039215803, green: 0.8039215803, blue: 0.8039215803, alpha: 1).cgColor + layer.addSublayer(backgrondCircleLayer) + + circleLayer.lineWidth = lineWidth + circleLayer.fillColor = nil + circleLayer.strokeColor = strokeColor + layer.addSublayer(circleLayer) + } +} diff --git a/Example/WebimClientLibrary/Utilities/CustomUIButton.swift b/Example/WebimClientLibrary/Utilities/CustomUIButton.swift new file mode 100644 index 00000000..30ffa242 --- /dev/null +++ b/Example/WebimClientLibrary/Utilities/CustomUIButton.swift @@ -0,0 +1,54 @@ +// +// CustomUIButton.swift +// WebimClientLibrary_Example +// +// Created by Eugene Ilyin on 23.10.2019. +// Copyright © 2019 Webim. All rights reserved. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. +// + +import UIKit + +// UIButton with enlargedTapArea (by VALUE points) +class CustomUIButton: UIButton { + + // MARK: - Private properties + private let VALUE: CGFloat = 20.0 + + // MARK: - Methods + override func point(inside point: CGPoint, with event: UIEvent?) -> Bool { + let newArea = CGRect( + x: self.bounds.origin.x - VALUE / 2, + y: self.bounds.origin.y - VALUE / 2, + width: self.bounds.size.width + VALUE, + height: self.bounds.size.height + VALUE + ) + return newArea.contains(point) + } + + override init(frame: CGRect) { + super.init(frame: frame) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + +} diff --git a/Example/WebimClientLibrary/AppearanceSettings/ButtonConstants.swift b/Example/WebimClientLibrary/Utilities/CustomUIImage.swift similarity index 68% rename from Example/WebimClientLibrary/AppearanceSettings/ButtonConstants.swift rename to Example/WebimClientLibrary/Utilities/CustomUIImage.swift index f4b945f9..392f0f3f 100644 --- a/Example/WebimClientLibrary/AppearanceSettings/ButtonConstants.swift +++ b/Example/WebimClientLibrary/Utilities/CustomUIImage.swift @@ -1,9 +1,9 @@ // -// ButtonConstants.swift +// CustomUIImage.swift // WebimClientLibrary_Example // -// Created by Nikita Lazarev-Zubov on 28.11.17. -// Copyright © 2017 Webim. All rights reserved. +// Created by Eugene Ilyin on 19.11.2019. +// Copyright © 2019 Webim. All rights reserved. // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal @@ -26,23 +26,10 @@ import UIKit -//startChatButton -enum Button: CGFloat { - case borderWidth = 0 - case cornerRadius = 8.0 -} - -enum TransparentButton: CGFloat { - case borderWidth = 1.0 - case cornerRadius = 8.0 -} - -// Scroll to bottom button. -enum ScrollToBottomButton: CGFloat { - case margin = 8.0 - case size = 48.0 - case visibilityThreshold = 60.0 -} -enum ScrollToBottomButtonAnimation: Double { - case duration = 0.2 +// UIImage without render +class CustomUIImage: UIImage { + + // MARK: - Methods + override func withRenderingMode(_ renderingMode: UIImage.RenderingMode) -> UIImage { self } + } diff --git a/Example/WebimClientLibrary/Utilities/CustomUIView.swift b/Example/WebimClientLibrary/Utilities/CustomUIView.swift new file mode 100644 index 00000000..71a7e6d0 --- /dev/null +++ b/Example/WebimClientLibrary/Utilities/CustomUIView.swift @@ -0,0 +1,54 @@ +// +// CustomUIView.swift +// WebimClientLibrary_Example +// +// Created by Eugene Ilyin on 18.11.2019. +// Copyright © 2019 Webim. All rights reserved. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. +// + +import UIKit + +// UIView with enlargedTapArea on the X axis(by VALUE points) +class CustomUIView: UIView { + + // MARK: - Private properties + private let VALUE: CGFloat = + UIApplication.shared.userInterfaceLayoutDirection == .rightToLeft ? -500.0 : 500.0 + + // MARK: - Methods + override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { + let bounds = self.bounds + let frame = CGRect( + origin: bounds.origin, + size: CGSize(width: bounds.width + VALUE, height: bounds.height) + ) + return frame.contains(point) ? self : nil + } + + override init(frame: CGRect) { + super.init(frame: frame) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + +} diff --git a/Example/WebimClientLibrary/Utilities/Extensions/Message.swift b/Example/WebimClientLibrary/Utilities/Extensions/Message.swift new file mode 100644 index 00000000..ad91dc82 --- /dev/null +++ b/Example/WebimClientLibrary/Utilities/Extensions/Message.swift @@ -0,0 +1,56 @@ +// +// Message.swift +// WebimClientLibrary_Example +// +// Created by Eugene Ilyin on 28.10.2019. +// Copyright © 2019 Webim. All rights reserved. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. +// + +import Foundation +import WebimClientLibrary + +extension Message { + + // MARK: - Methods + public func isSystemType() -> Bool { + return self.getType() == .actionRequest + || self.getType() == .contactInformationRequest + || self.getType() == .info + || self.getType() == .keyboard + || self.getType() == .keyboardResponse + || self.getType() == .operatorBusy + } + + public func isVisitorType() -> Bool { + return self.getType() == .visitorMessage + || self.getType() == .fileFromVisitor + } + + public func isOperatorType() -> Bool { + return self.getType() == .operatorMessage + || self.getType() == .fileFromOperator + } + + public func canBeCopied() -> Bool { + return self.getType() == .operatorMessage + || self.getType() == .visitorMessage + } +} diff --git a/Example/WebimClientLibrary/Utilities/Extensions/Notification.Name.swift b/Example/WebimClientLibrary/Utilities/Extensions/Notification.Name.swift new file mode 100644 index 00000000..e4e887c0 --- /dev/null +++ b/Example/WebimClientLibrary/Utilities/Extensions/Notification.Name.swift @@ -0,0 +1,55 @@ +// +// Notification.Name.swift +// WebimClientLibrary_Example +// +// Created by Eugene Ilyin on 16/09/2019. +// Copyright © 2019 Webim. All rights reserved. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. +// + +import Foundation + +extension Notification.Name { + static let shouldShowQuoteEditBar = Notification.Name("shouldShowQuoteEditBar") + static let shouldHideQuoteEditBar = Notification.Name("shouldHideQuoteEditBar") + + static let shouldCopyMessage = Notification.Name("shouldCopyMessage") + static let shouldDeleteMessage = Notification.Name("shouldDeleteMessage") + + static let shouldSetVisitorTypingDraft = Notification.Name("shouldSetVisitorTypingDraft") + + static let shouldShowScrollButton = Notification.Name("shouldAddScrollButton") + static let shouldHideScrollButton = Notification.Name("shouldHideScrollButton") + + static let shouldShowRatingDialog = Notification.Name("shouldShowRatingDialog") + static let shouldRateOperator = Notification.Name("shouldRateOperator") + + static let shouldShowFile = Notification.Name("shouldShowFile") + + static let shouldChangeOperatorStatus = Notification.Name("shouldChangeOperatorStatus") + static let shouldUpdateOperatorInfo = Notification.Name("shouldUpdateOperatorInfo") + + static let shouldHidePopupActionsViewController = Notification.Name("shouldHidePopupActionsViewController") + static let shouldHideOverlayWindow = Notification.Name("shouldHideOverlayWindow") + + static let shouldHideRatingDialogViewController = Notification.Name("shouldHideRatingDialogViewController") + + static let shouldSendKeyboardRequest = Notification.Name("shouldSendKeyboardRequest") +} diff --git a/Example/WebimClientLibrary/Utilities/Extensions/String.swift b/Example/WebimClientLibrary/Utilities/Extensions/String.swift index 98e38907..df9af818 100644 --- a/Example/WebimClientLibrary/Utilities/Extensions/String.swift +++ b/Example/WebimClientLibrary/Utilities/Extensions/String.swift @@ -90,4 +90,9 @@ extension String { return convertedString } + public func trimWhitespacesIn() -> String { + let components = self.components(separatedBy: .whitespaces) + return components.filter { !$0.isEmpty }.joined(separator: " ") + } + } diff --git a/Example/WebimClientLibrary/Utilities/Extensions/UIButton.swift b/Example/WebimClientLibrary/Utilities/Extensions/UIButton.swift new file mode 100644 index 00000000..12ba5ae3 --- /dev/null +++ b/Example/WebimClientLibrary/Utilities/Extensions/UIButton.swift @@ -0,0 +1,70 @@ +// +// UIButton.swift +// WebimClientLibrary_Example +// +// Created by Возлеев Юрий on 25.06.2020. +// Copyright © 2020 Webim. All rights reserved. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. +// + +import UIKit + +extension UIButton { + + public func makeMultiLineSupport() { + guard let titleLabel = titleLabel else { + return + } + titleLabel.numberOfLines = 0 + titleLabel.setContentHuggingPriority(.required, for: .vertical) + titleLabel.setContentHuggingPriority(.required, for: .horizontal) + addConstraints([ + .init(item: titleLabel, + attribute: .top, + relatedBy: .greaterThanOrEqual, + toItem: self, + attribute: .top, + multiplier: 1.0, + constant: contentEdgeInsets.top), + .init(item: titleLabel, + attribute: .bottom, + relatedBy: .greaterThanOrEqual, + toItem: self, + attribute: .bottom, + multiplier: 1.0, + constant: contentEdgeInsets.bottom), + .init(item: titleLabel, + attribute: .left, + relatedBy: .greaterThanOrEqual, + toItem: self, + attribute: .left, + multiplier: 1.0, + constant: contentEdgeInsets.left), + .init(item: titleLabel, + attribute: .right, + relatedBy: .greaterThanOrEqual, + toItem: self, + attribute: .right, + multiplier: 1.0, + constant: contentEdgeInsets.right) + ]) + } + +} diff --git a/Example/WebimClientLibrary/Utilities/Extensions/UIImage.swift b/Example/WebimClientLibrary/Utilities/Extensions/UIImage.swift index abe2ca3b..cfd88c54 100644 --- a/Example/WebimClientLibrary/Utilities/Extensions/UIImage.swift +++ b/Example/WebimClientLibrary/Utilities/Extensions/UIImage.swift @@ -2,8 +2,8 @@ // UIImage.swift // WebimClientLibrary_Example // -// Created by Nikita Lazarev-Zubov on 14.10.17. -// Copyright © 2017 Webim. All rights reserved. +// Created by Eugene Ilyin on 27.11.2019. +// Copyright © 2019 Webim. All rights reserved. // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal @@ -27,39 +27,47 @@ import UIKit extension UIImage { + public enum FlipOrientation { + case vertically, horizontally + } - // MARK: - Methods - func roundImage() -> UIImage { - let minEdge = min(self.size.height, - self.size.width) - let size = CGSize(width: minEdge, - height: minEdge) - - UIGraphicsBeginImageContextWithOptions(size, - false, - 0.0) - let context = UIGraphicsGetCurrentContext()! + public func flipImage(_ orientation: FlipOrientation) -> UIImage { + defer { UIGraphicsEndImageContext() } - self.draw(in: CGRect(origin: CGPoint.zero, - size: size), - blendMode: .copy, - alpha: 1.0) + UIGraphicsBeginImageContextWithOptions(self.size, false, UIScreen.main.scale) - context.setBlendMode(.copy) - context.setFillColor(UIColor.clear.cgColor) + guard let context = UIGraphicsGetCurrentContext() else { return self } + context.translateBy(x: size.width / 2, y: size.height / 2) - let rectPath = UIBezierPath(rect: CGRect(origin: CGPoint.zero, - size: size)) - let circlePath = UIBezierPath(ovalIn: CGRect(origin: CGPoint.zero, - size: size)) - rectPath.append(circlePath) - rectPath.usesEvenOddFillRule = true - rectPath.fill() + switch orientation { + case .horizontally: + context.scaleBy(x: -1.0, y: -1.0) + case .vertically: + context.scaleBy(x: -1.0, y: 1.0) + } - let result = UIGraphicsGetImageFromCurrentImageContext()! - UIGraphicsEndImageContext() + context.translateBy(x: -size.width / 2, y: -size.height / 2) + context.draw(self.cgImage!, in: CGRect(x: 0, y: 0, width: size.width, height: size.height)) - return result + return UIGraphicsGetImageFromCurrentImageContext() ?? self } + public func colour(_ colour: UIColor) -> UIImage { + defer { UIGraphicsEndImageContext() } + + UIGraphicsBeginImageContextWithOptions(self.size, false, self.scale) + + guard let context = UIGraphicsGetCurrentContext() else { return self } + context.scaleBy(x: 1.0, y: -1.0) + context.translateBy(x: 0.0, y: -size.height) + + context.setBlendMode(.multiply) + + let rectangle = CGRect(x: 0, y: 0, width: size.width, height: size.height) + context.clip(to: rectangle, mask: cgImage!) + colour.setFill() + context.fill(rectangle) + + return UIGraphicsGetImageFromCurrentImageContext() ?? self + } } diff --git a/Example/WebimClientLibrary/Utilities/Extensions/UIImageView.swift b/Example/WebimClientLibrary/Utilities/Extensions/UIImageView.swift deleted file mode 100644 index 2bc69057..00000000 --- a/Example/WebimClientLibrary/Utilities/Extensions/UIImageView.swift +++ /dev/null @@ -1,63 +0,0 @@ -// -// UIImageView.swift -// WebimClientLibrary_Example -// -// Created by Nikita Lazarev-Zubov on 13.10.17. -// Copyright © 2017 Webim. All rights reserved. -// -// Permission is hereby granted, free of charge, to any person obtaining a copy -// of this software and associated documentation files (the "Software"), to deal -// in the Software without restriction, including without limitation the rights -// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -// copies of the Software, and to permit persons to whom the Software is -// furnished to do so, subject to the following conditions: -// -// The above copyright notice and this permission notice shall be included in all -// copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -// SOFTWARE. -// - -import UIKit - -extension UIImageView { - - // MARK: - Methods - public func loadImageAsynchronouslyFrom(url: URL, - rounded: Bool = false, - completion: ((_ image: UIImage) -> ())? = nil) { - let request = URLRequest(url: url) - - print("Requesting image: \(url.absoluteString)") - - URLSession.shared.dataTask(with: request, - completionHandler: { [weak self] (data, _, _) in - if let data = data { - DispatchQueue.main.async { - if let image = UIImage(data: data) { - var imageToSave = image - - if rounded { - imageToSave = imageToSave.roundImage() - } - - self?.image = imageToSave - - if let completion = completion { - completion(imageToSave) - } - } - } - } else { - return - } - }).resume() - } - -} diff --git a/Example/WebimClientLibrary/RatingViewController.swift b/Example/WebimClientLibrary/Utilities/Extensions/UIStackView.swift similarity index 66% rename from Example/WebimClientLibrary/RatingViewController.swift rename to Example/WebimClientLibrary/Utilities/Extensions/UIStackView.swift index b6e0d1b1..50de6636 100644 --- a/Example/WebimClientLibrary/RatingViewController.swift +++ b/Example/WebimClientLibrary/Utilities/Extensions/UIStackView.swift @@ -1,9 +1,9 @@ // -// RatingViewController.swift +// UIStackView.swift // WebimClientLibrary_Example // -// Created by Nikita Lazarev-Zubov on 15.10.17. -// Copyright © 2017 Webim. All rights reserved. +// Created by Eugene Ilyin on 12.11.2019. +// Copyright © 2019 Webim. All rights reserved. // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal @@ -24,23 +24,15 @@ // SOFTWARE. // -import Cosmos import UIKit -final class RatingViewController: UIViewController { - - // MARK: - Properties - // MARK: Outlets - @IBOutlet weak var backgroundView: UIView! - @IBOutlet weak var ratingView: CosmosView! - @IBOutlet weak var textLabel: UILabel! +extension UIStackView { // MARK: - Methods - override func viewDidLoad() { - super.viewDidLoad() - - textLabel.textColor = textMainColor.color() - backgroundView.backgroundColor = backgroundSecondaryColor.color() + func removeAllArrangedSubviews() { + for subView in arrangedSubviews { + removeArrangedSubview(subView) + subView.removeFromSuperview() + } } - } diff --git a/Example/WebimClientLibrary/Utilities/Extensions/UITableView.swift b/Example/WebimClientLibrary/Utilities/Extensions/UITableView.swift index 3e3655ec..b0233b23 100644 --- a/Example/WebimClientLibrary/Utilities/Extensions/UITableView.swift +++ b/Example/WebimClientLibrary/Utilities/Extensions/UITableView.swift @@ -35,7 +35,7 @@ extension UITableView { width: self.bounds.size.width, height: self.bounds.size.height)) messageLabel.attributedText = NSAttributedString(string: message) - messageLabel.textColor = textMainColor.color() + messageLabel.textColor = textMainColour messageLabel.numberOfLines = 0 messageLabel.textAlignment = .center messageLabel.sizeToFit() diff --git a/Example/WebimClientLibrary/Utilities/Extensions/UIView.swift b/Example/WebimClientLibrary/Utilities/Extensions/UIView.swift new file mode 100644 index 00000000..7e374335 --- /dev/null +++ b/Example/WebimClientLibrary/Utilities/Extensions/UIView.swift @@ -0,0 +1,95 @@ +// +// UIView.swift +// WebimClientLibrary_Example +// +// Created by Eugene Ilyin on 16/09/2019. +// Copyright © 2019 Webim. All rights reserved. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. +// + +import UIKit + +extension UIView { + + // MARK: - Methods + func fadeTransition(_ duration: CFTimeInterval) { + let animation = CATransition() + animation.timingFunction = CAMediaTimingFunction(name: .easeInEaseOut) + animation.type = .fade + animation.duration = duration + layer.add(animation, forKey: CATransitionType.fade.rawValue) + } + + func fadeIn(_ duration: TimeInterval = 0.2, onCompletion: (() -> Void)? = nil) { + self.alpha = 0 + self.isHidden = false + UIView.animate(withDuration: duration, + animations: { self.alpha = 1 }, + completion: { (value: Bool) in + if let complete = onCompletion { complete() } + } + ) + } + + func fadeOut(_ duration: TimeInterval = 0.2, onCompletion: (() -> Void)? = nil) { + UIView.animate(withDuration: duration, + animations: { self.alpha = 0 }, + completion: { (value: Bool) in + self.isHidden = true + if let complete = onCompletion { complete() } + } + ) + } + + func roundCorners(_ corners: CACornerMask, radius: CGFloat) { + if #available(iOS 11, *) { + self.layer.cornerRadius = radius + self.layer.maskedCorners = corners + } else { + var cornerMask = UIRectCorner() + if (corners.contains(.layerMinXMinYCorner)) { + cornerMask.insert(.topLeft) + } + if (corners.contains(.layerMaxXMinYCorner)) { + cornerMask.insert(.topRight) + } + if (corners.contains(.layerMinXMaxYCorner)) { + cornerMask.insert(.bottomLeft) + } + if (corners.contains(.layerMaxXMaxYCorner)) { + cornerMask.insert(.bottomRight) + } + let path = UIBezierPath(roundedRect: self.bounds, byRoundingCorners: cornerMask, cornerRadii: CGSize(width: radius, height: radius)) + let mask = CAShapeLayer() + mask.path = path.cgPath + self.layer.mask = mask + } + } + + func takeScreenshot() -> UIImage { + UIGraphicsBeginImageContextWithOptions(self.bounds.size, false, UIScreen.main.scale) + drawHierarchy(in: self.bounds, afterScreenUpdates: true) + + let image = UIGraphicsGetImageFromCurrentImageContext() + UIGraphicsEndImageContext() + + return image ?? UIImage() + } +} diff --git a/Example/WebimClientLibrary/Utilities/Extensions/UIViewController.swift b/Example/WebimClientLibrary/Utilities/Extensions/UIViewController.swift index 0096a839..28f7702b 100644 --- a/Example/WebimClientLibrary/Utilities/Extensions/UIViewController.swift +++ b/Example/WebimClientLibrary/Utilities/Extensions/UIViewController.swift @@ -30,7 +30,6 @@ import UIKit extension UIViewController { // MARK: - Methods - func hideKeyboardOnTap() { let tap: UITapGestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(dismissKeyboard)) @@ -38,7 +37,7 @@ extension UIViewController { view.addGestureRecognizer(tap) } - // MARK: Private methods + // MARK: - Private methods @objc private func dismissKeyboard() { view.endEditing(true) diff --git a/Example/WebimClientLibrary/Utilities/FilePicker.swift b/Example/WebimClientLibrary/Utilities/FilePicker.swift new file mode 100644 index 00000000..b875df8d --- /dev/null +++ b/Example/WebimClientLibrary/Utilities/FilePicker.swift @@ -0,0 +1,289 @@ +// +// FilePicker.swift +// WebimClientLibrary_Example +// +// Created by Eugene Ilyin on 21.10.2019. +// Copyright © 2019 Webim. All rights reserved. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. +// + +import UIKit +import MobileCoreServices +import AVFoundation +import CloudKit + +public protocol FilePickerDelegate: class { + func didSelect(image: UIImage?, imageURL: URL?) + func didSelect(file: Data?, fileURL: URL?) +} + +open class FilePicker: NSObject { + + // MARK: - Private properties + private let imagePickerController: UIImagePickerController + private let documentPickerController: UIDocumentPickerViewController + private let alertDialogHandler: UIAlertHandler + + private weak var presentationController: UIViewController? + private weak var delegate: FilePickerDelegate? + + // MARK: - Methods + public init(presentationController: UIViewController, + delegate: FilePickerDelegate) { + self.imagePickerController = UIImagePickerController() + self.documentPickerController = UIDocumentPickerViewController( + documentTypes: [String(kUTTypePDF)], + in: .open + ) + self.alertDialogHandler = UIAlertHandler(delegate: presentationController) + + super.init() + + self.presentationController = presentationController + self.delegate = delegate + + self.imagePickerController.delegate = self + self.imagePickerController.allowsEditing = true + self.imagePickerController.mediaTypes = ["public.image"] + + if #available(iOS 11.0, *) { + self.documentPickerController.delegate = self + self.documentPickerController.allowsMultipleSelection = false + } + } + + public func showSendFileMenu(from sourceView: UIView) { + let fileMenuSheet = UIAlertController( + title: nil, + message: nil, + preferredStyle: .actionSheet + ) + + let cameraAction = UIAlertAction( + title: FilePickerObject.actionCamera.rawValue.localized, + style: .default, + handler: { _ in + let cameraAuthorizationStatus = AVCaptureDevice.authorizationStatus(for: .video) + switch cameraAuthorizationStatus { + case .notDetermined: + self.requesetCameraPermission() + case .authorized: + self.presentCamera() + case .restricted, .denied: + self.showAlertForCameraAccess() + @unknown default: + // Handle possibly added (in future) values + break + } + } + ) + + let photoLibraryAction = UIAlertAction( + title: FilePickerObject.actionPhotoLibrary.rawValue.localized, + style: .default, + handler: { _ in + self.imagePickerController.sourceType = .photoLibrary + self.presentationController?.present( + self.imagePickerController, + animated: true + ) + } + ) + + let fileAction = UIAlertAction( + title: FilePickerObject.actionFile.rawValue.localized, + style: .default, + handler: { _ in + self.presentationController?.present( + self.documentPickerController, + animated: true) + } + ) + + let cancelAction = UIAlertAction( + title: FilePickerObject.actionCancel.rawValue.localized, + style: .cancel + ) + + fileMenuSheet.addAction(cameraAction) + fileMenuSheet.addAction(photoLibraryAction) + fileMenuSheet.addAction(cancelAction) + + /// Files App was presented in iOS 11.0 + if #available(iOS 11.0, *) { + fileMenuSheet.addAction(fileAction) + } + + // Workaround for iPads + if UIDevice.current.userInterfaceIdiom == .pad { + fileMenuSheet.popoverPresentationController?.sourceView = sourceView + fileMenuSheet.popoverPresentationController?.sourceRect = sourceView.bounds + fileMenuSheet.popoverPresentationController?.permittedArrowDirections = [.down, .up] + } + + self.presentationController?.present(fileMenuSheet, animated: true) + } + + + // MARK: - Private methods + private func pickerControllerImage( + _ controller: UIImagePickerController, + didSelect image: UIImage? = nil, + withURL imageURL: URL? = nil + ) { + controller.dismiss(animated: true, completion: nil) + + self.delegate?.didSelect(image: image, imageURL: imageURL) + } + + private func pickerControllerDocument( + _ controller: UIDocumentPickerViewController, + didSelect file: Data? = nil, + withURL documentURL: URL? = nil + ) { + controller.dismiss(animated: true, completion: nil) + + self.delegate?.didSelect(file: file, fileURL: documentURL) + } + + private func requesetCameraPermission() { + AVCaptureDevice.requestAccess(for: .video) { (access) in + guard access == true else { return } + self.presentCamera() + } + } + + private func presentCamera() { + if UIImagePickerController.isSourceTypeAvailable(.camera) { + DispatchQueue.main.async { + self.imagePickerController.sourceType = .camera + self.presentationController?.present(self.imagePickerController, animated: true) + } + } else { + let ac = UIAlertController( + title: FilePickerObject.cameraNotAvailable.rawValue.localized, + message: nil, + preferredStyle: .alert + ) + + let okAction = UIAlertAction( + title: FilePickerObject.ok.rawValue.localized, + style: .cancel + ) + + ac.addAction(okAction) + + self.presentationController?.present(ac, animated: true) + } + } + + private func showAlertForCameraAccess() { + guard let settingsAppURL = URL(string: UIApplication.openSettingsURLString) else { return } + + let ac = UIAlertController( + title: FilePickerObject.cameraAccessTitle.rawValue.localized, + message: FilePickerObject.cameraAccessMessage.rawValue.localized, + preferredStyle: .alert + ) + + let showAppSettingsAction = UIAlertAction( + title: FilePickerObject.cameraAccessOpenSetting.rawValue.localized, + style: .default, + handler: { _ in + UIApplication.shared.open( + settingsAppURL, + options: [:]) + } + ) + + let cancelAction = UIAlertAction( + title: FilePickerObject.cameraAccessCancel.rawValue.localized, + style: .cancel + ) + + ac.addAction(showAppSettingsAction) + ac.addAction(cancelAction) + + self.presentationController?.present(ac, animated: true) + } +} + +// MARK: - UIImagePickerController extensions +extension FilePicker: UIImagePickerControllerDelegate { + public func imagePickerControllerDidCancel(_ picker: UIImagePickerController) { + self.pickerControllerImage(picker) + } + + public func imagePickerController( + _ picker: UIImagePickerController, + didFinishPickingMediaWithInfo info: [UIImagePickerController.InfoKey : Any] + ) { + guard let image = info[.editedImage] as? UIImage else { + return self.pickerControllerImage(picker) + } + + guard let imageURL = info[.referenceURL] as? URL else { + return self.pickerControllerImage(picker, didSelect: image) + } + + self.pickerControllerImage( + picker, + didSelect: image, + withURL: imageURL + ) + } +} + +extension FilePicker: UIDocumentMenuDelegate, UIDocumentPickerDelegate { + public func documentPicker( + _ picker: UIDocumentPickerViewController, + didPickDocumentsAt urls: [URL] + ) { + guard let fileURL = urls.first else { + return self.pickerControllerDocument(picker) + } + + do { + let data = try Data(contentsOf: fileURL) + self.pickerControllerDocument( + picker, + didSelect: data, + withURL: fileURL + ) } catch { + alertDialogHandler.showFileLoadingFailureDialog(withError: error) + } + } + + public func documentMenu( + _ documentMenu: UIDocumentMenuViewController, + didPickDocumentPicker documentPicker: UIDocumentPickerViewController + ) { + // TODO: Check what for this method is responsible + documentPicker.delegate = self + self.presentationController?.present(documentPicker, animated: true) + } + + public func documentPickerWasCancelled(_ picker: UIDocumentPickerViewController) { + print("view was cancelled") + self.pickerControllerDocument(picker) + } +} + +extension FilePicker: UINavigationControllerDelegate { } diff --git a/Example/WebimClientLibrary/Utilities/FlexibleTableViewCell.swift b/Example/WebimClientLibrary/Utilities/FlexibleTableViewCell.swift new file mode 100644 index 00000000..7d053423 --- /dev/null +++ b/Example/WebimClientLibrary/Utilities/FlexibleTableViewCell.swift @@ -0,0 +1,1879 @@ +// +// FlexibleTableViewCell.swift +// WebimClientLibrary_Example +// +// Created by Eugene Ilyin on 24/09/2019. +// Copyright © 2019 Webim. All rights reserved. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. +// + +import UIKit +import WebimClientLibrary +import SnapKit +import Nuke + +class FlexibleTableViewCell: UITableViewCell { + + // MARK: - Size constants + private let CHAT_BUBBLE_MAX_WIDTH: CGFloat = { + let width = min(UIScreen.main.bounds.width, UIScreen.main.bounds.height) + return width < 350 ? 253.0 : 265.0 + }() + private let CHAT_BUBBLE_MIN_WIDTH: CGFloat = 55.0 + private let SPACING_DEFAULT: CGFloat = 10.0 + private let SPACING_CELL: CGFloat = 5.0 + private let USERAVATARIMAGEVIEW_WIDTH: CGFloat = 40.0 + + // MARK: - Properties + public var hasImageAsDocument: Bool { imageAsDocument } + + // MARK: - Private Properties + private var isForOperator = false + private var imageAsDocument = false + private var messageFromCell: Message? + + private lazy var urlSession = URLSession() + private lazy var downloadTask = URLSessionDownloadTask() + + private let numberFormatter: NumberFormatter = { + let formatter = NumberFormatter() + formatter.maximumFractionDigits = 0 + formatter.minimumFractionDigits = 0 + return formatter + }() + private let calendar: Calendar = { + let calendar = Calendar.current + return calendar + }() + private let dateFormatter: DateFormatter = { + let dateFormatter = DateFormatter() + dateFormatter.dateFormat = "dd MMM, yyyy" + return dateFormatter + }() + private let timeFormatter: DateFormatter = { + let dateFormatter = DateFormatter() + dateFormatter.dateFormat = "HH:mm" + return dateFormatter + }() + private let byteCountFormatter: ByteCountFormatter = { + let byteCountFormatter = ByteCountFormatter() + byteCountFormatter.allowedUnits = .useAll + byteCountFormatter.countStyle = .file + byteCountFormatter.includesUnit = true + byteCountFormatter.isAdaptive = true + return byteCountFormatter + }() + + // MARK: - Subviews + lazy var dateLabel: UILabel = { + let label = createUILabel( + textAlignment: .center, + systemFontSize: 15, + systemFontWeight: .light + ) + label.textColor = dateLabelColour + return label + }() + + // Message + lazy var messageUsernameLabel: UILabel = { + return createUILabel(systemFontSize: 15) + }() + + lazy var messageBodyLabel: UILabel = { + return createUILabel(systemFontSize: 17, numberOfLines: 0) + }() + + lazy var messageBackgroundView: UIView = { + return createUIView() + }() + + // Document + lazy var documentFileNameLabel: UILabel = { + return createUILabel(systemFontSize: 17) + }() + + lazy var documentFileDescriptionLabel: UILabel = { + return createUILabel(systemFontSize: 15, systemFontWeight: .light, numberOfLines: 0) + }() + + lazy var documentFileStatusPercentageIndicator: CircleProgressIndicator = { + let indicator = CircleProgressIndicator() + indicator.lineWidth = 1 + indicator.strokeColor = documentFileStatusPercentageIndicatorColour + indicator.isUserInteractionEnabled = false + indicator.isHidden = true + indicator.translatesAutoresizingMaskIntoConstraints = false + return indicator + }() + + lazy var documentFileCancelDownloadButton: UIButton = { + let button = createUIButton(type: .system) + button.setBackgroundImage(closeButtonImage, for: .normal) + button.addTarget( + self, + action: #selector(cancelDownload), + for: .touchUpInside + ) + button.isHidden = true + return button + }() + + lazy var documentFileStatusButton: UIButton = { + return createUIButton(type: .system) + }() + + // Image + lazy var imageUsernameLabel: UILabel = { + return createUILabel(systemFontSize: 15) + }() + + lazy var imageUsernameLabelBackgroundView: UIView = { + return createUIView() + }() + + lazy var imageImageView: UIImageView = { + return createUIImageView() + }() + + // Quote + lazy var quoteLineView: UIView = { + return createUIView() + }() + + lazy var quoteAttachmentImageView: UIImageView = { + return createUIImageView(contentMode: .scaleAspectFill) + }() + + lazy var quoteUsernameLabel: UILabel = { + return createUILabel(systemFontSize: 17, systemFontWeight: .heavy) + }() + + lazy var quoteBodyLabel: UILabel = { + return createUILabel(systemFontSize: 15, systemFontWeight: .light) + }() + + // Time + lazy var timeLabel: UILabel = { + let label = createUILabel(systemFontSize: 15, systemFontWeight: .light) + label.textColor = timeLabelColour + return label + }() + + lazy var messageStatusImageView: UIImageView = { + return createUIImageView() + }() + + lazy var messageStatusIndicator: SpinningIndicator = { + let indicator = SpinningIndicator() + indicator.strokeColor = messageStatusIndicatorColour + indicator.animating = false + indicator.translatesAutoresizingMaskIntoConstraints = false + return indicator + }() + + lazy var messageStatusLabel: UILabel = { + let label = UILabel() + label.font = .italicSystemFont(ofSize: 15) + label.textColor = timeLabelColour + label.translatesAutoresizingMaskIntoConstraints = false + return label + }() + + // Avatar + lazy var userAvatarImageView: UIImageView = { + return createUIImageView() + }() + + // MARK: - Methods + func configureTheCell( + forMessage message: Message, + showFullDate: Bool, + shouldShowOperatorInfo: Bool = false, + isEdited: Bool = false + ) { + switch message.getType() { + + // Keyboard message layout + case .keyboard: + + cellLayoutButtons( + forMessage: message, + showFullDate: showFullDate + ) + + // System message layout + case .actionRequest, + .contactInformationRequest, + .info, + .operatorBusy, + .keyboardResponse: + + cellLayoutSystem( + forMessage: message, + showFullDate: showFullDate + ) + + // Visitor message layout + case .visitorMessage, + .fileFromVisitor: + + cellLayoutOther( + forMessage: message, + showFullDate: showFullDate, + forOperator: false, + isEdited: isEdited + ) + + // Operator message layout + case .operatorMessage, + .fileFromOperator: + + cellLayoutOther( + forMessage: message, + showFullDate: showFullDate, + forOperator: true, + shouldShowOperatorInfo: shouldShowOperatorInfo, + isEdited: isEdited + ) + + case .stickerVisitor: + break + } + } + + // MARK: - Private methods + private func calculateImageViewSize( + imageHeight: CGFloat, + imageWidth: CGFloat, + forOperator: Bool + ) -> CGSize { + guard imageHeight >= CHAT_BUBBLE_MIN_WIDTH else { return CGSize() } + + if forOperator { + guard imageWidth >= CHAT_BUBBLE_MIN_WIDTH * 2 else { return CGSize() } + } else { + guard imageWidth >= CHAT_BUBBLE_MIN_WIDTH else { return CGSize() } + } + + var biggerSide = imageWidth + var lesserSide = imageHeight + + if imageHeight > imageWidth { + biggerSide = imageHeight + lesserSide = imageWidth + } + + if biggerSide > CHAT_BUBBLE_MAX_WIDTH { + let resizeRatio = biggerSide / CHAT_BUBBLE_MAX_WIDTH + biggerSide = CHAT_BUBBLE_MAX_WIDTH + lesserSide /= resizeRatio + } + + if imageHeight > imageWidth { + return CGSize(width: lesserSide, height: biggerSide) + } + return CGSize(width: biggerSide, height: lesserSide) + + } + + private func emptyTheCell() { + dateLabel.removeFromSuperview() + messageUsernameLabel.removeFromSuperview() + messageBodyLabel.removeFromSuperview() + documentFileNameLabel.removeFromSuperview() + documentFileDescriptionLabel.removeFromSuperview() + documentFileStatusPercentageIndicator.removeFromSuperview() + documentFileCancelDownloadButton.removeFromSuperview() + documentFileStatusButton.removeFromSuperview() + imageImageView.removeFromSuperview() + imageUsernameLabel.removeFromSuperview() + imageUsernameLabelBackgroundView.removeFromSuperview() + quoteLineView.removeFromSuperview() + quoteAttachmentImageView.removeFromSuperview() + quoteUsernameLabel.removeFromSuperview() + quoteBodyLabel.removeFromSuperview() + timeLabel.removeFromSuperview() + messageStatusIndicator.removeFromSuperview() + messageStatusImageView.removeFromSuperview() + messageStatusLabel.removeFromSuperview() + userAvatarImageView.removeFromSuperview() + buttonsVerticalStack.removeFromSuperview() + } + + private func configureURLSession() { + // FIXME: Impossible to create another background session to handle background download? + // Somehow it works now, but feels like it should not work + let configuration = URLSessionConfiguration.default +// let configuration = URLSessionConfiguration.background(withIdentifier: "\(Bundle.main.bundleIdentifier ?? "").background") +// configuration.isDiscretionary = true +// configuration.sessionSendsLaunchEvents = true + urlSession = URLSession( + configuration: configuration, + delegate: self, + delegateQueue: nil + ) + } + + lazy var buttonsVerticalStack: UIStackView = { + let stackView = UIStackView() + stackView.axis = .vertical + stackView.translatesAutoresizingMaskIntoConstraints = false + //stackView.alignment = .center + stackView.distribution = .fill + stackView.spacing = SPACING_CELL * 2 + return stackView + }() + + // MARK: - LAYOUT METHODS + private func cellLayoutSystem( + forMessage message: Message, + showFullDate: Bool + ) { + emptyTheCell() + self.messageFromCell = message + + if showFullDate { + // dateLabel + self.addSubview(dateLabel) + } + + // messageBodylabel + messageBackgroundView.addSubview(messageBodyLabel) + /// Attributes + messageBodyLabel.textAlignment = .center + messageBodyLabel.textColor = messageBodyLabelColourSystem + + // messageBackgroundView + self.addSubview(messageBackgroundView) + /// Set round corners + messageBackgroundView.roundCorners( + [.layerMinXMinYCorner, + .layerMaxXMinYCorner, + .layerMaxXMaxYCorner, + .layerMinXMaxYCorner], + radius: 15 + ) + /// Set colour + messageBackgroundView.backgroundColor = messageBackgroundViewColourSystem + + // timeLabel + self.addSubview(timeLabel) + + fillSystemCell( + message: message, + showFullDate: showFullDate + ) + } + + private func cellLayoutButtons( + forMessage message: Message, + showFullDate: Bool + ) { + emptyTheCell() + buttonsVerticalStack.removeAllArrangedSubviews() + self.messageFromCell = message + + if showFullDate { + // dateLabel + self.addSubview(dateLabel) + } + + // buttonsVerticalStack + messageBackgroundView.addSubview(buttonsVerticalStack) + + // timeLabel + messageBackgroundView.addSubview(timeLabel) + + // messageBackgroundView + self.addSubview(messageBackgroundView) + + /// Set round corners + messageBackgroundView.layer.cornerRadius = 0 + + /// Set colour + messageBackgroundView.backgroundColor = messageBackgroundViewColourSystem + + fillButtonsCell( + message: message, + showFullDate: showFullDate + ) + } + + private func cellLayoutOther( + forMessage message: Message, + showFullDate: Bool, + forOperator: Bool, + shouldShowOperatorInfo: Bool = false, + isEdited: Bool = false + ) { + emptyTheCell() + if message.getData()?.getAttachment() != nil { + configureURLSession() + } + self.messageFromCell = message + + var hasQuote = false + var hasQuoteAttachment = false + var hasQuoteImage = false + var hasAttachment = false + var hasImage = false + var hasSendingFile = false + var imageViewSize = CGSize() + + self.isForOperator = forOperator + + if showFullDate { + // dateLabel + self.addSubview(dateLabel) + } + + if forOperator { + // userAvatarImageView + self.addSubview(userAvatarImageView) + userAvatarImageView.clipsToBounds = true + userAvatarImageView.roundCorners( + [.layerMinXMinYCorner, + .layerMaxXMinYCorner, + .layerMinXMaxYCorner, + .layerMaxXMaxYCorner], + radius: 20 + ) + + // documentFileStatusPercentageIndicator + userAvatarImageView.addSubview(documentFileStatusPercentageIndicator) + + // messageUsernameLabel + messageBackgroundView.addSubview(messageUsernameLabel) + /// Text + messageUsernameLabel.textColor = messageUsernameLabelColourOperator + } + + if let quote = message.getQuote() { // Quote + hasQuote = true + + if let quoteAttachment = quote.getMessageAttachment(), + let contentType = quoteAttachment.getContentType() { + hasQuoteAttachment = true + + if isImage(contentType: contentType) { + hasQuoteImage = true + } + + // messageBackgroundView + messageBackgroundView.addSubview(quoteAttachmentImageView) + + // documentFileStatusPercentageIndicator + quoteAttachmentImageView.addSubview(documentFileStatusPercentageIndicator) + + // Attributes + quoteAttachmentImageView.clipsToBounds = false + } + // quoteLineView + messageBackgroundView.addSubview(quoteLineView) + /// Set colour + + if forOperator { + quoteLineView.backgroundColor = quoteLineViewColourOperator + } else { + quoteLineView.backgroundColor = quoteLineViewColourVisitor + } + + // quoteUsernameLabel + messageBackgroundView.addSubview(quoteUsernameLabel) + + // quoteBodyLabel + messageBackgroundView.addSubview(quoteBodyLabel) + } + if let attachment = message.getData()?.getAttachment(), + let contentType = attachment.getFileInfo().getContentType() { + let fileInfo = attachment.getFileInfo() + + hasAttachment = true + + if isImage(contentType: contentType) { + hasImage = true + + guard let imageHeight = fileInfo.getImageInfo()?.getHeight(), + let imageWidth = fileInfo.getImageInfo()?.getWidth() + else { return } + + imageViewSize = calculateImageViewSize( + imageHeight: CGFloat(imageHeight), + imageWidth: CGFloat(imageWidth), + forOperator: forOperator + ) + + if imageViewSize == CGSize() { // Image is too small to display it properly + self.imageAsDocument = true + } + } + + if hasImage && !hasImageAsDocument { + // imageImageView + messageBackgroundView.addSubview(imageImageView) + + // documentFileStatusPercentageIndicator + imageImageView.addSubview(documentFileStatusPercentageIndicator) + + /// Attributes + imageImageView.clipsToBounds = false + + if forOperator { + // imageUsernameLabel + imageUsernameLabelBackgroundView.addSubview(imageUsernameLabel) + /// Text + imageUsernameLabel.textColor = imageUsernameLabelColourOperator + + // imageUsernamaLabelBackgroundView + messageBackgroundView.addSubview(imageUsernameLabelBackgroundView) + imageUsernameLabelBackgroundView.backgroundColor = imageUsernameLabelBackgroundViewColourOperator + imageUsernameLabelBackgroundView.roundCorners( + [.layerMinXMinYCorner, + .layerMaxXMinYCorner, + .layerMinXMaxYCorner, + .layerMaxXMaxYCorner], + radius: 15 + ) + } + + // messageBackgroundView + /// Set colour + messageBackgroundView.backgroundColor = messageBackgroundViewColourClear + } else { + // documentFileNameLabel + messageBackgroundView.addSubview(documentFileNameLabel) + + // documentFileDescriptionLabel + messageBackgroundView.addSubview(documentFileDescriptionLabel) + + // documentFileStatusPercentageIndicator + documentFileStatusButton.addSubview(documentFileStatusPercentageIndicator) + + // documentFileCancelDownloadButton + documentFileStatusButton.addSubview(documentFileCancelDownloadButton) + + // documentFileStatusButton + messageBackgroundView.addSubview(documentFileStatusButton) + + // messageBackgroundView + /// Set colour + if forOperator { + messageBackgroundView.backgroundColor = messageBackgroundViewColourOperator + } else { + messageBackgroundView.backgroundColor = messageBackgroundViewColourVisitor + } + } + } else { + // messageBodyLabel + messageBackgroundView.addSubview(messageBodyLabel) + /// Text + messageBodyLabel.textAlignment = .left + if forOperator { + messageBodyLabel.textColor = messageBodyLabelColourOperator + } else { + messageBodyLabel.textColor = messageBodyLabelColourVisitor + } + + // messageBackgroundView + /// Set colour + if forOperator { + messageBackgroundView.backgroundColor = messageBackgroundViewColourOperator + } else { + messageBackgroundView.backgroundColor = messageBackgroundViewColourVisitor + } + } + + // messageBackgroundView + self.addSubview(messageBackgroundView) + roundCornersForMessage(on: messageBackgroundView, forOperator: forOperator) + + //timeLabel + self.addSubview(timeLabel) + + //messageStatusImageView + self.addSubview(messageStatusImageView) + + if !forOperator { + //messageStatusIndicator + self.addSubview(messageStatusIndicator) + } + + // messageStatusLabel + self.addSubview(messageStatusLabel) + + if message.getType() == .fileFromVisitor && message.getSendStatus() == .sending { + hasSendingFile = true + messageBackgroundView.addSubview(documentFileNameLabel) + messageBackgroundView.addSubview(documentFileDescriptionLabel) + messageBackgroundView.addSubview(documentFileStatusButton) + } + + fillOtherCell( + showFullDate: showFullDate, + forOperator: forOperator, + message: message, + hasQuote: hasQuote, + hasQuoteAttachment: hasQuoteAttachment, + hasQuoteImage: hasQuoteImage, + hasAttachment: hasAttachment, + hasImage: hasImage, + hasSendingFile: hasSendingFile, + imageViewSize: imageViewSize, + shouldShowOperatorInfo: shouldShowOperatorInfo, + isEdited: isEdited + ) + } + + // MARK: - SET CONTENT METHODS + private func fillSystemCell( + message: Message, + showFullDate: Bool + ) { + if showFullDate { + if calendar.isDateInToday(message.getTime()) { + dateLabel.text = FlexibleCellDate.dateToday.rawValue.localized + } else if calendar.isDateInYesterday(message.getTime()) { + dateLabel.text = FlexibleCellDate.dateYesterday.rawValue.localized + } else { + dateLabel.text = dateFormatter.string(from: message.getTime()) + } + } + + var messageText = message.getText() + if message.getType() == .keyboardResponse, + let buttonText = message.getKeyboardRequest()?.getButton().getText() { + messageText += " \"\(buttonText)\"" + } + messageBodyLabel.text = messageText + + timeLabel.text = timeFormatter.string(from: message.getTime()) + + cellLayoutConstraintSystem(showFullDate: showFullDate) + } + + private func fillButtonsCell( + message: Message, + showFullDate: Bool + ) { + if showFullDate { + if calendar.isDateInToday(message.getTime()) { + dateLabel.text = FlexibleCellDate.dateToday.rawValue.localized + } else if calendar.isDateInYesterday(message.getTime()) { + dateLabel.text = FlexibleCellDate.dateYesterday.rawValue.localized + } else { + dateLabel.text = dateFormatter.string(from: message.getTime()) + } + } + + guard let keyboard = message.getKeyboard() else { return } + let buttonsArray = keyboard.getButtons() + + var response: KeyboardResponse? + var isActive = false + + switch keyboard.getState() { + case .pending: + isActive = true + case .canceled: + isActive = false + case .completed: + isActive = false + response = keyboard.getResponse() + } + + for buttonsStack in buttonsArray { + for button in buttonsStack { + let uiButton = UIButton(type: .system), + buttonID = button.getID(), + buttonText = button.getText() + + uiButton.accessibilityIdentifier = buttonID + uiButton.setTitle(buttonText, for: .normal) + + ///add buttons only with text + guard let titleLabel = uiButton.titleLabel else { + continue + } + titleLabel.font = UIFont.systemFont(ofSize: 17.0) + titleLabel.textAlignment = .center + titleLabel.numberOfLines = 0 + + ///button text insets + titleLabel.snp.remakeConstraints { make in + make.top.bottom.equalToSuperview().inset(10) + make.left.right.equalToSuperview().inset(16) + make.height.greaterThanOrEqualTo(20) + } + + uiButton.clipsToBounds = true + uiButton.translatesAutoresizingMaskIntoConstraints = false + uiButton.layer.cornerRadius = 20 + + if isActive { + uiButton.addTarget( + self, + action: #selector(sendButton), + for: .touchUpInside + ) + } + + if isActive { + // set default buttons + uiButton.backgroundColor = buttonDefaultBackgroundColour + uiButton.tintColor = buttonDefaultTitleColour + } else { + if let response = response, + response.getButtonID() == buttonID { + // set choosen button + uiButton.backgroundColor = buttonChoosenBackgroundColour + uiButton.tintColor = buttonChoosenTitleColour + } else { + // set inactive button + uiButton.backgroundColor = buttonCanceledBackgroundColour + uiButton.tintColor = buttonCanceledTitleColour + } + } + + buttonsVerticalStack.addArrangedSubview(uiButton) + uiButton.snp.remakeConstraints { make in + make.leading.trailing.equalToSuperview() + } + } + } + timeLabel.text = timeFormatter.string(from: message.getTime()) + cellLayoutConstraintButtons(showFullDate: showFullDate) + } + + private func fillOtherCell( + showFullDate: Bool, + forOperator: Bool, + message: Message, + hasQuote: Bool, + hasQuoteAttachment: Bool, + hasQuoteImage: Bool, + hasAttachment:Bool, + hasImage: Bool, + hasSendingFile: Bool, + imageViewSize: CGSize, + shouldShowOperatorInfo: Bool = false, + isEdited: Bool = false + ) { + if showFullDate { + if calendar.isDateInToday(message.getTime()) { + dateLabel.text = FlexibleCellDate.dateToday.rawValue.localized + } else if calendar.isDateInYesterday(message.getTime()) { + dateLabel.text = FlexibleCellDate.dateYesterday.rawValue.localized + } else { + dateLabel.text = dateFormatter.string(from: message.getTime()) + } + } + + if hasAttachment, + let attachment = message.getData()?.getAttachment(), + let url = attachment.getFileInfo().getURL(), + let contentType = attachment.getFileInfo().getContentType(){ + let fileInfo = attachment.getFileInfo() + + if hasImageAsDocument { + let request = ImageRequest(url: url) + if ImageCache.shared[request] != nil { + self.documentFileStatusPercentageIndicator.isHidden = true + if forOperator { + documentFileStatusButton.setBackgroundImage( + documentFileStatusButtonDownloadSuccessOperator, + for: .normal + ) + } else { + documentFileStatusButton.setBackgroundImage( + documentFileStatusButtonDownloadSuccessVisitor, + for: .normal + ) + } + } else { + if forOperator { + documentFileStatusButton.setBackgroundImage( + documentFileStatusButtonDownloadOperator, + for: .normal + ) + } else { + documentFileStatusButton.setBackgroundImage( + documentFileStatusButtonDownloadVisitor, + for: .normal + ) + } + Nuke.ImagePipeline.shared.loadImage( + with: url, + progress: { _, completed, total in + self.updateImageDownloadProgress(completed: completed, total: total) + }, + completion: { _ in + if forOperator { + self.documentFileStatusButton.setBackgroundImage( + documentFileStatusButtonDownloadSuccessOperator, + for: .normal + ) + } else { + self.documentFileStatusButton.setBackgroundImage( + documentFileStatusButtonDownloadSuccessVisitor, + for: .normal + ) + } + self.documentFileStatusPercentageIndicator.isHidden = true + } + ) + } + } + + if hasImage && !hasImageAsDocument { + if forOperator && shouldShowOperatorInfo { + imageUsernameLabel.text = message.getSenderName() + } else { + imageUsernameLabel.text = nil + } + + imageImageView.clipsToBounds = true + roundCornersForMessage(on: imageImageView, forOperator: forOperator) + + let request = ImageRequest(url: url) + if let image = ImageCache.shared[request] { + self.documentFileStatusPercentageIndicator.isHidden = true + self.imageImageView.image = image + } else { + self.imageImageView.image = loadingPlaceholderImage + + Nuke.ImagePipeline.shared.loadImage( + with: url, + progress: { _, completed, total in + self.updateImageDownloadProgress( + completed: completed, + total: total + ) + }, + completion: { _ in + self.imageImageView.image = ImageCache.shared[request] + self.documentFileStatusPercentageIndicator.isHidden = true + } + ) + } + } else { + documentFileNameLabel.text = fileInfo.getFileName() + documentFileDescriptionLabel.text = + byteCountFormatter.string(fromByteCount: fileInfo.getSize() ?? 0) + + if forOperator { + documentFileNameLabel.textColor = documentFileNameLabelColourOperator + documentFileDescriptionLabel.textColor = documentFileDescriptionLabelColourOperator + } else { + documentFileNameLabel.textColor = documentFileNameLabelColourVisitor + documentFileDescriptionLabel.textColor = documentFileDescriptionLabelColourVisitor + } + + if isAcceptableFile(contentType: contentType) { + if isFileExist(fileName: fileInfo.getFileName()) { + if forOperator { + documentFileStatusButton.setBackgroundImage( + documentFileStatusButtonDownloadSuccessOperator, + for: .normal + ) + } else { + documentFileStatusButton.setBackgroundImage( + documentFileStatusButtonDownloadSuccessVisitor, + for: .normal + ) + } + + } else { + if forOperator { + documentFileStatusButton.setBackgroundImage( + documentFileStatusButtonDownloadOperator, + for: .normal + ) + } else { + documentFileStatusButton.setBackgroundImage( + documentFileStatusButtonDownloadVisitor, + for: .normal + ) + } + } + } else if !hasImageAsDocument { + documentFileStatusButton.setBackgroundImage( + documentFileStatusButtonDownloadError, + for: .normal) + } + if !hasImageAsDocument { + documentFileStatusButton.addTarget( + self, + action: #selector(downloadFile), + for: .touchUpInside + ) + } + } + } else { + messageBodyLabel.text = message.getText() + if hasQuote { + + if hasQuoteAttachment, + let attachment = message.getQuote()?.getMessageAttachment(), + let url = attachment.getURL() { + + if hasQuoteImage { + quoteAttachmentImageView.clipsToBounds = true + quoteAttachmentImageView.roundCorners( + [.layerMinXMinYCorner, + .layerMaxXMinYCorner, + .layerMinXMaxYCorner, + .layerMaxXMaxYCorner], + radius: 5 + ) + + let request = ImageRequest(url: url) + if let image = ImageCache.shared[request] { + self.documentFileStatusPercentageIndicator.isHidden = true + self.quoteAttachmentImageView.image = image + } else { + self.quoteAttachmentImageView.image = loadingPlaceholderImage + + Nuke.ImagePipeline.shared.loadImage( + with: url, + progress: { _, completed, total in + self.updateImageDownloadProgress( + completed: completed, + total: total + ) + }, + completion: { _ in + self.quoteAttachmentImageView.image = ImageCache.shared[request] + self.documentFileStatusPercentageIndicator.isHidden = true + } + ) + } + } else { + quoteAttachmentImageView.image = nil + } + } + + + if message.getQuote()?.getSenderName() == "Посетитель" { + quoteUsernameLabel.text = ChatView.hardcodedVisitorMessageName.rawValue.localized + } else { + quoteUsernameLabel.text = message.getQuote()?.getSenderName() + } + + quoteBodyLabel.text = message.getQuote()?.getMessageText()?.replacingOccurrences(of: "\n+", with: " ", options: .regularExpression) + if forOperator { + quoteUsernameLabel.textColor = quoteUsernameLabelColourOperator + quoteBodyLabel.textColor = quoteBodyLabelColourOperator + } else { + quoteUsernameLabel.textColor = quoteUsernameLabelColourVisitor + quoteBodyLabel.textColor = quoteBodyLabelColourVisitor + } + } + } + + timeLabel.text = timeFormatter.string(from: message.getTime()) + timeLabel.textColor = timeLabelColour + + if forOperator { + messageStatusImageView.image = messageStatusImageViewImageRead + + if shouldShowOperatorInfo && !hasImage { + messageUsernameLabel.text = message.getSenderName() + } else { + messageUsernameLabel.text = nil + } + + if shouldShowOperatorInfo { + if let url = message.getSenderAvatarFullURL() { + let request = ImageRequest(url: url) + if let image = ImageCache.shared[request] { + self.documentFileStatusPercentageIndicator.isHidden = true + self.userAvatarImageView.image = image + } else { + self.userAvatarImageView.image = loadingPlaceholderImage + + Nuke.ImagePipeline.shared.loadImage( + with: url, + progress: { _, completed, total in + self.updateImageDownloadProgress( + completed: completed, + total: total + ) + }, + completion: { _ in + self.userAvatarImageView.image = ImageCache.shared[request] + self.documentFileStatusPercentageIndicator.isHidden = true + } + ) + } + } else { + userAvatarImageView.image = userAvatarImagePlaceholder + } + } else { + userAvatarImageView.image = nil + } + } else { + if message.getSendStatus() == .sending { + messageStatusIndicator.animating = true + messageStatusImageView.image = nil + if hasSendingFile { + documentFileNameLabel.text = UploadingFileDescription.uploadingFile.rawValue.localized + documentFileDescriptionLabel.text = UploadingFileDescription.counting.rawValue.localized + documentFileNameLabel.textColor = documentFileNameLabelColourVisitor + documentFileDescriptionLabel.textColor = documentFileDescriptionLabelColourVisitor + documentFileStatusButton.setBackgroundImage( + documentFileStatusButtonUploadVisitor, + for: .normal + ) + } + } else { + messageStatusIndicator.animating = false + if message.isReadByOperator() { + self.messageStatusImageView.image = messageStatusImageViewImageRead + } else { + messageStatusImageView.image = messageStatusImageViewImageSent + } + } + } + + if isEdited { + messageStatusLabel.text = MessageStatus.editedMessage.rawValue.localized + } else { + messageStatusLabel.text = "" + } + + cellLayoutConstraintOther( + showFullDate: showFullDate, + forOperator: forOperator, + hasQuote: hasQuote, + hasQuoteAttachment: hasQuoteAttachment, + hasQuoteImage: hasQuoteImage, + hasAttachment: hasAttachment, + hasImage: hasImage, + hasSendingFile: hasSendingFile, + imageViewSize: imageViewSize, + shouldShowOperatorInfo: shouldShowOperatorInfo + ) + } + + // MARK: - CONSTRAINT LAYOUT METHODS + private func cellLayoutConstraintSystem(showFullDate: Bool) { + if showFullDate { + // dateLabel + dateLabel.snp.remakeConstraints { (make) -> Void in + make.centerX.equalToSuperview() + if #available(iOS 11.0, *) { + make.top.equalTo(self.safeAreaLayoutGuide) + .inset(SPACING_CELL) + } else { + make.top.equalToSuperview() + .inset(SPACING_CELL) + } + } + } + + // messageBodyLabel + messageBodyLabel.snp.remakeConstraints { (make) -> Void in + make.edges.equalToSuperview() + .inset(SPACING_DEFAULT) + } + + // messageBackgroundView + messageBackgroundView.snp.remakeConstraints { (make) -> Void in + if #available(iOS 11.0, *) { + make.leading.trailing.equalTo(self.safeAreaLayoutGuide) + .inset(SPACING_DEFAULT) + if showFullDate { + make.top.equalTo(dateLabel.snp.bottom) + .offset(SPACING_DEFAULT) + } else { + make.top.equalTo(self.safeAreaLayoutGuide) + .inset(SPACING_CELL) + } + } else { + make.leading.trailing.equalToSuperview() + .inset(SPACING_DEFAULT) + if showFullDate { + make.top.equalTo(dateLabel.snp.bottom) + .offset(SPACING_DEFAULT) + } else { + make.top.equalToSuperview() + .inset(SPACING_CELL) + } + } + } + + // timeLabel + timeLabel.snp.remakeConstraints { (make) -> Void in + make.centerX.equalTo(messageBackgroundView.snp.centerX) + make.top.equalTo(messageBackgroundView.snp.bottom) + .offset(SPACING_CELL) + if #available(iOS 11.0, *) { + make.bottom.equalTo(self.safeAreaLayoutGuide) + .inset(SPACING_CELL) + } else { + make.bottom.equalToSuperview() + .inset(SPACING_CELL) + } + } + + } + + private func cellLayoutConstraintButtons(showFullDate: Bool) { + + if showFullDate { + // dateLabel + dateLabel.snp.remakeConstraints { (make) -> Void in + make.centerX.equalToSuperview() + if #available(iOS 11.0, *) { + make.top.equalTo(self.safeAreaLayoutGuide) + .inset(SPACING_CELL) + } else { + make.top.equalToSuperview() + .inset(SPACING_CELL) + } + } + } + + // buttonsVerticalStack + buttonsVerticalStack.snp.remakeConstraints { (make) -> Void in + make.leading.trailing.top.equalToSuperview() + .inset(SPACING_DEFAULT) + } + + // timeLabel + timeLabel.snp.remakeConstraints { (make) -> Void in + make.centerX.equalTo(messageBackgroundView.snp.centerX) + make.top.equalTo(buttonsVerticalStack.snp.bottom) + .offset(SPACING_CELL) + make.bottom.equalToSuperview() + .inset(SPACING_CELL) + } + + // messageBackgroundView + messageBackgroundView.snp.remakeConstraints { (make) -> Void in + if #available(iOS 11.0, *) { + make.leading.trailing.bottom.equalTo(self.safeAreaLayoutGuide) + + if showFullDate { + make.top.equalTo(dateLabel.snp.bottom) + } else { + make.top.equalTo(self.safeAreaLayoutGuide) + } + } else { + make.leading.trailing.bottom.equalToSuperview() + + if showFullDate { + make.top.equalTo(dateLabel.snp.bottom) + } else { + make.top.equalToSuperview() + } + } + } + } + + private func cellLayoutConstraintOther( + showFullDate: Bool, + forOperator: Bool, + hasQuote: Bool, + hasQuoteAttachment: Bool, + hasQuoteImage: Bool, + hasAttachment:Bool, + hasImage: Bool, + hasSendingFile: Bool, + imageViewSize: CGSize, + shouldShowOperatorInfo: Bool + ) { + if showFullDate { + // dateLabel + dateLabel.snp.remakeConstraints { (make) -> Void in + make.centerX.equalToSuperview() + if #available(iOS 11.0, *) { + make.top.equalTo(self.safeAreaLayoutGuide) + .inset(SPACING_CELL) + } else { + make.top.equalToSuperview() + .inset(SPACING_CELL) + } + } + } + + if forOperator { + // userAvatarImageView + userAvatarImageView.snp.remakeConstraints { (make) -> Void in + if #available(iOS 11.0, *) { + make.leading.equalTo(self.safeAreaLayoutGuide.snp.leading) + .inset(SPACING_DEFAULT) + } else { + make.leading.equalToSuperview() + .inset(SPACING_DEFAULT) + } + make.bottom.equalTo(messageBackgroundView.snp.bottom) + make.width.height.equalTo(USERAVATARIMAGEVIEW_WIDTH) + } + + // documentFileStatusPercentageIndicator + documentFileStatusPercentageIndicator.snp.remakeConstraints { (make) -> Void in + make.edges.equalToSuperview() + .inset(5) + } + } + + // messageBackgroundView + messageBackgroundView.snp.remakeConstraints { (make) -> Void in + if #available(iOS 11.0, *) { + if showFullDate { + make.top.equalTo(dateLabel.snp.bottom) + .offset(SPACING_DEFAULT) + } else { + make.top.equalTo(self.safeAreaLayoutGuide) + .inset(SPACING_CELL) + } + + if !forOperator { + make.trailing.equalTo(self.safeAreaLayoutGuide.snp.trailing) + .inset(SPACING_DEFAULT) + } + } else { + if showFullDate { + make.top.equalTo(dateLabel.snp.bottom) + .offset(SPACING_DEFAULT) + } else { + make.top.equalToSuperview() + .inset(SPACING_CELL) + } + + if !forOperator { + make.trailing.equalToSuperview() + .inset(SPACING_DEFAULT) + } + } + if forOperator { + make.leading.equalTo(userAvatarImageView.snp.trailing) + .offset(SPACING_DEFAULT) + } + make.width.greaterThanOrEqualTo(CHAT_BUBBLE_MIN_WIDTH) + make.width.lessThanOrEqualTo(CHAT_BUBBLE_MAX_WIDTH) + } + + if forOperator && shouldShowOperatorInfo { + // messageUsernameLabel + messageUsernameLabel.snp.remakeConstraints { (make) -> Void in + make.top.equalToSuperview() + .inset(SPACING_DEFAULT) + make.trailing.leading.equalToSuperview() + .inset(SPACING_DEFAULT) + } + } + + // timeLabel + timeLabel.snp.remakeConstraints { (make) -> Void in + make.top.equalTo(messageBackgroundView.snp.bottom) + .offset(SPACING_CELL) + if #available(iOS 11.0, *) { + make.bottom.equalTo(self.safeAreaLayoutGuide.snp.bottom) + .inset(SPACING_CELL).priority(999) + } else { + make.bottom.equalToSuperview() + .inset(SPACING_CELL).priority(999) + } + + + if forOperator { + make.leading.equalTo(messageBackgroundView.snp.leading) + .inset(5) + } + } + + // messageStatusImageView + messageStatusImageView.snp.remakeConstraints { (make) -> Void in + make.leading.equalTo(timeLabel.snp.trailing) + .offset(5) + if !forOperator { + if #available(iOS 11.0, *) { + make.trailing.equalTo(self.safeAreaLayoutGuide.snp.trailing) + .inset(15) + } else { + make.trailing.equalToSuperview() + .inset(15) + } + } + make.centerY.equalTo(timeLabel.snp.centerY) + + make.width.height.equalTo(15) + } + + if !forOperator { + // messageStatusIndicator + messageStatusIndicator.snp.remakeConstraints { (make) -> Void in + make.center.equalTo(messageStatusImageView) + make.width.height.equalTo(messageStatusImageView.snp.width) + } + } + + // messageStatusLabel + messageStatusLabel.snp.remakeConstraints { (make) -> Void in + make.centerY.equalTo(timeLabel.snp.centerY) + if forOperator { + make.leading.equalTo(messageStatusImageView.snp.trailing) + .offset(5) + } else { + make.trailing.equalTo(timeLabel.snp.leading) + .offset(-5) + } + } + + if hasQuote { + // quoteLineView + quoteLineView.snp.remakeConstraints { (make) -> Void in + make.height.equalTo(45) + make.width.equalTo(2) + make.leading.equalToSuperview() + .inset(SPACING_DEFAULT) + + if forOperator && shouldShowOperatorInfo { + make.top.equalTo(messageUsernameLabel.snp.bottom) + .offset(SPACING_DEFAULT) + } else { + make.top.equalToSuperview() + .inset(SPACING_DEFAULT) + } + } + + if hasQuoteAttachment { + if hasQuoteImage { + // quoteAttachmentImageView + quoteAttachmentImageView.snp.remakeConstraints { (make) -> Void in + make.height.width.equalTo(quoteLineView.snp.height) + if forOperator && shouldShowOperatorInfo { + make.top.equalTo(messageUsernameLabel.snp.bottom) + .offset(SPACING_DEFAULT) + } else { + make.top.equalToSuperview() + .inset(SPACING_DEFAULT) + } + make.leading.equalTo(quoteLineView.snp.trailing) + .offset(SPACING_DEFAULT) + } + + // documentFileStatusPercentageIndicator + documentFileStatusPercentageIndicator.snp.remakeConstraints { (make) -> Void in + make.edges.equalToSuperview() + .inset(5) + } + } + + // quoteUsernameLabel + quoteUsernameLabel.snp.remakeConstraints { (make) -> Void in + + if forOperator && shouldShowOperatorInfo { + make.top.equalTo(messageUsernameLabel.snp.bottom) + .offset(SPACING_DEFAULT) + } else { + make.top.equalToSuperview() + .inset(SPACING_DEFAULT) + } + make.trailing.equalToSuperview() + .inset(SPACING_DEFAULT) + if hasQuoteImage { + make.leading.equalTo(quoteAttachmentImageView.snp.trailing) + .offset(SPACING_DEFAULT) + } else { + make.leading.equalTo(quoteLineView.snp.trailing) + .offset(SPACING_DEFAULT) + } + } + + // quoteBodyLabel + quoteBodyLabel.snp.remakeConstraints { (make) -> Void in + make.top.equalTo(quoteUsernameLabel.snp.bottom) + .offset(5) + if hasQuoteImage { + make.leading.equalTo(quoteAttachmentImageView.snp.trailing) + .offset(SPACING_DEFAULT) + } else { + make.leading.equalTo(quoteLineView.snp.trailing) + .offset(SPACING_DEFAULT) + } + make.trailing.equalToSuperview() + .inset(SPACING_DEFAULT) + } + + } else { + // quoteUsernameLabel + quoteUsernameLabel.snp.remakeConstraints { (make) -> Void in + make.trailing.equalToSuperview() + .inset(SPACING_DEFAULT) + + if forOperator && shouldShowOperatorInfo { + make.top.equalTo(messageUsernameLabel.snp.bottom) + .offset(SPACING_DEFAULT) + } else { + make.top.equalToSuperview() + .inset(SPACING_DEFAULT) + } + make.leading.equalTo(quoteLineView.snp.trailing) + .offset(SPACING_DEFAULT) + } + + // quoteBodyLabel + quoteBodyLabel.snp.remakeConstraints { (make) -> Void in + make.top.equalTo(quoteUsernameLabel.snp.bottom) + .offset(5) + make.leading.equalTo(quoteLineView.snp.trailing) + .offset(SPACING_DEFAULT) + make.trailing.equalToSuperview() + .inset(SPACING_DEFAULT) + } + } + } + + if hasAttachment { + if hasImage && !hasImageAsDocument { + if forOperator && shouldShowOperatorInfo { + // imageUsernameLabel + imageUsernameLabel.snp.remakeConstraints { (make) -> Void in + make.top.bottom.equalToSuperview() + .inset(5) + make.trailing.leading.equalToSuperview() + .inset(SPACING_DEFAULT) + } + + // imageUsernameLabelBackgroundView + imageUsernameLabelBackgroundView.snp.remakeConstraints { (make) -> Void in + make.top.leading.equalToSuperview() + .inset(SPACING_DEFAULT) + } + } + + // imageImageView + imageImageView.snp.remakeConstraints { (make) -> Void in + make.height.equalTo(imageViewSize.height) + make.width.equalTo(imageViewSize.width) + make.edges.equalToSuperview() + } + + // documentFileStatusPercentageIndicator + documentFileStatusPercentageIndicator.snp.remakeConstraints { (make) -> Void in + make.centerX.equalToSuperview() + make.centerY.equalToSuperview() + make.height.equalTo(45) + make.width.equalTo(45) + } + + // Since imageImageView is a SubView of messageBackgroundView we have to delete (or remake) constraint with minWidth of the background. + messageBackgroundView.snp.remakeConstraints { (make) -> Void in + if #available(iOS 11.0, *) { + if showFullDate { + make.top.equalTo(dateLabel.snp.bottom) + .offset(SPACING_DEFAULT) + } else { + make.top.equalTo(self.safeAreaLayoutGuide.snp.top) + .inset(SPACING_CELL) + } + + if !forOperator { + make.trailing.equalTo(self.safeAreaLayoutGuide.snp.trailing) + .inset(SPACING_DEFAULT) + } + } else { + if showFullDate { + make.top.equalTo(dateLabel.snp.bottom) + .offset(SPACING_DEFAULT) + } else { + make.top.equalToSuperview() + .inset(SPACING_CELL) + } + + if !forOperator { + make.trailing.equalToSuperview() + .inset(SPACING_DEFAULT) + } + } + if forOperator { + make.leading.equalTo(userAvatarImageView.snp.trailing) + .offset(SPACING_DEFAULT) + } + make.width.lessThanOrEqualTo(CHAT_BUBBLE_MAX_WIDTH) + } + } else { + // documentFileStatusPercentageIndicator + documentFileStatusPercentageIndicator.snp.remakeConstraints { (make) -> Void in + make.edges.equalToSuperview() + } + + // documentFileCancelDownloadButton + documentFileCancelDownloadButton.snp.remakeConstraints { (make) -> Void in + make.edges.equalToSuperview() + } + + // documentFileStatusImageView + documentFileStatusButton.snp.remakeConstraints { (make) -> Void in + make.height.equalTo(45) + make.width.equalTo(45) + make.leading.equalToSuperview() + .inset(SPACING_DEFAULT) + + if forOperator && shouldShowOperatorInfo { + make.top.equalTo(messageUsernameLabel.snp.bottom) + .offset(SPACING_DEFAULT) + } else { + make.top.equalToSuperview() + .inset(SPACING_DEFAULT) + } + } + + // documentFileNameLabel + documentFileNameLabel.snp.remakeConstraints { (make) -> Void in + make.trailing.equalToSuperview() + .inset(SPACING_DEFAULT) + + if forOperator && shouldShowOperatorInfo { + make.top.equalTo(messageUsernameLabel.snp.bottom) + .offset(SPACING_DEFAULT) + } else { + make.top.equalToSuperview() + .inset(SPACING_DEFAULT) + } + make.leading.equalTo(documentFileStatusButton.snp.trailing) + .offset(SPACING_DEFAULT) + } + + // documentFileDescriptionLabel + documentFileDescriptionLabel.snp.remakeConstraints { (make) -> Void in + make.top.equalTo(documentFileNameLabel.snp.bottom) + .offset(5) + make.leading.equalTo(documentFileStatusButton.snp.trailing) + .offset(SPACING_DEFAULT) + make.trailing.bottom.equalToSuperview() + .inset(SPACING_DEFAULT) + } + } + } else if hasSendingFile { + documentFileStatusButton.snp.remakeConstraints { (make) -> Void in + make.height.equalTo(45) + make.width.equalTo(45) + make.leading.equalToSuperview() + .inset(SPACING_DEFAULT) + + if forOperator && shouldShowOperatorInfo { + make.top.equalTo(messageUsernameLabel.snp.bottom) + .offset(SPACING_DEFAULT) + } else { + make.top.equalToSuperview() + .inset(SPACING_DEFAULT) + } + } + + // documentFileNameLabel + documentFileNameLabel.snp.remakeConstraints { (make) -> Void in + make.trailing.equalToSuperview() + .inset(SPACING_DEFAULT) + + if forOperator && shouldShowOperatorInfo { + make.top.equalTo(messageUsernameLabel.snp.bottom) + .offset(SPACING_DEFAULT) + } else { + make.top.equalToSuperview() + .inset(SPACING_DEFAULT) + } + make.leading.equalTo(documentFileStatusButton.snp.trailing) + .offset(SPACING_DEFAULT) + } + + // documentFileDescriptionLabel + documentFileDescriptionLabel.snp.remakeConstraints { (make) -> Void in + make.top.equalTo(documentFileNameLabel.snp.bottom) + .offset(5) + make.leading.equalTo(documentFileStatusButton.snp.trailing) + .offset(SPACING_DEFAULT) + make.trailing.bottom.equalToSuperview() + .inset(SPACING_DEFAULT) + } + } else { + // messageBodyLabel + messageBodyLabel.snp.remakeConstraints { (make) -> Void in + if hasQuote { + make.top.equalTo(quoteBodyLabel.snp.bottom) + .offset(SPACING_DEFAULT) + } else { + if forOperator && shouldShowOperatorInfo { + make.top.equalTo(messageUsernameLabel.snp.bottom) + .offset(SPACING_DEFAULT) + } else { + make.top.equalToSuperview() + .inset(SPACING_DEFAULT) + } + + } + make.trailing.bottom.leading.equalToSuperview() + .inset(SPACING_DEFAULT) + } + } + } + + @objc + private func downloadFile(_ sender: UIButton) { + guard let message = messageFromCell, + let url = message.getData()?.getAttachment()?.getFileInfo().getURL() + else { return } + downloadTask = urlSession.downloadTask(with: url) + + guard let fullURL = downloadTask.originalRequest?.url, + let documentsDirectory = FileManager.default.urls( + for: .cachesDirectory, + in: .userDomainMask + ).first + else { return } + + let fileName = fullURL.lastPathComponent + let fileDestinationURLString = documentsDirectory.appendingPathComponent(fileName).path + if FileManager.default.fileExists(atPath: fileDestinationURLString) { + // TODO: Compare files here before moving forward + showFile(fileName: fileName) + } else { + //documentFileStatusButton.setBackgroundImage(UIImage(), for: .normal) + downloadTask.resume() + } + } + + @objc + private func cancelDownload(_ sender: UIButton) { + downloadTask.cancel() + + DispatchQueue.main.async { + if self.isForOperator { + self.documentFileStatusButton.setBackgroundImage( + documentFileStatusButtonDownloadOperator, + for: .normal + ) + } else { + self.documentFileStatusButton.setBackgroundImage( + documentFileStatusButtonDownloadVisitor, + for: .normal + ) + } + self.documentFileStatusPercentageIndicator.isHidden = true + self.documentFileCancelDownloadButton.isHidden = true + } + } + + @objc + private func sendButton(sender: UIButton) { + guard let message = messageFromCell else { return } + + let messageID = message.getID() + guard let title = sender.titleLabel?.text, + let id = sender.accessibilityIdentifier + else { return } + + print("Buttton \(title) with tag\\ID \(id) of message \(messageID) was tapped!") + print(message.getText()) + + let buttonInfoDictionary = [ + "Message": messageID, + "ButtonID": id, + "ButtonTitle": title + ] + + NotificationCenter.default.post( + name: .shouldSendKeyboardRequest, + object: nil, + userInfo: buttonInfoDictionary + ) + } + + private func isFileExist(fileName: String) -> Bool { + guard let documentsDirectory = FileManager.default.urls( + for: .cachesDirectory, + in: .userDomainMask + ).first + else { return false } + + let fileDestinationURLString = documentsDirectory.appendingPathComponent(fileName).path + return FileManager.default.fileExists(atPath: fileDestinationURLString) + } + + private func updateFileDownloadProgress(progress: Float) { + if self.documentFileStatusPercentageIndicator.isHidden { + self.documentFileCancelDownloadButton.isHidden = false + + self.documentFileStatusPercentageIndicator.isHidden = false + self.documentFileStatusPercentageIndicator.enableRotationAnimation() + self.documentFileStatusButton.setBackgroundImage(UIImage(), for: .normal) + } + self.documentFileStatusPercentageIndicator.setProgressWithAnimation( + duration: 0.1, + value: progress + ) + } + + private func updateImageDownloadProgress(completed: Int64, total: Int64) { + let progress = Float(completed) / Float(total) + + if self.documentFileStatusPercentageIndicator.isHidden { + self.documentFileStatusPercentageIndicator.isHidden = false + self.documentFileStatusPercentageIndicator.enableRotationAnimation() + } + self.documentFileStatusPercentageIndicator.setProgressWithAnimation( + duration: 0.1, + value: progress + ) + } + + private func showFile(fileName file: String) { + let filesDictionary = ["FullName": file] + NotificationCenter.default.post( + name: .shouldShowFile, + object: nil, + userInfo: filesDictionary + ) + } +} + +// MARK: - UI methods +extension FlexibleTableViewCell { + func createUILabel( + textAlignment: NSTextAlignment = .left, + systemFontSize: CGFloat, + systemFontWeight: UIFont.Weight = .regular, + numberOfLines: Int = 1 + ) -> UILabel { + let label = UILabel() + label.textAlignment = textAlignment + label.font = .systemFont(ofSize: systemFontSize, weight: systemFontWeight ) + label.numberOfLines = numberOfLines + label.translatesAutoresizingMaskIntoConstraints = false + return label + } + + func createUIView() -> UIView { + let view = UIView() + view.translatesAutoresizingMaskIntoConstraints = false + return view + } + + func createUIImageView(contentMode: UIView.ContentMode = .scaleAspectFit) -> UIImageView { + let imageView = UIImageView() + imageView.contentMode = contentMode + imageView.translatesAutoresizingMaskIntoConstraints = false + return imageView + } + + func createUIButton(type: UIButton.ButtonType) -> UIButton { + let button = UIButton(type: type) + button.translatesAutoresizingMaskIntoConstraints = false + return button + } + + func roundCornersForMessage(on uiView: UIView, forOperator: Bool) { + var cornerMask = CACornerMask() + if UIApplication.shared.userInterfaceLayoutDirection == .rightToLeft && forOperator { + cornerMask = [ + .layerMinXMinYCorner, + .layerMaxXMinYCorner, + .layerMinXMaxYCorner + ] + } else if UIApplication.shared.userInterfaceLayoutDirection == .rightToLeft || forOperator { + cornerMask = [ + .layerMinXMinYCorner, + .layerMaxXMinYCorner, + .layerMaxXMaxYCorner + ] + } else { + cornerMask = [ + .layerMinXMinYCorner, + .layerMaxXMinYCorner, + .layerMinXMaxYCorner + ] + } + uiView.roundCorners(cornerMask, radius: 15) + } +} + + +// MARK: - URLSessionDownloadDelegate +extension FlexibleTableViewCell: URLSessionDownloadDelegate { + + func urlSession( + _ session: URLSession, + downloadTask: URLSessionDownloadTask, + didFinishDownloadingTo location: URL + ) { + guard let httpResponse = downloadTask.response as? HTTPURLResponse,httpResponse.statusCode >= 200, + httpResponse.statusCode < 300 + else { + print ("Server error") + return + } + + // Create destination URL with original name + guard let url = downloadTask.originalRequest?.url, + let documentsDirectory = FileManager.default.urls( + for: .cachesDirectory, + in: .userDomainMask + ).first + else { return } + + let destinationURL = documentsDirectory.appendingPathComponent(url.lastPathComponent) + + // Delete original copy + try? FileManager.default.removeItem(at: destinationURL) + + // Copy from temp to Cache + do { + try FileManager.default.copyItem(at: location, to: destinationURL) + } catch let error { + print("Copy Error: \(error.localizedDescription)") + } + + // Cache location for debug + // print("Save path: \(documentsDirectory)") + + DispatchQueue.main.async { + if self.isForOperator { + self.documentFileStatusButton.setBackgroundImage( + documentFileStatusButtonDownloadSuccessOperator, + for: .normal + ) + } else { + self.documentFileStatusButton.setBackgroundImage( + documentFileStatusButtonDownloadSuccessVisitor, + for: .normal + ) + } + self.documentFileStatusPercentageIndicator.isHidden = true + self.documentFileCancelDownloadButton.isHidden = true + } + } + + func urlSession( + _ session: URLSession, + downloadTask: URLSessionDownloadTask, + didWriteData bytesWritten: Int64, + totalBytesWritten: Int64, + totalBytesExpectedToWrite: Int64 + ) { + let calculatedProgress = Float(totalBytesWritten) / Float(totalBytesExpectedToWrite) + DispatchQueue.main.async { + self.updateFileDownloadProgress(progress: calculatedProgress) + } + } +} diff --git a/Example/WebimClientLibrary/MessageTableViewCell.swift b/Example/WebimClientLibrary/Utilities/MessageTableViewCell.swift similarity index 97% rename from Example/WebimClientLibrary/MessageTableViewCell.swift rename to Example/WebimClientLibrary/Utilities/MessageTableViewCell.swift index bb119777..e8078e14 100644 --- a/Example/WebimClientLibrary/MessageTableViewCell.swift +++ b/Example/WebimClientLibrary/Utilities/MessageTableViewCell.swift @@ -142,6 +142,10 @@ final class MessageTableViewCell: UITableViewCell { case .visitorMessage: layoutVisitor(message: message) + break + case .stickerVisitor: + layoutSticker(message: message) + break } @@ -301,6 +305,11 @@ final class MessageTableViewCell: UITableViewCell { avatarImageView.isHidden = true } + private func layoutSticker(message: Message) { + layoutVisitor(message: message) + bodyLabel.text = "sticker with id: \(String(describing: message.getSticker()?.getStickerId()))" + } + private func layoutVisitor(message: Message) { bodyLabel.text = message.getText().decodePercentEscapedLinksIfPresent() bodyLabel.textColor = textMainColor.color() diff --git a/Example/WebimClientLibrary/Utilities/MimeType.swift b/Example/WebimClientLibrary/Utilities/MimeType.swift index e19b486f..fc765d3a 100755 --- a/Example/WebimClientLibrary/Utilities/MimeType.swift +++ b/Example/WebimClientLibrary/Utilities/MimeType.swift @@ -133,7 +133,10 @@ fileprivate let mimeTypes = [ "asx" : "video/x-ms-asf", "asf" : "video/x-ms-asf", "wmv" : "video/x-ms-wmv", - "avi" : "video/x-msvideo" + "avi" : "video/x-msvideo", + "key" : "application/x-iwork-keynote-sffkey", + "pages" : "application/x-iwork-pages-sffpages", + "numbers" : "application/x-iwork-numbers-sffnumbers" ] // MARK: - @@ -163,3 +166,22 @@ func isImage(contentType: String) -> Bool { || (contentType == "image/png") || (contentType == "image/tiff")) } + +// Check if file is acceptable to show in WKWebView +func isAcceptableFile(contentType: String) -> Bool { + return ((contentType == mimeTypes["txt"]) + || (contentType == mimeTypes["rtf"]) + || (contentType == mimeTypes["pdf"]) + || (contentType == mimeTypes["doc"]) + || (contentType == mimeTypes["docx"]) + || (contentType == mimeTypes["xls"]) + || (contentType == mimeTypes["xlsx"]) + || (contentType == mimeTypes["ppt"]) + || (contentType == mimeTypes["pptx"]) + || (contentType == mimeTypes["mp4"]) + || (contentType == mimeTypes["key"]) + || (contentType == mimeTypes["pages"]) + || (contentType == mimeTypes["numbers"]) + + || (contentType == "text/rtf")) +} diff --git a/Example/WebimClientLibrary/Utilities/PopupDialogHandler.swift b/Example/WebimClientLibrary/Utilities/PopupDialogHandler.swift deleted file mode 100644 index a0c16391..00000000 --- a/Example/WebimClientLibrary/Utilities/PopupDialogHandler.swift +++ /dev/null @@ -1,230 +0,0 @@ -// -// PopupDialogHandler.swift -// WebimClientLibrary_Tests -// -// Created by Nikita Lazarev-Zubov on 13.02.18. -// Copyright © 2018 Webim. All rights reserved. -// -// Permission is hereby granted, free of charge, to any person obtaining a copy -// of this software and associated documentation files (the "Software"), to deal -// in the Software without restriction, including without limitation the rights -// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -// copies of the Software, and to permit persons to whom the Software is -// furnished to do so, subject to the following conditions: -// -// The above copyright notice and this permission notice shall be included in all -// copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -// SOFTWARE. -// - -import PopupDialog -import UIKit -import WebimClientLibrary - -final class PopupDialogHandler { - - // MARK: - Constants - static let BUTTON_HEIGHT = 60 - - // MARK: - Properties - private weak var delegate: UIViewController? - - // MARK - Initializer - init(delegate: UIViewController) { - self.delegate = delegate - } - - // MARK: - Methods - - func showSettingsAlertDialog(withMessage message: String) { - let popup = PopupDialog(title: SettingsErrorDialog.title.rawValue.localized, - message: message) - popup.view.backgroundColor = backgroundSecondaryColor.color() - (popup.viewController as! PopupDialogDefaultViewController).titleColor = textMainColor.color() - - let okButton = CancelButton(title: SettingsErrorDialog.buttonTitle.rawValue.localized, - action: nil) - okButton.accessibilityHint = SettingsErrorDialog.buttonAccessibilityHint.rawValue.localized - okButton.titleColor = textMainColor.color() - okButton.backgroundColor = backgroundSecondaryColor.color() - popup.addButton(okButton) - - delegate?.present(popup, - animated: true, - completion: nil) - } - - func showCreatingSessionFailureDialog(withMessage message: String) { - let popup = PopupDialog(title: SessionCreationErrorDialog.title.rawValue.localized, - message: message.localized) - popup.view.backgroundColor = backgroundSecondaryColor.color() - (popup.viewController as! PopupDialogDefaultViewController).titleColor = textMainColor.color() - - let okButton = CancelButton(title: SessionCreationErrorDialog.buttonTitle.rawValue.localized, - action: nil) - okButton.accessibilityHint = SessionCreationErrorDialog.buttonAccessibilityHint.rawValue.localized - okButton.titleColor = textMainColor.color() - okButton.backgroundColor = backgroundSecondaryColor.color() - popup.addButton(okButton) - - delegate?.present(popup, - animated: true, - completion: nil) - } - - func showDepartmentListDialog(withDepartmentList departmentList: [Department], - action: @escaping (String) -> ()) { - let popup = PopupDialog(title: DepartmentListDialog.title.rawValue.localized, - message: nil, - image: nil, - buttonAlignment: .vertical, - transitionStyle: .bounceDown, - completion: nil) - popup.view.backgroundColor = backgroundSecondaryColor.color() - (popup.viewController as! PopupDialogDefaultViewController).titleColor = textMainColor.color() - - for department in departmentList { - let button = DefaultButton(title: department.getName()) { - action(department.getKey()) - } - button.accessibilityHint = DepartmentListDialog.buttonAccessibilityHint.rawValue.localized - button.titleColor = textMainColor.color() - button.backgroundColor = backgroundSecondaryColor.color() - popup.addButton(button) - } - - let cancelButton = CancelButton(title: DepartmentListDialog.cancelButtonTitle.rawValue.localized, - height: PopupDialogHandler.BUTTON_HEIGHT, - dismissOnTap: true, - action: nil) - cancelButton.accessibilityHint = DepartmentListDialog.cancelButtonAccessibilityHint.rawValue.localized - cancelButton.backgroundColor = backgroundSecondaryColor.color() - popup.addButton(cancelButton) - - delegate?.present(popup, - animated: true, - completion: nil) - } - - func showFileSendFailureDialog(withMessage message: String, - action: (() -> ())? = nil) { - let popupDialog = PopupDialog(title: SendFileErrorMessage.title.rawValue.localized, - message: message) - popupDialog.view.backgroundColor = backgroundSecondaryColor.color() - (popupDialog.viewController as! PopupDialogDefaultViewController).titleColor = textMainColor.color() - - let okButton = CancelButton(title: SendFileErrorMessage.buttonTitle.rawValue.localized) { - action?() - } - okButton.accessibilityHint = SendFileErrorMessage.buttonAccessibilityHint.rawValue.localized - okButton.backgroundColor = backgroundSecondaryColor.color() - okButton.titleColor = textMainColor.color() - popupDialog.addButton(okButton) - - delegate?.present(popupDialog, - animated: true, - completion: nil) - } - - func showFileDialog(withMessage message: String?, - title: String, - image: UIImage?) { - let button = CancelButton(title: ShowFileDialog.buttonTitle.rawValue.localized, - action: nil) - button.accessibilityHint = ShowFileDialog.accessibilityHint.rawValue.localized - button.backgroundColor = backgroundSecondaryColor.color() - button.titleColor = textMainColor.color() - - let popup = PopupDialog(title: title, - message: message, - image: image, - buttonAlignment: .horizontal, - transitionStyle: .bounceUp, - tapGestureDismissal: true, - completion: nil) - popup.view.backgroundColor = backgroundSecondaryColor.color() - (popup.viewController as! PopupDialogDefaultViewController).titleColor = textMainColor.color() - popup.addButton(button) - - delegate?.present(popup, - animated: true, - completion: nil) - } - - func showRatingDialog(forOperator operatorID: String, - action: @escaping (_ rating: Int) -> ()) { - let ratingViewController = RatingViewController(nibName: "RatingViewController", - bundle: nil) - let popup = PopupDialog(viewController: ratingViewController, - buttonAlignment: .horizontal, - transitionStyle: .bounceUp, - tapGestureDismissal: true) - - let cancelButton = CancelButton(title: RatingDialog.cancelButtonTitle.rawValue.localized, - height: PopupDialogHandler.BUTTON_HEIGHT, - dismissOnTap: true, - action: nil) - cancelButton.accessibilityHint = RatingDialog.cancelButtonAccessibilityHint.rawValue.localized - cancelButton.backgroundColor = backgroundSecondaryColor.color() - - let rateButton = DefaultButton(title: RatingDialog.actionButtonTitle.rawValue.localized, - height: PopupDialogHandler.BUTTON_HEIGHT, - dismissOnTap: true) { - action(Int(ratingViewController.ratingView.rating)) - } - rateButton.accessibilityHint = RatingDialog.actionButtonAccessibilityHint.rawValue.localized - rateButton.backgroundColor = backgroundSecondaryColor.color() - rateButton.titleColor = textMainColor.color() - - popup.addButtons([cancelButton, - rateButton]) - - delegate?.present(popup, - animated: true, - completion: nil) - } - - func showRatingFailureDialog() { - let popupDialog = PopupDialog(title: RateOperatorErrorMessage.title.rawValue.localized, - message: RateOperatorErrorMessage.message.rawValue.localized) - popupDialog.view.backgroundColor = backgroundSecondaryColor.color() - (popupDialog.viewController as! PopupDialogDefaultViewController).titleColor = textMainColor.color() - - let okButton = CancelButton(title: RateOperatorErrorMessage.buttonTitle.rawValue.localized, - action: nil) - okButton.accessibilityHint = RateOperatorErrorMessage.buttonAccessibilityHint.rawValue.localized - okButton.titleColor = textMainColor.color() - okButton.backgroundColor = backgroundSecondaryColor.color() - popupDialog.addButton(okButton) - - delegate?.present(popupDialog, - animated: true, - completion: nil) - } - - func showChatClosedDialog() { - let popupDialog = PopupDialog(title: nil, - message: ChatClosedDialog.message.rawValue.localized) - popupDialog.view.backgroundColor = backgroundSecondaryColor.color() - (popupDialog.viewController as! PopupDialogDefaultViewController).messageColor = textMainColor.color() - - let okButton = CancelButton(title: ChatClosedDialog.buttonTitle.rawValue.localized, - action: nil) - okButton.accessibilityHint = ChatClosedDialog.buttonAccessibilityHint.rawValue.localized - okButton.titleColor = textMainColor.color() - okButton.backgroundColor = backgroundSecondaryColor.color() - popupDialog.addButton(okButton) - - delegate?.present(popupDialog, - animated: true, - completion: nil) - } - -} diff --git a/Example/WebimClientLibrary/Utilities/SpinningIndicator.swift b/Example/WebimClientLibrary/Utilities/SpinningIndicator.swift new file mode 100644 index 00000000..1f6a25cb --- /dev/null +++ b/Example/WebimClientLibrary/Utilities/SpinningIndicator.swift @@ -0,0 +1,145 @@ +// +// SpinningIndicator.swift +// WebimClientLibrary_Example +// +// Created by Eugene Ilyin on 14.10.2019. +// Copyright © 2019 Webim. All rights reserved. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. +// + +import Foundation +import UIKit + +class SpinningIndicator: UIView { + + // MARK: - Properties + var animating: Bool = true { + didSet { + updateAnimation() + } + } + var lineWidth: CGFloat = 2 { + didSet { + circleLayer.lineWidth = lineWidth + setNeedsLayout() + } + } + var strokeColor: CGColor = UIColor.red.cgColor { + didSet { + circleLayer.strokeColor = strokeColor + setNeedsLayout() + } + } + + // MARK: - Private properties + private let circleLayer = CAShapeLayer() + private let strokeEndAnimation: CAAnimation = { + let animation = CABasicAnimation(keyPath: "strokeEnd") + animation.fromValue = 0 + animation.toValue = 1 + animation.duration = 2 + animation.timingFunction = CAMediaTimingFunction(name: .easeInEaseOut) + + let group = CAAnimationGroup() + group.duration = 2.5 + group.repeatCount = .infinity + group.animations = [animation] + + return group + }() + private let strokeStartAnimation: CAAnimation = { + let animation = CABasicAnimation(keyPath: "strokeStart") + animation.beginTime = 0.5 + animation.fromValue = 0 + animation.toValue = 1 + animation.duration = 2 + animation.timingFunction = CAMediaTimingFunction(name: .easeInEaseOut) + + let group = CAAnimationGroup() + group.duration = 2.5 + group.repeatCount = .infinity + group.animations = [animation] + + return group + }() + private let rotationAnimation: CAAnimation = { + let animation = CABasicAnimation(keyPath: "transform.rotation.z") + animation.fromValue = 0 + animation.toValue = Double.pi * 2 + animation.duration = 4 + animation.repeatCount = .infinity + + return animation + }() + + // MARK: - Methods + override init(frame: CGRect) { + super.init(frame: frame) + setup() + } + + required init?(coder: NSCoder) { + super.init(coder: coder) + setup() + } + + override func layoutSubviews() { + super.layoutSubviews() + + let center = CGPoint(x: bounds.midX, y: bounds.midY) + let radius = min(bounds.width, bounds.height) / 2 - circleLayer.lineWidth / 2 + + let startAngle = CGFloat(-Double.pi/2) // -90° + let endAngle = startAngle + CGFloat(Double.pi * 2) + let path = UIBezierPath( + arcCenter: CGPoint.zero, + radius: radius, + startAngle: startAngle, + endAngle: endAngle, + clockwise: true + ) + + circleLayer.position = center + circleLayer.path = path.cgPath + } + + // MARK: - Private methods + private func setup() { + circleLayer.lineWidth = lineWidth + circleLayer.fillColor = nil + circleLayer.strokeColor = strokeColor + layer.addSublayer(circleLayer) + updateAnimation() + } + + private func updateAnimation() { + if animating { + circleLayer.isHidden = false + circleLayer.add(strokeEndAnimation, forKey: "strokeEnd") + circleLayer.add(strokeStartAnimation, forKey: "strokeStart") + circleLayer.add(rotationAnimation, forKey: "rotation") + } else { + circleLayer.isHidden = true + circleLayer.removeAnimation(forKey: "strokeEnd") + circleLayer.removeAnimation(forKey: "strokeStart") + circleLayer.removeAnimation(forKey: "rotation") + } + } +} diff --git a/Example/WebimClientLibrary/Utilities/TypingIndicator.swift b/Example/WebimClientLibrary/Utilities/TypingIndicator.swift new file mode 100644 index 00000000..0e5d4897 --- /dev/null +++ b/Example/WebimClientLibrary/Utilities/TypingIndicator.swift @@ -0,0 +1,195 @@ +// +// TypingIndicator.swift +// WebimClientLibrary_Example +// +// Created by Eugene Ilyin on 05.11.2019. +// Copyright © 2019 Webim. All rights reserved. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. +// + +import Foundation +import UIKit + +class TypingIndicator: UIView { + + // MARK: - Properties + enum CirclePosition { + case leading, center, trailing + } + var circleDiameter: CGFloat = 20.0 { + didSet { + setNeedsLayout() + } + } + var circleColour = UIColor.red { + didSet { + colourChangeAnimation.values = [ + circleColour.cgColor, + circleColour.withAlphaComponent(0.5).cgColor + ] + } + } + var animationDuration: CFTimeInterval = 1.0 { + didSet { + for index in 0...TypingIndicator.QUANTITY - 1 { + groupAnimations[index].duration = animationDuration + let i = Double(index) + groupAnimations[index].timeOffset = i * 1 / 3 + } + setNeedsLayout() + } + } + + // MARK: - Private properties + private static let QUANTITY = 3 + + private var colourChangeAnimation = CAKeyframeAnimation() + + private var midX = CGFloat() + private var midY = CGFloat() + private var circles = Array( + repeating: CAShapeLayer(), + count: QUANTITY + ) + private var groupAnimations = Array( + repeating: CAAnimationGroup(), + count: QUANTITY + ) + private var paths = Array( + repeating: UIBezierPath(), + count: QUANTITY + ) + private var positionChangeAnimations = Array( + repeating: CAKeyframeAnimation(), + count: QUANTITY + ) + + // MARK: - Methods + override init(frame: CGRect) { + super.init(frame: frame) + setupCircles() + } + + override func layoutSubviews() { + super.layoutSubviews() + + midX = bounds.midX + midY = bounds.midY + positionCircles() + setupPaths() + setupPositionChangeAnimations() + setupColourChangeAnimation() + setupGroupAnimations() + } + + required init?(coder: NSCoder) { + super.init(coder: coder) + setupCircles() + setupPaths() + setupPositionChangeAnimations() + setupColourChangeAnimation() + setupGroupAnimations() + } + + func addAllAnimations() { + for index in 0...TypingIndicator.QUANTITY - 1 { + let groupAnimation = groupAnimations[index] + circles[index].add(groupAnimation, forKey: "multiAnimation") + } + } + + func removeAllAnimations() { + for index in 0...TypingIndicator.QUANTITY - 1 { + circles[index].removeAllAnimations() + } + } + + // MARK: - Private methods + private func createCircle() -> CAShapeLayer { + let circle = CAShapeLayer() + circle.frame = CGRect(x: 0, y: 0, width: circleDiameter, height: circleDiameter) + circle.cornerRadius = circleDiameter / 2 + return circle + } + + private func setupCircles() { + for index in 0...TypingIndicator.QUANTITY - 1 { + circles[index] = createCircle() + layer.addSublayer(circles[index]) + } + } + + private func positionCircles() { + for index in 0...TypingIndicator.QUANTITY - 1 { + let i = CGFloat(index) + circles[index].frame = CGRect( + x: midX / 2 + (i * midX / 2) - circleDiameter / 2, + y: 3 * midY / 2 - circleDiameter / 2, + width: circleDiameter, + height: circleDiameter + ) + circles[index].cornerRadius = circleDiameter / 2 + } + } + + private func setupPaths() { + for index in 0...TypingIndicator.QUANTITY - 1 { + paths[index] = UIBezierPath() + let i = CGFloat(index) + let startPoint = CGPoint(x: midX / 2 + (i * midX / 2), y: 3 * midY / 2) + let endPoint = CGPoint(x: midX / 2 + (i * midX / 2), y: midY / 2) + paths[index].move(to: startPoint) + paths[index].addLine(to: endPoint) + } + } + + private func setupPositionChangeAnimations() { + for index in 0...TypingIndicator.QUANTITY - 1 { + positionChangeAnimations[index] = CAKeyframeAnimation() +// positionChangeAnimations[index].keyPath = "position" + positionChangeAnimations[index].keyPath = #keyPath(CALayer.position) + positionChangeAnimations[index].path = paths[index].cgPath + } + } + + private func setupColourChangeAnimation() { + let colours = [circleColour.cgColor, circleColour.withAlphaComponent(0.5).cgColor] +// colourChangeAnimation.keyPath = "backgroundColor" + colourChangeAnimation.keyPath = #keyPath(CALayer.backgroundColor) + colourChangeAnimation.values = colours + } + + private func setupGroupAnimations() { + for index in 0...TypingIndicator.QUANTITY - 1 { + let i = Double(index) + groupAnimations[index] = CAAnimationGroup() + groupAnimations[index].animations = [ + positionChangeAnimations[index], + colourChangeAnimation + ] + groupAnimations[index].duration = animationDuration + groupAnimations[index].timingFunction = CAMediaTimingFunction(name: .easeIn) + groupAnimations[index].timeOffset = i * 1 / 3 + groupAnimations[index].repeatCount = .infinity + groupAnimations[index].autoreverses = true + } + } + +} diff --git a/Example/WebimClientLibrary/Utilities/UIAlertHandler.swift b/Example/WebimClientLibrary/Utilities/UIAlertHandler.swift new file mode 100644 index 00000000..14f8fed4 --- /dev/null +++ b/Example/WebimClientLibrary/Utilities/UIAlertHandler.swift @@ -0,0 +1,181 @@ +// +// UIAlertHandler.swift +// WebimClientLibrary_Tests +// +// Created by Nikita Lazarev-Zubov on 13.02.18. +// Copyright © 2018 Webim. All rights reserved. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. +// + +import UIKit +import WebimClientLibrary + +final class UIAlertHandler { + + // MARK: - Properties + private weak var delegate: UIViewController? + + // MARK: - Initializer + init(delegate: UIViewController) { + self.delegate = delegate + } + + // MARK: - Methods + func showDialog( + withMessage message: String, + title: String?, + buttonTitle: String = AlertDialog.buttonTitle.rawValue.localized, + buttonStyle: UIAlertAction.Style = .cancel, + action: (() -> ())? = nil + ) { + let alertController = UIAlertController( + title: title, + message: message, + preferredStyle: .alert + ) + + let alertAction = UIAlertAction( + title: buttonTitle, + style: buttonStyle, + handler: { _ in + action?() + }) + + alertController.addAction(alertAction) + + delegate?.present(alertController, animated: true) + } + + func showDepartmentListDialog( + withDepartmentList departmentList: [Department], + action: @escaping (String) -> () + ) { + let alertController = UIAlertController( + title: DepartmentListDialog.title.rawValue.localized, + message: nil, + preferredStyle: .actionSheet + ) + + for department in departmentList { + let departmentAction = UIAlertAction( + title: department.getName(), + style: .default, + handler: { _ in + action(department.getKey()) + } + ) + + alertController.addAction(departmentAction) + } + + let alertAction = UIAlertAction( + title: DepartmentListDialog.cancelButtonTitle.rawValue.localized, + style: .cancel) + + alertController.addAction(alertAction) + + delegate?.present(alertController, animated: true) + } + + func showSendFailureDialog( + withMessage message: String, + title: String, + action: (() -> ())? = nil + ) { + showDialog( + withMessage: message, + title: title, + buttonTitle: SendErrorMessage.buttonTitle.rawValue.localized, + action: action + ) + } + + func showChatClosedDialog() { + showDialog( + withMessage: ChatClosedDialog.message.rawValue.localized, + title: nil, + buttonTitle: ChatClosedDialog.buttonTitle.rawValue.localized + ) + } + + func showCreatingSessionFailureDialog(withMessage message: String) { + showDialog( + withMessage: message, + title: SessionCreationErrorDialog.title.rawValue.localized, + buttonTitle: SessionCreationErrorDialog.buttonTitle.rawValue.localized + ) + } + + func showFileLoadingFailureDialog(withError error: Error) { + showDialog( + withMessage: error.localizedDescription, + title: LoadingFileDialog.loadErrorTitle.rawValue.localized, + buttonTitle: LoadingFileDialog.buttonTitle.rawValue.localized + ) + } + + func showFileSavingFailureDialog(withError error: Error) { + showDialog( + withMessage: error.localizedDescription, + title: SavingFileDialog.saveErrorTitle.rawValue.localized, + buttonTitle: SavingFileDialog.buttonTitle.rawValue.localized + ) + } + + func showFileSavingSuccessDialog() { + showDialog( + withMessage: SavingFileDialog.saveSuccessMessage.rawValue.localized, + title: SavingFileDialog.saveSuccessTitle.rawValue.localized, + buttonTitle: SavingFileDialog.buttonTitle.rawValue.localized + ) + } + + func showImageSavingFailureDialog(withError error: NSError) { + showDialog( + withMessage: error.localizedDescription, + title: SavingImageDialog.saveErrorTitle.rawValue.localized, + buttonTitle: SavingImageDialog.buttonTitle.rawValue.localized + ) + } + + func showImageSavingSuccessDialog() { + showDialog( + withMessage: SavingImageDialog.saveSuccessMessage.rawValue.localized, + title: SavingImageDialog.saveSuccessTitle.rawValue.localized, + buttonTitle: SavingImageDialog.buttonTitle.rawValue.localized + ) + } + + func showNoCurrentOperatorDialog() { + showDialog( + withMessage: NoCurrentOperatorErrorMessage.message.rawValue.localized, + title: NoCurrentOperatorErrorMessage.title.rawValue.localized, + buttonTitle: NoCurrentOperatorErrorMessage.buttonTitle.rawValue.localized + ) + } + + func showSettingsAlertDialog(withMessage message: String) { + showDialog( + withMessage: message, + title: SettingsErrorDialog.title.rawValue.localized, + buttonTitle: SettingsErrorDialog.buttonTitle.rawValue.localized + ) + } +} diff --git a/Example/WebimClientLibrary/Utilities/WebimService.swift b/Example/WebimClientLibrary/Utilities/WebimService.swift index efe6195a..cc2e5921 100644 --- a/Example/WebimClientLibrary/Utilities/WebimService.swift +++ b/Example/WebimClientLibrary/Utilities/WebimService.swift @@ -45,7 +45,7 @@ final class WebimService { case crc = "ffadeb6aa3c788200824e311b9aa44cb" } - // MARK: - Properties + // MARK: - Private Properties private weak var fatalErrorHandlerDelegate: FatalErrorHandlerDelegate? private weak var departmentListHandlerDelegate: DepartmentListHandlerDelegate? private var messageStream: MessageStream? @@ -60,9 +60,10 @@ final class WebimService { } // MARK: - Methods - func createSession() { - let deviceToken: String? = UserDefaults.standard.object(forKey: AppDelegate.UserDefaultsKey.deviceToken.rawValue) as? String + let deviceToken: String? = UserDefaults.standard.object( + forKey: AppDelegate.UserDefaultsKey.deviceToken.rawValue + ) as? String var sessionBuilder = Webim.newSessionBuilder() .set(accountName: Settings.shared.accountName) @@ -72,8 +73,7 @@ final class WebimService { .set(remoteNotificationSystem: ((deviceToken != nil) ? .apns : .none)) .set(deviceToken: deviceToken) .set(isVisitorDataClearingEnabled: false) - .set(webimLogger: self, - verbosityLevel: .verbose) + .set(webimLogger: self, verbosityLevel: .verbose) if (Settings.shared.accountName == Settings.DefaultSettings.accountName.rawValue) { sessionBuilder = sessionBuilder.set(visitorFieldsJSONString: "{\"\(VisitorFields.id.rawValue)\":\"\(VisitorFieldsValue.id.rawValue)\",\"\(VisitorFields.name.rawValue)\":\"\(VisitorFieldsValue.name.rawValue)\",\"\(VisitorFields.crc.rawValue)\":\"\(VisitorFieldsValue.crc.rawValue)\"}") // Hardcoded values that work with "demo" account only! @@ -160,6 +160,10 @@ final class WebimService { } } + func set(unreadByVisitorMessageCountChangeListener listener: UnreadByVisitorMessageCountChangeListener) { + webimSession?.getStream().set(unreadByVisitorMessageCountChangeListener: listener) + } + func setMessageStream() { messageStream = webimSession?.getStream() } @@ -188,8 +192,10 @@ final class WebimService { } } - func send(message: String, - completion: (() -> ())? = nil) { + func send( + message: String, + completion: (() -> ())? = nil + ) { do { if messageStream == nil { setMessageStream() @@ -197,11 +203,16 @@ final class WebimService { if messageStream?.getVisitSessionState() == .departmentSelection, let departments = messageStream?.getDepartmentList() { - departmentListHandlerDelegate?.show(departmentList: departments) { [weak self] departmentKey in - self?.startChat(departmentKey: departmentKey, - message: message) - completion?() - } + departmentListHandlerDelegate?.show( + departmentList: departments, + action: { [weak self] departmentKey in + self?.startChat( + departmentKey: departmentKey, + message: message + ) + completion?() + } + ) } else { _ = try messageStream?.send(message: message) // Returned message ID ignored. completion?() @@ -224,29 +235,144 @@ final class WebimService { } } - func send(file data: Data, - fileName: String, - mimeType: String, - completionHandler: SendFileCompletionHandler) { + func send( + file data: Data, + fileName: String, + mimeType: String, + completionHandler: SendFileCompletionHandler + ) { if messageStream == nil { setMessageStream() } if messageStream?.getVisitSessionState() == .departmentSelection, let departments = messageStream?.getDepartmentList() { - departmentListHandlerDelegate?.show(departmentList: departments) { [weak self] departmentKey in - self?.startChat(departmentKey: departmentKey, - message: nil) - self?.sendFile(data: data, - fileName: fileName, - mimeType: mimeType, - completionHandler: completionHandler) + departmentListHandlerDelegate?.show( + departmentList: departments, + action: { [weak self] departmentKey in + self?.startChat( + departmentKey: departmentKey, + message: nil + ) + self?.sendFile( + data: data, + fileName: fileName, + mimeType: mimeType, + completionHandler: completionHandler + ) + } + ) + } else { + sendFile( + data: data, + fileName: fileName, + mimeType: mimeType, + completionHandler: completionHandler + ) + } + } + + func reply( + message: String, + repliedMessage: Message, + completion: (() -> ())? = nil + ) { + do { + if messageStream == nil { + setMessageStream() + } + + if messageStream?.getVisitSessionState() == .departmentSelection, + let departments = messageStream?.getDepartmentList() { + departmentListHandlerDelegate?.show( + departmentList: departments, + action: { [weak self] departmentKey in + self?.startChat( + departmentKey: departmentKey, + message: message + ) + completion?() + } + ) + } else { + _ = try messageStream?.reply(message: message, repliedMessage: repliedMessage) + completion?() + } + + } catch let error as AccessError { + switch error { + case .invalidThread: + // Assuming to check Webim session object lifecycle or re-creating Webim session object. + print("Message replying failed because it was called when session object is invalid.") + case .invalidSession: + // Assuming to check concurrent calls of WebimClientLibrary methods. + print("Message replying failed because it was called from a wrong thread.") } + } catch { + print("Message replying failed with unknown error: \(error.localizedDescription)") + } + } + + func edit( + message: Message, + text: String, + completionHandler: EditMessageCompletionHandler + ) { + if messageStream == nil { + setMessageStream() + } + + if messageStream?.getVisitSessionState() == .departmentSelection, + let departments = messageStream?.getDepartmentList() { + departmentListHandlerDelegate?.show( + departmentList: departments, + action: { [weak self] departmentKey in + self?.startChat( + departmentKey: departmentKey, + message: nil + ) + self?.editMessage( + message: message, + text: text, + completionHandler: completionHandler + ) + } + ) + } else { + editMessage( + message: message, + text: text, + completionHandler: completionHandler + ) + } + } + + func delete( + message: Message, + completionHandler: DeleteMessageCompletionHandler + ) { + if messageStream == nil { + setMessageStream() + } + + if messageStream?.getVisitSessionState() == .departmentSelection, + let departments = messageStream?.getDepartmentList() { + departmentListHandlerDelegate?.show( + departmentList: departments, + action: { [weak self] departmentKey in + self?.startChat(departmentKey: departmentKey) + + self?.deleteMessage( + message: message, + completionHandler: completionHandler + ) + } + ) } else { - sendFile(data: data, - fileName: fileName, - mimeType: mimeType, - completionHandler: completionHandler) + deleteMessage( + message: message, + completionHandler: completionHandler + ) } } @@ -284,17 +410,21 @@ final class WebimService { } } - func rateOperator(withID operatorID: String, - byRating rating: Int, - completionHandler: RateOperatorCompletionHandler?) { + func rateOperator( + withID operatorID: String, + byRating rating: Int, + completionHandler: RateOperatorCompletionHandler? + ) { do { if messageStream == nil { setMessageStream() } - try messageStream?.rateOperatorWith(id: operatorID, - byRating: rating, - completionHandler: completionHandler) + try messageStream?.rateOperatorWith( + id: operatorID, + byRating: rating, + completionHandler: completionHandler + ) } catch let error as AccessError { switch error { case .invalidSession: @@ -326,7 +456,9 @@ final class WebimService { setMessageStream() } - try messageTracker = messageStream?.newMessageTracker(messageListener: messageListener) + try messageTracker = messageStream?.newMessageTracker( + messageListener: messageListener + ) } catch let error as AccessError { switch error { case .invalidSession: @@ -347,8 +479,10 @@ final class WebimService { func getLastMessages(completion: @escaping (_ result: [Message]) -> ()) { do { - try messageTracker?.getLastMessages(byLimit: ChatSettings.messagesPerRequest.rawValue, - completion: completion) + try messageTracker?.getLastMessages( + byLimit: ChatSettings.messagesPerRequest.rawValue, + completion: completion + ) } catch let error as AccessError { switch error { case .invalidSession: @@ -369,8 +503,10 @@ final class WebimService { func getNextMessages(completion: @escaping (_ result: [Message]) -> ()) { do { - try messageTracker?.getNextMessages(byLimit: ChatSettings.messagesPerRequest.rawValue, - completion: completion) + try messageTracker?.getNextMessages( + byLimit: ChatSettings.messagesPerRequest.rawValue, + completion: completion + ) } catch let error as AccessError { switch error { case .invalidSession: @@ -389,17 +525,107 @@ final class WebimService { } } + func setChatRead() { + do { + if messageStream == nil { + setMessageStream() + } + + try messageStream?.setChatRead() + } catch { + print("Read chat failed with unknown error: \(error.localizedDescription)") + } + } + + func getUnreadMessagesByVisitor() -> Int { + if messageStream == nil { + setMessageStream() + } + return messageStream?.getUnreadByVisitorMessageCount() ?? 0 + } + + func set(operatorTypingListener: OperatorTypingListener) { + if messageStream == nil { + setMessageStream() + } + messageStream?.set(operatorTypingListener: operatorTypingListener) + } + + func set(currentOperatorChangeListener: CurrentOperatorChangeListener) { + if messageStream == nil { + setMessageStream() + } + messageStream?.set(currentOperatorChangeListener: currentOperatorChangeListener) + } + + func getCurrentOperator() -> Operator? { + if messageStream == nil { + setMessageStream() + } + return messageStream?.getCurrentOperator() + } + + func getLastRatingOfOperatorWith(id: String) -> Int { + if messageStream == nil { + setMessageStream() + } + return messageStream?.getLastRatingOfOperatorWith(id: id) ?? 0 + } + + func set(chatStateListener: ChatStateListener) { + if messageStream == nil { + setMessageStream() + } + messageStream?.set(chatStateListener: chatStateListener) + } + + func sendKeyboardRequest( + button: KeyboardButton, + message: Message, + completionHandler: SendKeyboardRequestCompletionHandler + ) { + if messageStream == nil { + setMessageStream() + } + + if messageStream?.getVisitSessionState() == .departmentSelection, + let departments = messageStream?.getDepartmentList() { + departmentListHandlerDelegate?.show( + departmentList: departments, + action: { [weak self] departmentKey in + self?.startChat(departmentKey: departmentKey) + + self?.sendKeyboard( + button: button, + message: message, + completionHandler: completionHandler + ) + } + ) + } else { + sendKeyboard( + button: button, + message: message, + completionHandler: completionHandler + ) + } + } + // MARK: Private methods - private func startChat(departmentKey: String? = nil, - message: String? = nil) { + private func startChat( + departmentKey: String? = nil, + message: String? = nil + ) { do { if messageStream == nil { setMessageStream() } - try messageStream?.startChat(departmentKey: departmentKey, - firstQuestion: message) + try messageStream?.startChat( + departmentKey: departmentKey, + firstQuestion: message + ) } catch let error as AccessError { switch error { case .invalidSession: @@ -419,44 +645,121 @@ final class WebimService { } } - func setChatRead() { + private func sendFile( + data: Data, + fileName: String, + mimeType: String, + completionHandler: SendFileCompletionHandler + ) { do { - if messageStream == nil { - setMessageStream() + _ = try messageStream?.send( + file: data, + filename: fileName, + mimeType: mimeType, + completionHandler: completionHandler + ) // Returned message ID ignored. + } catch let error as AccessError { + switch error { + case .invalidSession: + // Assuming to check Webim session object lifecycle or re-creating Webim session object. + print("Message sending failed because it was called when session object is invalid.") + + break + case .invalidThread: + // Assuming to check concurrent calls of WebimClientLibrary methods. + print("Message sending failed because it was called from a wrong thread.") + + break } - - try messageStream?.setChatRead() } catch { - print("Read chat failed with unknown error: \(error.localizedDescription)") + print("Message status sending failed with unknown error: \(error.localizedDescription)") } } - private func sendFile(data: Data, - fileName: String, - mimeType: String, - completionHandler: SendFileCompletionHandler) { + private func editMessage( + message: Message, + text: String, + completionHandler: EditMessageCompletionHandler + ) { do { - _ = try messageStream?.send(file: data, - filename: fileName, - mimeType: mimeType, - completionHandler: completionHandler) // Returned message ID ignored. + _ = try messageStream?.edit( + message: message, + text: text, + completionHandler: completionHandler + ) } catch let error as AccessError { switch error { case .invalidSession: // Assuming to check Webim session object lifecycle or re-creating Webim session object. - print("Message sending failed because it was called when session object is invalid.") + print("Message editing failed because it was called when session object is invalid.") break case .invalidThread: // Assuming to check concurrent calls of WebimClientLibrary methods. - print("Message sending failed because it was called from a wrong thread.") + print("Message editing failed because it was called from a wrong thread.") break } } catch { - print("Message status sending failed with unknown error: \(error.localizedDescription)") + print("Message status editing failed with unknown error: \(error.localizedDescription)") + } + } + + private func deleteMessage( + message: Message, + completionHandler: DeleteMessageCompletionHandler + ){ + do { + _ = try messageStream?.delete( + message: message, + completionHandler: completionHandler + ) + } catch let error as AccessError { + switch error { + case .invalidSession: + // Assuming to check Webim session object lifecycle or re-creating Webim session object. + print("Message deleting failed because it was called when session object is invalid.") + + break + case .invalidThread: + // Assuming to check concurrent calls of WebimClientLibrary methods. + print("Message deleting failed because it was called from a wrong thread.") + + break + } + } catch { + print("Message status deleting failed with unknown error: \(error.localizedDescription)") } } + + private func sendKeyboard( + button: KeyboardButton, + message: Message, + completionHandler: SendKeyboardRequestCompletionHandler + ) { + do { + _ = try messageStream?.sendKeyboardRequest( + button: button, + message: message, + completionHandler: completionHandler + ) + } catch let error as AccessError { + switch error { + case .invalidSession: + // Assuming to check Webim session object lifecycle or re-creating Webim session object. + print("Sending keyboard request failed because it was called when session object is invalid.") + + break + case .invalidThread: + // Assuming to check concurrent calls of WebimClientLibrary methods. + print("Sending keyboard request failed because it was called from a wrong thread.") + + break + } + } catch { + print("Sending keyboard requestg failed with unknown error: \(error.localizedDescription)") + } + } } @@ -519,7 +822,9 @@ protocol FatalErrorHandlerDelegate: AnyObject { protocol DepartmentListHandlerDelegate: AnyObject { // MARK: - Methods - func show(departmentList: [Department], - action: @escaping (String) -> ()) + func show( + departmentList: [Department], + action: @escaping (String) -> () + ) } diff --git a/Example/WebimClientLibrary/en.lproj/LaunchScreen.storyboard b/Example/WebimClientLibrary/en.lproj/LaunchScreen.storyboard new file mode 100644 index 00000000..d5feaa7f --- /dev/null +++ b/Example/WebimClientLibrary/en.lproj/LaunchScreen.storyboard @@ -0,0 +1,68 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Example/WebimClientLibrary/en.lproj/LaunchScreenController.storyboard b/Example/WebimClientLibrary/en.lproj/LaunchScreenController.storyboard new file mode 100644 index 00000000..35daa6f2 --- /dev/null +++ b/Example/WebimClientLibrary/en.lproj/LaunchScreenController.storyboard @@ -0,0 +1,73 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Example/WebimClientLibrary/en.lproj/Main.strings b/Example/WebimClientLibrary/en.lproj/Main.strings new file mode 100644 index 00000000..ff8b1d99 --- /dev/null +++ b/Example/WebimClientLibrary/en.lproj/Main.strings @@ -0,0 +1,96 @@ + +/* Class = "UILabel"; text = "If you are registered in Webim service you can use your own account name and location."; ObjectID = "4zV-yL-SPS"; */ +"4zV-yL-SPS.text" = "If you are registered in Webim service you can use your own account name and location."; + +/* Class = "UITextField"; accessibilityLabel = "Page title text field"; ObjectID = "6Zu-48-Ouw"; */ +"6Zu-48-Ouw.accessibilityLabel" = "Page title text field"; + +/* Class = "UITextField"; text = "iOS demo app"; ObjectID = "6Zu-48-Ouw"; */ +"6Zu-48-Ouw.text" = "iOS demo app"; + +/* Class = "UIButton"; accessibilityHint = "Saves current settings and shows start screen."; ObjectID = "QrB-ab-D1k"; */ +"QrB-ab-D1k.accessibilityHint" = "Saves current settings and shows start screen."; + +/* Class = "UIButton"; accessibilityLabel = "Save"; ObjectID = "QrB-ab-D1k"; */ +"QrB-ab-D1k.accessibilityLabel" = "Save"; + +/* Class = "UIButton"; normalTitle = "Save"; ObjectID = "QrB-ab-D1k"; */ +"QrB-ab-D1k.normalTitle" = "Save"; + +/* Class = "UILabel"; text = "Location*"; ObjectID = "Urf-Fq-NZO"; */ +"Urf-Fq-NZO.text" = "Location*"; + +/* Class = "UITextField"; accessibilityLabel = "Location text field"; ObjectID = "ViG-wh-bJl"; */ +"ViG-wh-bJl.accessibilityLabel" = "Location text field"; + +/* Class = "UITextField"; placeholder = "mobile"; ObjectID = "ViG-wh-bJl"; */ +"ViG-wh-bJl.placeholder" = "mobile"; + +/* Class = "UITextField"; text = "mobile"; ObjectID = "ViG-wh-bJl"; */ +"ViG-wh-bJl.text" = "mobile"; + +/* Class = "UILabel"; text = "* Account name can't be empty."; ObjectID = "Wgr-pM-cdE"; */ +"Wgr-pM-cdE.text" = "* This field can't be empty"; + +/* Class = "UIBarButtonItem"; title = "Back"; ObjectID = "akL-LD-AeE"; */ +"akL-LD-AeE.title" = "Back"; + +/* Class = "UITextField"; accessibilityLabel = "Account name text field"; ObjectID = "bC4-gI-YBa"; */ +"bC4-gI-YBa.accessibilityLabel" = "Account name text field"; + +/* Class = "UITextField"; placeholder = "wmtest"; ObjectID = "bC4-gI-YBa"; */ +"bC4-gI-YBa.placeholder" = "wmtest"; + +/* Class = "UITextField"; text = "wmtest"; ObjectID = "bC4-gI-YBa"; */ +"bC4-gI-YBa.text" = "wmtest"; + +/* Class = "UITextView"; accessibilityLabel = "Greeting words"; ObjectID = "bPt-yP-yHN"; */ +"bPt-yP-yHN.accessibilityLabel" = "Greeting words"; + +/* Class = "UITextView"; text = "To start a chat tap on the button below.\n\nOperator can answer to your chat at:\nhttps://demo.webim.ru/\nLogin: o@webim.ru\nPassword: password\n\nThis app source code can be found at:\nhttps://github.com/webim/webim-client-sdk-ios"; ObjectID = "bPt-yP-yHN"; */ +"bPt-yP-yHN.text" = "To start a chat tap on the button below.\n\nOperator can answer to your chat at:\nhttps://demo.webim.ru/\nLogin: o@webim.ru\nPassword: password\n\nThis app source code can be found at:\nhttps://github.com/webim/webim-client-sdk-ios"; + +/* Class = "UILabel"; text = "99"; ObjectID = "d8r-qX-LmS"; */ +"d8r-qX-LmS.text" = "99"; + +/* Class = "UIButton"; accessibilityHint = "Starts chat."; ObjectID = "edo-SU-HN8"; */ +"edo-SU-HN8.accessibilityHint" = "Starts chat."; + +/* Class = "UIButton"; accessibilityLabel = "Start chat"; ObjectID = "edo-SU-HN8"; */ +"edo-SU-HN8.accessibilityLabel" = "Start chat"; + +/* Class = "UIButton"; normalTitle = "Start chat"; ObjectID = "edo-SU-HN8"; */ +"edo-SU-HN8.normalTitle" = "Start chat"; + +/* Class = "UILabel"; accessibilityLabel = "Greeting title"; ObjectID = "f2l-5B-zsO"; */ +"f2l-5B-zsO.accessibilityLabel" = "Greeting title"; + +/* Class = "UILabel"; text = "Welcome to the WebimClientLibrary demo app!"; ObjectID = "f2l-5B-zsO"; */ +"f2l-5B-zsO.text" = "Welcome to the WebimClientLibrary demo app!"; + +/* Class = "UIImageView"; accessibilityLabel = "Webim logo"; ObjectID = "fbO-33-eR3"; */ +"fbO-33-eR3.accessibilityLabel" = "Webim logo"; + +/* Class = "UILabel"; accessibilityLabel = "Account section title"; ObjectID = "iei-A8-OZ9"; */ +"iei-A8-OZ9.accessibilityLabel" = "Account section title"; + +/* Class = "UILabel"; text = "Account"; ObjectID = "iei-A8-OZ9"; */ +"iei-A8-OZ9.text" = "Account"; + +/* Class = "UILabel"; text = "* Location can't be empty."; ObjectID = "kOW-ZJ-ash"; */ +"kOW-ZJ-ash.text" = "* This field can't be empty"; + +/* Class = "UIButton"; accessibilityHint = "Shows settings."; ObjectID = "rMR-x2-dzu"; */ +"rMR-x2-dzu.accessibilityHint" = "Shows settings."; + +/* Class = "UIButton"; accessibilityLabel = "Settings"; ObjectID = "rMR-x2-dzu"; */ +"rMR-x2-dzu.accessibilityLabel" = "Settings"; + +/* Class = "UIButton"; normalTitle = "Settings"; ObjectID = "rMR-x2-dzu"; */ +"rMR-x2-dzu.normalTitle" = "Settings"; + +/* Class = "UILabel"; text = "Account name*"; ObjectID = "rPK-KX-bO9"; */ +"rPK-KX-bO9.text" = "Account name*"; + +/* Class = "UILabel"; text = "Page title"; ObjectID = "uW6-TG-iNP"; */ +"uW6-TG-iNP.text" = "Page title"; diff --git a/Example/WebimClientLibrary/ru-RU.lproj/InfoPlist.strings b/Example/WebimClientLibrary/ru-RU.lproj/InfoPlist.strings new file mode 100644 index 00000000..388a3857 --- /dev/null +++ b/Example/WebimClientLibrary/ru-RU.lproj/InfoPlist.strings @@ -0,0 +1,29 @@ +/* + InfoPlist.strings + WebimClientLibrary + + Created by Eugene Ilyin on 04.11.2019. + Copyright © 2019 Webim. All rights reserved. + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in all + copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + SOFTWARE. +*/ + +NSCameraUsageDescription = "Это приложение хочет делать снимки"; +NSPhotoLibraryAddUsageDescription = "Это приложение хочет сохранять изображения в Вашу фотогалерею"; +NSPhotoLibraryUsageDescription = "Это приложение хочет иметь доступ к изображениям в Вашей фотогалерее"; diff --git a/Example/WebimClientLibrary/ru-RU.lproj/Localizable.strings b/Example/WebimClientLibrary/ru-RU.lproj/Localizable.strings index ba709981..51fe37d3 100644 --- a/Example/WebimClientLibrary/ru-RU.lproj/Localizable.strings +++ b/Example/WebimClientLibrary/ru-RU.lproj/Localizable.strings @@ -34,12 +34,6 @@ // MARK: - StringConstants.swift -// EMPTY_TABLE_VIEW_TEXT -"EmptyChat" = "Отправьте первое сообщение, чтобы начать чат."; - -// REFRESH_CONTROL_TEXT -"LoadingMessages" = "Сообщения загружаются..."; - // Avatar "SenderAvatarImage" = "Изображение аватара отправителя сообщения"; "ShowsRatingDialog" = "Показывает окно оценки оператора."; @@ -64,33 +58,67 @@ "ClosesDialog" = "Закрывает окно."; // FileMessage -"FileUnavailable" = "Файл недоступен."; +"FileUnavailable" = "Файл недоступен"; // LeftButton "ChooseFile" = "Выбрать файл для отправки"; "ShowsImagePicker" = "Показывает окно выбора изображения для отправки."; -// RateOperatorErrorMessage -"OperatorRatingFailed" = "Неудачная попытка оценить оператора"; +// NoCurrentOperatorErrorMessage +"NoCurrentOperator" = "Нет доступных операторов"; +"OK" = "OК"; +"NoAvailableOperator" = "Нет оператора для выставления оценки"; + +// AlertDialog +"RateSuccessTitle" = "Спасибо!"; +"RateSuccessMessage" = "Вы помогаете нам становиться лучше"; "OK" = "OK"; "ClosesRateOperatorError" = "Закрывает окно."; -"RateOperatorErrorMessage" = "Произошла неизвестная ошибка."; -// RatingDialog -"Rate" = "Оценить"; -"Cancel" = "Отмена"; -"RatesOperator" = "Оценивает оператора."; -"ClosesRatingDialog" = "Закрывает диалог."; +// RateOperatorErrorMessage +"OperatorRatingFailed" = "Неудачная попытка оценить оператора"; +"RateOperatorNoChat" = "Этот чат не существует"; +"RateOperatorWrongID" = "Этот оператор не находится в данном чате"; +"RateOperatorLongNote" = "Комментарий к оценке слишком длинный"; // SendFileErrorMessage "FileSendingFailed" = "Неудачная попытка отправки файла"; "ClosesSendFileError" = "Закрывает окно."; -"FileTooLarge" = "Размер файла превышен."; -"FileTypeNotSupported" = "Данный тип файла не поддерживается."; -"FileNotFound" = "Отправляемый файл не был получен."; +"FileTooLarge" = "Размер файла превышен"; +"FileTypeNotSupported" = "Данный тип файла не поддерживается"; +"FileNotFound" = "Отправляемый файл не был получен"; "FileSendingUnknownError" = "Неизвестная ошибка при отправке файла"; "FileSengingUnauthorized" = "Не удалось загрузить файл: посетитель не авторизован"; +// SendMessageErrorMessage +"SendMessageFailed" = "Неудачная попытка отправки сообщения"; +"MessageIsEmpty" = "Отправляемое сообщение является пустым"; +"MaxMessageLengthExceeded" = "Превышена максимальная длина сообщения"; + +// EditMessageErrorMessage +"EditMessageFailed" = "Неудачная попытка редактирования сообщения"; +"EditMessageUnknownError" = "Неизвестная ошибка при редактировании сообщения"; +"EditingMessagesIsTurnedOffOnTheServer" = "Редактирование сообщений отключено на сервере"; +"EditingMessageIsEmpty" = "Редактируемое сообщение является пустым"; +"MessageNotOwnedByVisitor" = "Сообщение не принадлежит посетителю"; +"MaxMessageLengthExceeded" = "Превышена максимальная длина сообщения"; +"WrongMessageKind" = "Неправильный тип сообщения (не текст)"; + +// DeleteMessageErrorMessage +"DeleteMessageFailed" = "Неудачная попытка удаления сообщения"; +"DeleteMessageUnknownError" = "Неизвестная ошибка при удалении сообщения"; +"DeletingMessagesIsTurnedOffOnTheServer" = "Удаление сообщений отключено на сервере"; +"MessageNotOwnedByVisitor" = "Сообщение не принадлежит посетителю"; +"MessageNotFound" = "Сообщение не найдено"; + +// SendKeyboardRequestErrorMessage +"SendKeyboardRequestFailed" = "Неудачная попытка отправки запроса клавиатуры"; +"SendKeyboardRequestUnknownError" = "Неизвестная ошибка при отправке запроса клавиатуры"; +"ChatDoesNotExist" = "Чат не существует"; +"WrongButtonID" = "Неправильный ID кнопки в запросе"; +"WrongMessageID" = "Неправильный ID сообщения в запросе"; +"ResponseCannotBeCreated" = "Ответ не может быть создан для данного запроса"; + // SessionCreationErrorDialog "ClosesSessionError" = "Закрывает окно."; "SessionCreationFailed" = "Неудачная попытка создания сессии"; @@ -100,11 +128,86 @@ // SettingsErrorDialog "ClosesSettingsError" = "Закрывает окно."; "InvalidSettings" = "Неверные данные учетной записи"; -"AccountNameEmpty" = "Название учетной записи не может быть пустым."; -"LocationEmpty" = "Значение размещения не может быть пустым."; +"AccountNameEmpty" = "Название учетной записи не может быть пустым"; +"LocationEmpty" = "Значение размещения не может быть пустым"; // ShowFileDialog -"ImageFormatInvalid" = "Неверный формат изображения."; -"ImageLinkInvalid" = "Ссылка недействительна."; -"PreviewUnavailable" = "Просмотр недоступен."; +"ImageFormatInvalid" = "Неверный формат изображения"; +"ImageLinkInvalid" = "Ссылка недействительна"; +"PreviewUnavailable" = "Просмотр недоступен"; "ClosesFilePreview" = "Закрывает окно."; + +// StartView +"WelcomeTitle" = "Добро пожаловать в демо приложение WebimClientLibrary!"; +"WelcomeText" = "Чтобы начать чат, нажмите кнопку ниже.\n\nОператор может ответить в Ваш чат по ссылке:\nhttps://demo.webim.ru/\nЛогин: o@webim.ru\nПароль: password\n\nИсходный код этого приложения может быть найден по ссылке:\nhttps://github.com/webim/webim-client-sdk-ios"; +"StartChat" = "Начать чат"; +"Settings" = "Настройки"; + +// TableView +"EmptyChat" = "Отправьте первое сообщение, чтобы начать чат."; + +// ChatTableView +"LoadMessages" = "Сообщения загружаются..."; + +// ChatView +"HardcodedVisitorMessageName" = "Вы"; +"EditMessage" = "Редактирование"; +"InputPlaceholderText" = "Сообщение"; +"AccessibilityTextWebimLogo" = "Логотип Вебим"; + +// FileView +"LoadingFile" = "Загружаем файл..."; + +// PopupActions +"Reply" = "Ответить"; +"Copy" = "Копировать"; +"Edit" = "Изменить"; +"Delete" = "Удалить"; + +// RatingDialogView +"RateOperator" = "Пожалуйста, оцените работу оператора"; + +// SavingImageDialog +"OK" = "ОК"; +"SaveError" = "Ошибка сохранения"; +"Saved" = "Сохранено!"; +"ImageSaved" = "Изображение было сохранено в Вашу фотогалерею"; + +// SavingFileDialog +"OK" = "ОК"; +"SaveError" = "Ошибка сохранения"; +"Saved" = "Сохранено!"; +"FileSaved" = "Файл было сохранено на устройстве в приложении Файлы"; + +// LoadingFileDialog +"OK" = "ОК"; +"LoadError" = "Ошибка загрузки"; + +// OperatorStatus +"NoOperator" = "Webim демо-чат"; +"OperatorsOffline" = "Нет оператора"; +"Online" = "В сети"; +"IsTyping" = "печатает"; + +// FilePicker +"Camera" = "Камера"; +"PhotoLibrary" = "Фотогалерея"; +"Cancel" = "Отменить"; +"File" = "Документ"; +"CameraIsNotAvailable" = "Камера недоступна"; +"OK" = "ОК"; +"CameraAccessTitle" = "Необходим доступ к камере"; +"CameraAccessMessage" = "Требуется доступ к камере для полноценного использования приложения"; +"CameraAccessOpenSettings" = "Открыть настройки приложения"; +"CameraAccessCancel" = "Отменить"; + +// MessageStatus +"EditedMessage" = "изменено"; + +// FlexibleCellDate +"DateToday" = "Сегодня"; +"DateYesterday" = "Вчера"; + +// UploadingFileDescription +"UploadingFile" = "Отправка файла"; +"Counting" = "Подсчет"; diff --git a/Example/WebimClientLibrary/ru-RU.lproj/Main.strings b/Example/WebimClientLibrary/ru-RU.lproj/Main.strings index 6bfda1dc..bbbf03b9 100644 --- a/Example/WebimClientLibrary/ru-RU.lproj/Main.strings +++ b/Example/WebimClientLibrary/ru-RU.lproj/Main.strings @@ -1,15 +1,15 @@ -/* Class = "UILabel"; text = "Account name *"; ObjectID = "7Wr-eX-iv8"; */ -"7Wr-eX-iv8.text" = "Название аккаунта *"; +/* Class = "UILabel"; text = "If you are registered in Webim service you can use your own account name and location."; ObjectID = "4zV-yL-SPS"; */ +"4zV-yL-SPS.text" = "Если Вы зарегистрированы в сервисе Webim, тогда можете использовать собственное название аккаунта и размещение."; -/* Class = "UITableViewSection"; footerTitle = "If you are registered in Webim service you can use your own account name and location."; ObjectID = "FX1-M6-A8v"; */ -"FX1-M6-A8v.footerTitle" = "Если вы зарегистрированы в сервисе \"Webim\", вы можете использовать свою учетную запись."; +/* Class = "UITextField"; accessibilityLabel = "Page title text field"; ObjectID = "6Zu-48-Ouw"; */ +"6Zu-48-Ouw.accessibilityLabel" = "Текстовое поле названия экрана чата"; -/* Class = "UITableViewSection"; headerTitle = "ACCOUNT"; ObjectID = "FX1-M6-A8v"; */ -"FX1-M6-A8v.headerTitle" = "УЧЕТНАЯ ЗАПИСЬ"; +/* Class = "UITextField"; text = "iOS demo app"; ObjectID = "6Zu-48-Ouw"; */ +"6Zu-48-Ouw.text" = "iOS demo app"; /* Class = "UIButton"; accessibilityHint = "Saves current settings and shows start screen."; ObjectID = "QrB-ab-D1k"; */ -"QrB-ab-D1k.accessibilityHint" = "Сохраняет текущие настройки и возвращается к начальной странице."; +"QrB-ab-D1k.accessibilityHint" = "Сохраняет текущие настройки и возвращает к начальной странице."; /* Class = "UIButton"; accessibilityLabel = "Save"; ObjectID = "QrB-ab-D1k"; */ "QrB-ab-D1k.accessibilityLabel" = "Сохранить"; @@ -17,30 +17,41 @@ /* Class = "UIButton"; normalTitle = "Save"; ObjectID = "QrB-ab-D1k"; */ "QrB-ab-D1k.normalTitle" = "Сохранить"; -/* Class = "UILabel"; accessibilityLabel = "Classic color theme"; ObjectID = "Xp5-8q-gOf"; */ -"Xp5-8q-gOf.accessibilityLabel" = "Стандартная цветовая схема"; +/* Class = "UILabel"; text = "Location*"; ObjectID = "Urf-Fq-NZO"; */ +"Urf-Fq-NZO.text" = "Размещение*"; -/* Class = "UILabel"; text = "Classic"; ObjectID = "Xp5-8q-gOf"; */ -"Xp5-8q-gOf.text" = "Стандартная"; +/* Class = "UITextField"; accessibilityLabel = "Location text field"; ObjectID = "ViG-wh-bJl"; */ +"ViG-wh-bJl.accessibilityLabel" = "Текстовое поле размещения"; -/* Class = "UITextField"; accessibilityLabel = "Page title field"; ObjectID = "YbA-Bc-Fio"; */ -"YbA-Bc-Fio.accessibilityLabel" = "Поле названия страницы чата"; +/* Class = "UITextField"; placeholder = "mobile"; ObjectID = "ViG-wh-bJl"; */ +"ViG-wh-bJl.placeholder" = "mobile"; -/* Class = "UITextField"; text = "iOS Demo App"; ObjectID = "YbA-Bc-Fio"; */ -"YbA-Bc-Fio.text" = "iOS Demo App"; +/* Class = "UITextField"; text = "mobile"; ObjectID = "ViG-wh-bJl"; */ +"ViG-wh-bJl.text" = "mobile"; -/* Class = "UITextField"; accessibilityLabel = "Location name field"; ObjectID = "ZbE-AG-UMG"; */ -"ZbE-AG-UMG.accessibilityLabel" = "Поле названия размещения (локации)"; +/* Class = "UILabel"; text = "* Account name can't be empty."; ObjectID = "Wgr-pM-cdE"; */ +"Wgr-pM-cdE.text" = "* Это поле не может быть пустым"; -/* Class = "UITextField"; text = "mobile"; ObjectID = "ZbE-AG-UMG"; */ -"ZbE-AG-UMG.text" = "mobile"; +/* Class = "UIBarButtonItem"; title = "Back"; ObjectID = "akL-LD-AeE"; */ +"akL-LD-AeE.title" = "Назад"; + +/* Class = "UITextField"; accessibilityLabel = "Account name text field"; ObjectID = "bC4-gI-YBa"; */ +"bC4-gI-YBa.accessibilityLabel" = "Текстовое поле названия учетной записи"; + +/* Class = "UITextField"; placeholder = "wmtest"; ObjectID = "bC4-gI-YBa"; */ +"bC4-gI-YBa.placeholder" = "wmtest"; + +/* Class = "UITextField"; text = "wmtest"; ObjectID = "bC4-gI-YBa"; */ +"bC4-gI-YBa.text" = "wmtest"; /* Class = "UITextView"; accessibilityLabel = "Greeting words"; ObjectID = "bPt-yP-yHN"; */ "bPt-yP-yHN.accessibilityLabel" = "Приветственные слова"; -// Xcode does not localize UITextView text automatically. -/* Class = "UITextView"; text = "Welcome to the WebimClientLibrary demo app!\n\nTo start a chat tap on the button below.\n\nOperator can answer to your chat at:\nhttps://demo.webim.ru/\nLogin: o@webim.ru\nPassword: password\n\nThis app source code can be found at:\nhttps://github.com/webim/webim-client-sdk-ios"; ObjectID = "bPt-yP-yHN"; */ -"Welcome to the WebimClientLibrary demo app!\n\nTo start a chat tap on the button below.\n\nOperator can answer to your chat at:\nhttps://demo.webim.ru/\nLogin: o@webim.ru\nPassword: password\n\nThis app source code can be found at:\nhttps://github.com/webim/webim-client-sdk-ios" = "Добро пожаловать в демо-приложение WebimClientLibrary!\n\nЧтобы начать чат, нажмите кнопку внизу.\n\nОператор может ответить на обращение здесь:\nhttps://demo.webim.ru/\nЛогин: o@webim.ru\nПароль: password\n\nИсходный код данного приложения может быть найден здесь:\nhttps://github.com/webim/webim-client-sdk-ios"; +/* Class = "UITextView"; text = "To start a chat tap on the button below.\n\nOperator can answer to your chat at:\nhttps://demo.webim.ru/\nLogin: o@webim.ru\nPassword: password\n\nThis app source code can be found at:\nhttps://github.com/webim/webim-client-sdk-ios"; ObjectID = "bPt-yP-yHN"; */ +"bPt-yP-yHN.text" = "Чтобы начать чат, нажмите кнопку внизу.\n\nОператор может ответить на обращение здесь:\nhttps://demo.webim.ru/\nЛогин: o@webim.ru\nПароль: password\n\nИсходный код данного приложения может быть найден здесь:\nhttps://github.com/webim/webim-client-sdk-ios"; + +/* Class = "UILabel"; text = "99"; ObjectID = "d8r-qX-LmS"; */ +"d8r-qX-LmS.text" = "99"; /* Class = "UIButton"; accessibilityHint = "Starts chat."; ObjectID = "edo-SU-HN8"; */ "edo-SU-HN8.accessibilityHint" = "Начинает чат."; @@ -51,20 +62,23 @@ /* Class = "UIButton"; normalTitle = "Start chat"; ObjectID = "edo-SU-HN8"; */ "edo-SU-HN8.normalTitle" = "Начать чат"; -/* Class = "UITableViewSection"; headerTitle = "COLOR THEME"; ObjectID = "fJX-AG-mdU"; */ -"fJX-AG-mdU.headerTitle" = "ЦВЕТОВАЯ СХЕМА"; +/* Class = "UILabel"; accessibilityLabel = "Greeting title"; ObjectID = "f2l-5B-zsO"; */ +"f2l-5B-zsO.accessibilityLabel" = "Приветственное заглавие"; -/* Class = "UILabel"; text = "Page title"; ObjectID = "kTz-jD-ziQ"; */ -"kTz-jD-ziQ.text" = "Название экрана чата"; +/* Class = "UILabel"; text = "Welcome to the WebimClientLibrary demo app!"; ObjectID = "f2l-5B-zsO"; */ +"f2l-5B-zsO.text" = "Добро пожаловать в демо-приложение WebimClientLibrary!"; -/* Class = "UITextField"; accessibilityLabel = "Account name field"; ObjectID = "qLZ-bH-B74"; */ -"qLZ-bH-B74.accessibilityLabel" = "Поле названия учетной записи"; +/* Class = "UIImageView"; accessibilityLabel = "Webim logo"; ObjectID = "fbO-33-eR3"; */ +"fbO-33-eR3.accessibilityLabel" = "Лого Вебим"; -/* Class = "UITextField"; text = "demo"; ObjectID = "qLZ-bH-B74"; */ -"qLZ-bH-B74.text" = "demo"; +/* Class = "UILabel"; accessibilityLabel = "Account section title"; ObjectID = "iei-A8-OZ9"; */ +"iei-A8-OZ9.accessibilityLabel" = "Заглавие секции учетной записи"; -/* Class = "UILabel"; text = "Location *"; ObjectID = "qQH-qh-OFD"; */ -"qQH-qh-OFD.text" = "Размещение *"; +/* Class = "UILabel"; text = "Account"; ObjectID = "iei-A8-OZ9"; */ +"iei-A8-OZ9.text" = "Учетная запись"; + +/* Class = "UILabel"; text = "* Location can't be empty."; ObjectID = "kOW-ZJ-ash"; */ +"kOW-ZJ-ash.text" = "* Это поле не может быть пустым"; /* Class = "UIButton"; accessibilityHint = "Shows settings."; ObjectID = "rMR-x2-dzu"; */ "rMR-x2-dzu.accessibilityHint" = "Показывает настройки."; @@ -75,14 +89,8 @@ /* Class = "UIButton"; normalTitle = "Settings"; ObjectID = "rMR-x2-dzu"; */ "rMR-x2-dzu.normalTitle" = "Настройки"; -/* Class = "UILabel"; accessibilityLabel = "Dark color theme"; ObjectID = "slT-Ti-eBV"; */ -"slT-Ti-eBV.accessibilityLabel" = "Темная цветовая СХЕМА"; - -/* Class = "UILabel"; text = "Dark"; ObjectID = "slT-Ti-eBV"; */ -"slT-Ti-eBV.text" = "Темная"; - -/* Class = "UILabel"; text = "* Account name can't be empty."; ObjectID = "5Lf-Fb-XN7"; */ -"5Lf-Fb-XN7.text" = "* Название учетной записи не может быть пустым."; +/* Class = "UILabel"; text = "Account name*"; ObjectID = "rPK-KX-bO9"; */ +"rPK-KX-bO9.text" = "Название аккаунта*"; -/* Class = "UILabel"; text = "* Location can't be empty."; ObjectID = "6r5-5x-xIB"; */ -"6r5-5x-xIB.text" = "* Размещение (локация) не может быть пустым."; +/* Class = "UILabel"; text = "Page title"; ObjectID = "uW6-TG-iNP"; */ +"uW6-TG-iNP.text" = "Название экрана чата"; diff --git a/Example/WebimClientLibrary/ru-RU.lproj/RatingViewController.strings b/Example/WebimClientLibrary/ru-RU.lproj/RatingViewController.strings deleted file mode 100644 index 6f91939e..00000000 --- a/Example/WebimClientLibrary/ru-RU.lproj/RatingViewController.strings +++ /dev/null @@ -1,9 +0,0 @@ - -/* Class = "UIView"; accessibilityHint = "Устанавливает оценку оператора числом от одного до пяти."; ObjectID = "TCC-HX-zVh"; */ -"TCC-HX-zVh.accessibilityHint" = "Устанавливает оценку оператора числом от одного до пяти."; - -/* Class = "UIView"; accessibilityLabel = "Оценка"; ObjectID = "TCC-HX-zVh"; */ -"TCC-HX-zVh.accessibilityLabel" = "Оценка"; - -/* Class = "UILabel"; text = "Оценить оператора"; ObjectID = "XLW-Lu-VBj"; */ -"XLW-Lu-VBj.text" = "Оценить оператора"; diff --git a/LICENSE b/LICENSE index b116da07..e35565d9 100644 --- a/LICENSE +++ b/LICENSE @@ -1,4 +1,4 @@ -Copyright (c) 2017-2019 Webim +Copyright (c) 2017-2020 Webim Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/README.md b/README.md index eecf742f..4d8ce01d 100644 --- a/README.md +++ b/README.md @@ -16,7 +16,7 @@ This library provides [_Webim SDK_ for _iOS_](https://webim.ru/integration/mobil Add following line for your target in your **Podfile**: ``` -pod 'WebimClientLibrary', :git => 'https://github.com/webim/webim-client-sdk-ios.git', :branch => 'master', :tag => '3.32.1' +pod 'WebimClientLibrary', :git => 'https://github.com/webim/webim-client-sdk-ios.git', :branch => 'master', :tag => '3.33.0' ``` `use_frameworks!` must be specified. @@ -24,7 +24,7 @@ pod 'WebimClientLibrary', :git => 'https://github.com/webim/webim-client-sdk-ios Add following line to your **Cartfile**: ``` -github "webim/webim-client-sdk-ios" ~> 3.32.1 +github "webim/webim-client-sdk-ios" ~> 3.33.0 ``` ### Additional notes @@ -38,7 +38,11 @@ Trying to integrate _WebimClientLibrary_ into your _Objective-C_ code? Try out o Previous _Objective-C_ version (version numbers 2.x.x) can be reached from **version2** branch. ## Release notes -* History bug fixed. +* New demo application design. +* Method `isEdited()` added. +* `getNextMessages` bug fixed. +* Sticker support added. +* Method `changed(operator:to:)` bug fixed. ## Example diff --git a/WebimClientLibrary.podspec b/WebimClientLibrary.podspec index 2d3ec997..e57323ef 100644 --- a/WebimClientLibrary.podspec +++ b/WebimClientLibrary.podspec @@ -1,7 +1,7 @@ Pod::Spec.new do |s| s.name = 'WebimClientLibrary' - s.version = '3.32.1' + s.version = '3.33.0' s.author = { 'Webim.ru Ltd.' => 'n.lazarev-zubov@webim.ru' } s.homepage = 'https://webim.ru/integration/mobile-sdk/ios-sdk-howto/' diff --git a/WebimClientLibrary/Backend/AbstractRequestLoop.swift b/WebimClientLibrary/Backend/AbstractRequestLoop.swift index a027041e..2a947d32 100644 --- a/WebimClientLibrary/Backend/AbstractRequestLoop.swift +++ b/WebimClientLibrary/Backend/AbstractRequestLoop.swift @@ -101,7 +101,7 @@ class AbstractRequestLoop { func perform(request: URLRequest) throws -> Data { var requestWithUesrAngent = request - requestWithUesrAngent.setValue("iOS: Webim-Client 3.32.1; (\(UIDevice.current.model); \(UIDevice.current.systemVersion)); Bundle ID and version: \(Bundle.main.bundleIdentifier ?? "none") \(Bundle.main.infoDictionary?["CFBundleVersion"] ?? "none")", forHTTPHeaderField: "User-Agent") + requestWithUesrAngent.setValue("iOS: Webim-Client 3.33.0; (\(UIDevice.current.model); \(UIDevice.current.systemVersion)); Bundle ID and version: \(Bundle.main.bundleIdentifier ?? "none") \(Bundle.main.infoDictionary?["CFBundleVersion"] ?? "none")", forHTTPHeaderField: "User-Agent") var errorCounter = 0 var lastHTTPCode = -1 diff --git a/WebimClientLibrary/Backend/ActionRequestLoop.swift b/WebimClientLibrary/Backend/ActionRequestLoop.swift index a475967d..c78cd585 100644 --- a/WebimClientLibrary/Backend/ActionRequestLoop.swift +++ b/WebimClientLibrary/Backend/ActionRequestLoop.swift @@ -207,6 +207,9 @@ class ActionRequestLoop: AbstractRequestLoop { ofRequest: request) break + case WebimInternalError.noStickerId.rawValue: + self.handleSendStickerError(error: error, + ofRequest: request) default: self.running = false @@ -450,6 +453,22 @@ class ActionRequestLoop: AbstractRequestLoop { } } + private func handleSendStickerError(error errorString: String, + ofRequest webimRequest: WebimRequest) { + if let sendStickerCompletionHandler = webimRequest.getSendStickerCompletionHandler() { + completionHandlerExecutor?.execute(task: DispatchWorkItem { + let sendStickerError: SendStickerError + switch errorString { + case WebimInternalError.noStickerId.rawValue: + sendStickerError = .noStickerId + default: + sendStickerError = .noChat + } + sendStickerCompletionHandler.onFailure(error: sendStickerError) + }) + } + } + private func handleKeyboardResponse(error errorString: String, ofRequest webimRequest: WebimRequest) { if let keyboardResponseCompletionHandler = webimRequest.getKeyboardResponseCompletionHandler() { @@ -564,6 +583,7 @@ class ActionRequestLoop: AbstractRequestLoop { request.getSendSurveyAnswerCompletionHandler()?.onSuccess() request.getSurveyCloseCompletionHandler()?.onSuccess() request.getRateOperatorCompletionHandler()?.onSuccess() + request.getSendStickerCompletionHandler()?.onSuccess() guard let messageID = request.getMessageID() else { WebimInternalLogger.shared.log(entry: "Request has not message ID in ActionRequestLoop.\(#function)") return diff --git a/WebimClientLibrary/Backend/DeltaCallback.swift b/WebimClientLibrary/Backend/DeltaCallback.swift index 9e4e2785..58f2a104 100644 --- a/WebimClientLibrary/Backend/DeltaCallback.swift +++ b/WebimClientLibrary/Backend/DeltaCallback.swift @@ -299,6 +299,8 @@ final class DeltaCallback { private func handleChatOperatorUpdateBy(delta: DeltaItem) { guard delta.getEvent() == .update, let deltaData = delta.getData() as? [String : Any] else { + currentChat?.set(operator: nil) + messageStream?.changingChatStateOf(chat: currentChat) return } diff --git a/WebimClientLibrary/Backend/DeltaRequestLoop.swift b/WebimClientLibrary/Backend/DeltaRequestLoop.swift index 71c97f76..532b4cbe 100644 --- a/WebimClientLibrary/Backend/DeltaRequestLoop.swift +++ b/WebimClientLibrary/Backend/DeltaRequestLoop.swift @@ -147,7 +147,7 @@ class DeltaRequestLoop: AbstractRequestLoop { func requestInitialization() { let url = URL(string: getDeltaServerURLString() + "?" + getInitializationParameterString()) var request = URLRequest(url: url!) - request.setValue("3.32.1", forHTTPHeaderField: WebimActions.Parameter.webimSDKVersion.rawValue) + request.setValue("3.33.0", forHTTPHeaderField: WebimActions.Parameter.webimSDKVersion.rawValue) request.httpMethod = AbstractRequestLoop.HTTPMethods.get.rawValue do { diff --git a/WebimClientLibrary/Backend/ExecIfNotDestroyedHandlerExecutor.swift b/WebimClientLibrary/Backend/ExecIfNotDestroyedHandlerExecutor.swift index b716fe7e..b26ae6aa 100644 --- a/WebimClientLibrary/Backend/ExecIfNotDestroyedHandlerExecutor.swift +++ b/WebimClientLibrary/Backend/ExecIfNotDestroyedHandlerExecutor.swift @@ -49,7 +49,7 @@ final class ExecIfNotDestroyedHandlerExecutor { // MARK: - Methods func execute(task: DispatchWorkItem) { if !sessionDestroyer.isDestroyed() { - queue.async { + DispatchQueue.main.async { if !self.sessionDestroyer.isDestroyed() { task.perform() } diff --git a/WebimClientLibrary/Backend/FAQSQLiteHistoryStorage.swift b/WebimClientLibrary/Backend/FAQSQLiteHistoryStorage.swift index e6fb873d..1604d95f 100644 --- a/WebimClientLibrary/Backend/FAQSQLiteHistoryStorage.swift +++ b/WebimClientLibrary/Backend/FAQSQLiteHistoryStorage.swift @@ -160,6 +160,7 @@ final class FAQSQLiteHistoryStorage { } } } catch { + completion(nil) } } } diff --git a/WebimClientLibrary/Backend/Items/MessageItem.swift b/WebimClientLibrary/Backend/Items/MessageItem.swift index 5d8baefc..6006e498 100644 --- a/WebimClientLibrary/Backend/Items/MessageItem.swift +++ b/WebimClientLibrary/Backend/Items/MessageItem.swift @@ -47,6 +47,7 @@ final class MessageItem { case data = "data" case deleted = "deleted" case id = "id" + case isEdited = "edited" case kind = "kind" case quote = "quote" case read = "read" @@ -67,6 +68,7 @@ final class MessageItem { private var data: MessageData? private var deleted: Bool? private var id: String? + private var isEdited: Bool? private var kind: MessageKind? private var quote: QuoteItem? private var read: Bool? @@ -144,6 +146,10 @@ final class MessageItem { if let timestampInSecond = jsonDictionary[JSONField.timestampInSecond.rawValue] as? Double { self.timestampInSecond = timestampInSecond } + + if let isEdited = jsonDictionary[JSONField.isEdited.rawValue] as? Bool { + self.isEdited = isEdited + } } // MARK: - Methods @@ -222,6 +228,10 @@ final class MessageItem { return canBeReplied ?? false } + func getIsEdited() -> Bool { + return isEdited ?? false + } + // MARK: - enum MessageKind: String { // Raw values equal to field names received in responses from server. @@ -238,6 +248,7 @@ final class MessageItem { case keyboard_response = "" case operatorMessage = "operator" case operatorBusy = "operator_busy" + case stickerVisitor = "sticker_visitor" case visitorMessage = "visitor" // MARK: - Initialization @@ -282,6 +293,10 @@ final class MessageItem { case .visitorMessage: self = .visitorMessage + break + case .stickerVisitor: + self = .stickerVisitor + break } } diff --git a/WebimClientLibrary/Backend/Items/StickerItem.swift b/WebimClientLibrary/Backend/Items/StickerItem.swift new file mode 100644 index 00000000..56682b55 --- /dev/null +++ b/WebimClientLibrary/Backend/Items/StickerItem.swift @@ -0,0 +1,56 @@ +// +// StickerItem.swift +// WebimClientLibrary +// +// Created by Yury Vozleev on 08.10.2020. +// Copyright © 2020 Webim. All rights reserved. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. +// + +import Foundation + +class StickerItem { + // MARK: - Constants + // Raw values equal to field names received in responses from server. + private enum JSONField: String { + case stickerId = "stickerId" + } + + // MARK: - Properties + private let stickerId: Int + + // MARK: - Initialization + init?(jsonDictionary: [String: Any?]) { + guard let stickerId = jsonDictionary[JSONField.stickerId.rawValue] as? Int else { + return nil + } + + self.stickerId = stickerId + } + + init(stickerId: Int) { + self.stickerId = stickerId + } + + // MARK: - Methods + func getStickerId() -> Int { + return stickerId + } +} diff --git a/WebimClientLibrary/Backend/MessageHolder.swift b/WebimClientLibrary/Backend/MessageHolder.swift index b86b93ff..b18e282d 100644 --- a/WebimClientLibrary/Backend/MessageHolder.swift +++ b/WebimClientLibrary/Backend/MessageHolder.swift @@ -42,6 +42,7 @@ final class MessageHolder { private var lastChatMessageIndex = 0 private lazy var messagesToSend = [MessageToSend]() private var messageTracker: MessageTrackerImpl? + private var currentChatMessagesWereReceived = false private var reachedEndOfLocalHistory = false private var reachedEndOfRemoteHistory: Bool @@ -82,6 +83,14 @@ final class MessageHolder { self.messagesToSend = messagesToSend } + func getCurrentChatMessagesWereReceived() -> Bool { + return currentChatMessagesWereReceived + } + + func set(currentChatMessagesWereReceived: Bool) { + self.currentChatMessagesWereReceived = currentChatMessagesWereReceived + } + func getLatestMessages(byLimit limitOfMessages: Int, completion: @escaping ([Message]) -> ()) { if !currentChatMessages.isEmpty { @@ -113,6 +122,7 @@ final class MessageHolder { } if message == firstMessage { if !firstMessage.hasHistoryComponent() { + currentChatMessagesWereReceived = true historyStorage.getLatestHistory(byLimit: limit, completion: completion) } else { @@ -310,6 +320,7 @@ final class MessageHolder { senderAvatarURLString: messageImpl.getSenderAvatarURLString(), senderName: messageImpl.getSenderName(), sendStatus: .sending, + sticker: messageImpl.getSticker(), type: messageImpl.getType(), rawData: messageImpl.getRawData(), data: messageImpl.getData(), @@ -320,7 +331,8 @@ final class MessageHolder { rawText: messageImpl.getRawText(), read: messageImpl.isReadByOperator(), messageCanBeEdited: messageImpl.canBeEdited(), - messageCanBeReplied: messageImpl.canBeReplied()) + messageCanBeReplied: messageImpl.canBeReplied(), + messageIsEdited: messageImpl.isEdited()) messageTracker?.messageListener?.changed(message: messageImpl, to: newMessage) return messageImpl.getText() } @@ -352,6 +364,7 @@ final class MessageHolder { senderAvatarURLString: messageImpl.getSenderAvatarURLString(), senderName: messageImpl.getSenderName(), sendStatus: .sent, + sticker: messageImpl.getSticker(), type: messageImpl.getType(), rawData: messageImpl.getRawData(), data: messageImpl.getData(), @@ -362,7 +375,8 @@ final class MessageHolder { rawText: messageImpl.getRawText(), read: messageImpl.isReadByOperator(), messageCanBeEdited: messageImpl.canBeEdited(), - messageCanBeReplied: messageImpl.canBeReplied()) + messageCanBeReplied: messageImpl.canBeReplied(), + messageIsEdited: messageImpl.isEdited()) messageTracker?.messageListener?.changed(message: messageImpl, to: newMessage) } diff --git a/WebimClientLibrary/Backend/MessageToSend.swift b/WebimClientLibrary/Backend/MessageToSend.swift index 004183d2..bcdc7da5 100644 --- a/WebimClientLibrary/Backend/MessageToSend.swift +++ b/WebimClientLibrary/Backend/MessageToSend.swift @@ -42,7 +42,8 @@ final class MessageToSend: MessageImpl { type: MessageType, text: String, timeInMicrosecond: Int64, - quote: Quote? = nil) { + quote: Quote? = nil, + sticker: Sticker? = nil) { super.init(serverURLString: serverURLString, id: id, keyboard: nil, @@ -52,6 +53,7 @@ final class MessageToSend: MessageImpl { senderAvatarURLString: nil, senderName: senderName, sendStatus: .sending, + sticker: sticker, type: type, rawData: nil, data: nil, @@ -62,7 +64,8 @@ final class MessageToSend: MessageImpl { rawText: nil, read: false, messageCanBeEdited: false, - messageCanBeReplied: false) + messageCanBeReplied: false, + messageIsEdited: false) } } diff --git a/WebimClientLibrary/Backend/SQLiteHistoryStorage.swift b/WebimClientLibrary/Backend/SQLiteHistoryStorage.swift index e02b2bf4..09c63432 100644 --- a/WebimClientLibrary/Backend/SQLiteHistoryStorage.swift +++ b/WebimClientLibrary/Backend/SQLiteHistoryStorage.swift @@ -109,11 +109,11 @@ final class SQLiteHistoryStorage: HistoryStorage { func getMajorVersion() -> Int { // No need in this implementation. - return 5 + return 6 } func getVersionDB() -> Int { - return 5 + return 6 } func set(reachedHistoryEnd: Bool) { @@ -646,13 +646,12 @@ final class SQLiteHistoryStorage: HistoryStorage { } var keyboard: Keyboard? = nil - if let data = rawData { - keyboard = KeyboardImpl.getKeyboard(jsonDictionary: data) - } - var keyboardRequest: KeyboardRequest? = nil + var sticker: Sticker? if let data = rawData { + keyboard = KeyboardImpl.getKeyboard(jsonDictionary: data) keyboardRequest = KeyboardRequestImpl.getKeyboardRequest(jsonDictionary: data) + sticker = StickerImpl.getSticker(jsonDictionary: data) } var quote: Quote? @@ -669,6 +668,7 @@ final class SQLiteHistoryStorage: HistoryStorage { quote: quote, senderAvatarURLString: row[SQLiteHistoryStorage.avatarURLString], senderName: row[SQLiteHistoryStorage.senderName], + sticker: sticker, type: type, rawData: rawData, data: data, @@ -679,7 +679,8 @@ final class SQLiteHistoryStorage: HistoryStorage { rawText: rawText, read: row[SQLiteHistoryStorage.timestamp] <= readBeforeTimestamp || readBeforeTimestamp == -1, messageCanBeEdited: false, - messageCanBeReplied: false) + messageCanBeReplied: false, + messageIsEdited: false) } private func insert(message: MessageImpl) throws { diff --git a/WebimClientLibrary/Backend/Utilities/Extensions/UInt32.swift b/WebimClientLibrary/Backend/Utilities/Extensions/UInt32.swift index 26b641d1..ed65cbd9 100644 --- a/WebimClientLibrary/Backend/Utilities/Extensions/UInt32.swift +++ b/WebimClientLibrary/Backend/Utilities/Extensions/UInt32.swift @@ -36,7 +36,7 @@ extension UInt32 { - copyright: 2018 Webim */ - @_specialize(exported: true, where T == ArraySlice) + @_specialize(where T == ArraySlice) init(bytes: T, fromIndex index: T.Index) where T.Element == UInt8, T.Index == Int { if bytes.isEmpty { diff --git a/WebimClientLibrary/Backend/Utilities/MessageFactories.swift b/WebimClientLibrary/Backend/Utilities/MessageFactories.swift index 978aae5a..3d89627d 100644 --- a/WebimClientLibrary/Backend/Utilities/MessageFactories.swift +++ b/WebimClientLibrary/Backend/Utilities/MessageFactories.swift @@ -100,6 +100,7 @@ class MessageMapper { var text: String? var rawText: String? var data: MessageData? + var sticker: Sticker? guard let messageItemText = messageItem.getText() else { WebimInternalLogger.shared.log(entry: "Message Item Text is nil in MessageFactories.\(#function)") @@ -136,6 +137,10 @@ class MessageMapper { keyboardRequest = KeyboardRequestImpl.getKeyboardRequest(jsonDictionary: data) } + if kind == .stickerVisitor, let data = messageItem.getRawData() { + sticker = StickerImpl.getSticker(jsonDictionary: data) + } + let quote = messageItem.getQuote() var messageAttachmentFromQuote: FileInfo? = nil if let kind = quote?.getMessageKind(), kind == .fileFromVisitor || kind == .fileFromOperator { @@ -176,6 +181,7 @@ class MessageMapper { quote: QuoteImpl.getQuote(quoteItem: quote, messageAttachment: messageAttachmentFromQuote), senderAvatarURLString: messageItem.getSenderAvatarURLString(), senderName: senderName, + sticker: sticker, type: type, rawData: messageItem.getRawData(), data: data, @@ -186,7 +192,8 @@ class MessageMapper { rawText: rawText, read: messageItem.getRead() ?? true, messageCanBeEdited: messageItem.getCanBeEdited(), - messageCanBeReplied: messageItem.getCanBeReplied()) + messageCanBeReplied: messageItem.getCanBeReplied(), + messageIsEdited: messageItem.getIsEdited()) } func set(webimClient: WebimClient) { @@ -300,4 +307,14 @@ final class SendingFactory { timeInMicrosecond: InternalUtils.getCurrentTimeInMicrosecond()) } + func createStickerMessageToSendWith(id: String, stickerId: Int) -> MessageToSend { + return MessageToSend(serverURLString: serverURLString, + id: id, + senderName: "", + type: .stickerVisitor, + text: "", + timeInMicrosecond: InternalUtils.getCurrentTimeInMicrosecond(), + sticker: StickerImpl(stickerId: stickerId)) + } + } diff --git a/WebimClientLibrary/Backend/WebimActions.swift b/WebimClientLibrary/Backend/WebimActions.swift index e157e6c2..ec290ea4 100644 --- a/WebimClientLibrary/Backend/WebimActions.swift +++ b/WebimClientLibrary/Backend/WebimActions.swift @@ -75,6 +75,7 @@ class WebimActions { case visitorNote = "visitor_note" case visitSessionID = "visit-session-id" case since = "since" + case stickerId = "sticker-id" case surveyAnswer = "answer" case surveyFormID = "form-id" case surveyID = "survey-id" @@ -120,6 +121,7 @@ class WebimActions { case chatRead = "chat.read_by_visitor" case widgetUpdate = "widget.update" case keyboardResponse = "chat.keyboard_response" + case sendSticker = "sticker" } // MARK: - Properties @@ -417,6 +419,26 @@ class WebimActions { sendDialogToEmailAddressCompletionHandler: completionHandler)) } + func sendSticker(stickerId:Int, + clientSideId: String, + completionHandler: SendStickerCompletionHandler? = nil) { + let dataToPost = [ + Parameter.actionn.rawValue: Action.sendSticker.rawValue, + Parameter.stickerId.rawValue: String(stickerId), + Parameter.clientSideID.rawValue: clientSideId + ] as [String: Any] + + let urlString = baseURL + ServerPathSuffix.doAction.rawValue + + actionRequestLoop.enqueue(request: WebimRequest( + httpMethod: .post, + primaryData: dataToPost, + contentType: ContentType.urlEncoded.rawValue, + baseURLString: urlString, + sendStickerCompletionHandler: completionHandler + )) + } + func sendQuestionAnswer(surveyID: String, formID: Int, questionID: Int, diff --git a/WebimClientLibrary/Backend/WebimInternalError.swift b/WebimClientLibrary/Backend/WebimInternalError.swift index d9dba375..351c6972 100644 --- a/WebimClientLibrary/Backend/WebimInternalError.swift +++ b/WebimClientLibrary/Backend/WebimInternalError.swift @@ -42,6 +42,7 @@ enum WebimInternalError: String, Error { case fileTypeNotAllowed = "not_allowed_file_type" case notAllowedMimeType = "not_allowed_mime_type"; case noPreviousChats = "no_previous_chats"; + case noStickerId = "no-sticker-id" case notMatchingMagicNumbers = "not_matching_magic_numbers"; case providedVisitorFieldsExpired = "provided-visitor-expired" case reinitializationRequired = "reinit-required" diff --git a/WebimClientLibrary/Backend/WebimRequest.swift b/WebimClientLibrary/Backend/WebimRequest.swift index 9d000362..81ba4759 100644 --- a/WebimClientLibrary/Backend/WebimRequest.swift +++ b/WebimClientLibrary/Backend/WebimRequest.swift @@ -55,6 +55,7 @@ final class WebimRequest { private var editMessageCompletionHandler: EditMessageCompletionHandler? private var sendKeyboardRequestCompletionHandler: SendKeyboardRequestCompletionHandler? private var sendDialogToEmailAddressCompletionHandler: SendDialogToEmailAddressCompletionHandler? + private var sendStickerCompletionHandler: SendStickerCompletionHandler? private var sendSurveyAnswerCompletionHandler: SendSurveyAnswerCompletionHandlerWrapper? private var surveyCloseCompletionHandler: SurveyCloseCompletionHandler? @@ -77,6 +78,7 @@ final class WebimRequest { editMessageCompletionHandler: EditMessageCompletionHandler? = nil, keyboardResponseCompletionHandler: SendKeyboardRequestCompletionHandler? = nil, sendDialogToEmailAddressCompletionHandler: SendDialogToEmailAddressCompletionHandler? = nil, + sendStickerCompletionHandler: SendStickerCompletionHandler? = nil, sendMessageComplitionHandler: SendMessageCompletionHandler? = nil, sendSurveyAnswerCompletionHandler: SendSurveyAnswerCompletionHandlerWrapper? = nil, surveyCloseCompletionHandler: SurveyCloseCompletionHandler? = nil) { @@ -99,6 +101,7 @@ final class WebimRequest { self.sendKeyboardRequestCompletionHandler = keyboardResponseCompletionHandler self.faqCompletionHandler = faqCompletionHandler self.sendDialogToEmailAddressCompletionHandler = sendDialogToEmailAddressCompletionHandler + self.sendStickerCompletionHandler = sendStickerCompletionHandler self.sendSurveyAnswerCompletionHandler = sendSurveyAnswerCompletionHandler self.surveyCloseCompletionHandler = surveyCloseCompletionHandler } @@ -182,6 +185,10 @@ final class WebimRequest { return sendDialogToEmailAddressCompletionHandler } + func getSendStickerCompletionHandler() -> SendStickerCompletionHandler? { + return sendStickerCompletionHandler + } + func getSendSurveyAnswerCompletionHandler() -> SendSurveyAnswerCompletionHandlerWrapper? { return sendSurveyAnswerCompletionHandler } diff --git a/WebimClientLibrary/Implementation/MessageImpl.swift b/WebimClientLibrary/Implementation/MessageImpl.swift index 98bde0f8..bdc9677e 100644 --- a/WebimClientLibrary/Implementation/MessageImpl.swift +++ b/WebimClientLibrary/Implementation/MessageImpl.swift @@ -46,6 +46,7 @@ class MessageImpl { private let senderName: String private let sendStatus: MessageSendStatus private let serverURLString: String + private let sticker: Sticker? private let text: String private let timeInMicrosecond: Int64 private let type: MessageType @@ -57,6 +58,7 @@ class MessageImpl { private var read: Bool private var messageCanBeEdited: Bool private var messageCanBeReplied: Bool + private var messageIsEdited: Bool // MARK: - Initialization init(serverURLString: String, @@ -68,6 +70,7 @@ class MessageImpl { senderAvatarURLString: String?, senderName: String, sendStatus: MessageSendStatus = .sent, + sticker: Sticker?, type: MessageType, rawData: [String: Any?]?, data: MessageData?, @@ -78,7 +81,8 @@ class MessageImpl { rawText: String?, read: Bool, messageCanBeEdited: Bool, - messageCanBeReplied: Bool) { + messageCanBeReplied: Bool, + messageIsEdited: Bool) { self.data = data self.id = id self.keyboard = keyboard @@ -90,6 +94,7 @@ class MessageImpl { self.senderAvatarURLString = senderAvatarURLString self.senderName = senderName self.sendStatus = sendStatus + self.sticker = sticker self.serverURLString = serverURLString self.text = text self.timeInMicrosecond = timeInMicrosecond @@ -97,6 +102,7 @@ class MessageImpl { self.read = read self.messageCanBeEdited = messageCanBeEdited self.messageCanBeReplied = messageCanBeReplied + self.messageIsEdited = messageIsEdited self.historyMessage = historyMessage if historyMessage { @@ -336,6 +342,10 @@ extension MessageImpl: Message { return senderName } + func getSticker() -> Sticker? { + return sticker + } + func getText() -> String { return text } @@ -367,6 +377,10 @@ extension MessageImpl: Message { return messageCanBeReplied } + func isEdited() -> Bool { + return messageIsEdited + } + } // MARK: - Equatable @@ -383,7 +397,8 @@ extension MessageImpl: Equatable { && (lhs.timeInMicrosecond == rhs.timeInMicrosecond)) && (lhs.type == rhs.type)) && (lhs.isReadByOperator() == rhs.isReadByOperator() - && (lhs.canBeEdited() == rhs.canBeEdited())) + && (lhs.canBeEdited() == rhs.canBeEdited() + && (lhs.isEdited() == rhs.isEdited()))) } } @@ -712,6 +727,38 @@ final class KeyboardImpl: Keyboard { } } +/** + - seealso: + `Sticker` + - author: + Yury Vozleev + - copyright: + 2020 Webim + */ +final class StickerImpl: Sticker { + private let stickerItem: StickerItem + + init?(data: [String: Any?]) { + if let sticker = StickerItem(jsonDictionary: data) { + self.stickerItem = sticker + } else { + return nil + } + } + + init(stickerId: Int) { + self.stickerItem = StickerItem(stickerId: stickerId) + } + + static func getSticker(jsonDictionary: [String : Any?]) -> Sticker? { + return StickerImpl(data: jsonDictionary) + } + + func getStickerId() -> Int { + return stickerItem.getStickerId() + } +} + // MARK: - /** - seealso: diff --git a/WebimClientLibrary/Implementation/MessageStreamImpl.swift b/WebimClientLibrary/Implementation/MessageStreamImpl.swift index 463ab97e..25c2c4f4 100644 --- a/WebimClientLibrary/Implementation/MessageStreamImpl.swift +++ b/WebimClientLibrary/Implementation/MessageStreamImpl.swift @@ -547,6 +547,15 @@ extension MessageStreamImpl: MessageStream { completionHandler: completionHandler) } + func sendSticker(withId stickerId: Int, completionHandler: SendStickerCompletionHandler?) throws { + try accessChecker.checkAccess() + + let messageID = ClientSideID.generateClientSideID() + messageHolder.sending(message: sendingMessageFactory.createStickerMessageToSendWith(id: messageID, stickerId: stickerId)) + webimActions.sendSticker(stickerId: stickerId, clientSideId: messageID, completionHandler: completionHandler) + } + + func updateWidgetStatus(data: String) throws { try accessChecker.checkAccess() diff --git a/WebimClientLibrary/Implementation/MessageTrackerImpl.swift b/WebimClientLibrary/Implementation/MessageTrackerImpl.swift index 5eac5dd7..11d840df 100644 --- a/WebimClientLibrary/Implementation/MessageTrackerImpl.swift +++ b/WebimClientLibrary/Implementation/MessageTrackerImpl.swift @@ -46,6 +46,7 @@ final class MessageTrackerImpl { private var headMessage: MessageImpl? private var firstHistoryUpdateReceived: Bool? private var messagesLoading: Bool? + private var currentChatMessagesWereReceived = false // MARK: - Initialization init(messageListener: MessageListener, @@ -417,7 +418,7 @@ final class MessageTrackerImpl { let headMessageTime = headMessage?.getTime() ?? messageTime if (messageTime >= first.getTime()) && (messageTime <= last.getTime()) - && (messageTime > headMessageTime) { + && messageHolder.getCurrentChatMessagesWereReceived() { for currentChatMessage in currentChatMessages { if currentChatMessage.getID() == message.getID() { @@ -530,6 +531,7 @@ extension MessageTrackerImpl: MessageTracker { allMessageSourcesEnded = false messageHolder.set(reachedEndOfLocalHistory: false) + messageHolder.set(currentChatMessagesWereReceived: false) let currentChatMessages = messageHolder.getCurrentChatMessages() if currentChatMessages.isEmpty { messagesLoading = true diff --git a/WebimClientLibrary/Implementation/WebimSessionImpl.swift b/WebimClientLibrary/Implementation/WebimSessionImpl.swift index b609d51f..a62a58ff 100644 --- a/WebimClientLibrary/Implementation/WebimSessionImpl.swift +++ b/WebimClientLibrary/Implementation/WebimSessionImpl.swift @@ -188,7 +188,7 @@ final class WebimSessionImpl { UserDefaults.standard.set(userDefaults, forKey: userDefaultsKey) } - + guard let dbName = userDefaults?[UserDefaultsMainPrefix.historyDBname.rawValue] as? String else { WebimInternalLogger.shared.log(entry: "Can not find or write DB Name to UserDefaults in WebimSessionImpl.\(#function)") fatalError("Can not find or write DB Name to UserDefaults in WebimSessionImpl.\(#function)") diff --git a/WebimClientLibrary/Message.swift b/WebimClientLibrary/Message.swift index 84e2359b..0a453540 100644 --- a/WebimClientLibrary/Message.swift +++ b/WebimClientLibrary/Message.swift @@ -129,6 +129,16 @@ public protocol Message { */ func getQuote() -> Quote? + /** + - returns: + The sticker item that was sent to the server. + - author: + Yury Vozleev + - copyright: + 2020 Webim + */ + func getSticker() -> Sticker? + /** - returns: URL of a sender's avatar or `nil` if one does not exist. @@ -234,6 +244,15 @@ public protocol Message { */ func canBeReplied() -> Bool + /** + - returns: + True if this message is edited. + - author: + Eugene Ilyin + - copyright: + 2019 Webim + */ + func isEdited() -> Bool } /** @@ -768,6 +787,28 @@ public protocol Quote { func getState() -> QuoteState } +/** + Contains information about sticker. + - seealso: + `Message.getSticker()` + - author: + Yury Vozleev + - copyright: + 2020 Webim + */ +public protocol Sticker { + + /** + - returns: + Sticker ID. + - author: + Yury Vozleev + - copyright: + 2020 Webim + */ + func getStickerId() -> Int +} + // MARK: - /** Supported quote states. @@ -1009,7 +1050,17 @@ public enum MessageType { @available(*, unavailable, renamed: "visitorMessage") case VISITOR - + + /** + A sticker message sent by a visitor. + - seealso: + `Message.getText()` + - author: + Yury Vozleev + - copyright: + 2020 Webim + */ + case stickerVisitor } /** diff --git a/WebimClientLibrary/MessageStream.swift b/WebimClientLibrary/MessageStream.swift index 737d3f30..a8e65cb5 100644 --- a/WebimClientLibrary/MessageStream.swift +++ b/WebimClientLibrary/MessageStream.swift @@ -502,6 +502,24 @@ public protocol MessageStream: class { mimeType: String, completionHandler: SendFileCompletionHandler?) throws -> String + /** + Send sticker to chat. + When calling this method, if there is an active `MessageTracker` object (see `newMessageTracker(messageListener:)` method), `MessageListener.added(message:after:)` with a message `MessageSendStatus.sending` in the status is also called. + - parameter withId: + Contains the id of the sticker to send + - parameter completionHandler: + Completion handler that executes when operation is done. + - throws: + `AccessError.invalidThread` if the method was called not from the thread the WebimSession was created in. + `AccessError.invalidSession` if WebimSession was destroyed. + - author: + Yury Vozleev + - copyright: + 2020 Webim + */ + func sendSticker(withId: Int, + completionHandler: SendStickerCompletionHandler?) throws + /** Send keyboard request with button. - parameter button: @@ -1142,6 +1160,40 @@ public protocol SendDialogToEmailAddressCompletionHandler: class { } +/** + - seealso: + `MessageStream.sendSticker(withId:completionHandler:)`. + - author: + Yury Vozleev + - copyright: + 2020 Webim + */ +public protocol SendStickerCompletionHandler: class { + + /** + Executed when operation is done successfully. + - author: + Yury Vozleev + - copyright: + 2020 Webim + */ + func onSuccess() + + /** + Executed when operation is failed. + - parameter error: + Error. + - seealso: + `SendStickerError`. + - author: + Yury Vozleev + - copyright: + 2020 Webim + */ + func onFailure(error: SendStickerError) + +} + /** - seealso: `MessageStream.send(surveyAnswer:completionHandler:)`. @@ -2291,6 +2343,34 @@ public enum SendDialogToEmailAddressError: Error { case UNKNOWN } +/** +- seealso: +`SendStickerCompletionHandler.onFailure(error:)` +- author: +Yury Vozleev +- copyright: +2020 Webim +*/ +public enum SendStickerError: Error { + /** + There is no chat to send it to the sticker. + - author: + Yury Vozleev + - copyright: + 2020 Webim + */ + case noChat + + /** + Not set sticker id + - author: + Yury Vozleev + - copyright: + 2020 Webim + */ + case noStickerId +} + /** - seealso: `SendSurveyAnswerCompletionHandler.onFailure(error:)`