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 917ce63c..00000000 Binary files a/Example/WebimClientLibrary/Images.xcassets/Check.imageset/check_Icon-32.png and /dev/null differ diff --git a/Example/WebimClientLibrary/Images.xcassets/Gradient.imageset/Contents.json b/Example/WebimClientLibrary/Images.xcassets/Gradient.imageset/Contents.json new file mode 100644 index 00000000..92fbe4b5 --- /dev/null +++ b/Example/WebimClientLibrary/Images.xcassets/Gradient.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "gradient-1.jpg", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Example/WebimClientLibrary/Images.xcassets/Gradient.imageset/gradient-1.jpg b/Example/WebimClientLibrary/Images.xcassets/Gradient.imageset/gradient-1.jpg new file mode 100644 index 00000000..4e3c59b5 Binary files /dev/null and b/Example/WebimClientLibrary/Images.xcassets/Gradient.imageset/gradient-1.jpg differ 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 00000000..3df6b75e Binary files /dev/null and b/Example/WebimClientLibrary/Images.xcassets/Icons/AttachmentButton.imageset/Vector.pdf differ diff --git a/Example/WebimClientLibrary/Images.xcassets/Icons/Back/Back.imageset/back_Icon-32.png b/Example/WebimClientLibrary/Images.xcassets/Icons/Back/Back.imageset/back_Icon-32.png deleted file mode 100644 index 7ffe69b3..00000000 Binary files a/Example/WebimClientLibrary/Images.xcassets/Icons/Back/Back.imageset/back_Icon-32.png and /dev/null differ 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 7ffe69b3..00000000 Binary files a/Example/WebimClientLibrary/Images.xcassets/Icons/Back/Back_dark.imageset/back_Icon-32.png and /dev/null differ 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 6fafcae7..00000000 Binary files a/Example/WebimClientLibrary/Images.xcassets/Icons/Clip/Clip.imageset/ClipIcon.pdf and /dev/null differ diff --git a/Example/WebimClientLibrary/Images.xcassets/Icons/Clip/Clip_dark.imageset/Clip_dark.pdf b/Example/WebimClientLibrary/Images.xcassets/Icons/Clip/Clip_dark.imageset/Clip_dark.pdf deleted file mode 100644 index b742ded6..00000000 Binary files a/Example/WebimClientLibrary/Images.xcassets/Icons/Clip/Clip_dark.imageset/Clip_dark.pdf and /dev/null differ 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 a4f95b2e..00000000 Binary files a/Example/WebimClientLibrary/Images.xcassets/Icons/Close/Close.imageset/close_Icon-32.png and /dev/null differ diff --git a/Example/WebimClientLibrary/Images.xcassets/Icons/Close/Close_dark.imageset/Contents.json b/Example/WebimClientLibrary/Images.xcassets/Icons/Close/Close_dark.imageset/Contents.json deleted file mode 100644 index a05197e9..00000000 --- a/Example/WebimClientLibrary/Images.xcassets/Icons/Close/Close_dark.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_dark.imageset/close_Icon-32.png b/Example/WebimClientLibrary/Images.xcassets/Icons/Close/Close_dark.imageset/close_Icon-32.png deleted file mode 100644 index a4f95b2e..00000000 Binary files a/Example/WebimClientLibrary/Images.xcassets/Icons/Close/Close_dark.imageset/close_Icon-32.png and /dev/null differ diff --git a/Example/WebimClientLibrary/Images.xcassets/Icons/CloseButton.imageset/CloseButton-1.pdf b/Example/WebimClientLibrary/Images.xcassets/Icons/CloseButton.imageset/CloseButton-1.pdf new file mode 100644 index 00000000..3f35bd82 Binary files /dev/null and b/Example/WebimClientLibrary/Images.xcassets/Icons/CloseButton.imageset/CloseButton-1.pdf differ 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 f1716049..00000000 Binary files a/Example/WebimClientLibrary/Images.xcassets/Icons/Empty.imageset/Empty.pdf and /dev/null differ diff --git a/Example/WebimClientLibrary/Images.xcassets/Icons/Back/Contents.json b/Example/WebimClientLibrary/Images.xcassets/Icons/FileStatus/Contents.json similarity index 100% rename from Example/WebimClientLibrary/Images.xcassets/Icons/Back/Contents.json rename to Example/WebimClientLibrary/Images.xcassets/Icons/FileStatus/Contents.json diff --git a/Example/WebimClientLibrary/Images.xcassets/Icons/FileStatus/FIleDownloadSeccessVisitor.imageset/Contents.json b/Example/WebimClientLibrary/Images.xcassets/Icons/FileStatus/FIleDownloadSeccessVisitor.imageset/Contents.json new file mode 100644 index 00000000..09b61d50 --- /dev/null +++ b/Example/WebimClientLibrary/Images.xcassets/Icons/FileStatus/FIleDownloadSeccessVisitor.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "FIleDownloadSeccessVisitor.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/FIleDownloadSeccessVisitor.imageset/FIleDownloadSeccessVisitor.pdf b/Example/WebimClientLibrary/Images.xcassets/Icons/FileStatus/FIleDownloadSeccessVisitor.imageset/FIleDownloadSeccessVisitor.pdf new file mode 100644 index 00000000..f0759857 Binary files /dev/null and b/Example/WebimClientLibrary/Images.xcassets/Icons/FileStatus/FIleDownloadSeccessVisitor.imageset/FIleDownloadSeccessVisitor.pdf differ 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 00000000..2e7c0f13 Binary files /dev/null and b/Example/WebimClientLibrary/Images.xcassets/Icons/FileStatus/FileDownloadButtonOperator.imageset/FileDownloadButtonOperator.pdf differ 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 00000000..733e1a18 Binary files /dev/null and b/Example/WebimClientLibrary/Images.xcassets/Icons/FileStatus/FileDownloadButtonVisitor.imageset/FileDownloadButtonVisitor.pdf differ 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 00000000..d7ffab1c Binary files /dev/null and b/Example/WebimClientLibrary/Images.xcassets/Icons/FileStatus/FileDownloadError.imageset/FileDownloadError.pdf differ diff --git a/Example/WebimClientLibrary/Images.xcassets/Icons/FileStatus/FileDownloadSuccessOperator.imageset/Contents.json b/Example/WebimClientLibrary/Images.xcassets/Icons/FileStatus/FileDownloadSuccessOperator.imageset/Contents.json new file mode 100644 index 00000000..bf6c8150 --- /dev/null +++ b/Example/WebimClientLibrary/Images.xcassets/Icons/FileStatus/FileDownloadSuccessOperator.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "FileDownloadSuccessOperator.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/FileDownloadSuccessOperator.imageset/FileDownloadSuccessOperator.pdf b/Example/WebimClientLibrary/Images.xcassets/Icons/FileStatus/FileDownloadSuccessOperator.imageset/FileDownloadSuccessOperator.pdf new file mode 100644 index 00000000..eac0e142 Binary files /dev/null and b/Example/WebimClientLibrary/Images.xcassets/Icons/FileStatus/FileDownloadSuccessOperator.imageset/FileDownloadSuccessOperator.pdf differ 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 00000000..c11530cb Binary files /dev/null and b/Example/WebimClientLibrary/Images.xcassets/Icons/FileStatus/FileUploadButtonVisitor.imageset/FileUploadButtonVisitor.pdf differ 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 00000000..91ff6fe8 Binary files /dev/null and b/Example/WebimClientLibrary/Images.xcassets/Icons/ImageDownload.imageset/ImageDownload.pdf differ 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 00000000..e06beef2 Binary files /dev/null and b/Example/WebimClientLibrary/Images.xcassets/Icons/MessageActions/ActionCopy.imageset/ActionCopy.pdf differ diff --git a/Example/WebimClientLibrary/Images.xcassets/Icons/Clip/Clip_dark.imageset/Contents.json b/Example/WebimClientLibrary/Images.xcassets/Icons/MessageActions/ActionCopy.imageset/Contents.json similarity index 84% rename from Example/WebimClientLibrary/Images.xcassets/Icons/Clip/Clip_dark.imageset/Contents.json rename to Example/WebimClientLibrary/Images.xcassets/Icons/MessageActions/ActionCopy.imageset/Contents.json index f7203dd1..7dd763dc 100644 --- a/Example/WebimClientLibrary/Images.xcassets/Icons/Clip/Clip_dark.imageset/Contents.json +++ b/Example/WebimClientLibrary/Images.xcassets/Icons/MessageActions/ActionCopy.imageset/Contents.json @@ -2,7 +2,7 @@ "images" : [ { "idiom" : "universal", - "filename" : "Clip_dark.pdf" + "filename" : "ActionCopy.pdf" } ], "info" : { diff --git a/Example/WebimClientLibrary/Images.xcassets/Icons/MessageActions/ActionDelete.imageset/ActionDelete.pdf b/Example/WebimClientLibrary/Images.xcassets/Icons/MessageActions/ActionDelete.imageset/ActionDelete.pdf new file mode 100644 index 00000000..f1875d0c Binary files /dev/null and b/Example/WebimClientLibrary/Images.xcassets/Icons/MessageActions/ActionDelete.imageset/ActionDelete.pdf differ 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 00000000..3441f121 Binary files /dev/null and b/Example/WebimClientLibrary/Images.xcassets/Icons/MessageActions/ActionEdit.imageset/ActionEdit.pdf differ 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 00000000..4adc0d31 Binary files /dev/null and b/Example/WebimClientLibrary/Images.xcassets/Icons/MessageActions/ActionReply.imageset/ActionReply.pdf differ 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 00000000..7b62dbf9 Binary files /dev/null and b/Example/WebimClientLibrary/Images.xcassets/Icons/MessageStatus/ReadByOperator.imageset/ReadByOperator.pdf differ diff --git a/Example/WebimClientLibrary/Images.xcassets/Icons/Empty.imageset/Contents.json b/Example/WebimClientLibrary/Images.xcassets/Icons/MessageStatus/Sent.imageset/Contents.json similarity index 86% rename from Example/WebimClientLibrary/Images.xcassets/Icons/Empty.imageset/Contents.json rename to Example/WebimClientLibrary/Images.xcassets/Icons/MessageStatus/Sent.imageset/Contents.json index 00234c1e..467fa619 100644 --- a/Example/WebimClientLibrary/Images.xcassets/Icons/Empty.imageset/Contents.json +++ b/Example/WebimClientLibrary/Images.xcassets/Icons/MessageStatus/Sent.imageset/Contents.json @@ -2,7 +2,7 @@ "images" : [ { "idiom" : "universal", - "filename" : "Empty.pdf" + "filename" : "Sent.pdf" } ], "info" : { diff --git a/Example/WebimClientLibrary/Images.xcassets/Icons/MessageStatus/Sent.imageset/Sent.pdf b/Example/WebimClientLibrary/Images.xcassets/Icons/MessageStatus/Sent.imageset/Sent.pdf new file mode 100644 index 00000000..0d0dab91 Binary files /dev/null and b/Example/WebimClientLibrary/Images.xcassets/Icons/MessageStatus/Sent.imageset/Sent.pdf differ diff --git a/Example/WebimClientLibrary/Images.xcassets/Icons/ReplyCircleToTheLeft.imageset/Contents.json b/Example/WebimClientLibrary/Images.xcassets/Icons/ReplyCircleToTheLeft.imageset/Contents.json new file mode 100644 index 00000000..3f346208 --- /dev/null +++ b/Example/WebimClientLibrary/Images.xcassets/Icons/ReplyCircleToTheLeft.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "left.pdf" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Example/WebimClientLibrary/Images.xcassets/Icons/ReplyCircleToTheLeft.imageset/left.pdf b/Example/WebimClientLibrary/Images.xcassets/Icons/ReplyCircleToTheLeft.imageset/left.pdf new file mode 100644 index 00000000..a7bdd058 Binary files /dev/null and b/Example/WebimClientLibrary/Images.xcassets/Icons/ReplyCircleToTheLeft.imageset/left.pdf differ diff --git a/Example/WebimClientLibrary/Images.xcassets/Icons/SendMessage/Contents.json b/Example/WebimClientLibrary/Images.xcassets/Icons/SendMessage/Contents.json deleted file mode 100644 index da4a164c..00000000 --- a/Example/WebimClientLibrary/Images.xcassets/Icons/SendMessage/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/Icons/SendMessage/SendMessage.imageset/Contents.json b/Example/WebimClientLibrary/Images.xcassets/Icons/SendMessage/SendMessage.imageset/Contents.json deleted file mode 100644 index ac44b5a8..00000000 --- a/Example/WebimClientLibrary/Images.xcassets/Icons/SendMessage/SendMessage.imageset/Contents.json +++ /dev/null @@ -1,15 +0,0 @@ -{ - "images" : [ - { - "idiom" : "universal", - "filename" : "SendMessageIcon.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.imageset/SendMessageIcon.pdf b/Example/WebimClientLibrary/Images.xcassets/Icons/SendMessage/SendMessage.imageset/SendMessageIcon.pdf deleted file mode 100644 index 5b8b0bb5..00000000 Binary files a/Example/WebimClientLibrary/Images.xcassets/Icons/SendMessage/SendMessage.imageset/SendMessageIcon.pdf and /dev/null differ 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 4e8ba63b..00000000 Binary files a/Example/WebimClientLibrary/Images.xcassets/Icons/SendMessage/SendMessage_dark.imageset/SendMessage_dark.pdf and /dev/null differ diff --git a/Example/WebimClientLibrary/Images.xcassets/Icons/SendMessageButton.imageset/Contents.json b/Example/WebimClientLibrary/Images.xcassets/Icons/SendMessageButton.imageset/Contents.json new file mode 100644 index 00000000..c8ceeab1 --- /dev/null +++ b/Example/WebimClientLibrary/Images.xcassets/Icons/SendMessageButton.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "SendMessageButton.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/SendMessageButton.imageset/SendMessageButton.pdf b/Example/WebimClientLibrary/Images.xcassets/Icons/SendMessageButton.imageset/SendMessageButton.pdf new file mode 100644 index 00000000..dc6bc773 Binary files /dev/null and b/Example/WebimClientLibrary/Images.xcassets/Icons/SendMessageButton.imageset/SendMessageButton.pdf differ 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 00000000..26a0a013 Binary files /dev/null and b/Example/WebimClientLibrary/Images.xcassets/ImagePlaceholder.imageset/placeholder.png differ 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 357d8d18..00000000 Binary files a/Example/WebimClientLibrary/Images.xcassets/ScrollToBottom/ScrollToBottom.imageset/ScrollToBottom.pdf and /dev/null differ diff --git a/Example/WebimClientLibrary/Images.xcassets/ScrollToBottom/ScrollToBottom_dark.imageset/Contents.json b/Example/WebimClientLibrary/Images.xcassets/ScrollToBottom/ScrollToBottom_dark.imageset/Contents.json deleted file mode 100644 index 8fc17ead..00000000 --- a/Example/WebimClientLibrary/Images.xcassets/ScrollToBottom/ScrollToBottom_dark.imageset/Contents.json +++ /dev/null @@ -1,15 +0,0 @@ -{ - "images" : [ - { - "idiom" : "universal", - "filename" : "ScrollToBottom_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/ScrollToBottom/ScrollToBottom_dark.imageset/ScrollToBottom_dark.pdf b/Example/WebimClientLibrary/Images.xcassets/ScrollToBottom/ScrollToBottom_dark.imageset/ScrollToBottom_dark.pdf deleted file mode 100644 index d13c4f8b..00000000 Binary files a/Example/WebimClientLibrary/Images.xcassets/ScrollToBottom/ScrollToBottom_dark.imageset/ScrollToBottom_dark.pdf and /dev/null differ diff --git a/Example/WebimClientLibrary/Info.plist b/Example/WebimClientLibrary/Info.plist index e94eaeb7..bcc7c166 100644 --- a/Example/WebimClientLibrary/Info.plist +++ b/Example/WebimClientLibrary/Info.plist @@ -36,20 +36,34 @@ + 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:)`