Swiftgram/TelegramUI/ChatMessageItem.swift
2017-07-14 15:34:30 +03:00

247 lines
8.9 KiB
Swift

import Foundation
import UIKit
import Postbox
import AsyncDisplayKit
import Display
import SwiftSignalKit
import TelegramCore
private func mediaIsNotMergeable(_ media: Media) -> Bool {
if let file = media as? TelegramMediaFile, file.isSticker {
return true
}
if let _ = media as? TelegramMediaAction {
return true
}
return false
}
private func messagesShouldBeMerged(_ lhs: Message, _ rhs: Message) -> Bool {
if abs(lhs.timestamp - rhs.timestamp) < 5 * 60 && lhs.author?.id == rhs.author?.id {
for media in lhs.media {
if mediaIsNotMergeable(media) {
return false
}
}
for media in rhs.media {
if mediaIsNotMergeable(media) {
return false
}
}
for attribute in lhs.attributes {
if let attribute = attribute as? ReplyMarkupMessageAttribute {
if attribute.flags.contains(.inline) && !attribute.rows.isEmpty {
return false
}
break
}
}
return true
}
return false
}
func chatItemsHaveCommonDateHeader(_ lhs: ListViewItem, _ rhs: ListViewItem?) -> Bool{
let lhsHeader: ChatMessageDateHeader?
let rhsHeader: ChatMessageDateHeader?
if let lhs = lhs as? ChatMessageItem {
lhsHeader = lhs.header
} else if let lhs = lhs as? ChatHoleItem {
lhsHeader = lhs.header
} else if let lhs = lhs as? ChatUnreadItem {
lhsHeader = lhs.header
} else {
lhsHeader = nil
}
if let rhs = rhs {
if let rhs = rhs as? ChatMessageItem {
rhsHeader = rhs.header
} else if let rhs = rhs as? ChatHoleItem {
rhsHeader = rhs.header
} else if let rhs = rhs as? ChatUnreadItem {
rhsHeader = rhs.header
} else {
rhsHeader = nil
}
} else {
rhsHeader = nil
}
if let lhsHeader = lhsHeader, let rhsHeader = rhsHeader {
return lhsHeader.id == rhsHeader.id
} else {
return false
}
}
public final class ChatMessageItem: ListViewItem, CustomStringConvertible {
let theme: PresentationTheme
let strings: PresentationStrings
let account: Account
let peerId: PeerId
let controllerInteraction: ChatControllerInteraction
let message: Message
let read: Bool
public let accessoryItem: ListViewAccessoryItem?
let header: ChatMessageDateHeader
public init(theme: PresentationTheme, strings: PresentationStrings, account: Account, peerId: PeerId, controllerInteraction: ChatControllerInteraction, message: Message, read: Bool) {
self.theme = theme
self.strings = strings
self.account = account
self.peerId = peerId
self.controllerInteraction = controllerInteraction
self.message = message
self.read = read
var accessoryItem: ListViewAccessoryItem?
let incoming = message.effectivelyIncoming
let displayAuthorInfo = incoming && message.author != nil && peerId.isGroupOrChannel
self.header = ChatMessageDateHeader(timestamp: message.timestamp, theme: theme, strings: strings)
if displayAuthorInfo {
var hasActionMedia = false
for media in message.media {
if media is TelegramMediaAction {
hasActionMedia = true
break
}
}
var isBroadcastChannel = false
if let peer = message.peers[message.id.peerId] as? TelegramChannel, case .broadcast = peer.info {
isBroadcastChannel = true
}
if !hasActionMedia && !isBroadcastChannel {
if let author = message.author {
accessoryItem = ChatMessageAvatarAccessoryItem(account: account, peerId: author.id, peer: author, messageTimestamp: message.timestamp)
}
}
}
self.accessoryItem = accessoryItem
}
public func nodeConfiguredForWidth(async: @escaping (@escaping () -> Void) -> Void, width: CGFloat, previousItem: ListViewItem?, nextItem: ListViewItem?, completion: @escaping (ListViewItemNode, @escaping () -> (Signal<Void, NoError>?, () -> Void)) -> Void) {
var viewClassName: AnyClass = ChatMessageBubbleItemNode.self
loop: for media in message.media {
if let telegramFile = media as? TelegramMediaFile {
for attribute in telegramFile.attributes {
switch attribute {
case .Sticker:
viewClassName = ChatMessageStickerItemNode.self
break loop
case let .Video(_, _, flags):
if flags.contains(.instantRoundVideo) {
viewClassName = ChatMessageInstantVideoItemNode.self
break loop
}
default:
break
}
}
} else if let action = media as? TelegramMediaAction {
if case .phoneCall = action.action {
viewClassName = ChatMessageBubbleItemNode.self
} else {
viewClassName = ChatMessageActionItemNode.self
}
} else if let _ = media as? TelegramMediaExpiredContent {
viewClassName = ChatMessageActionItemNode.self
}
}
let configure = {
let node = (viewClassName as! ChatMessageItemView.Type).init()
node.controllerInteraction = self.controllerInteraction
node.setupItem(self)
let nodeLayout = node.asyncLayout()
let (top, bottom, dateAtBottom) = self.mergedWithItems(top: previousItem, bottom: nextItem)
let (layout, apply) = nodeLayout(self, width, top, bottom, dateAtBottom)
node.updateSelectionState(animated: false)
node.updateHighlightedState(animated: false)
node.contentSize = layout.contentSize
node.insets = layout.insets
completion(node, {
return (nil, { apply(.None) })
})
}
if Thread.isMainThread {
async {
configure()
}
} else {
configure()
}
}
final func mergedWithItems(top: ListViewItem?, bottom: ListViewItem?) -> (top: Bool, bottom: Bool, dateAtBottom: Bool) {
var mergedTop = false
var mergedBottom = false
var dateAtBottom = false
if let top = top as? ChatMessageItem {
if top.header.id != self.header.id {
mergedBottom = false
} else {
mergedBottom = messagesShouldBeMerged(message, top.message)
}
}
if let bottom = bottom as? ChatMessageItem {
if bottom.header.id != self.header.id {
mergedTop = false
dateAtBottom = true
} else {
mergedTop = messagesShouldBeMerged(bottom.message, message)
}
} else if let bottom = bottom as? ChatUnreadItem {
if bottom.header.id != self.header.id {
dateAtBottom = true
}
} else if let bottom = bottom as? ChatHoleItem {
if bottom.header.id != self.header.id {
dateAtBottom = true
}
} else {
dateAtBottom = true
}
return (mergedTop, mergedBottom, dateAtBottom)
}
public func updateNode(async: @escaping (@escaping () -> Void) -> Void, node: ListViewItemNode, width: CGFloat, previousItem: ListViewItem?, nextItem: ListViewItem?, animation: ListViewItemUpdateAnimation, completion: @escaping (ListViewItemNodeLayout, @escaping () -> Void) -> Void) {
if let node = node as? ChatMessageItemView {
Queue.mainQueue().async {
node.setupItem(self)
let nodeLayout = node.asyncLayout()
async {
let (top, bottom, dateAtBottom) = self.mergedWithItems(top: previousItem, bottom: nextItem)
let (layout, apply) = nodeLayout(self, width, top, bottom, dateAtBottom)
Queue.mainQueue().async {
completion(layout, {
apply(animation)
node.updateSelectionState(animated: false)
node.updateHighlightedState(animated: false)
})
}
}
}
}
}
public var description: String {
return "(ChatMessageItem id: \(self.message.id), text: \"\(self.message.text)\")"
}
}