Files
Telegram-iOS/submodules/TelegramUI/Sources/ChatHistoryEntriesForView.swift
T
isaacisaacClaude Opus 4.7
69bfc65da7 Postbox refactor waves 138–276: 168-commit squash
Consolidates 137 wave commits + 31 supporting commits (CLAUDE.md bump,
typealias additions, AnyObject→EngineMedia restoration) into one squashed
commit. Migrates dozens of consumer-side public APIs, struct fields,
protocol methods, and enum payloads from Postbox protocols/structs to
TelegramEngine engine wrappers and typealiases. Drops `import Postbox`
from many files. Adds new TelegramCore typealiases and one
TelegramEngineUnauthorized facade.

Notable changes by category:

**TelegramCore typealias additions** (rule 2 — narrow utility typealiases):
- EngineChatListIndex, EngineTempBoxFile, EngineItemCollectionItemIndex,
  EngineItemCollectionViewEntryIndex, EngineValueBoxEncryptionParameters,
  EngineMessageAndThreadId, EnginePeerStoryStats, EngineMessageHistoryAnchorIndex,
  EngineChatListTotalUnreadStateCategory, EngineChatListTotalUnreadStateStats,
  EnginePeerSummaryCounterTags, EngineChatListTotalUnreadState,
  EngineItemCacheEntryId, EngineHashFunctions,
  EngineCachedMediaResourceRepresentationResult,
  EngineMediaResourceDataFetchResult, EngineMediaResourceDataFetchError,
  EngineMediaResourceStatus, EngineCachedPeerData

**TelegramCore engine extensions/forwarders**:
- EngineMessage.engineMedia, EngineMessage.enginePeers,
  EngineMessage.adAttribute, EngineMessage.effectivelyIncoming
- engineFileSize forwarder
- TelegramEngine.Resources.clearCachedMediaResources(mediaResourceIds: Set<EngineMediaResource.Id>)
- TelegramEngine.Resources.fetchStatus(id:resourceSize:)
- TelegramEngineUnauthorized.UnauthorizedResources facade with storeResourceData

**Public API/struct migrations to engine types**:
- ChatAvailableMessageActions.banAuthor/banAuthors → EnginePeer?/[EnginePeer]
- WebSessionsContextState.peers → [EnginePeer.Id: EnginePeer]
- CacheUsageStats.peers → [EnginePeer.Id: EnginePeer]
- PeerCommand.peer → EnginePeer
- PeerInfoControllerMode.calls(messages:) → [EngineMessage]
- CallControllerNodeProtocol.updatePeer → EnginePeer params
- ChatHistoryListNode.messageInCurrentHistoryView (and 4 variants) → EngineMessage?
- ChatHistorySearchContainerNode.messageForGallery → EngineMessage?
- PeerInfoPaneNode.findLoadedMessage / ensureMessageIsVisible /
  transitionNodeForGallery → engine-typed
- GalleryHiddenMediaTarget.getTransitionInfo /
  GalleryHiddenMediaManager.findTarget → engine-typed
- ChatPanelInterfaceInteraction.presentReactionDeletionOptions /
  presentBan*MessageOptions → EnginePeer
- DrawingMessageRenderer.messages → [EngineMessage]
- ChatVideoGalleryItemScrubberView.setFetchStatusSignal →
  EngineMediaResource.FetchStatus
- ChannelDiscussionGroupActionSheetItem.peer, VoiceChatPeerEntry.peer,
  VoiceChatFullscreenParticipantItem.peer, MediaStreamComponent.chatPeer,
  MediaStreamVideoComponent.callPeer, ChatMessageContactBubbleContentNode.contactPeer,
  ChatMessageForwardInfoNode.peer, ChatMessageCommentFooterContentNode.replyPeers,
  ChatReportPeerTitlePanelNode.peer, ChatMessageActionUrlAuthController.bot,
  PeerMediaCollectionInterfaceState.peer, ChatMessageCallBubbleContentNode.peopleAvatars,
  ChatLoadingNode.renderedPeer (→ EngineRenderedPeer) — all to engine types

**Wave-71-shadow stored-field migrations** (Postbox Peer/Message → Engine wrapper):
- LegacyCallControllerNode.peer
- CallStatusBarNode.currentPeer

**Dead-code / dead-field removals**:
- CallController.peer, CallControllerNodeV2.account,
  ContactMultiselectionController PeerNameIndex fields,
  preparedChatListNodeViewTransition account: Account param,
  FetchResource.swift entirely (unused function)

**Module-level Postbox import drops**: 30+ files including TelegramRootController,
EditStories, GiftViewScreen, AnimatedStickerUtils, FetchPhotoLibraryImageResource,
PeerInfoGiftsPaneNode, PeerInfoPaneContainerNode, PresentAddMembers,
PeerInfoProfileItems, ChatControllerAdminBanUsers, PresentationData typealiases,
DefaultDayPresentationTheme, ChatListViewTransition, GalleryHiddenMediaManager,
RecentSessionsController, GifContext, AuthorizationSequenceController,
PeerInfoHeaderEditingContentNode, PeerInfoHeaderNode,
PeerAllowedReactionListController, CallControllerNodeV2, and 6 PeerInfo pane files.

