import Foundation import UIKit import Postbox import AsyncDisplayKit import Display import SwiftSignalKit import TelegramCore import TelegramPresentationData import TelegramUIPreferences import AccountContext import Emoji import PersistentStringHash import ChatControllerInteraction import ChatHistoryEntry import ChatMessageItem import ChatMessageItemView import ChatMessageStickerItemNode import ChatMessageAnimatedStickerItemNode import ChatMessageBubbleItemNode private func mediaMergeableStyle(_ media: Media) -> ChatMessageMerge { if let story = media as? TelegramMediaStory, story.isMention { return .none } 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 .none } 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 } } let lhsSourceAuthorInfo = lhs.sourceAuthorInfo if let sourceAuthorInfo = lhsSourceAuthorInfo { if let originalAuthor = sourceAuthorInfo.originalAuthor { lhsEffectiveAuthor = lhs.peers[originalAuthor] } } for attribute in rhs.attributes { if let attribute = attribute as? SourceReferenceMessageAttribute { rhsEffectiveAuthor = rhs.peers[attribute.messageId.peerId] break } } let rhsSourceAuthorInfo = rhs.sourceAuthorInfo if let sourceAuthorInfo = rhsSourceAuthorInfo { if let originalAuthor = sourceAuthorInfo.originalAuthor { rhsEffectiveAuthor = rhs.peers[originalAuthor] } } var sameThread = true if let lhsPeer = lhs.peers[lhs.id.peerId], let rhsPeer = rhs.peers[rhs.id.peerId], arePeersEqual(lhsPeer, rhsPeer), let channel = lhsPeer as? TelegramChannel, channel.flags.contains(.isForum), lhs.threadId != rhs.threadId { sameThread = false } var sameAuthor = false if lhsEffectiveAuthor?.id == rhsEffectiveAuthor?.id && lhs.effectivelyIncoming(accountPeerId) == rhs.effectivelyIncoming(accountPeerId) { sameAuthor = true } if let lhsSourceAuthorInfo, let rhsSourceAuthorInfo { if lhsSourceAuthorInfo.originalAuthor != rhsSourceAuthorInfo.originalAuthor { sameAuthor = false } else if lhsSourceAuthorInfo.originalAuthorName != rhsSourceAuthorInfo.originalAuthorName { sameAuthor = false } } else if (lhsSourceAuthorInfo == nil) != (rhsSourceAuthorInfo == nil) { sameAuthor = false } var lhsEffectiveTimestamp = lhs.timestamp var rhsEffectiveTimestamp = rhs.timestamp if let lhsForwardInfo = lhs.forwardInfo, lhsForwardInfo.flags.contains(.isImported), let rhsForwardInfo = rhs.forwardInfo, rhsForwardInfo.flags.contains(.isImported) { lhsEffectiveTimestamp = lhsForwardInfo.date rhsEffectiveTimestamp = rhsForwardInfo.date if (lhsForwardInfo.author?.id != nil) == (rhsForwardInfo.author?.id != nil) && (lhsForwardInfo.authorSignature != nil) == (rhsForwardInfo.authorSignature != nil) { if let lhsAuthorId = lhsForwardInfo.author?.id, let rhsAuthorId = rhsForwardInfo.author?.id { sameAuthor = lhsAuthorId == rhsAuthorId } else if let lhsAuthorSignature = lhsForwardInfo.authorSignature, let rhsAuthorSignature = rhsForwardInfo.authorSignature { sameAuthor = lhsAuthorSignature == rhsAuthorSignature } } else { sameAuthor = false } } 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 } } if abs(lhsEffectiveTimestamp - rhsEffectiveTimestamp) < Int32(10 * 60) && sameAuthor && sameThread { 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.none.rawValue } break } } let style = min(upperStyle, lowerStyle) return ChatMessageMerge(rawValue: style)! } return .none } public func chatItemsHaveCommonDateHeader(_ lhs: ListViewItem, _ rhs: ListViewItem?) -> Bool{ let lhsHeader: ChatMessageDateHeader? let rhsHeader: ChatMessageDateHeader? if let lhs = lhs as? ChatMessageItemImpl { lhsHeader = lhs.dateHeader } else if let lhs = lhs as? ChatUnreadItem { lhsHeader = lhs.header } else if let lhs = lhs as? ChatReplyCountItem { lhsHeader = lhs.header } else { lhsHeader = nil } if let rhs = rhs { if let rhs = rhs as? ChatMessageItemImpl { rhsHeader = rhs.dateHeader } else if let rhs = rhs as? ChatUnreadItem { rhsHeader = rhs.header } else if let rhs = rhs as? ChatReplyCountItem { 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 ChatMessageItemImpl: ChatMessageItem, CustomStringConvertible { public let presentationData: ChatPresentationData public let context: AccountContext public let chatLocation: ChatLocation public let associatedData: ChatMessageItemAssociatedData public let controllerInteraction: ChatControllerInteraction public let content: ChatMessageItemContent public let disableDate: Bool public let effectiveAuthorId: PeerId? public let additionalContent: ChatMessageItemAdditionalContent? let dateHeader: ChatMessageDateHeader let avatarHeader: ChatMessageAvatarHeader? public let headers: [ListViewItemHeader] public var message: Message { switch self.content { case let .message(message, _, _, _, _): return message case let .group(messages): return messages[0].0 } } public var read: Bool { switch self.content { case let .message(_, read, _, _, _): return read case let .group(messages): return messages[0].1 } } public var unsent: Bool { switch self.content { case let .message(message, _, _, _, _): return message.flags.contains(.Unsent) case let .group(messages): return messages[0].0.flags.contains(.Unsent) } } public var sending: Bool { switch self.content { case let .message(message, _, _, _, _): return message.flags.contains(.Sending) case let .group(messages): return messages[0].0.flags.contains(.Sending) } } public var failed: Bool { switch self.content { case let .message(message, _, _, _, _): return message.flags.contains(.Failed) case let .group(messages): return messages[0].0.flags.contains(.Failed) } } 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 || !controllerInteraction.chatIsRotated self.additionalContent = additionalContent var avatarHeader: ChatMessageAvatarHeader? let incoming = content.effectivelyIncoming(self.context.account.peerId) var effectiveAuthor: Peer? let displayAuthorInfo: Bool let messagePeerId: PeerId = chatLocation.peerId ?? content.firstMessage.id.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: PeerId.Id._internalFromInt64Value(Int64(authorSignature.persistentHashValue % 32))), accessHash: nil, firstName: authorSignature, lastName: nil, username: nil, phone: nil, photo: [], botInfo: nil, restrictionInfo: nil, flags: [], emojiStatus: nil, usernames: [], storiesHidden: nil, nameColor: nil, backgroundEmojiId: nil, profileColor: nil, profileBackgroundEmojiId: nil) } } if let sourceAuthorInfo = content.firstMessage.sourceAuthorInfo { if let originalAuthor = sourceAuthorInfo.originalAuthor, let peer = content.firstMessage.peers[originalAuthor] { effectiveAuthor = peer } else if let authorSignature = sourceAuthorInfo.originalAuthorName { effectiveAuthor = TelegramUser(id: PeerId(namespace: Namespaces.Peer.Empty, id: PeerId.Id._internalFromInt64Value(Int64(authorSignature.persistentHashValue % 32))), accessHash: nil, firstName: authorSignature, lastName: nil, username: nil, phone: nil, photo: [], botInfo: nil, restrictionInfo: nil, flags: [], emojiStatus: nil, usernames: [], storiesHidden: nil, nameColor: nil, backgroundEmojiId: nil, profileColor: nil, profileBackgroundEmojiId: nil) } } 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 var isScheduledMessages = false if case .scheduledMessages = associatedData.subject { isScheduledMessages = true } self.dateHeader = ChatMessageDateHeader(timestamp: content.index.timestamp, scheduled: isScheduledMessages, presentationData: presentationData, controllerInteraction: controllerInteraction, context: context, action: { timestamp, alreadyThere 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), alreadyThere) } }) 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 } } else if case let .replyThread(replyThreadMessage) = chatLocation, replyThreadMessage.isChannelPost, replyThreadMessage.effectiveTopId == message.id { isBroadcastChannel = true } var hasAvatar = false if !hasActionMedia && !isBroadcastChannel { hasAvatar = true } if let adAttribute = message.adAttribute { if adAttribute.displayAvatar { hasAvatar = adAttribute.displayAvatar } } if hasAvatar { if let effectiveAuthor = effectiveAuthor { var storyStats: PeerStoryStats? if case .peer(id: context.account.peerId) = chatLocation { } else { switch content { case let .message(_, _, _, attributes, _): storyStats = attributes.authorStoryStats case let .group(messages): storyStats = messages.first?.3.authorStoryStats } } avatarHeader = ChatMessageAvatarHeader(timestamp: content.index.timestamp, peerId: effectiveAuthor.id, peer: effectiveAuthor, messageReference: MessageReference(message), message: message, presentationData: presentationData, context: context, controllerInteraction: controllerInteraction, storyStats: storyStats) } } } self.avatarHeader = avatarHeader var headers: [ListViewItemHeader] = [] if !self.disableDate { headers.append(self.dateHeader) } if case .messageOptions = associatedData.subject { headers = [] } if !controllerInteraction.chatIsRotated { headers = [] } if let avatarHeader = self.avatarHeader { headers.append(avatarHeader) } self.headers = headers } 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.isVideoSticker { viewClassName = ChatMessageAnimatedStickerItemNode.self break loop } if telegramFile.isAnimatedSticker, 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 { var isValidated = false for attribute in telegramFile.attributes { if case .hintIsValidated = attribute { isValidated = true break } } inner: for attribute in telegramFile.attributes { if case let .Sticker(_, packReference, _) = attribute { if case .name = packReference { viewClassName = ChatMessageAnimatedStickerItemNode.self } else if isValidated { viewClassName = ChatMessageAnimatedStickerItemNode.self } break inner } } } } 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 = ChatMessageBubbleItemNode.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(rotated: self.controllerInteraction.chatIsRotated) node.setupItem(self, synchronousLoad: synchronousLoads) let nodeLayout = node.asyncLayout() let (top, bottom, dateAtBottom) = self.mergedWithItems(top: previousItem, bottom: nextItem, isRotated: self.controllerInteraction.chatIsRotated) var disableDate = self.disableDate if let subject = self.associatedData.subject, case let .messageOptions(_, _, info) = subject { switch info { case .reply, .link: disableDate = true default: break } } let (layout, apply) = nodeLayout(self, params, top, bottom, dateAtBottom && !disableDate) node.contentSize = layout.contentSize node.insets = layout.insets node.safeInsets = UIEdgeInsets(top: 0.0, left: params.leftInset, bottom: 0.0, right: params.rightInset) node.updateSelectionState(animated: false) node.updateHighlightedState(animated: false) Queue.mainQueue().async { completion(node, { return (nil, { _ in apply(.None, ListViewItemApply(isOnScreen: false), synchronousLoads) }) }) } } if Thread.isMainThread { async { configure() } } else { configure() } } public func mergedWithItems(top: ListViewItem?, bottom: ListViewItem?, isRotated: Bool) -> (top: ChatMessageMerge, bottom: ChatMessageMerge, dateAtBottom: Bool) { var top = top var bottom = bottom if !isRotated { let previousTop = top top = bottom bottom = previousTop } var mergedTop: ChatMessageMerge = .none var mergedBottom: ChatMessageMerge = .none var dateAtBottom = false if let top = top as? ChatMessageItemImpl { if top.dateHeader.id != self.dateHeader.id { mergedBottom = .none } else { mergedBottom = messagesShouldBeMerged(accountPeerId: self.context.account.peerId, message, top.message) } } if let bottom = bottom as? ChatMessageItemImpl { if bottom.dateHeader.id != self.dateHeader.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.dateHeader.id { dateAtBottom = true } } else if let bottom = bottom as? ChatReplyCountItem { if bottom.header.id != self.dateHeader.id { 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, synchronousLoad: false) let nodeLayout = nodeValue.asyncLayout() let isRotated = self.controllerInteraction.chatIsRotated async { let (top, bottom, dateAtBottom) = self.mergedWithItems(top: previousItem, bottom: nextItem, isRotated: isRotated) var disableDate = self.disableDate if let subject = self.associatedData.subject, case let .messageOptions(_, _, info) = subject { switch info { case .reply, .link: disableDate = true default: break } } let (layout, apply) = nodeLayout(self, params, top, bottom, dateAtBottom && !disableDate) Queue.mainQueue().async { completion(layout, { info in apply(animation, info, false) if let nodeValue = node() as? ChatMessageItemView { nodeValue.safeInsets = UIEdgeInsets(top: 0.0, left: params.leftInset, bottom: 0.0, right: params.rightInset) nodeValue.updateSelectionState(animated: false) nodeValue.updateHighlightedState(animated: false) } }) } } } } } public var description: String { return "(ChatMessageItem id: \(self.message.id), text: \"\(self.message.text)\")" } }