import Foundation import UIKit import Postbox import AsyncDisplayKit import Display import SwiftSignalKit import TelegramCore import SyncCore import TelegramPresentationData import TelegramUIPreferences import AccountContext import Emoji import PersistentStringHash public enum ChatMessageItemContent: Sequence { case message(message: Message, read: Bool, selection: ChatHistoryMessageSelection, attributes: ChatMessageEntryAttributes) case group(messages: [(Message, Bool, ChatHistoryMessageSelection, ChatMessageEntryAttributes)]) func effectivelyIncoming(_ accountPeerId: PeerId) -> Bool { switch self { case let .message(message, _, _, _): return message.effectivelyIncoming(accountPeerId) case let .group(messages): return messages[0].0.effectivelyIncoming(accountPeerId) } } var index: MessageIndex { switch self { case let .message(message, _, _, _): return message.index case let .group(messages): return messages[0].0.index } } var firstMessage: Message { switch self { case let .message(message, _, _, _): return message case let .group(messages): return messages[0].0 } } var firstMessageAttributes: ChatMessageEntryAttributes { switch self { case let .message(message): return message.attributes case let .group(messages): return messages[0].3 } } public func makeIterator() -> AnyIterator<(Message, ChatMessageEntryAttributes)> { var index = 0 return AnyIterator { () -> (Message, ChatMessageEntryAttributes)? in switch self { case let .message(message): if index == 0 { index += 1 return (message.message, message.attributes) } else { index += 1 return nil } case let .group(messages): if index < messages.count { let currentIndex = index index += 1 return (messages[currentIndex].0, messages[currentIndex].3) } else { return nil } } } } } private func mediaMergeableStyle(_ media: Media) -> ChatMessageMerge { if let file = media as? TelegramMediaFile { for attribute in file.attributes { switch attribute { case .Sticker: return .semanticallyMerged case let .Video(_, _, flags): if flags.contains(.instantRoundVideo) { return .semanticallyMerged } default: break } } return .fullyMerged } if let _ = media as? TelegramMediaAction { return .none } if let _ = media as? TelegramMediaExpiredContent { return .none } return .fullyMerged } private func messagesShouldBeMerged(accountPeerId: PeerId, _ lhs: Message, _ rhs: Message) -> ChatMessageMerge { var lhsEffectiveAuthor: Peer? = lhs.author var rhsEffectiveAuthor: Peer? = rhs.author for attribute in lhs.attributes { if let attribute = attribute as? SourceReferenceMessageAttribute { lhsEffectiveAuthor = lhs.peers[attribute.messageId.peerId] break } } for attribute in rhs.attributes { if let attribute = attribute as? SourceReferenceMessageAttribute { rhsEffectiveAuthor = rhs.peers[attribute.messageId.peerId] break } } if lhs.id.peerId.isRepliesOrSavedMessages(accountPeerId: accountPeerId) { if let forwardInfo = lhs.forwardInfo { lhsEffectiveAuthor = forwardInfo.author } } if rhs.id.peerId.isRepliesOrSavedMessages(accountPeerId: accountPeerId) { if let forwardInfo = rhs.forwardInfo { rhsEffectiveAuthor = forwardInfo.author } } var sameAuthor = false if lhsEffectiveAuthor?.id == rhsEffectiveAuthor?.id && lhs.effectivelyIncoming(accountPeerId) == rhs.effectivelyIncoming(accountPeerId) { sameAuthor = true } if abs(lhs.timestamp - rhs.timestamp) < Int32(10 * 60) && sameAuthor { if let channel = lhs.peers[lhs.id.peerId] as? TelegramChannel, case .group = channel.info, lhsEffectiveAuthor?.id == channel.id, !lhs.effectivelyIncoming(accountPeerId) { return .none } var upperStyle: Int32 = ChatMessageMerge.fullyMerged.rawValue var lowerStyle: Int32 = ChatMessageMerge.fullyMerged.rawValue for media in lhs.media { let style = mediaMergeableStyle(media).rawValue if style < upperStyle { upperStyle = style } } for media in rhs.media { let style = mediaMergeableStyle(media).rawValue if style < lowerStyle { lowerStyle = style } } for attribute in lhs.attributes { if let attribute = attribute as? ReplyMarkupMessageAttribute { if attribute.flags.contains(.inline) && !attribute.rows.isEmpty { upperStyle = ChatMessageMerge.semanticallyMerged.rawValue } break } } let style = min(upperStyle, lowerStyle) return ChatMessageMerge(rawValue: style)! } return .none } 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 as? ChatHoleItem { //lhsHeader = lhs.header lhsHeader = nil } 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 as? ChatHoleItem { //rhsHeader = rhs.header rhsHeader = nil } 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 enum ChatMessageItemAdditionalContent { case eventLogPreviousMessage(Message) case eventLogPreviousDescription(Message) case eventLogPreviousLink(Message) } enum ChatMessageMerge: Int32 { case none = 0 case fullyMerged = 1 case semanticallyMerged = 2 var merged: Bool { if case .none = self { return false } else { return true } } } public final class ChatMessageItem: ListViewItem, CustomStringConvertible { let presentationData: ChatPresentationData let context: AccountContext let chatLocation: ChatLocation let associatedData: ChatMessageItemAssociatedData let controllerInteraction: ChatControllerInteraction let content: ChatMessageItemContent let disableDate: Bool let effectiveAuthorId: PeerId? let additionalContent: ChatMessageItemAdditionalContent? public let accessoryItem: ListViewAccessoryItem? let header: ChatMessageDateHeader var message: Message { switch self.content { case let .message(message, _, _, _): return message case let .group(messages): return messages[0].0 } } var read: Bool { switch self.content { case let .message(_, read, _, _): return read case let .group(messages): return messages[0].1 } } public init(presentationData: ChatPresentationData, context: AccountContext, chatLocation: ChatLocation, associatedData: ChatMessageItemAssociatedData, controllerInteraction: ChatControllerInteraction, content: ChatMessageItemContent, disableDate: Bool = false, additionalContent: ChatMessageItemAdditionalContent? = nil) { self.presentationData = presentationData self.context = context self.chatLocation = chatLocation self.associatedData = associatedData self.controllerInteraction = controllerInteraction self.content = content self.disableDate = disableDate self.additionalContent = additionalContent var accessoryItem: ListViewAccessoryItem? let incoming = content.effectivelyIncoming(self.context.account.peerId) var effectiveAuthor: Peer? let displayAuthorInfo: Bool let messagePeerId: PeerId switch chatLocation { case let .peer(peerId): messagePeerId = peerId case let .replyThread(messageId, _, _): messagePeerId = messageId.peerId } do { let peerId = messagePeerId if peerId.isRepliesOrSavedMessages(accountPeerId: context.account.peerId) { if let forwardInfo = content.firstMessage.forwardInfo { effectiveAuthor = forwardInfo.author if effectiveAuthor == nil, let authorSignature = forwardInfo.authorSignature { effectiveAuthor = TelegramUser(id: PeerId(namespace: Namespaces.Peer.Empty, id: Int32(clamping: authorSignature.persistentHashValue)), accessHash: nil, firstName: authorSignature, lastName: nil, username: nil, phone: nil, photo: [], botInfo: nil, restrictionInfo: nil, flags: UserInfoFlags()) } } displayAuthorInfo = incoming && effectiveAuthor != nil } else { effectiveAuthor = content.firstMessage.author for attribute in content.firstMessage.attributes { if let attribute = attribute as? SourceReferenceMessageAttribute { effectiveAuthor = content.firstMessage.peers[attribute.messageId.peerId] break } } displayAuthorInfo = incoming && peerId.isGroupOrChannel && effectiveAuthor != nil } } self.effectiveAuthorId = effectiveAuthor?.id self.header = ChatMessageDateHeader(timestamp: content.index.timestamp, scheduled: associatedData.isScheduledMessages, presentationData: presentationData, context: context, action: { timestamp in var calendar = NSCalendar.current calendar.timeZone = TimeZone(abbreviation: "UTC")! let date = Date(timeIntervalSince1970: TimeInterval(timestamp)) let components = calendar.dateComponents([.year, .month, .day], from: date) if let date = calendar.date(from: components) { controllerInteraction.navigateToFirstDateMessage(Int32(date.timeIntervalSince1970)) } }) if displayAuthorInfo { let message = content.firstMessage var hasActionMedia = false for media in message.media { if media is TelegramMediaAction { hasActionMedia = true break } } var isBroadcastChannel = false if case .peer = chatLocation { if let peer = message.peers[message.id.peerId] as? TelegramChannel, case .broadcast = peer.info { isBroadcastChannel = true } } if !hasActionMedia && !isBroadcastChannel { if let effectiveAuthor = effectiveAuthor { accessoryItem = ChatMessageAvatarAccessoryItem(context: context, peerId: effectiveAuthor.id, peer: effectiveAuthor, messageReference: MessageReference(message), messageTimestamp: content.index.timestamp, emptyColor: presentationData.theme.theme.chat.message.incoming.bubble.withoutWallpaper.fill, controllerInteraction: controllerInteraction) } } } self.accessoryItem = accessoryItem } public func nodeConfiguredForParams(async: @escaping (@escaping () -> Void) -> Void, params: ListViewItemLayoutParams, synchronousLoads: Bool, previousItem: ListViewItem?, nextItem: ListViewItem?, completion: @escaping (ListViewItemNode, @escaping () -> (Signal?, (ListViewItemApply) -> Void)) -> Void) { var viewClassName: AnyClass = ChatMessageBubbleItemNode.self loop: for media in self.message.media { if let telegramFile = media as? TelegramMediaFile { if telegramFile.isAnimatedSticker, (self.message.id.peerId.namespace == Namespaces.Peer.SecretChat || !telegramFile.previewRepresentations.isEmpty), let size = telegramFile.size, size > 0 && size <= 128 * 1024 { if self.message.id.peerId.namespace == Namespaces.Peer.SecretChat { if telegramFile.fileId.namespace == Namespaces.Media.CloudFile { viewClassName = ChatMessageAnimatedStickerItemNode.self } } else { viewClassName = ChatMessageAnimatedStickerItemNode.self } break loop } for attribute in telegramFile.attributes { switch attribute { case .Sticker: if let size = telegramFile.size, size > 0 && size <= 512 * 1024 { viewClassName = ChatMessageStickerItemNode.self } break loop case let .Video(_, _, flags): if flags.contains(.instantRoundVideo) { viewClassName = ChatMessageInstantVideoItemNode.self break loop } default: break } } } else if media is TelegramMediaAction { viewClassName = ChatMessageBubbleItemNode.self } else if media is TelegramMediaExpiredContent { viewClassName = ChatMessageBubbleItemNode.self } else if media is TelegramMediaDice { viewClassName = ChatMessageAnimatedStickerItemNode.self } } if viewClassName == ChatMessageBubbleItemNode.self && self.presentationData.largeEmoji && self.message.media.isEmpty { if case let .message(_, _, _, attributes) = self.content { switch attributes.contentTypeHint { case .largeEmoji: viewClassName = ChatMessageStickerItemNode.self case .animatedEmoji: viewClassName = ChatMessageAnimatedStickerItemNode.self default: break } } } let configure = { let node = (viewClassName as! ChatMessageItemView.Type).init() node.setupItem(self) let nodeLayout = node.asyncLayout() let (top, bottom, dateAtBottom) = self.mergedWithItems(top: previousItem, bottom: nextItem) let (layout, apply) = nodeLayout(self, params, top, bottom, dateAtBottom && !self.disableDate) node.contentSize = layout.contentSize node.insets = layout.insets node.updateSelectionState(animated: false) node.updateHighlightedState(animated: false) Queue.mainQueue().async { completion(node, { return (nil, { _ in apply(.None, synchronousLoads) }) }) } } if Thread.isMainThread { async { configure() } } else { configure() } } final func mergedWithItems(top: ListViewItem?, bottom: ListViewItem?) -> (top: ChatMessageMerge, bottom: ChatMessageMerge, dateAtBottom: Bool) { var mergedTop: ChatMessageMerge = .none var mergedBottom: ChatMessageMerge = .none var dateAtBottom = false if let top = top as? ChatMessageItem { if top.header.id != self.header.id { mergedBottom = .none } else { mergedBottom = messagesShouldBeMerged(accountPeerId: self.context.account.peerId, message, top.message) } } if let bottom = bottom as? ChatMessageItem { if bottom.header.id != self.header.id { mergedTop = .none dateAtBottom = true } else { mergedTop = messagesShouldBeMerged(accountPeerId: self.context.account.peerId, bottom.message, message) } } else if let bottom = bottom as? ChatUnreadItem { if bottom.header.id != self.header.id { dateAtBottom = true } } else if let _ = bottom as? ChatHoleItem { dateAtBottom = true } else { dateAtBottom = true } return (mergedTop, mergedBottom, dateAtBottom) } public func updateNode(async: @escaping (@escaping () -> Void) -> Void, node: @escaping () -> ListViewItemNode, params: ListViewItemLayoutParams, previousItem: ListViewItem?, nextItem: ListViewItem?, animation: ListViewItemUpdateAnimation, completion: @escaping (ListViewItemNodeLayout, @escaping (ListViewItemApply) -> Void) -> Void) { Queue.mainQueue().async { if let nodeValue = node() as? ChatMessageItemView { nodeValue.setupItem(self) let nodeLayout = nodeValue.asyncLayout() async { let (top, bottom, dateAtBottom) = self.mergedWithItems(top: previousItem, bottom: nextItem) let (layout, apply) = nodeLayout(self, params, top, bottom, dateAtBottom && !self.disableDate) Queue.mainQueue().async { completion(layout, { _ in apply(animation, false) if let nodeValue = node() as? ChatMessageItemView { nodeValue.updateSelectionState(animated: false) nodeValue.updateHighlightedState(animated: false) } }) } } } } } public var description: String { return "(ChatMessageItem id: \(self.message.id), text: \"\(self.message.text)\")" } }