**AnyObject restoration**: rule 8 added (never substitute Postbox protocols
with Any/AnyObject) — undid previous AnyObject substitutions in waves 141/143
back to EngineMedia.

Doc maintenance: CLAUDE.md updated to reflect new typealiases and forwarders.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-05 00:44:27 +02:00

856 lines
44 KiB
Swift

import Foundation
import UIKit
import Postbox
import TelegramCore
import TemporaryCachedPeerDataManager
import Emoji
import AccountContext
import TelegramPresentationData
import ChatHistoryEntry
import ChatMessageItemCommon
import TextFormat
import Markdown
import Display
import TelegramStringFormatting
struct ChatHistoryEntriesForViewState {
private var messageStableIdToLocalId: [UInt32: Int64] = [:]
init() {
}
mutating func messageGroupStableId(messageStableId: UInt32, groupId: Int64, isLocal: Bool) -> Int64 {
if isLocal {
self.messageStableIdToLocalId[messageStableId] = groupId
return groupId
} else {
if let value = self.messageStableIdToLocalId[messageStableId] {
return value
} else {
return groupId
}
}
}
}
func chatHistoryEntriesForView(
currentState: ChatHistoryEntriesForViewState,
context: AccountContext,
location: ChatLocation,
subject: ChatControllerSubject?,
view: MessageHistoryView,
includeUnreadEntry: Bool,
includeEmptyEntry: Bool,
includeChatInfoEntry: Bool,
includeEmbeddedSavedChatInfo: Bool,
reverse: Bool,
groupMessages: Bool,
reverseGroupedMessages: Bool,
selectedMessages: Set<MessageId>?,
presentationData: ChatPresentationData,
historyAppearsCleared: Bool,
skipViewOnceMedia: Bool,
pendingUnpinnedAllMessages: Bool,
pendingRemovedMessages: Set<MessageId>,
associatedData: ChatMessageItemAssociatedData,
updatingMedia: [MessageId: ChatUpdatingMessageMedia],
customChannelDiscussionReadState: MessageId?,
customThreadOutgoingReadState: MessageId?,
cachedData: CachedPeerData?,
adMessage: Message?,
dynamicAdMessages: [Message],
isMusicPlaylist: Bool,
pinToTopStableId: EngineMessage.StableId?
) -> ([ChatHistoryEntry], ChatHistoryEntriesForViewState) {
var currentState = currentState
if historyAppearsCleared {
return ([], currentState)
}
var entries: [ChatHistoryEntry] = []
var adminRanks: [PeerId: CachedChannelAdminRank] = [:]
var stickersEnabled = true
var chatPeer: Peer?
var cachedPeerDataPeers: [PeerId: Peer] = [:]
if let peerId = location.peerId {
for additionalEntry in view.additionalData {
if case let .cacheEntry(id, data) = additionalEntry {
if peerId.namespace == Namespaces.Peer.CloudChannel {
if id == cachedChannelAdminRanksEntryId(peerId: peerId), let data = data?.get(CachedChannelAdminRanks.self) {
adminRanks = data.ranks
}
}
} else if case let .peer(_, peer) = additionalEntry {
chatPeer = peer
if let channel = peer as? TelegramChannel, !channel.flags.contains(.isGigagroup) {
if let defaultBannedRights = channel.defaultBannedRights, defaultBannedRights.flags.contains(.banSendStickers) {
stickersEnabled = false
}
}
} else if case let .cachedPeerData(_, peerData) = additionalEntry {
if let cachedGroupData = peerData as? CachedGroupData, let participants = cachedGroupData.participants {
for participant in participants.participants {
switch participant {
case let .member(_, _, _, rank):
adminRanks[participant.peerId] = .member(rank)
case let .creator(_, rank):
adminRanks[participant.peerId] = .creator(rank)
case let .admin(_, _, _, rank):
adminRanks[participant.peerId] = .admin(rank)
}
}
}
} else if case let .cachedPeerDataPeers(_, peers) = additionalEntry {
cachedPeerDataPeers = peers
}
}
}
var joinMessage: Message?
if (associatedData.subject?.isService ?? false) {
} else {
if let peer = chatPeer as? TelegramChannel, case .broadcast = peer.info, case .member = peer.participationStatus, !peer.flags.contains(.isCreator) {
joinMessage = Message(
stableId: UInt32.max - 1000,
stableVersion: 0,
id: MessageId(peerId: peer.id, namespace: Namespaces.Message.Local, id: 0),
globallyUniqueId: nil,
groupingKey: nil,
groupInfo: nil,
threadId: nil,
timestamp: peer.creationDate,
flags: [.Incoming],
tags: [],
globalTags: [],
localTags: [],
customTags: [],
forwardInfo: nil,
author: chatPeer,
text: "",
attributes: [],
media: [TelegramMediaAction(action: .joinedChannel)],
peers: SimpleDictionary<PeerId, Peer>(),
associatedMessages: SimpleDictionary<MessageId, Message>(),
associatedMessageIds: [],
associatedMedia: [:],
associatedThreadInfo: nil,
associatedStories: [:]
)
}
}
var count = 0
loop: for entry in view.entries {
var message = entry.message
var isRead = entry.isRead
var pinToTop = false
if message.stableId == pinToTopStableId {
pinToTop = true
}
if pendingRemovedMessages.contains(message.id) {
continue
}
if case let .replyThread(replyThreadMessage) = location, replyThreadMessage.isForumPost {
for media in message.media {
if let action = media as? TelegramMediaAction {
if case .topicCreated = action.action {
continue loop
} else if case .groupCreated = action.action {
var chatPeer: Peer?
for entry in view.additionalData {
if case let .peer(_, peer) = entry {
chatPeer = peer
}
}
if let channel = chatPeer as? TelegramChannel, channel.isMonoForum {
continue loop
} else if let user = chatPeer as? TelegramUser, user.isForum {
continue loop
}
}
}
}
} else if case .peer = location {
for media in message.media {
if let action = media as? TelegramMediaAction, case .groupCreated = action.action {
var chatPeer: Peer?
for entry in view.additionalData {
if case let .peer(_, peer) = entry {
chatPeer = peer
}
}
if let channel = chatPeer as? TelegramChannel, channel.isMonoForum {
continue loop
} else if let user = chatPeer as? TelegramChannel, user.isForum {
continue loop
}
}
}
}
count += 1
if let customThreadOutgoingReadState = customThreadOutgoingReadState {
isRead = customThreadOutgoingReadState >= message.id
}
if let customChannelDiscussionReadState = customChannelDiscussionReadState {
attibuteLoop: for i in 0 ..< message.attributes.count {
if let attribute = message.attributes[i] as? ReplyThreadMessageAttribute {
if let maxReadMessageId = attribute.maxReadMessageId {
if maxReadMessageId < customChannelDiscussionReadState.id {
var attributes = message.attributes
attributes[i] = ReplyThreadMessageAttribute(count: attribute.count, latestUsers: attribute.latestUsers, commentsPeerId: attribute.commentsPeerId, maxMessageId: attribute.maxMessageId, maxReadMessageId: customChannelDiscussionReadState.id)
message = message.withUpdatedAttributes(attributes)
}
}
break attibuteLoop
}
}
}
if skipViewOnceMedia, let minAutoremoveOrClearTimeout = message.minAutoremoveOrClearTimeout {
if minAutoremoveOrClearTimeout <= 60 {
continue loop
}
}
var contentTypeHint: ChatMessageEntryContentType = .generic
for media in message.media {
if media is TelegramMediaDice {
contentTypeHint = .animatedEmoji
}
if let action = media as? TelegramMediaAction {
switch action.action {
case .channelMigratedFromGroup, .groupMigratedToChannel, .historyCleared:
continue loop
default:
break
}
}
}
var adminRank: CachedChannelAdminRank?
if let author = message.author {
adminRank = adminRanks[author.id]
}
if presentationData.largeEmoji, message.media.isEmpty {
if messageIsEligibleForLargeCustomEmoji(EngineMessage(message)) {
contentTypeHint = .animatedEmoji
} else if stickersEnabled && message.text.count == 1, let _ = associatedData.animatedEmojiStickers[message.text.basicEmoji.0], (message.textEntitiesAttribute?.entities.isEmpty ?? true) {
contentTypeHint = .animatedEmoji
} else if messageIsEligibleForLargeEmoji(EngineMessage(message)) {
contentTypeHint = .animatedEmoji
}
}
if groupMessages || reverseGroupedMessages {
if let messageGroupingKey = message.groupingKey {
let selection: ChatHistoryMessageSelection
if let selectedMessages = selectedMessages {
selection = .selectable(selected: selectedMessages.contains(message.id), num: nil)
} else {
selection = .none
}
var isCentered = false
if case let .messageOptions(_, _, info) = associatedData.subject, case let .link(link) = info {
isCentered = link.isCentered
}
let attributes = ChatMessageEntryAttributes(rank: adminRank, isContact: entry.attributes.authorIsContact, contentTypeHint: contentTypeHint, updatingMedia: updatingMedia[message.id], isPlaying: message.index == associatedData.currentlyPlayingMessageId, isCentered: isCentered, authorStoryStats: message.author.flatMap { view.peerStoryStats[$0.id] }, displayContinueThreadFooter: false, pinToTop: pinToTop)
let groupStableId = currentState.messageGroupStableId(messageStableId: message.stableId, groupId: messageGroupingKey, isLocal: Namespaces.Message.allLocal.contains(message.id.namespace))
var found = false
for i in 0 ..< entries.count {
if case let .MessageEntry(currentMessage, _, currentIsRead, currentLocation, currentSelection, currentAttributes) = entries[i], let currentGroupingKey = currentMessage.groupingKey, currentState.messageGroupStableId(messageStableId: currentMessage.stableId, groupId: currentGroupingKey, isLocal: Namespaces.Message.allLocal.contains(currentMessage.id.namespace)) == groupStableId {
found = true
var currentMessages: [(Message, Bool, ChatHistoryMessageSelection, ChatMessageEntryAttributes, MessageHistoryEntryLocation?)] = []
currentMessages.append((currentMessage, currentIsRead, currentSelection, currentAttributes, currentLocation))
if reverseGroupedMessages {
currentMessages.insert((message, isRead, selection, attributes, entry.location), at: 0)
} else {
currentMessages.append((message, isRead, selection, attributes, entry.location))
}
entries[i] = .MessageGroupEntry(groupStableId, currentMessages, presentationData)
} else if case let .MessageGroupEntry(currentGroupStableId, currentMessages, _) = entries[i], currentGroupStableId == groupStableId {
found = true
var currentMessages = currentMessages
if reverseGroupedMessages {
currentMessages.insert((message, isRead, selection, attributes, entry.location), at: 0)
} else {
currentMessages.append((message, isRead, selection, attributes, entry.location))
}
entries[i] = .MessageGroupEntry(currentGroupStableId, currentMessages, presentationData)
}
}
if !found {
entries.append(.MessageEntry(message, presentationData, isRead, entry.location, selection, attributes))
}
} else {
let selection: ChatHistoryMessageSelection
if let selectedMessages = selectedMessages {
selection = .selectable(selected: selectedMessages.contains(message.id), num: nil)
} else {
selection = .none
}
var isCentered = false
if case let .messageOptions(_, _, info) = associatedData.subject, case let .link(link) = info {
isCentered = link.isCentered
}
entries.append(.MessageEntry(message, presentationData, isRead, entry.location, selection, ChatMessageEntryAttributes(rank: adminRank, isContact: entry.attributes.authorIsContact, contentTypeHint: contentTypeHint, updatingMedia: updatingMedia[message.id], isPlaying: message.index == associatedData.currentlyPlayingMessageId, isCentered: isCentered, authorStoryStats: message.author.flatMap { view.peerStoryStats[$0.id] }, displayContinueThreadFooter: false, pinToTop: pinToTop)))
}
} else {
let selection: ChatHistoryMessageSelection
if let selectedMessages = selectedMessages {
selection = .selectable(selected: selectedMessages.contains(message.id), num: nil)
} else {
selection = .none
}
entries.append(.MessageEntry(message, presentationData, isRead, entry.location, selection, ChatMessageEntryAttributes(rank: adminRank, isContact: entry.attributes.authorIsContact, contentTypeHint: contentTypeHint, updatingMedia: updatingMedia[message.id], isPlaying: message.index == associatedData.currentlyPlayingMessageId, isCentered: false, authorStoryStats: message.author.flatMap { view.peerStoryStats[$0.id] }, displayContinueThreadFooter: false, pinToTop: pinToTop)))
}
}
if !groupMessages && reverseGroupedMessages {
var flatEntries: [ChatHistoryEntry] = []
for entry in entries {
switch entry {
case let .MessageGroupEntry(_, messages, presentationData):
for (message, isRead, selection, attributes, location) in messages {
flatEntries.append(.MessageEntry(message, presentationData, isRead, location, selection, attributes))
}
default:
flatEntries.append(entry)
}
}
entries = flatEntries
}
var addBotForumHeader = false
if location.threadId == nil, let user = chatPeer as? TelegramUser, let botInfo = user.botInfo, botInfo.flags.contains(.hasForum), botInfo.flags.contains(.forumManagedByUser), !entries.isEmpty, !view.holeEarlier, !view.isLoading {
addBotForumHeader = true
outer: for i in (0 ..< entries.count).reversed() {
switch entries[i] {
case let .MessageEntry(message, presentationData, isRead, location, selection, attributes):
if message.threadId == nil {
continue outer
}
for media in message.media {
if let _ = media as? TelegramMediaAction {
continue outer
}
}
var attributes = attributes
if let subject, case .pinnedMessages = subject {
} else {
attributes.displayContinueThreadFooter = true
}
entries[i] = .MessageEntry(message, presentationData, isRead, location, selection, attributes)
break outer
default:
break
}
}
}
let insertPendingProcessingMessage: ([Message], Int) -> Void = { messages, index in
let serviceMessage = Message(
stableId: UInt32.max - messages[0].stableId,
stableVersion: 0,
id: MessageId(peerId: messages[0].id.peerId, namespace: -1, id: messages[0].id.id),
globallyUniqueId: nil,
groupingKey: nil,
groupInfo: nil,
threadId: nil,
timestamp: messages[0].timestamp,
flags: [.Incoming],
tags: [],
globalTags: [],
localTags: [],
customTags: [],
forwardInfo: nil,
author: nil,
text: "",
attributes: [],
media: [TelegramMediaAction(action: .customText(text: presentationData.strings.Chat_VideoProcessingServiceMessage(Int32(messages.count)), entities: [], additionalAttributes: nil))],
peers: SimpleDictionary<PeerId, Peer>(),
associatedMessages: SimpleDictionary<MessageId, Message>(),
associatedMessageIds: [],
associatedMedia: [:],
associatedThreadInfo: nil,
associatedStories: [:]
)
entries.insert(.MessageEntry(serviceMessage, presentationData, false, nil, .none, ChatMessageEntryAttributes(rank: nil, isContact: false, contentTypeHint: .generic, updatingMedia: nil, isPlaying: false, isCentered: false, authorStoryStats: nil, displayContinueThreadFooter: false, pinToTop: false)), at: index)
}
for i in (0 ..< entries.count).reversed() {
switch entries[i] {
case let .MessageEntry(message, _, _, _, _, _):
if message.id.namespace == Namespaces.Message.ScheduledCloud && message.pendingProcessingAttribute != nil {
insertPendingProcessingMessage([message], i)
}
case let .MessageGroupEntry(_, messages, _):
if !messages.isEmpty && messages[0].0.id.namespace == Namespaces.Message.ScheduledCloud {
var videoCount = 0
for message in messages {
if message.0.pendingProcessingAttribute != nil {
videoCount += 1
}
}
if videoCount != 0 {
insertPendingProcessingMessage(messages.map(\.0), i)
}
}
default:
break
}
}
if let lowerTimestamp = view.entries.last?.message.timestamp, let upperTimestamp = view.entries.first?.message.timestamp {
if let joinMessage {
var insertAtPosition: Int?
if joinMessage.timestamp >= lowerTimestamp && view.laterId == nil {
insertAtPosition = entries.count
} else if joinMessage.timestamp < lowerTimestamp && joinMessage.timestamp > upperTimestamp {
for i in 0 ..< entries.count {
if let timestamp = entries[i].timestamp, timestamp > joinMessage.timestamp {
insertAtPosition = i
break
}
}
}
if let insertAtPosition {
entries.insert(.MessageEntry(joinMessage, presentationData, false, nil, .none, ChatMessageEntryAttributes(rank: nil, isContact: false, contentTypeHint: .generic, updatingMedia: nil, isPlaying: false, isCentered: false, authorStoryStats: nil, displayContinueThreadFooter: false, pinToTop: false)), at: insertAtPosition)
}
}
}
if let maxReadIndex = view.maxReadIndex, includeUnreadEntry {
var i = 0
let unreadEntry: ChatHistoryEntry = .UnreadEntry(maxReadIndex, presentationData)
for entry in entries {
if case let .MessageGroupEntry(_, messages, _) = entry {
if !messages.isEmpty && maxReadIndex >= messages[0].0.index {
i += 1
continue
}
}
if entry > unreadEntry {
if i != 0 {
entries.insert(unreadEntry, at: i)
}
break
}
i += 1
}
}
var addedThreadHead = false
if case let .replyThread(replyThreadMessage) = location, !replyThreadMessage.isForumPost, view.earlierId == nil, !view.holeEarlier, !view.isLoading, !isMusicPlaylist {
loop: for entry in view.additionalData {
switch entry {
case let .message(id, messages) where id == replyThreadMessage.effectiveTopId:
if !messages.isEmpty {
let selection: ChatHistoryMessageSelection = .none
let topMessage = messages[0]
var hasTopicCreated = false
inner: for media in topMessage.media {
if let action = media as? TelegramMediaAction {
switch action.action {
case .topicCreated:
hasTopicCreated = true
break inner
default:
break
}
}
}
var adminRank: CachedChannelAdminRank?
if let author = topMessage.author {
adminRank = adminRanks[author.id]
}
var contentTypeHint: ChatMessageEntryContentType = .generic
if presentationData.largeEmoji, topMessage.media.isEmpty {
if messageIsEligibleForLargeCustomEmoji(EngineMessage(topMessage)) {
contentTypeHint = .animatedEmoji
} else if stickersEnabled && topMessage.text.count == 1, let _ = associatedData.animatedEmojiStickers[topMessage.text.basicEmoji.0] {
contentTypeHint = .animatedEmoji
} else if messageIsEligibleForLargeEmoji(EngineMessage(topMessage)) {
contentTypeHint = .animatedEmoji
}
}
addedThreadHead = true
if messages.count > 1, let groupingKey = messages[0].groupingKey {
var groupMessages: [(Message, Bool, ChatHistoryMessageSelection, ChatMessageEntryAttributes, MessageHistoryEntryLocation?)] = []
for message in messages {
groupMessages.append((message, false, .none, ChatMessageEntryAttributes(rank: adminRank, isContact: false, contentTypeHint: contentTypeHint, updatingMedia: updatingMedia[message.id], isPlaying: false, isCentered: false, authorStoryStats: message.author.flatMap { view.peerStoryStats[$0.id] }, displayContinueThreadFooter: false, pinToTop: false), nil))
}
entries.insert(.MessageGroupEntry(groupingKey, groupMessages, presentationData), at: 0)
} else {
if !hasTopicCreated {
entries.insert(.MessageEntry(messages[0], presentationData, false, nil, selection, ChatMessageEntryAttributes(rank: adminRank, isContact: false, contentTypeHint: contentTypeHint, updatingMedia: updatingMedia[messages[0].id], isPlaying: false, isCentered: false, authorStoryStats: messages[0].author.flatMap { view.peerStoryStats[$0.id] }, displayContinueThreadFooter: false, pinToTop: false)), at: 0)
}
}
if !replyThreadMessage.isForumPost {
let replyCount = view.entries.isEmpty ? 0 : 1
entries.insert(.ReplyCountEntry(messages[0].index, replyThreadMessage.isChannelPost, replyCount, presentationData), at: 1)
}
}
break loop
default:
break
}
}
}
if includeChatInfoEntry {
if view.earlierId == nil, !view.isLoading {
var chatPeer: Peer?
var cachedPeerData: CachedPeerData?
for entry in view.additionalData {
if case let .cachedPeerData(_, data) = entry {
cachedPeerData = data
} else if case let .peer(_, peer) = entry {
chatPeer = peer
}
}
if case let .peer(peerId) = location, peerId.isReplies {
entries.insert(.ChatInfoEntry(.botInfo(title: "", text: presentationData.strings.RepliesChat_DescriptionText, photo: nil, video: nil, peer: nil, managedByBot: nil), presentationData), at: 0)
} else if case let .peer(peerId) = location, peerId.isVerificationCodes {
entries.insert(.ChatInfoEntry(.botInfo(title: "", text: presentationData.strings.VerificationCodes_DescriptionText, photo: nil, video: nil, peer: nil, managedByBot: nil), presentationData), at: 0)
} else if let cachedPeerData = cachedPeerData as? CachedUserData {
if let botInfo = cachedPeerData.botInfo, !botInfo.description.isEmpty {
if location.threadId == nil {
if let subject, case .pinnedMessages = subject {
} else {
entries.insert(.ChatInfoEntry(.botInfo(title: presentationData.strings.Bot_DescriptionTitle, text: botInfo.description, photo: botInfo.photo, video: botInfo.video, peer: nil, managedByBot: nil), presentationData), at: 0)
}
}
} else if let botManagerId = cachedPeerData.botManagerId, let managedByBot = cachedPeerDataPeers[botManagerId], let peer = chatPeer.flatMap(EnginePeer.init) {
entries.insert(.ChatInfoEntry(.botInfo(title: "", text: "", photo: nil, video: nil, peer: peer, managedByBot: EnginePeer(managedByBot)), presentationData), at: 0)
} else if let peerStatusSettings = cachedPeerData.peerStatusSettings, peerStatusSettings.registrationDate != nil || peerStatusSettings.phoneCountry != nil {
if peerStatusSettings.flags.contains(.canAddContact) || peerStatusSettings.flags.contains(.canReport) || peerStatusSettings.flags.contains(.canBlock) {
if let chatPeer, let photoChangeDate = peerStatusSettings.photoChangeDate, photoChangeDate > 0 {
let timeText = stringForIntervalSinceUpdateAction(strings: presentationData.strings, value: photoChangeDate)
let text = presentationData.strings.Chat_NonContactUser_UpdatedPhoto(timeText)
var entities: [MessageTextEntity] = []
for range in text.ranges {
entities.append(MessageTextEntity(range: range.range.lowerBound ..< range.range.upperBound, type: .Bold))
}
let message = Message(
stableId: UInt32.max - 1001,
stableVersion: 0,
id: MessageId(peerId: chatPeer.id, namespace: Namespaces.Message.Local, id: -1),
globallyUniqueId: nil,
groupingKey: nil,
groupInfo: nil,
threadId: nil,
timestamp: 2,
flags: [.Incoming],
tags: [],
globalTags: [],
localTags: [],
customTags: [],
forwardInfo: nil,
author: chatPeer,
text: "",
attributes: [],
media: [TelegramMediaAction(action: .customText(
text: text.string,
entities: entities,
additionalAttributes: nil
))],
peers: SimpleDictionary<PeerId, Peer>(),
associatedMessages: SimpleDictionary<MessageId, Message>(),
associatedMessageIds: [],
associatedMedia: [:],
associatedThreadInfo: nil,
associatedStories: [:]
)
entries.insert(.MessageEntry(message, presentationData, false, nil, .none, ChatMessageEntryAttributes(rank: nil, isContact: false, contentTypeHint: .generic, updatingMedia: nil, isPlaying: false, isCentered: false, authorStoryStats: nil, displayContinueThreadFooter: false, pinToTop: false)), at: 0)
}
if let chatPeer, let nameChangeDate = peerStatusSettings.nameChangeDate, nameChangeDate > 0 {
let timeText = stringForIntervalSinceUpdateAction(strings: presentationData.strings, value: nameChangeDate)
let text = presentationData.strings.Chat_NonContactUser_UpdatedName(timeText)
var entities: [MessageTextEntity] = []
for range in text.ranges {
entities.append(MessageTextEntity(range: range.range.lowerBound ..< range.range.upperBound, type: .Bold))
}
let message = Message(
stableId: UInt32.max - 1002,
stableVersion: 0,
id: MessageId(peerId: chatPeer.id, namespace: Namespaces.Message.Local, id: -2),
globallyUniqueId: nil,
groupingKey: nil,
groupInfo: nil,
threadId: nil,
timestamp: 1,
flags: [.Incoming],
tags: [],
globalTags: [],
localTags: [],
customTags: [],
forwardInfo: nil,
author: chatPeer,
text: "",
attributes: [],
media: [TelegramMediaAction(action: .customText(
text: text.string,
entities: entities,
additionalAttributes: nil
))],
peers: SimpleDictionary<PeerId, Peer>(),
associatedMessages: SimpleDictionary<MessageId, Message>(),
associatedMessageIds: [],
associatedMedia: [:],
associatedThreadInfo: nil,
associatedStories: [:]
)
entries.insert(.MessageEntry(message, presentationData, false, nil, .none, ChatMessageEntryAttributes(rank: nil, isContact: false, contentTypeHint: .generic, updatingMedia: nil, isPlaying: false, isCentered: false, authorStoryStats: nil, displayContinueThreadFooter: false, pinToTop: false)), at: 0)
}
if let peer = chatPeer.flatMap(EnginePeer.init) {
entries.insert(.ChatInfoEntry(.userInfo(peer: peer, verification: cachedPeerData.verification, registrationDate: peerStatusSettings.registrationDate, phoneCountry: peerStatusSettings.phoneCountry, groupsInCommonCount: cachedPeerData.commonGroupCount), presentationData), at: 0)
}
}
}
} else {
var isEmpty = true
if entries.count <= 3 {
loop: for entry in view.entries {
var isEmptyMedia = false
var isPeerJoined = false
for media in entry.message.media {
if let action = media as? TelegramMediaAction {
switch action.action {
case .groupCreated, .photoUpdated, .channelMigratedFromGroup, .groupMigratedToChannel:
isEmptyMedia = true
case .peerJoined:
isPeerJoined = true
default:
break
}
}
}
var isCreator = false
if let peer = entry.message.peers[entry.message.id.peerId] as? TelegramGroup, case .creator = peer.role {
isCreator = true
} else if let peer = entry.message.peers[entry.message.id.peerId] as? TelegramChannel, case .group = peer.info, peer.flags.contains(.isCreator) {
isCreator = true
}
if isPeerJoined || (isEmptyMedia && isCreator) {
} else {
isEmpty = false
break loop
}
}
} else {
isEmpty = false
}
if addedThreadHead {
isEmpty = false
}
if isEmpty {
entries.removeAll()
}
}
}
if !dynamicAdMessages.isEmpty {
assert(entries.sorted() == entries)
for message in dynamicAdMessages {
entries.append(.MessageEntry(message, presentationData, false, nil, .none, ChatMessageEntryAttributes(rank: nil, isContact: false, contentTypeHint: .generic, updatingMedia: nil, isPlaying: false, isCentered: false, authorStoryStats: nil, displayContinueThreadFooter: false, pinToTop: false)))
}
entries.sort()
}
if view.laterId == nil && !view.isLoading {
if !entries.isEmpty, case let .MessageEntry(lastMessage, _, _, _, _, _) = entries[entries.count - 1], let message = adMessage {
var nextAdMessageId: Int32 = 10000
let updatedMessage = Message(
stableId: ChatHistoryListNodeImpl.fixedAdMessageStableId,
stableVersion: message.stableVersion,
id: MessageId(peerId: message.id.peerId, namespace: message.id.namespace, id: nextAdMessageId),
globallyUniqueId: nil,
groupingKey: nil,
groupInfo: nil,
threadId: nil,
timestamp: lastMessage.timestamp,
flags: message.flags,
tags: message.tags,
globalTags: message.globalTags,
localTags: message.localTags,
customTags: message.customTags,
forwardInfo: message.forwardInfo,
author: message.author,
text: /*"\(message.adAttribute!.opaqueId.hashValue)" + */message.text,
attributes: message.attributes,
media: message.media,
peers: message.peers,
associatedMessages: message.associatedMessages,
associatedMessageIds: message.associatedMessageIds,
associatedMedia: message.associatedMedia,
associatedThreadInfo: message.associatedThreadInfo,
associatedStories: message.associatedStories
)
nextAdMessageId += 1
entries.append(.MessageEntry(updatedMessage, presentationData, false, nil, .none, ChatMessageEntryAttributes(rank: nil, isContact: false, contentTypeHint: .generic, updatingMedia: nil, isPlaying: false, isCentered: false, authorStoryStats: nil, displayContinueThreadFooter: false, pinToTop: false)))
}
}
}
if addBotForumHeader {
if let subject, case .pinnedMessages = subject {
} else {
entries.append(.ChatInfoEntry(.newThreadInfo, presentationData))
}
}
if includeEmbeddedSavedChatInfo, let peerId = location.peerId {
if !view.isLoading && view.laterId == nil {
let string = presentationData.strings.Chat_SavedMessagesTabInfoText
let formattedString = parseMarkdownIntoAttributedString(
string,
attributes: MarkdownAttributes(
body: MarkdownAttributeSet(font: Font.regular(15.0), textColor: .black),
bold: MarkdownAttributeSet(font: Font.regular(15.0), textColor: .black),
link: MarkdownAttributeSet(font: Font.regular(15.0), textColor: .white),
linkAttribute: { url in
return ("URL", url)
}
)
)
var entities: [MessageTextEntity] = []
formattedString.enumerateAttribute(.foregroundColor, in: NSRange(location: 0, length: formattedString.length), options: [], using: { value, range, _ in
if let value = value as? UIColor, value == .white {
entities.append(MessageTextEntity(range: range.lowerBound ..< range.upperBound, type: .Bold))
}
})
formattedString.enumerateAttribute(NSAttributedString.Key(rawValue: "URL"), in: NSRange(location: 0, length: formattedString.length), options: [], using: { value, range, _ in
if value != nil {
entities.append(MessageTextEntity(range: range.lowerBound ..< range.upperBound, type: .TextMention(peerId: context.account.peerId)))
}
})
let message = Message(
stableId: UInt32.max - 1001,
stableVersion: 0,
id: MessageId(peerId: peerId, namespace: Namespaces.Message.Local, id: 123),
globallyUniqueId: nil,
groupingKey: nil,
groupInfo: nil,
threadId: nil,
timestamp: Int32.max - 1,
flags: [.Incoming],
tags: [],
globalTags: [],
localTags: [],
customTags: [],
forwardInfo: nil,
author: nil,
text: "",
attributes: [],
media: [TelegramMediaAction(action: .customText(text: formattedString.string, entities: entities, additionalAttributes: nil))],
peers: SimpleDictionary<PeerId, Peer>(),
associatedMessages: SimpleDictionary<MessageId, Message>(),
associatedMessageIds: [],
associatedMedia: [:],
associatedThreadInfo: nil,
associatedStories: [:]
)
entries.append(.MessageEntry(message, presentationData, false, nil, .none, ChatMessageEntryAttributes(rank: nil, isContact: false, contentTypeHint: .generic, updatingMedia: nil, isPlaying: false, isCentered: false, authorStoryStats: nil, displayContinueThreadFooter: false, pinToTop: false)))
}
}
if let subject = associatedData.subject, case let .customChatContents(customChatContents) = subject, case let .quickReplyMessageInput(_, shortcutType) = customChatContents.kind, case .generic = shortcutType {
if !view.isLoading && view.laterId == nil && !view.entries.isEmpty {
for i in 0 ..< 2 {
let string = i == 1 ? presentationData.strings.Chat_QuickReply_ServiceHeader1 : presentationData.strings.Chat_QuickReply_ServiceHeader2
let formattedString = parseMarkdownIntoAttributedString(
string,
attributes: MarkdownAttributes(
body: MarkdownAttributeSet(font: Font.regular(15.0), textColor: .black),
bold: MarkdownAttributeSet(font: Font.regular(15.0), textColor: .black),
link: MarkdownAttributeSet(font: Font.regular(15.0), textColor: .white),
linkAttribute: { url in
return ("URL", url)
}
)
)
var entities: [MessageTextEntity] = []
formattedString.enumerateAttribute(.foregroundColor, in: NSRange(location: 0, length: formattedString.length), options: [], using: { value, range, _ in
if let value = value as? UIColor, value == .white {
entities.append(MessageTextEntity(range: range.lowerBound ..< range.upperBound, type: .Bold))
}
})
formattedString.enumerateAttribute(NSAttributedString.Key(rawValue: "URL"), in: NSRange(location: 0, length: formattedString.length), options: [], using: { value, range, _ in
if value != nil {
entities.append(MessageTextEntity(range: range.lowerBound ..< range.upperBound, type: .TextMention(peerId: context.account.peerId)))
}
})
let message = Message(
stableId: UInt32.max - 1001 - UInt32(i),
stableVersion: 0,
id: MessageId(peerId: context.account.peerId, namespace: Namespaces.Message.Local, id: Int32.max - 100 - Int32(i)),
globallyUniqueId: nil,
groupingKey: nil,
groupInfo: nil,
threadId: nil,
timestamp: -Int32(i),
flags: [.Incoming],
tags: [],
globalTags: [],
localTags: [],
customTags: [],
forwardInfo: nil,
author: nil,
text: "",
attributes: [],
media: [TelegramMediaAction(action: .customText(text: formattedString.string, entities: entities, additionalAttributes: nil))],
peers: SimpleDictionary<PeerId, Peer>(),
associatedMessages: SimpleDictionary<MessageId, Message>(),
associatedMessageIds: [],
associatedMedia: [:],
associatedThreadInfo: nil,
associatedStories: [:]
)
entries.insert(.MessageEntry(message, presentationData, false, nil, .none, ChatMessageEntryAttributes(rank: nil, isContact: false, contentTypeHint: .generic, updatingMedia: nil, isPlaying: false, isCentered: false, authorStoryStats: nil, displayContinueThreadFooter: false, pinToTop: false)), at: 0)
}
}
}
if isMusicPlaylist && entries.count == 1 {
return ([], currentState)
}
if reverse {
return (entries.reversed(), currentState)
} else {
// #if DEBUG
// assert(entries.map(\.stableId) == entries.sorted().map(\.stableId))
// #endif
return (entries, currentState)
}
}