Skip to content

Instantly share code, notes, and snippets.

@dodikk
Last active January 26, 2018 16:15
Show Gist options
  • Save dodikk/c0f0bb7c00380e9fe064e226f3909505 to your computer and use it in GitHub Desktop.
Save dodikk/c0f0bb7c00380e9fe064e226f3909505 to your computer and use it in GitHub Desktop.
Bubble code for NMessenger ticket #115
import UIKit
import AsyncDisplayKit
import NMessenger
import VHChatLogic
class ChatRoomView: NMessengerViewController, ViewType
{
public var controller: ChatRoomController?
private let vhBubbleConfig = VHBubbleConfiguration()
override func viewDidLoad()
{
super.viewDidLoad()
precondition(nil != self.controller)
self.controller?.delegate = self
super.sharedBubbleConfiguration = self.vhBubbleConfig
super.messagePadding = UIEdgeInsets(top: 30, left: 0, bottom: 30, right: 0)
self.messengerView.doesBatchFetch = true
self.controller?.getInitialHistoryAsync()
}
// MARK: - NMessengerViewController
//
//
override func sendText(_ text: String, isIncomingMessage:Bool) -> GeneralMessengerCell
{
let shouldSendToServer = !isIncomingMessage
if (shouldSendToServer)
{
self.controller?.sendMessageAsync(text)
}
let result = self.buildNodeForTextMessage(text, isIncomingMessage: isIncomingMessage)
self.addMessageToMessenger(result)
return result
}
fileprivate func buildNodeForTextMessage(_ text: String, isIncomingMessage:Bool) -> GeneralMessengerCell
{
// construct a node : "text bubble + avatar"
//
let contentNode = TextContentNode(textMessageString: text,
currentViewController: self,
bubbleConfiguration: self.vhBubbleConfig)
contentNode.incomingTextColor = UIColor.black
contentNode.outgoingTextColor = UIColor.black
// TODO: configure fonts
let mockTimestampStr = "4:07 pm \n\n"
let timestampStr = mockTimestampStr
let result = MessageNode(content: contentNode)
result.isIncomingMessage = isIncomingMessage
let avatar = ASImageNode()
avatar.image = self.myAvatarMock()
// https://github.com/eBay/NMessenger/issues/35
//
avatar.preferredFrameSize = CGSize(width: 35, height: 35)
result.avatarNode = avatar
// timestamp customization goes last
// due to side effects from setters `result.isIncomingMessage =`
if let messagePart = contentNode.textMessageString
{
let timestampColor = UIColor.lightGray
//UIColor.lightGray
let timestampAttributes =
[
NSStrokeColorAttributeName: timestampColor,
NSForegroundColorAttributeName: timestampColor
]
let timestampAddendum = NSAttributedString(string: timestampStr,
attributes: timestampAttributes)
let timestampAndMessage = NSMutableAttributedString(attributedString: timestampAddendum)
timestampAndMessage.append(messagePart)
contentNode.textMessageString = timestampAndMessage
}
// let timestamp = ASTextNode()
// timestamp.attributedText = NSAttributedString(string: "4:07 pm")
//
// result.headerNode = timestamp
// http://asyncdisplaykit.org/docs/automatic-layout-examples-2.html
// let isDebugging = false
// if (isDebugging)
// {
// result.backgroundColor = UIColor.orange
// result.cornerRadius = 0.1
//
// timestamp.backgroundColor = UIColor.cyan
// avatar.backgroundColor = UIColor.green
// }
return result
}
// MARK: - NMessengerDelegate
func batchFetchContent()
{
print("batchFetchContent")
// let numberOfMessagesToPreload = 20
// let messageCount = 10
// let upperLimit = messageCount + numberOfMessagesToPreload
//
// let range = Range(uncheckedBounds: (lower: messageCount, upper: upperLimit))
// self.controller?.preloadMessagesFor(range: range)
}
// MARK: - ViewType
//
// not in extension due to some compiler errors
//
public func update(state: MessageListState, oldState: MessageListState?)
{
// TODO: implement me
}
private func myAvatarMock() -> UIImage
{
// let result = UIImage(named: "test-avatar-sender")
let result = UIImage(named: "Carrie")
return result!
}
private func otherUserAvatarMock() -> UIImage
{
// let result = UIImage(named: "test-avatar-receiver")
let result = UIImage(named: "Jessie")
return result!
}
}
extension ChatRoomView : ChatRoomDelegate
{
public func chatDidReceiveInitialHistory(_ messageList: ChatMessageList)
{
self.messengerView.clearALLMessages()
messageList.forEach
{
let messageCell = self.buildNodeForTextMessage($0.text, isIncomingMessage: $0.isIncoming)
self.addMessageToMessenger(messageCell)
}
}
public func chatDidFailInitialHistory(_ error: Error)
{
// TODO: show alert
}
func chatDidSendMessage(_ message: ChatMessage)
{
// _ = self.sendText(message.text, isIncomingMessage: false)
// IDLE
}
func chatDidFailToSendMessage(_ message: ChatMessage, withError: Error)
{
// TODO: remove message from chat view
//
}
func chatDidReceiveMessages(_ messageList: ChatMessageList)
{
messageList.forEach
{
let messageNode = self.buildNodeForTextMessage($0.text, isIncomingMessage: true)
self.addMessageToMessenger(messageNode)
}
}
}
//
// VHBubble.swift
//
// Created by Alexander Dodatko on 4/5/17.
//
import Foundation
//
// Copyright (c) 2016 eBay Software Foundation
//
// 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
import NMessenger
/**
VHBubble has four rounded corners and a triangle pointing to the avatar.
*/
open class VHBubble: Bubble {
//MARK: Public Variables
/** Radius of the corners for the bubble. When this is set, you will need to call setNeedsLayout on your message for changes to take effect if the bubble has already been drawn*/
open var radius : CGFloat = 16
/** Should be less or equal to the *radius* property. When this is set, you will need to call setNeedsLayout on your message for changes to take effect if the bubble has already been drawn*/
open var borderWidth : CGFloat = 4 //TODO:
/** The color of the border around the bubble. When this is set, you will need to call setNeedsLayout on your message for changes to take effect if the bubble has already been drawn*/
public var triangleHeight: CGFloat = 10
open var bubbleBorderColor : UIColor = UIColor(red: 0xFD / 255.0,
green: 0xD6 / 255.0,
blue: 0xCC / 255.0,
alpha: 1)
/** Path used to cutout the bubble*/
open fileprivate(set) var path: CGMutablePath = CGMutablePath()
// MARK: Initialisers
/**
Initialiser class.
*/
public override init() {
super.init()
self.bubbleColor = UIColor.white
}
// MARK: Class methods
/**
Overriding sizeToBounds from super class
-parameter bounds: The bounds of the content
*/
open override func sizeToBounds(_ bounds: CGRect) {
super.sizeToBounds(bounds)
var rect = CGRect.zero
var radius2: CGFloat = 0
let radius = self.radius
let borderWidth = self.borderWidth
let diameter = 2*radius
if bounds.width < diameter || bounds.height < diameter
{
//if the rect calculation yeilds a negative result
let newRadiusW = bounds.width/2
let newRadiusH = bounds.height/2
let newRadius =
(newRadiusW>newRadiusH)
? newRadiusH
: newRadiusW
let newDiameter = 2*newRadius
rect = CGRect(x: newRadius,
y: newRadius,
width: bounds.width - newDiameter,
height: bounds.height - newDiameter)
radius2 = newRadius - (borderWidth / 2)
self.buildLegacyPath(rect: rect, radius2: radius2)
}
else
{
rect = CGRect(x: radius,
y: radius,
width: bounds.width - 2*radius,
height: bounds.height - 2*radius)
radius2 = radius - (borderWidth / 2)
self.buildPathWithTriangle(rect: bounds, radius2: radius2)
}
}
private func buildPathWithTriangle(rect: CGRect,
radius2: CGFloat)
{
// copy-pasted from SO. Replaced some code to make compiler happy.
// http://stackoverflow.com/questions/11196112/speech-bubble-in-ios-sdk-using-objective-c
//let LINE_WIDTH: CGFloat = 2
let TRIANGLE_HEIGHT: CGFloat = 16;
let radius: CGFloat = radius2
let minx = rect.minX
let midx = rect.midX
let maxx = rect.maxX
let miny = rect.minY
let midy = rect.midY
let maxy = rect.maxY
let TRIANGLE_MID_Y = miny + 2 * TRIANGLE_HEIGHT;
// TODO: uncomment if looks bad
//
// rect.origin.y += LINE_WIDTH;
// rect.size.width -= LINE_WIDTH * 2;
// rect.size.height -= LINE_WIDTH * 2;
let outlinePath = CGMutablePath()
// TODO: uncomment if looks bad
//
// minx += TRIANGLE_HEIGHT;
// maxx -= TRIANGLE_HEIGHT;
let topMid = CGPoint(x: midx, y: miny)
let topLeft = CGPoint(x: minx, y: miny)
let midLeft = CGPoint(x: minx, y: midy)
let bottomLeft = CGPoint(x: minx, y: maxy)
let bottomMid = CGPoint(x: midx, y: maxy)
let bottomRight = CGPoint(x: maxx, y: maxy)
let midRight = CGPoint(x: maxx, y: midy)
let topRight = CGPoint(x: maxx, y: miny)
let triangleBottomVertex =
CGPoint(x: maxx,
y: miny + TRIANGLE_MID_Y + TRIANGLE_HEIGHT)
let trianglePeakVertex =
CGPoint(x: maxx + TRIANGLE_HEIGHT,
y: miny + TRIANGLE_MID_Y)
let triangleTopVertex =
CGPoint(x: maxx,
y: miny + TRIANGLE_MID_Y - TRIANGLE_HEIGHT)
// top mid
//CGPathMoveToPoint(outlinePath, &noTransform, midx, miny);
outlinePath.move(to: topMid)
// top mid ==> top left ==> mid left ==> ...
//CGPathAddArcToPoint (outlinePath, &noTransform, minx, miny, minx, midy, radius);
outlinePath.addArc(tangent1End: topLeft, tangent2End: midLeft, radius: radius)
// mid left ==> bottom left ==> bottom mid ==> ...
// CGPathAddArcToPoint (outlinePath, nil, minx, maxy, midx, maxy, radius);
outlinePath.addArc(tangent1End: bottomLeft, tangent2End: bottomMid, radius: radius)
// bottom mid ==> bottom right ==> mid right ==> ...
// CGPathAddArcToPoint (outlinePath, nil, maxx, maxy, maxx, midy, radius);
outlinePath.addArc(tangent1End: bottomRight, tangent2End: midRight, radius: radius)
// draw right triangle
//
outlinePath.addLine(to: triangleBottomVertex)
outlinePath.addLine(to: trianglePeakVertex)
outlinePath.addLine(to: triangleTopVertex)
// CGPathAddLineToPoint(outlinePath, nil, maxx, miny + TRIANGLE_MID_Y + TRIANGLE_HEIGHT);
// CGPathAddLineToPoint(outlinePath, nil, maxx + TRIANGLE_HEIGHT, miny + TRIANGLE_MID_Y);
// CGPathAddLineToPoint(outlinePath, nil, maxx, miny + TRIANGLE_MID_Y - TRIANGLE_HEIGHT);
// mid right ==> top right ==> tom mid
// CGPathAddArcToPoint (outlinePath, nil, maxx, miny, midx, miny, radius);
outlinePath.addArc(tangent1End: topRight, tangent2End: topMid, radius: radius)
outlinePath.closeSubpath()
self.path = outlinePath
}
private func buildLegacyPath(rect: CGRect,
radius2: CGFloat)
{
self.path = CGMutablePath()
self.path.addArc(center: CGPoint(x: rect.maxX, y: rect.minY),
radius: radius2,
startAngle: CGFloat(-M_PI_2),
endAngle: 0,
clockwise: false)
self.path.addLine(to: CGPoint(x: rect.maxX + radius2,
y: rect.maxY + radius2))
self.path.addArc(center: CGPoint(x: rect.minX, y: rect.maxY),
radius: radius2,
startAngle: CGFloat(M_PI_2),
endAngle: CGFloat(M_PI),
clockwise: false)
self.path.addArc(center: CGPoint(x: rect.minX, y: rect.minY),
radius: radius2,
startAngle: CGFloat(M_PI),
endAngle: CGFloat(-M_PI_2),
clockwise: false)
//CGPathAddArc(path, nil, rect.maxX, rect.minY, radius2, CGFloat(-M_PI_2), 0, false)
//CGPathAddLineToPoint(path, nil, rect.maxX + radius2, rect.maxY + radius2)
//CGPathAddArc(path, nil, rect.minX, rect.maxY, radius2, CGFloat(M_PI_2), CGFloat(M_PI), false)
//CGPathAddArc(path, nil, rect.minX, rect.minY, radius2, CGFloat(M_PI), CGFloat(-M_PI_2), false)
self.path.closeSubpath()
}
/**
Overriding createLayer from super class
*/
open override func createLayer() {
super.createLayer()
CATransaction.begin()
CATransaction.setDisableActions(true)
self.layer.path = path
self.layer.fillColor = self.bubbleColor.cgColor
self.layer.strokeColor = self.bubbleBorderColor.cgColor
self.layer.lineWidth = self.borderWidth
self.layer.position = CGPoint.zero
self.maskLayer.fillColor = self.bubbleBorderColor.cgColor
self.maskLayer.path = path
self.maskLayer.position = CGPoint.zero
CATransaction.commit()
}
}
//
// VHBubbleConfiguration.swift
//
// Created by Alexander Dodatko on 4/3/17.
//
import Foundation
import NMessenger
public class VHBubbleConfiguration: BubbleConfigurationProtocol
{
public var isMasked: Bool = true
/** Create and return a UI color representing an incoming message */
public func getIncomingColor() -> UIColor
{
return UIColor.white
}
/** Create and return a UI color representing an outgoing message */
public func getOutgoingColor() -> UIColor
{
return UIColor.white
}
/** Create and return a bubble for the ContentNode */
public func getBubble() -> Bubble
{
let result = VHBubble()
result.hasLayerMask = isMasked
return result
}
/** Create and return a bubble that is used by the Message group for Message nodes after the first. This is typically used to "stack" messages */
public func getSecondaryBubble() -> Bubble
{
let newBubble = StackedBubble()
newBubble.hasLayerMask = isMasked
return newBubble
// let result = VHBubble()
// result.hasLayerMask = isMasked
// return result
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment