From 8134bbe479203997d8f014a886b76c2352d35820 Mon Sep 17 00:00:00 2001 From: Alexander Dodatko Date: Mon, 3 Apr 2017 14:11:03 +0300 Subject: [PATCH 1/4] [cookbook] added --- CustomizationCookbook.md | 51 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 51 insertions(+) create mode 100644 CustomizationCookbook.md diff --git a/CustomizationCookbook.md b/CustomizationCookbook.md new file mode 100644 index 0000000..47c43bb --- /dev/null +++ b/CustomizationCookbook.md @@ -0,0 +1,51 @@ +# This document contains some tutorials for typical tasks which are not covered by README + +## 1 - A Node With both Text and Avatar + +```swift + override func sendText(_ text: String, isIncomingMessage:Bool) -> GeneralMessengerCell + { + let shouldSendToServer = !isIncomingMessage + if (shouldSendToServer) + { + self.controller?.sendMessageAsync(text) + } + + // not calling `super.sendText()` since we do some customization now + let result = self.buildNodeForTextMessage(text, isIncomingMessage: isIncomingMessage) + + return result + } + + fileprivate func buildNodeForTextMessage(_ text: String, isIncomingMessage:Bool) -> GeneralMessengerCell + { + // Node for text content + // + let contentNode = TextContentNode(textMessageString: text, + currentViewController: self, + bubbleConfiguration: self.sharedBubbleConfiguration) + + + // Node for the entire "text + avatar" + // + let result = MessageNode(content: contentNode) + + // Setting `isIncomingMessage` to choose side + result.isIncomingMessage = isIncomingMessage + + // Creating AsyncDisplayKit avatar + // + let avatar = ASImageNode() + avatar.image = self.myAvatarMock() + + // !!! If you do not specify this size, you will not see the message at all + // + avatar.preferredFrameSize = CGSize(width: 35, height: 35) + + // add the avatar to + // + result.avatarNode = avatar + + return result + } +``` From 4c50d09066485db981c905e973bbc26dca5617a2 Mon Sep 17 00:00:00 2001 From: Alexander Dodatko Date: Tue, 4 Apr 2017 12:07:01 +0300 Subject: [PATCH 2/4] [docs] added instructions on network service instruction from branch --- Cookbook-ChatServiceIntegration.md | 131 +++++++++++++++++++++++++++++ 1 file changed, 131 insertions(+) create mode 100644 Cookbook-ChatServiceIntegration.md diff --git a/Cookbook-ChatServiceIntegration.md b/Cookbook-ChatServiceIntegration.md new file mode 100644 index 0000000..24cc731 --- /dev/null +++ b/Cookbook-ChatServiceIntegration.md @@ -0,0 +1,131 @@ +### Integration With Your Chat Service + +The main purpose of chat messages is exchanging them over the network. The topics above only cover the message rendering aspect. However, it might be unclear how to acthaully push your messages to the network or how to render the received ones. Let's dive in... + +Messages management the following two sub-tasks : +* sending realtime messages +* receivung realtime messages +* history integration (both sent and received messages) + + +Suppose, our chat service is limited to textin. The service, described below, can use any underlying protocol (such as XMPP, Telegram, etc.). `Disclaimer: your chat service might look differently`. + +```swift +public protocol IChatMessage +{ + var text: String { get } + var isIncoming: Bool { get } +} + +public protocol IChatServiceDelegate +{ + func chatServiceDidConnect(_ sender: IChatService) + func chatService(_ sender: IChatService, didSendMessage: IChatMessage) + func chatService(_ sender: IChatService, didReceiveMessage: IChatMessage) + func chatService(_ sender: IChatService, didReceiveHistory: [IChatMessage]]) + + // TODO: error handling methods are skipped for conciseness +} + +public protocol IChatService +{ + func connectAsync() + func disconnectAsync() + + func sendTextAsync(_ message: String) + func loadHistoryAsync() +} +``` + +Sending a message involves two phases : +1. Find out that the user has typed something and tapped "send" button. In other words, you have to handle the user's input. +2. Pass the user's input to the networking service. This is achieved as a plain method call. + +Intercepting the user's input might be not quite obvious since you do not need any delegate subscriptions. It is done by overriding the `NMessengerViewController.sendText()` instance method. + + +```swift +public class MyChatMessagingVC: NMessengerViewController, IChatServiceDelegate +{ + override func sendText(_ text: String, isIncomingMessage:Bool) -> GeneralMessengerCell + { + let shouldSendToServer = !isIncomingMessage + + if (shouldSendToServer) + { + // trigger network service + self.controller?.sendMessageAsync(text) + } + + // otherwise - just render + // `super` is critical to avoid recursion and "just render" + return super.sendText(text, isIncomingMessage: isIncomingMessage) + } +} +``` + +I'd like to highlight the importance of using `super.sendText()` at the end of the function. If `self` is used in this case, you're going to +1. end up with infinite recursion +2. eventually crash due to "stack overflow" reason +3. flood the chat with repeated messages + +You can use some other cell contruction code instead. See the ["Content Nodes and Custom Components"](https://github.com/eBay/NMessenger#content-nodes-and-custom-components) section for details. + + + +We can break down the process of "receiving a message" in two phases as well. Here they are: +1. Subscribe to events from your service. Usually it's done by one of iOS mechanics such as delegates, `NSNotification` or closures. +2. Render the message using `NMessenger` + +Let's see how it can be done : + +```swift +public class MyChatMessagingVC: NMessengerViewController, IChatServiceDelegate +{ + func chatService(_ sender: IChatService, didReceiveMessage message: IChatMessage) + { + // Using `super` to avoid side effects. + // + super.sendText(message.text, isIncomingMessage: true) + } +} +``` + +So, now your app should be ready to process realtime messaging. + + + +When it comes to history handling, one of the approaches might be purging all the messages from `NMessenger` and re-adding them. + +```swift +public class MyChatMessagingVC: NMessengerViewController, IChatServiceDelegate +{ + override func viewDidAppear(_ animated: Bool) + { + super.viewDidAppear(animated) + + // supposing `self._chatService` has been the setup in `viewDidLoad` + // or injected during the screen transition + // + self._chatService.loadHistoryAsync() + } + + + func chatService(_ sender: IChatService, didReceiveHistory: [IChatMessage]]) + { + super.clearALLMessages() + + messageList.forEach + { + // using `super`to avoid side effects + // + _ = super.sendText($0.text, isIncomingMessage: $0.isIncoming) + } + } +} +``` + +This approach might result in poor performance and some unwanted visual effects. +``` +TODO: describe a better approach +``` From 9da6219aedddbde7bb8ac1776457d7051aabfc39 Mon Sep 17 00:00:00 2001 From: Alexander Dodatko Date: Tue, 4 Apr 2017 12:08:03 +0300 Subject: [PATCH 3/4] [docs] separate directory added --- .../Cookbook-ChatServiceIntegration.md | 0 CustomizationCookbook.md => docs/CustomizationCookbook.md | 0 2 files changed, 0 insertions(+), 0 deletions(-) rename Cookbook-ChatServiceIntegration.md => docs/Cookbook-ChatServiceIntegration.md (100%) rename CustomizationCookbook.md => docs/CustomizationCookbook.md (100%) diff --git a/Cookbook-ChatServiceIntegration.md b/docs/Cookbook-ChatServiceIntegration.md similarity index 100% rename from Cookbook-ChatServiceIntegration.md rename to docs/Cookbook-ChatServiceIntegration.md diff --git a/CustomizationCookbook.md b/docs/CustomizationCookbook.md similarity index 100% rename from CustomizationCookbook.md rename to docs/CustomizationCookbook.md From 32b4cfe7447f9e4f0d724767ffb57ff2aa595d7d Mon Sep 17 00:00:00 2001 From: Alexander Dodatko Date: Fri, 14 Apr 2017 12:26:34 +0300 Subject: [PATCH 4/4] [docs] added "Customize Chat Placeholder String" section --- docs/CustomizationCookbook.md | 42 +++++++++++++++++++++++++++++++++++ 1 file changed, 42 insertions(+) diff --git a/docs/CustomizationCookbook.md b/docs/CustomizationCookbook.md index 47c43bb..1278e75 100644 --- a/docs/CustomizationCookbook.md +++ b/docs/CustomizationCookbook.md @@ -49,3 +49,45 @@ return result } ``` + + +## 2 - Customize Chat Placeholder String + +https://github.com/eBay/NMessenger/issues/120 + +An appropriate approach is manually creating an instance of `NMessengerBarView` and configuring it properly. However, this approach might be hard for newcommers and those who do not need much customization. + + +There is no separate property for a placeholder string, so you can use the plain text in your `viewDidLoad` method. + +```swift +self.inputBarView.textInputView.text = + NSLocalizedString("Chat.InputField.PlaceholderText", comment: "Write a message") +``` + +After the camera shows up and is dismissed, the placeholder text is set from the `NMessengerBarView.inputTextViewPlaceholder` property. +So far there is no control over it from the `NMessengerViewController` except having an explicit instance of `NMessengerBarView`. + +As a quick workaround, you can try changing the property initialization. Unfortunately, does not work well for `Cocoapods` and `Carthage` users since you need to have your own copy of `NMessenger` code base. +```swift +open var inputTextViewPlaceholder: String = + Bundle.main.localizedString(forKey: "Chat.InputField.PlaceholderText", + value: "==Write a message==", + table: nil) + { + willSet(newVal) + { + self.textInputView.text = newVal + } + } + +``` + + + +## 3 - Custom Bubble Shape + +Not composed yet. The details can be found in the issue listed below +https://github.com/eBay/NMessenger/issues/115 + +