Swiftgram/TelegramUI/ChatMessageItemView.swift
2018-12-01 02:42:58 +04:00

239 lines
9.3 KiB
Swift

import Foundation
import AsyncDisplayKit
import Display
import Postbox
import TelegramCore
struct ChatMessageItemWidthFill {
let compactInset: CGFloat
let compactWidthBoundary: CGFloat
let freeMaximumFillFactor: CGFloat
func widthFor(_ width: CGFloat) -> CGFloat {
if width <= self.compactWidthBoundary {
return max(1.0, width - self.compactInset)
} else {
return max(1.0, floor(width * self.freeMaximumFillFactor))
}
}
}
struct ChatMessageItemBubbleLayoutConstants {
let edgeInset: CGFloat
let defaultSpacing: CGFloat
let mergedSpacing: CGFloat
let maximumWidthFill: ChatMessageItemWidthFill
let minimumSize: CGSize
let contentInsets: UIEdgeInsets
}
struct ChatMessageItemTextLayoutConstants {
let bubbleInsets: UIEdgeInsets
}
struct ChatMessageItemImageLayoutConstants {
let bubbleInsets: UIEdgeInsets
let statusInsets: UIEdgeInsets
let defaultCornerRadius: CGFloat
let mergedCornerRadius: CGFloat
let contentMergedCornerRadius: CGFloat
let maxDimensions: CGSize
let minDimensions: CGSize
}
struct ChatMessageItemInstantVideoConstants {
let insets: UIEdgeInsets
let dimensions: CGSize
}
struct ChatMessageItemFileLayoutConstants {
let bubbleInsets: UIEdgeInsets
}
struct ChatMessageItemLayoutConstants {
let avatarDiameter: CGFloat
let timestampHeaderHeight: CGFloat
let bubble: ChatMessageItemBubbleLayoutConstants
let image: ChatMessageItemImageLayoutConstants
let text: ChatMessageItemTextLayoutConstants
let file: ChatMessageItemFileLayoutConstants
let instantVideo: ChatMessageItemInstantVideoConstants
init() {
self.avatarDiameter = 37.0
self.timestampHeaderHeight = 34.0
self.bubble = ChatMessageItemBubbleLayoutConstants(edgeInset: 4.0, defaultSpacing: 2.0 + UIScreenPixel, mergedSpacing: 1.0, maximumWidthFill: ChatMessageItemWidthFill(compactInset: 40.0, compactWidthBoundary: 500.0, freeMaximumFillFactor: 0.85), minimumSize: CGSize(width: 40.0, height: 35.0), contentInsets: UIEdgeInsets(top: 0.0, left: 6.0, bottom: 0.0, right: 0.0))
self.text = ChatMessageItemTextLayoutConstants(bubbleInsets: UIEdgeInsets(top: 6.0 + UIScreenPixel, left: 12.0, bottom: 6.0 - UIScreenPixel, right: 12.0))
self.image = ChatMessageItemImageLayoutConstants(bubbleInsets: UIEdgeInsets(top: 1.0 + UIScreenPixel, left: 1.0 + UIScreenPixel, bottom: 1.0 + UIScreenPixel, right: 1.0 + UIScreenPixel), statusInsets: UIEdgeInsets(top: 0.0, left: 0.0, bottom: 6.0, right: 6.0), defaultCornerRadius: 17.0, mergedCornerRadius: 5.0, contentMergedCornerRadius: 5.0, maxDimensions: CGSize(width: 300.0, height: 300.0), minDimensions: CGSize(width: 74.0, height: 74.0))
self.file = ChatMessageItemFileLayoutConstants(bubbleInsets: UIEdgeInsets(top: 15.0, left: 9.0, bottom: 15.0, right: 12.0))
self.instantVideo = ChatMessageItemInstantVideoConstants(insets: UIEdgeInsets(top: 4.0, left: 0.0, bottom: 4.0, right: 0.0), dimensions: CGSize(width: 212.0, height: 212.0))
}
}
enum ChatMessageItemBottomNeighbor {
case none
case merged(semi: Bool)
}
let defaultChatMessageItemLayoutConstants = ChatMessageItemLayoutConstants()
enum ChatMessagePeekPreviewContent {
case media(Media)
case url(ASDisplayNode, CGRect, String)
}
public class ChatMessageItemView: ListViewItemNode {
let layoutConstants = defaultChatMessageItemLayoutConstants
var item: ChatMessageItem?
public required convenience init() {
self.init(layerBacked: false)
}
public init(layerBacked: Bool) {
super.init(layerBacked: layerBacked, dynamicBounce: true, rotated: true)
self.transform = CATransform3DMakeRotation(CGFloat.pi, 0.0, 0.0, 1.0)
}
required public init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override public func reuse() {
super.reuse()
self.item = nil
self.frame = CGRect()
}
func setupItem(_ item: ChatMessageItem) {
self.item = item
}
override public func layoutForParams(_ params: ListViewItemLayoutParams, item: ListViewItem, previousItem: ListViewItem?, nextItem: ListViewItem?) {
if let item = item as? ChatMessageItem {
let doLayout = self.asyncLayout()
let merged = item.mergedWithItems(top: previousItem, bottom: nextItem)
let (layout, apply) = doLayout(item, params, merged.top, merged.bottom, merged.dateAtBottom)
self.contentSize = layout.contentSize
self.insets = layout.insets
apply(.None, false)
}
}
override public func layoutAccessoryItemNode(_ accessoryItemNode: ListViewAccessoryItemNode, leftInset: CGFloat, rightInset: CGFloat) {
if let avatarNode = accessoryItemNode as? ChatMessageAvatarAccessoryItemNode {
avatarNode.frame = CGRect(origin: CGPoint(x: leftInset + 3.0, y: self.apparentFrame.height - 38.0 - self.insets.top + 1.0), size: CGSize(width: 38.0, height: 38.0))
}
}
override public func animateInsertion(_ currentTimestamp: Double, duration: Double, short: Bool) {
if short {
//self.layer.animateBoundsOriginYAdditive(from: -self.bounds.size.height, to: 0.0, duration: 0.4, timingFunction: kCAMediaTimingFunctionSpring)
} else {
self.transitionOffset = -self.bounds.size.height * 1.6
self.addTransitionOffsetAnimation(0.0, duration: duration, beginAt: currentTimestamp)
}
}
func asyncLayout() -> (_ item: ChatMessageItem, _ params: ListViewItemLayoutParams, _ mergedTop: ChatMessageMerge, _ mergedBottom: ChatMessageMerge, _ dateHeaderAtBottom: Bool) -> (ListViewItemNodeLayout, (ListViewItemUpdateAnimation, Bool) -> Void) {
return { _, _, _, _, _ in
return (ListViewItemNodeLayout(contentSize: CGSize(width: 32.0, height: 32.0), insets: UIEdgeInsets()), { _, _ in
})
}
}
func transitionNode(id: MessageId, media: Media) -> (ASDisplayNode, () -> UIView?)? {
return nil
}
func peekPreviewContent(at point: CGPoint) -> (Message, ChatMessagePeekPreviewContent)? {
return nil
}
func updateHiddenMedia() {
}
func updateSelectionState(animated: Bool) {
}
func updateHighlightedState(animated: Bool) {
var isHighlightedInOverlay = false
if let item = self.item, let contextHighlightedState = item.controllerInteraction.contextHighlightedState {
switch item.content {
case let .message(message, _, _, _):
if contextHighlightedState.messageStableId == message.stableId {
isHighlightedInOverlay = true
}
case let .group(messages):
for (message, _, _, _) in messages {
if contextHighlightedState.messageStableId == message.stableId {
isHighlightedInOverlay = true
break
}
}
}
}
self.isHighlightedInOverlay = isHighlightedInOverlay
}
func updateAutomaticMediaDownloadSettings() {
}
override public func header() -> ListViewItemHeader? {
if let item = self.item {
return item.header
} else {
return nil
}
}
func performMessageButtonAction(button: ReplyMarkupButton) {
if let item = self.item {
switch button.action {
case .text:
item.controllerInteraction.sendMessage(button.title)
case let .url(url):
item.controllerInteraction.openUrl(url, true, nil)
case .requestMap:
item.controllerInteraction.shareCurrentLocation()
case .requestPhone:
item.controllerInteraction.shareAccountContact()
case .openWebApp:
item.controllerInteraction.requestMessageActionCallback(item.message.id, nil, true)
case let .callback(data):
item.controllerInteraction.requestMessageActionCallback(item.message.id, data, false)
case let .switchInline(samePeer, query):
var botPeer: Peer?
var found = false
for attribute in item.message.attributes {
if let attribute = attribute as? InlineBotMessageAttribute {
if let peerId = attribute.peerId {
botPeer = item.message.peers[peerId]
found = true
}
}
}
if !found {
botPeer = item.message.author
}
var peerId: PeerId?
if samePeer {
peerId = item.message.id.peerId
}
if let botPeer = botPeer, let addressName = botPeer.addressName {
item.controllerInteraction.activateSwitchInline(peerId, "@\(addressName) \(query)")
}
case .payment:
item.controllerInteraction.openCheckoutOrReceipt(item.message.id)
}
}
}
}