mirror of
https://github.com/Swiftgram/Telegram-iOS.git
synced 2025-06-15 13:35:19 +00:00
4930 lines
271 KiB
Swift
4930 lines
271 KiB
Swift
import SGSimpleSettings
|
|
import Foundation
|
|
import UIKit
|
|
import Postbox
|
|
import SwiftSignalKit
|
|
import Display
|
|
import AsyncDisplayKit
|
|
import TelegramCore
|
|
import Postbox
|
|
import TelegramPresentationData
|
|
import TelegramUIPreferences
|
|
import MediaResources
|
|
import AccountContext
|
|
import TemporaryCachedPeerDataManager
|
|
import ChatListSearchItemNode
|
|
import Emoji
|
|
import AppBundle
|
|
import ListMessageItem
|
|
import AccountContext
|
|
import ChatInterfaceState
|
|
import ChatListUI
|
|
import ComponentFlow
|
|
import ReactionSelectionNode
|
|
import ChatPresentationInterfaceState
|
|
import TelegramNotices
|
|
import ChatControllerInteraction
|
|
import TranslateUI
|
|
import ChatHistoryEntry
|
|
import ChatOverscrollControl
|
|
import ChatBotInfoItem
|
|
import ChatUserInfoItem
|
|
import ChatMessageItem
|
|
import ChatMessageItemImpl
|
|
import ChatMessageItemView
|
|
import ChatMessageBubbleItemNode
|
|
import ChatMessageTransitionNode
|
|
import ChatControllerInteraction
|
|
import DustEffect
|
|
import UrlHandling
|
|
import TextFormat
|
|
|
|
struct ChatTopVisibleMessageRange: Equatable {
|
|
var lowerBound: MessageIndex
|
|
var upperBound: MessageIndex
|
|
var isLast: Bool
|
|
var isLoading: Bool
|
|
}
|
|
|
|
private let historyMessageCount: Int = 44
|
|
|
|
enum ChatHistoryViewScrollPosition {
|
|
case unread(index: MessageIndex)
|
|
case positionRestoration(index: MessageIndex, relativeOffset: CGFloat)
|
|
case index(subject: MessageHistoryScrollToSubject, position: ListViewScrollPosition, directionHint: ListViewScrollToItemDirectionHint, animated: Bool, highlight: Bool, displayLink: Bool, setupReply: Bool)
|
|
}
|
|
|
|
enum ChatHistoryViewUpdateType {
|
|
case Initial(fadeIn: Bool)
|
|
case Generic(type: ViewUpdateType)
|
|
}
|
|
|
|
public struct ChatHistoryCombinedInitialReadStateData {
|
|
public let unreadCount: Int32
|
|
public let totalState: ChatListTotalUnreadState?
|
|
public let notificationSettings: PeerNotificationSettings?
|
|
}
|
|
|
|
public struct ChatHistoryCombinedInitialData {
|
|
var initialData: InitialMessageHistoryData?
|
|
var buttonKeyboardMessage: Message?
|
|
var cachedData: CachedPeerData?
|
|
var cachedDataMessages: [MessageId: Message]?
|
|
var readStateData: [PeerId: ChatHistoryCombinedInitialReadStateData]?
|
|
}
|
|
|
|
enum ChatHistoryViewUpdate {
|
|
case Loading(initialData: ChatHistoryCombinedInitialData?, type: ChatHistoryViewUpdateType)
|
|
case HistoryView(view: MessageHistoryView, type: ChatHistoryViewUpdateType, scrollPosition: ChatHistoryViewScrollPosition?, flashIndicators: Bool, originalScrollPosition: ChatHistoryViewScrollPosition?, initialData: ChatHistoryCombinedInitialData, id: Int32)
|
|
}
|
|
|
|
struct ChatHistoryView {
|
|
let originalView: MessageHistoryView
|
|
let filteredEntries: [ChatHistoryEntry]
|
|
let associatedData: ChatMessageItemAssociatedData
|
|
let lastHeaderId: Int64
|
|
let id: Int32
|
|
let locationInput: ChatHistoryLocationInput?
|
|
let ignoreMessagesInTimestampRange: ClosedRange<Int32>?
|
|
let ignoreMessageIds: Set<MessageId>
|
|
}
|
|
|
|
enum ChatHistoryViewTransitionReason {
|
|
case Initial(fadeIn: Bool)
|
|
case InteractiveChanges
|
|
case Reload
|
|
case HoleReload
|
|
}
|
|
|
|
struct ChatHistoryViewTransitionInsertEntry {
|
|
let index: Int
|
|
let previousIndex: Int?
|
|
let entry: ChatHistoryEntry
|
|
let directionHint: ListViewItemOperationDirectionHint?
|
|
}
|
|
|
|
struct ChatHistoryViewTransitionUpdateEntry {
|
|
let index: Int
|
|
let previousIndex: Int
|
|
let entry: ChatHistoryEntry
|
|
let directionHint: ListViewItemOperationDirectionHint?
|
|
}
|
|
|
|
struct ChatHistoryViewTransition {
|
|
var historyView: ChatHistoryView
|
|
var deleteItems: [ListViewDeleteItem]
|
|
var insertEntries: [ChatHistoryViewTransitionInsertEntry]
|
|
var updateEntries: [ChatHistoryViewTransitionUpdateEntry]
|
|
var options: ListViewDeleteAndInsertOptions
|
|
var scrollToItem: ListViewScrollToItem?
|
|
var stationaryItemRange: (Int, Int)?
|
|
var initialData: InitialMessageHistoryData?
|
|
var keyboardButtonsMessage: Message?
|
|
var cachedData: CachedPeerData?
|
|
var cachedDataMessages: [MessageId: Message]?
|
|
var readStateData: [PeerId: ChatHistoryCombinedInitialReadStateData]?
|
|
var scrolledToIndex: MessageHistoryScrollToSubject?
|
|
var scrolledToSomeIndex: Bool
|
|
var animateIn: Bool
|
|
var reason: ChatHistoryViewTransitionReason
|
|
var flashIndicators: Bool
|
|
}
|
|
|
|
struct ChatHistoryListViewTransition {
|
|
var historyView: ChatHistoryView
|
|
var deleteItems: [ListViewDeleteItem]
|
|
var insertItems: [ListViewInsertItem]
|
|
var updateItems: [ListViewUpdateItem]
|
|
var options: ListViewDeleteAndInsertOptions
|
|
var scrollToItem: ListViewScrollToItem?
|
|
var stationaryItemRange: (Int, Int)?
|
|
var initialData: InitialMessageHistoryData?
|
|
var keyboardButtonsMessage: Message?
|
|
var cachedData: CachedPeerData?
|
|
var cachedDataMessages: [MessageId: Message]?
|
|
var readStateData: [PeerId: ChatHistoryCombinedInitialReadStateData]?
|
|
var scrolledToIndex: MessageHistoryScrollToSubject?
|
|
var scrolledToSomeIndex: Bool
|
|
var peerType: MediaAutoDownloadPeerType
|
|
var networkType: MediaAutoDownloadNetworkType
|
|
var animateIn: Bool
|
|
var reason: ChatHistoryViewTransitionReason
|
|
var flashIndicators: Bool
|
|
var animateFromPreviousFilter: Bool
|
|
}
|
|
|
|
private func maxMessageIndexForEntries(_ view: ChatHistoryView, indexRange: (Int, Int)) -> (incoming: MessageIndex?, overall: MessageIndex?) {
|
|
var incoming: MessageIndex?
|
|
var overall: MessageIndex?
|
|
var nextLowestIndex: MessageIndex?
|
|
if indexRange.0 >= 0 && indexRange.0 < view.filteredEntries.count {
|
|
if indexRange.0 > 0 {
|
|
nextLowestIndex = view.filteredEntries[indexRange.0 - 1].index
|
|
}
|
|
}
|
|
var nextHighestIndex: MessageIndex?
|
|
if indexRange.1 >= 0 && indexRange.1 < view.filteredEntries.count {
|
|
if indexRange.1 < view.filteredEntries.count - 1 {
|
|
nextHighestIndex = view.filteredEntries[indexRange.1 + 1].index
|
|
}
|
|
}
|
|
for i in (0 ..< view.originalView.entries.count).reversed() {
|
|
let index = view.originalView.entries[i].index
|
|
if let nextLowestIndex = nextLowestIndex {
|
|
if index <= nextLowestIndex {
|
|
continue
|
|
}
|
|
}
|
|
if let nextHighestIndex = nextHighestIndex {
|
|
if index >= nextHighestIndex {
|
|
continue
|
|
}
|
|
}
|
|
let messageEntry = view.originalView.entries[i]
|
|
if overall == nil || overall! < index {
|
|
overall = index
|
|
}
|
|
if !messageEntry.message.flags.intersection(.IsIncomingMask).isEmpty {
|
|
if incoming == nil || incoming! < index {
|
|
incoming = index
|
|
}
|
|
}
|
|
if incoming != nil {
|
|
return (incoming, overall)
|
|
}
|
|
}
|
|
return (incoming, overall)
|
|
}
|
|
|
|
extension ListMessageItemInteraction {
|
|
convenience init(controllerInteraction: ChatControllerInteraction) {
|
|
self.init(openMessage: { message, mode -> Bool in
|
|
return controllerInteraction.openMessage(message, OpenMessageParams(mode: mode))
|
|
}, openMessageContextMenu: { message, bool, node, rect, gesture in
|
|
controllerInteraction.openMessageContextMenu(message, bool, node, rect, gesture, nil)
|
|
}, toggleMessagesSelection: { messageId, selected in
|
|
controllerInteraction.toggleMessagesSelection(messageId, selected)
|
|
}, openUrl: { url, param1, param2, message in
|
|
controllerInteraction.openUrl(ChatControllerInteraction.OpenUrl(url: url, concealed: param1, external: param2, message: message, progress: Promise()))
|
|
}, openInstantPage: { message, data in
|
|
controllerInteraction.openInstantPage(message, data)
|
|
}, longTap: { action, message in
|
|
controllerInteraction.longTap(action, ChatControllerInteraction.LongTapParams(message: message))
|
|
}, getHiddenMedia: {
|
|
return controllerInteraction.hiddenMedia
|
|
})
|
|
}
|
|
}
|
|
|
|
private func mappedInsertEntries(context: AccountContext, chatLocation: ChatLocation, associatedData: ChatMessageItemAssociatedData, controllerInteraction: ChatControllerInteraction, mode: ChatHistoryListMode, lastHeaderId: Int64, entries: [ChatHistoryViewTransitionInsertEntry]) -> [ListViewInsertItem] {
|
|
var disableFloatingDateHeaders = false
|
|
if case .customChatContents = chatLocation {
|
|
disableFloatingDateHeaders = true
|
|
}
|
|
|
|
return entries.map { entry -> ListViewInsertItem in
|
|
switch entry.entry {
|
|
case let .MessageEntry(message, presentationData, read, location, selection, attributes):
|
|
let item: ListViewItem
|
|
switch mode {
|
|
case .bubbles:
|
|
item = ChatMessageItemImpl(presentationData: presentationData, context: context, chatLocation: chatLocation, associatedData: associatedData, controllerInteraction: controllerInteraction, content: .message(message: message, read: read, selection: selection, attributes: attributes, location: location), disableDate: disableFloatingDateHeaders || message.timestamp < 10)
|
|
case let .list(_, _, _, displayHeaders, hintLinks, isGlobalSearch):
|
|
let displayHeader: Bool
|
|
switch displayHeaders {
|
|
case .none:
|
|
displayHeader = false
|
|
case .all:
|
|
displayHeader = true
|
|
case .allButLast:
|
|
displayHeader = listMessageDateHeaderId(timestamp: message.timestamp) != lastHeaderId
|
|
}
|
|
item = ListMessageItem(presentationData: presentationData, context: context, chatLocation: chatLocation, interaction: ListMessageItemInteraction(controllerInteraction: controllerInteraction), message: message, translateToLanguage: associatedData.translateToLanguage, selection: selection, displayHeader: displayHeader, hintIsLink: hintLinks, isGlobalSearchResult: isGlobalSearch)
|
|
}
|
|
return ListViewInsertItem(index: entry.index, previousIndex: entry.previousIndex, item: item, directionHint: entry.directionHint)
|
|
case let .MessageGroupEntry(_, messages, presentationData):
|
|
let item: ListViewItem
|
|
switch mode {
|
|
case .bubbles:
|
|
item = ChatMessageItemImpl(presentationData: presentationData, context: context, chatLocation: chatLocation, associatedData: associatedData, controllerInteraction: controllerInteraction, content: .group(messages: messages), disableDate: disableFloatingDateHeaders)
|
|
case .list:
|
|
assertionFailure()
|
|
item = ListMessageItem(presentationData: presentationData, context: context, chatLocation: chatLocation, interaction: ListMessageItemInteraction(controllerInteraction: controllerInteraction), message: messages[0].0, selection: .none, displayHeader: false)
|
|
}
|
|
return ListViewInsertItem(index: entry.index, previousIndex: entry.previousIndex, item: item, directionHint: entry.directionHint)
|
|
case let .UnreadEntry(_, presentationData):
|
|
return ListViewInsertItem(index: entry.index, previousIndex: entry.previousIndex, item: ChatUnreadItem(index: entry.entry.index, presentationData: presentationData, controllerInteraction: controllerInteraction, context: context), directionHint: entry.directionHint)
|
|
case let .ReplyCountEntry(_, isComments, count, presentationData):
|
|
return ListViewInsertItem(index: entry.index, previousIndex: entry.previousIndex, item: ChatReplyCountItem(index: entry.entry.index, isComments: isComments, count: count, presentationData: presentationData, context: context, controllerInteraction: controllerInteraction), directionHint: entry.directionHint)
|
|
case let .ChatInfoEntry(data, presentationData):
|
|
let item: ListViewItem
|
|
switch data {
|
|
case let .botInfo(title, text, photo, video):
|
|
item = ChatBotInfoItem(title: title, text: text, photo: photo, video: video, controllerInteraction: controllerInteraction, presentationData: presentationData, context: context)
|
|
case let .userInfo(peer, verification, registrationDate, phoneCountry, groupsInCommonCount):
|
|
item = ChatUserInfoItem(peer: peer, verification: verification, registrationDate: registrationDate, phoneCountry: phoneCountry, groupsInCommonCount: groupsInCommonCount, controllerInteraction: controllerInteraction, presentationData: presentationData, context: context)
|
|
}
|
|
return ListViewInsertItem(index: entry.index, previousIndex: entry.previousIndex, item: item, directionHint: entry.directionHint)
|
|
case let .SearchEntry(theme, strings):
|
|
return ListViewInsertItem(index: entry.index, previousIndex: entry.previousIndex, item: ChatListSearchItem(theme: theme, placeholder: strings.Common_Search, activate: {
|
|
controllerInteraction.openSearch()
|
|
}), directionHint: entry.directionHint)
|
|
}
|
|
}
|
|
}
|
|
|
|
private func mappedUpdateEntries(context: AccountContext, chatLocation: ChatLocation, associatedData: ChatMessageItemAssociatedData, controllerInteraction: ChatControllerInteraction, mode: ChatHistoryListMode, lastHeaderId: Int64, entries: [ChatHistoryViewTransitionUpdateEntry]) -> [ListViewUpdateItem] {
|
|
var disableFloatingDateHeaders = false
|
|
if case .customChatContents = chatLocation {
|
|
disableFloatingDateHeaders = true
|
|
}
|
|
|
|
return entries.map { entry -> ListViewUpdateItem in
|
|
switch entry.entry {
|
|
case let .MessageEntry(message, presentationData, read, location, selection, attributes):
|
|
let item: ListViewItem
|
|
switch mode {
|
|
case .bubbles:
|
|
item = ChatMessageItemImpl(presentationData: presentationData, context: context, chatLocation: chatLocation, associatedData: associatedData, controllerInteraction: controllerInteraction, content: .message(message: message, read: read, selection: selection, attributes: attributes, location: location), disableDate: disableFloatingDateHeaders || message.timestamp < 10)
|
|
case let .list(_, _, _, displayHeaders, hintLinks, isGlobalSearch):
|
|
let displayHeader: Bool
|
|
switch displayHeaders {
|
|
case .none:
|
|
displayHeader = false
|
|
case .all:
|
|
displayHeader = true
|
|
case .allButLast:
|
|
displayHeader = listMessageDateHeaderId(timestamp: message.timestamp) != lastHeaderId
|
|
}
|
|
item = ListMessageItem(presentationData: presentationData, context: context, chatLocation: chatLocation, interaction: ListMessageItemInteraction(controllerInteraction: controllerInteraction), message: message, translateToLanguage: associatedData.translateToLanguage, selection: selection, displayHeader: displayHeader, hintIsLink: hintLinks, isGlobalSearchResult: isGlobalSearch)
|
|
}
|
|
return ListViewUpdateItem(index: entry.index, previousIndex: entry.previousIndex, item: item, directionHint: entry.directionHint)
|
|
case let .MessageGroupEntry(_, messages, presentationData):
|
|
let item: ListViewItem
|
|
switch mode {
|
|
case .bubbles:
|
|
item = ChatMessageItemImpl(presentationData: presentationData, context: context, chatLocation: chatLocation, associatedData: associatedData, controllerInteraction: controllerInteraction, content: .group(messages: messages), disableDate: disableFloatingDateHeaders)
|
|
case .list:
|
|
assertionFailure()
|
|
item = ListMessageItem(presentationData: presentationData, context: context, chatLocation: chatLocation, interaction: ListMessageItemInteraction(controllerInteraction: controllerInteraction), message: messages[0].0, selection: .none, displayHeader: false)
|
|
}
|
|
return ListViewUpdateItem(index: entry.index, previousIndex: entry.previousIndex, item: item, directionHint: entry.directionHint)
|
|
case let .UnreadEntry(_, presentationData):
|
|
return ListViewUpdateItem(index: entry.index, previousIndex: entry.previousIndex, item: ChatUnreadItem(index: entry.entry.index, presentationData: presentationData, controllerInteraction: controllerInteraction, context: context), directionHint: entry.directionHint)
|
|
case let .ReplyCountEntry(_, isComments, count, presentationData):
|
|
return ListViewUpdateItem(index: entry.index, previousIndex: entry.previousIndex, item: ChatReplyCountItem(index: entry.entry.index, isComments: isComments, count: count, presentationData: presentationData, context: context, controllerInteraction: controllerInteraction), directionHint: entry.directionHint)
|
|
case let .ChatInfoEntry(data, presentationData):
|
|
let item: ListViewItem
|
|
switch data {
|
|
case let .botInfo(title, text, photo, video):
|
|
item = ChatBotInfoItem(title: title, text: text, photo: photo, video: video, controllerInteraction: controllerInteraction, presentationData: presentationData, context: context)
|
|
case let .userInfo(peer, verification, registrationDate, phoneCountry, groupsInCommonCount):
|
|
item = ChatUserInfoItem(peer: peer, verification: verification, registrationDate: registrationDate, phoneCountry: phoneCountry, groupsInCommonCount: groupsInCommonCount, controllerInteraction: controllerInteraction, presentationData: presentationData, context: context)
|
|
}
|
|
return ListViewUpdateItem(index: entry.index, previousIndex: entry.previousIndex, item: item, directionHint: entry.directionHint)
|
|
case let .SearchEntry(theme, strings):
|
|
return ListViewUpdateItem(index: entry.index, previousIndex: entry.previousIndex, item: ChatListSearchItem(theme: theme, placeholder: strings.Common_Search, activate: {
|
|
controllerInteraction.openSearch()
|
|
}), directionHint: entry.directionHint)
|
|
}
|
|
}
|
|
}
|
|
|
|
private func mappedChatHistoryViewListTransition(context: AccountContext, chatLocation: ChatLocation, associatedData: ChatMessageItemAssociatedData, controllerInteraction: ChatControllerInteraction, mode: ChatHistoryListMode, lastHeaderId: Int64, animateFromPreviousFilter: Bool, transition: ChatHistoryViewTransition) -> ChatHistoryListViewTransition {
|
|
return ChatHistoryListViewTransition(historyView: transition.historyView, deleteItems: transition.deleteItems, insertItems: mappedInsertEntries(context: context, chatLocation: chatLocation, associatedData: associatedData, controllerInteraction: controllerInteraction, mode: mode, lastHeaderId: lastHeaderId, entries: transition.insertEntries), updateItems: mappedUpdateEntries(context: context, chatLocation: chatLocation, associatedData: associatedData, controllerInteraction: controllerInteraction, mode: mode, lastHeaderId: lastHeaderId, entries: transition.updateEntries), options: transition.options, scrollToItem: transition.scrollToItem, stationaryItemRange: transition.stationaryItemRange, initialData: transition.initialData, keyboardButtonsMessage: transition.keyboardButtonsMessage, cachedData: transition.cachedData, cachedDataMessages: transition.cachedDataMessages, readStateData: transition.readStateData, scrolledToIndex: transition.scrolledToIndex, scrolledToSomeIndex: transition.scrolledToSomeIndex, peerType: associatedData.automaticDownloadPeerType, networkType: associatedData.automaticDownloadNetworkType, animateIn: transition.animateIn, reason: transition.reason, flashIndicators: transition.flashIndicators, animateFromPreviousFilter: animateFromPreviousFilter)
|
|
}
|
|
|
|
private final class ChatHistoryTransactionOpaqueState {
|
|
let historyView: ChatHistoryView
|
|
|
|
init(historyView: ChatHistoryView) {
|
|
self.historyView = historyView
|
|
}
|
|
}
|
|
|
|
private func extractAssociatedData(
|
|
translateToLanguageSG: String?,
|
|
translationSettings: TranslationSettings,
|
|
chatLocation: ChatLocation,
|
|
view: MessageHistoryView,
|
|
automaticDownloadNetworkType: MediaAutoDownloadNetworkType,
|
|
preferredStoryHighQuality: Bool,
|
|
animatedEmojiStickers: [String: [StickerPackItem]],
|
|
additionalAnimatedEmojiStickers: [String: [Int: StickerPackItem]],
|
|
subject: ChatControllerSubject?,
|
|
currentlyPlayingMessageId: MessageIndex?,
|
|
isCopyProtectionEnabled: Bool,
|
|
availableReactions: AvailableReactions?,
|
|
availableMessageEffects: AvailableMessageEffects?,
|
|
savedMessageTags: SavedMessageTags?,
|
|
defaultReaction: MessageReaction.Reaction?,
|
|
isPremium: Bool,
|
|
alwaysDisplayTranscribeButton: ChatMessageItemAssociatedData.DisplayTranscribeButton,
|
|
accountPeer: EnginePeer?,
|
|
topicAuthorId: EnginePeer.Id?,
|
|
hasBots: Bool,
|
|
translateToLanguage: String?,
|
|
maxReadStoryId: Int32?,
|
|
recommendedChannels: RecommendedChannels?,
|
|
audioTranscriptionTrial: AudioTranscription.TrialState,
|
|
chatThemes: [TelegramTheme],
|
|
deviceContactsNumbers: Set<String>,
|
|
isInline: Bool,
|
|
showSensitiveContent: Bool
|
|
) -> ChatMessageItemAssociatedData {
|
|
var automaticDownloadPeerId: EnginePeer.Id?
|
|
var automaticMediaDownloadPeerType: MediaAutoDownloadPeerType = .channel
|
|
var contactsPeerIds: Set<PeerId> = Set()
|
|
var channelDiscussionGroup: ChatMessageItemAssociatedData.ChannelDiscussionGroupStatus = .unknown
|
|
if case let .peer(peerId) = chatLocation {
|
|
automaticDownloadPeerId = peerId
|
|
|
|
if peerId.namespace == Namespaces.Peer.CloudUser || peerId.namespace == Namespaces.Peer.SecretChat {
|
|
var isContact = false
|
|
for entry in view.additionalData {
|
|
if case let .peerIsContact(_, value) = entry {
|
|
isContact = value
|
|
break
|
|
}
|
|
}
|
|
automaticMediaDownloadPeerType = isContact ? .contact : .otherPrivate
|
|
} else if peerId.namespace == Namespaces.Peer.CloudGroup {
|
|
automaticMediaDownloadPeerType = .group
|
|
|
|
for entry in view.entries {
|
|
if entry.attributes.authorIsContact, let peerId = entry.message.author?.id {
|
|
contactsPeerIds.insert(peerId)
|
|
}
|
|
}
|
|
} else if peerId.namespace == Namespaces.Peer.CloudChannel {
|
|
for entry in view.additionalData {
|
|
if case let .peer(_, value) = entry {
|
|
if let channel = value as? TelegramChannel, case .group = channel.info {
|
|
automaticMediaDownloadPeerType = .group
|
|
}
|
|
} else if case let .cachedPeerData(dataPeerId, cachedData) = entry, dataPeerId == peerId {
|
|
if let cachedData = cachedData as? CachedChannelData {
|
|
switch cachedData.linkedDiscussionPeerId {
|
|
case let .known(value):
|
|
channelDiscussionGroup = .known(value)
|
|
case .unknown:
|
|
channelDiscussionGroup = .unknown
|
|
}
|
|
}
|
|
}
|
|
}
|
|
if automaticMediaDownloadPeerType == .group {
|
|
for entry in view.entries {
|
|
if entry.attributes.authorIsContact, let peerId = entry.message.author?.id {
|
|
contactsPeerIds.insert(peerId)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
} else if case let .replyThread(message) = chatLocation, message.isForumPost {
|
|
automaticDownloadPeerId = message.peerId
|
|
}
|
|
|
|
return ChatMessageItemAssociatedData(translateToLanguageSG: translateToLanguageSG, translationSettings: translationSettings, /* MARK: Swiftgram */ automaticDownloadPeerType: automaticMediaDownloadPeerType, automaticDownloadPeerId: automaticDownloadPeerId, automaticDownloadNetworkType: automaticDownloadNetworkType, preferredStoryHighQuality: preferredStoryHighQuality, isRecentActions: false, subject: subject, contactsPeerIds: contactsPeerIds, channelDiscussionGroup: channelDiscussionGroup, animatedEmojiStickers: animatedEmojiStickers, additionalAnimatedEmojiStickers: additionalAnimatedEmojiStickers, currentlyPlayingMessageId: currentlyPlayingMessageId, isCopyProtectionEnabled: isCopyProtectionEnabled, availableReactions: availableReactions, availableMessageEffects: availableMessageEffects, savedMessageTags: savedMessageTags, defaultReaction: defaultReaction, isPremium: isPremium, accountPeer: accountPeer, alwaysDisplayTranscribeButton: alwaysDisplayTranscribeButton, topicAuthorId: topicAuthorId, hasBots: hasBots, translateToLanguage: translateToLanguage, maxReadStoryId: maxReadStoryId, recommendedChannels: recommendedChannels, audioTranscriptionTrial: audioTranscriptionTrial, chatThemes: chatThemes, deviceContactsNumbers: deviceContactsNumbers, isInline: isInline, showSensitiveContent: showSensitiveContent)
|
|
}
|
|
|
|
private extension ChatHistoryLocationInput {
|
|
var isAtUpperBound: Bool {
|
|
switch self.content {
|
|
case .Navigation(index: .upperBound, anchorIndex: .upperBound, count: _, highlight: _):
|
|
return true
|
|
case let .Scroll(subject, anchorIndex, _, _, _, _, _):
|
|
if case .upperBound = anchorIndex, case .upperBound = subject.index {
|
|
return true
|
|
} else {
|
|
return false
|
|
}
|
|
default:
|
|
return false
|
|
}
|
|
}
|
|
}
|
|
|
|
private struct ChatHistoryAnimatedEmojiConfiguration {
|
|
static var defaultValue: ChatHistoryAnimatedEmojiConfiguration {
|
|
return ChatHistoryAnimatedEmojiConfiguration(scale: 0.625)
|
|
}
|
|
|
|
public let scale: CGFloat
|
|
|
|
fileprivate init(scale: CGFloat) {
|
|
self.scale = scale
|
|
}
|
|
|
|
static func with(appConfiguration: AppConfiguration) -> ChatHistoryAnimatedEmojiConfiguration {
|
|
if let data = appConfiguration.data, let scale = data["emojies_animated_zoom"] as? Double {
|
|
return ChatHistoryAnimatedEmojiConfiguration(scale: CGFloat(scale))
|
|
} else {
|
|
return .defaultValue
|
|
}
|
|
}
|
|
}
|
|
|
|
private var nextClientId: Int32 = 1
|
|
|
|
public final class ChatHistoryListNodeImpl: ListView, ChatHistoryNode, ChatHistoryListNode {
|
|
static let fixedAdMessageStableId: UInt32 = UInt32.max - 5000
|
|
|
|
public let context: AccountContext
|
|
private let chatLocation: ChatLocation
|
|
private let chatLocationContextHolder: Atomic<ChatLocationContextHolder?>
|
|
private let source: ChatHistoryListSource
|
|
private let subject: ChatControllerSubject?
|
|
private(set) var tag: HistoryViewInputTag?
|
|
private let controllerInteraction: ChatControllerInteraction
|
|
private let selectedMessages: Signal<Set<MessageId>?, NoError>
|
|
private let messageTransitionNode: () -> ChatMessageTransitionNodeImpl?
|
|
private let mode: ChatHistoryListMode
|
|
|
|
private var enableUnreadAlignment: Bool = true
|
|
|
|
private var historyView: ChatHistoryView?
|
|
public var originalHistoryView: MessageHistoryView? {
|
|
return self.historyView?.originalView
|
|
}
|
|
|
|
private let historyDisposable = MetaDisposable()
|
|
private let readHistoryDisposable = MetaDisposable()
|
|
|
|
private var dequeuedInitialTransitionOnLayout = false
|
|
private var enqueuedHistoryViewTransitions: [ChatHistoryListViewTransition] = []
|
|
private var hasActiveTransition = false
|
|
var layoutActionOnViewTransition: ((ChatHistoryListViewTransition) -> (ChatHistoryListViewTransition, ListViewUpdateSizeAndInsets?), Int64?)?
|
|
|
|
public let historyState = ValuePromise<ChatHistoryNodeHistoryState>()
|
|
public var currentHistoryState: ChatHistoryNodeHistoryState?
|
|
|
|
private let _initialData = Promise<ChatHistoryCombinedInitialData?>()
|
|
private var didSetInitialData = false
|
|
public var initialData: Signal<ChatHistoryCombinedInitialData?, NoError> {
|
|
return self._initialData.get()
|
|
}
|
|
|
|
private let _cachedPeerDataAndMessages = Promise<(CachedPeerData?, [MessageId: Message]?)>()
|
|
public var cachedPeerDataAndMessages: Signal<(CachedPeerData?, [MessageId: Message]?), NoError> {
|
|
return self._cachedPeerDataAndMessages.get()
|
|
}
|
|
|
|
private var _buttonKeyboardMessage = Promise<Message?>(nil)
|
|
private var currentButtonKeyboardMessage: Message?
|
|
public var buttonKeyboardMessage: Signal<Message?, NoError> {
|
|
return self._buttonKeyboardMessage.get()
|
|
}
|
|
|
|
private let maxVisibleIncomingMessageIndex = ValuePromise<MessageIndex>(ignoreRepeated: true)
|
|
let canReadHistory = Promise<Bool>()
|
|
private var canReadHistoryValue: Bool = false
|
|
private var canReadHistoryDisposable: Disposable?
|
|
|
|
var suspendReadingReactions: Bool = false {
|
|
didSet {
|
|
if self.suspendReadingReactions != oldValue {
|
|
if !self.suspendReadingReactions {
|
|
self.attemptReadingReactions()
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
private var messageIdsScheduledForMarkAsSeen = Set<MessageId>()
|
|
private var messageIdsWithReactionsScheduledForMarkAsSeen = Set<MessageId>()
|
|
|
|
private var chatHistoryLocationValue: ChatHistoryLocationInput? {
|
|
didSet {
|
|
if let chatHistoryLocationValue = self.chatHistoryLocationValue, chatHistoryLocationValue != oldValue {
|
|
chatHistoryLocationPromise.set(chatHistoryLocationValue)
|
|
}
|
|
}
|
|
}
|
|
private let chatHistoryLocationPromise = ValuePromise<ChatHistoryLocationInput>()
|
|
private var nextHistoryLocationId: Int32 = 1
|
|
private func takeNextHistoryLocationId() -> Int32 {
|
|
let id = self.nextHistoryLocationId
|
|
self.nextHistoryLocationId += 5
|
|
return id
|
|
}
|
|
|
|
private let ignoreMessagesInTimestampRangePromise = ValuePromise<ClosedRange<Int32>?>(nil)
|
|
var ignoreMessagesInTimestampRange: ClosedRange<Int32>? = nil {
|
|
didSet {
|
|
if self.ignoreMessagesInTimestampRange != oldValue {
|
|
self.ignoreMessagesInTimestampRangePromise.set(self.ignoreMessagesInTimestampRange)
|
|
}
|
|
}
|
|
}
|
|
|
|
private let ignoreMessageIdsPromise = ValuePromise<Set<EngineMessage.Id>>(Set())
|
|
var ignoreMessageIds: Set<EngineMessage.Id> = Set() {
|
|
didSet {
|
|
if self.ignoreMessageIds != oldValue {
|
|
self.ignoreMessageIdsPromise.set(self.ignoreMessageIds)
|
|
}
|
|
}
|
|
}
|
|
|
|
private let chatHasBotsPromise = ValuePromise<Bool>(false)
|
|
var chatHasBots: Bool = false {
|
|
didSet {
|
|
if self.chatHasBots != oldValue {
|
|
self.chatHasBotsPromise.set(self.chatHasBots)
|
|
}
|
|
}
|
|
}
|
|
|
|
private let galleryHiddenMesageAndMediaDisposable = MetaDisposable()
|
|
|
|
private let messageProcessingManager = ChatMessageThrottledProcessingManager()
|
|
private let messageWithReactionsProcessingManager = ChatMessageThrottledProcessingManager(submitInterval: 4.0)
|
|
private let seenLiveLocationProcessingManager = ChatMessageThrottledProcessingManager()
|
|
private let unsupportedMessageProcessingManager = ChatMessageThrottledProcessingManager()
|
|
private let refreshMediaProcessingManager = ChatMessageThrottledProcessingManager()
|
|
private let messageMentionProcessingManager = ChatMessageThrottledProcessingManager(delay: 0.2)
|
|
private let unseenReactionsProcessingManager = ChatMessageThrottledProcessingManager(delay: 0.2, submitInterval: 0.0)
|
|
private let extendedMediaProcessingManager = ChatMessageVisibleThrottledProcessingManager(interval: 5.0)
|
|
private let translationProcessingManager = ChatMessageThrottledProcessingManager(submitInterval: 1.0)
|
|
private let refreshStoriesProcessingManager = ChatMessageThrottledProcessingManager()
|
|
private let factCheckProcessingManager = ChatMessageThrottledProcessingManager(submitInterval: 1.0)
|
|
private let inlineGroupCallsProcessingManager = ChatMessageThrottledProcessingManager(submitInterval: 1.0)
|
|
|
|
let prefetchManager: InChatPrefetchManager
|
|
private var currentEarlierPrefetchMessages: [(Message, Media)] = []
|
|
private var currentLaterPrefetchMessages: [(Message, Media)] = []
|
|
private var currentPrefetchDirectionIsToLater: Bool = false
|
|
|
|
private var maxVisibleMessageIndexReported: MessageIndex?
|
|
var maxVisibleMessageIndexUpdated: ((MessageIndex) -> Void)?
|
|
|
|
var scrolledToIndex: ((MessageHistoryScrollToSubject, Bool) -> Void)?
|
|
var scrolledToSomeIndex: (() -> Void)?
|
|
var beganDragging: (() -> Void)?
|
|
|
|
private let hasVisiblePlayableItemNodesPromise = ValuePromise<Bool>(false, ignoreRepeated: true)
|
|
var hasVisiblePlayableItemNodes: Signal<Bool, NoError> {
|
|
return self.hasVisiblePlayableItemNodesPromise.get()
|
|
}
|
|
|
|
private var isInteractivelyScrollingValue: Bool = false
|
|
private let isInteractivelyScrollingPromise = ValuePromise<Bool>(false, ignoreRepeated: true)
|
|
var isInteractivelyScrolling: Signal<Bool, NoError> {
|
|
return self.isInteractivelyScrollingPromise.get()
|
|
}
|
|
|
|
private var currentPresentationData: ChatPresentationData
|
|
private var chatPresentationDataPromise: Promise<ChatPresentationData>
|
|
private var presentationDataDisposable: Disposable?
|
|
|
|
private let historyAppearsClearedPromise = ValuePromise<Bool>(false)
|
|
var historyAppearsCleared: Bool = false {
|
|
didSet {
|
|
if self.historyAppearsCleared != oldValue {
|
|
self.historyAppearsClearedPromise.set(self.historyAppearsCleared)
|
|
}
|
|
}
|
|
}
|
|
|
|
private let pendingUnpinnedAllMessagesPromise = ValuePromise<Bool>(false)
|
|
var pendingUnpinnedAllMessages: Bool = false {
|
|
didSet {
|
|
if self.pendingUnpinnedAllMessages != oldValue {
|
|
self.pendingUnpinnedAllMessagesPromise.set(self.pendingUnpinnedAllMessages)
|
|
}
|
|
}
|
|
}
|
|
|
|
private let pendingRemovedMessagesPromise = ValuePromise<Set<MessageId>>(Set())
|
|
var pendingRemovedMessages: Set<MessageId> = Set() {
|
|
didSet {
|
|
if self.pendingRemovedMessages != oldValue {
|
|
self.pendingRemovedMessagesPromise.set(self.pendingRemovedMessages)
|
|
}
|
|
}
|
|
}
|
|
|
|
private let justSentTextMessagePromise = ValuePromise<Bool>(false)
|
|
var justSentTextMessage: Bool = false {
|
|
didSet {
|
|
if self.justSentTextMessage != oldValue {
|
|
self.justSentTextMessagePromise.set(self.justSentTextMessage)
|
|
}
|
|
}
|
|
}
|
|
|
|
private var appliedScrollToMessageId: MessageIndex? = nil
|
|
private let scrollToMessageIdPromise = Promise<MessageIndex?>(nil)
|
|
|
|
private let currentlyPlayingMessageIdPromise = Promise<(MessageIndex, Bool)?>(nil)
|
|
private var appliedPlayingMessageId: (MessageIndex, Bool)? = nil
|
|
|
|
private(set) var isScrollAtBottomPosition = false
|
|
public var isScrollAtBottomPositionUpdated: (() -> Void)?
|
|
|
|
private var interactiveReadActionDisposable: Disposable?
|
|
private var interactiveReadReactionsDisposable: Disposable?
|
|
private var displayUnseenReactionAnimationsTimestamps: [MessageId: Double] = [:]
|
|
|
|
public var contentPositionChanged: (ListViewVisibleContentOffset) -> Void = { _ in }
|
|
|
|
public private(set) var loadState: ChatHistoryNodeLoadState?
|
|
private var loadStateUpdated: ((ChatHistoryNodeLoadState, Bool) -> Void)?
|
|
private var additionalLoadStateUpdated: [(ChatHistoryNodeLoadState, Bool) -> Void] = []
|
|
|
|
public private(set) var hasAtLeast3Messages: Bool = false
|
|
public var hasAtLeast3MessagesUpdated: ((Bool) -> Void)?
|
|
|
|
public private(set) var hasPlentyOfMessages: Bool = false
|
|
public var hasPlentyOfMessagesUpdated: ((Bool) -> Void)?
|
|
|
|
public private(set) var hasLotsOfMessages: Bool = false
|
|
public var hasLotsOfMessagesUpdated: ((Bool) -> Void)?
|
|
|
|
private var loadedMessagesFromCachedDataDisposable: Disposable?
|
|
|
|
let isTopReplyThreadMessageShown = ValuePromise<Bool>(false, ignoreRepeated: true)
|
|
|
|
private var topVisibleMessageRangeValueInitialized: Bool = false
|
|
private var topVisibleMessageRangeValue: ChatTopVisibleMessageRange?
|
|
private func updateTopVisibleMessageRange(_ value: ChatTopVisibleMessageRange?) {
|
|
if value != self.topVisibleMessageRangeValue || !self.topVisibleMessageRangeValueInitialized {
|
|
self.topVisibleMessageRangeValueInitialized = true
|
|
self.topVisibleMessageRangeValue = value
|
|
self.topVisibleMessageRange.set(.single(value))
|
|
}
|
|
}
|
|
let topVisibleMessageRange = Promise<ChatTopVisibleMessageRange?>(nil)
|
|
|
|
var isSelectionGestureEnabled = true
|
|
|
|
private var overscrollView: ComponentHostView<Empty>?
|
|
var nextChannelToRead: (peer: EnginePeer, threadData: (id: Int64, data: MessageHistoryThreadData)?, unreadCount: Int, location: TelegramEngine.NextUnreadChannelLocation)?
|
|
var offerNextChannelToRead: Bool = false
|
|
var nextChannelToReadDisplayName: Bool = false
|
|
private var currentOverscrollExpandProgress: CGFloat = 0.0
|
|
private var freezeOverscrollControl: Bool = false
|
|
private var freezeOverscrollControlProgress: Bool = false
|
|
private var feedback: HapticFeedback?
|
|
var openNextChannelToRead: ((EnginePeer, (id: Int64, data: MessageHistoryThreadData)?, TelegramEngine.NextUnreadChannelLocation) -> Void)?
|
|
private var contentInsetAnimator: DisplayLinkAnimator?
|
|
|
|
let adMessagesContext: AdMessagesHistoryContext?
|
|
private var adMessagesDisposable: Disposable?
|
|
private var preloadAdPeerName: String?
|
|
private let preloadAdPeerDisposable = MetaDisposable()
|
|
private var didSetupRecommendedChannelsPreload = false
|
|
private let preloadRecommendedChannelsDisposable = MetaDisposable()
|
|
private var seenAdIds: [Data] = []
|
|
private var pendingDynamicAdMessages: [Message] = []
|
|
private var pendingDynamicAdMessageInterval: Int?
|
|
private var remainingDynamicAdMessageInterval: Int?
|
|
private var remainingDynamicAdMessageDistance: CGFloat?
|
|
private var nextPendingDynamicMessageId: Int32 = 1
|
|
private var allAdMessages: (fixed: Message?, opportunistic: [Message], version: Int) = (nil, [], 0) {
|
|
didSet {
|
|
self.allAdMessagesPromise.set(.single(self.allAdMessages))
|
|
}
|
|
}
|
|
private let allAdMessagesPromise = Promise<(fixed: Message?, opportunistic: [Message], version: Int)>((nil, [], 0))
|
|
private var seenMessageIds = Set<MessageId>()
|
|
|
|
private var refreshDisplayedItemRangeTimer: SwiftSignalKit.Timer?
|
|
|
|
private var genericReactionEffect: String?
|
|
private var genericReactionEffectDisposable: Disposable?
|
|
|
|
private var visibleMessageRange = Atomic<VisibleMessageRange?>(value: nil)
|
|
|
|
private let clientId: Atomic<Int32>
|
|
|
|
private var translationLang: (fromLang: String?, toLang: String)?
|
|
|
|
private var allowDustEffect: Bool = true
|
|
private var dustEffectLayer: DustEffectLayer?
|
|
|
|
var frozenMessageForScrollingReset: EngineMessage.Id?
|
|
|
|
private var hasDisplayedBusinessBotMessageTooltip: Bool = false
|
|
|
|
private let _isReady = ValuePromise<Bool>(false, ignoreRepeated: true)
|
|
public var isReady: Signal<Bool, NoError> {
|
|
return self._isReady.get()
|
|
}
|
|
private var didSetReady: Bool = false
|
|
|
|
private let initTimestamp: Double
|
|
|
|
public init(context: AccountContext, updatedPresentationData: (initial: PresentationData, signal: Signal<PresentationData, NoError>), chatLocation: ChatLocation, chatLocationContextHolder: Atomic<ChatLocationContextHolder?>, tag: HistoryViewInputTag?, source: ChatHistoryListSource, subject: ChatControllerSubject?, controllerInteraction: ChatControllerInteraction, selectedMessages: Signal<Set<MessageId>?, NoError>, mode: ChatHistoryListMode = .bubbles, rotated: Bool = false, isChatPreview: Bool, messageTransitionNode: @escaping () -> ChatMessageTransitionNodeImpl?) {
|
|
self.initTimestamp = CFAbsoluteTimeGetCurrent()
|
|
|
|
var tag = tag
|
|
if case .pinnedMessages = subject {
|
|
tag = .tag(.pinned)
|
|
}
|
|
|
|
self.context = context
|
|
self.chatLocation = chatLocation
|
|
self.chatLocationContextHolder = chatLocationContextHolder
|
|
self.source = source
|
|
self.subject = subject
|
|
self.tag = tag
|
|
self.controllerInteraction = controllerInteraction
|
|
self.selectedMessages = selectedMessages
|
|
self.messageTransitionNode = messageTransitionNode
|
|
self.mode = mode
|
|
|
|
if SGSimpleSettings.shared.disableSnapDeletionEffect { self.allowDustEffect = false }
|
|
if let data = context.currentAppConfiguration.with({ $0 }).data {
|
|
if let _ = data["ios_killswitch_disable_unread_alignment"] {
|
|
self.enableUnreadAlignment = false
|
|
}
|
|
if let _ = data["ios_killswitch_disable_dust_effect"] {
|
|
self.allowDustEffect = false
|
|
}
|
|
}
|
|
|
|
let presentationData = updatedPresentationData.initial
|
|
self.currentPresentationData = ChatPresentationData(theme: ChatPresentationThemeData(theme: presentationData.theme, wallpaper: presentationData.chatWallpaper), fontSize: presentationData.chatFontSize, strings: presentationData.strings, dateTimeFormat: presentationData.dateTimeFormat, nameDisplayOrder: presentationData.nameDisplayOrder, disableAnimations: true, largeEmoji: presentationData.largeEmoji, chatBubbleCorners: presentationData.chatBubbleCorners, animatedEmojiScale: 1.0)
|
|
|
|
self.chatPresentationDataPromise = Promise()
|
|
|
|
self.prefetchManager = InChatPrefetchManager(context: context)
|
|
|
|
var displayAdPeer: PeerId?
|
|
if !isChatPreview {
|
|
switch subject {
|
|
case .none, .message:
|
|
if case let .peer(peerId) = chatLocation {
|
|
displayAdPeer = peerId
|
|
}
|
|
default:
|
|
break
|
|
}
|
|
}
|
|
var adMessages: Signal<(interPostInterval: Int32?, messages: [Message]), NoError>
|
|
if case .bubbles = mode, let peerId = displayAdPeer {
|
|
let adMessagesContext = context.engine.messages.adMessages(peerId: peerId)
|
|
self.adMessagesContext = adMessagesContext
|
|
if peerId.namespace == Namespaces.Peer.CloudUser {
|
|
adMessages = .single((nil, []))
|
|
} else {
|
|
if context.sharedContext.immediateExperimentalUISettings.fakeAds {
|
|
adMessages = context.engine.data.get(
|
|
TelegramEngine.EngineData.Item.Peer.Peer(id: peerId)
|
|
)
|
|
|> map { peer -> (interPostInterval: Int32?, messages: [Message]) in
|
|
let fakeAdMessages: [Message] = (0 ..< 10).map { i -> Message in
|
|
var attributes: [MessageAttribute] = []
|
|
|
|
let mappedMessageType: AdMessageAttribute.MessageType = .sponsored
|
|
attributes.append(AdMessageAttribute(opaqueId: "fake_ad_\(i)".data(using: .utf8)!, messageType: mappedMessageType, url: "t.me/telegram", buttonText: "VIEW", sponsorInfo: nil, additionalInfo: nil, canReport: false, hasContentMedia: false))
|
|
|
|
var messagePeers = SimpleDictionary<PeerId, Peer>()
|
|
|
|
if let peer {
|
|
messagePeers[peer.id] = peer._asPeer()
|
|
}
|
|
|
|
let author: Peer = TelegramChannel(
|
|
id: PeerId(namespace: Namespaces.Peer.CloudChannel, id: PeerId.Id._internalFromInt64Value(1)),
|
|
accessHash: nil,
|
|
title: "Fake Ad",
|
|
username: nil,
|
|
photo: [],
|
|
creationDate: 0,
|
|
version: 0,
|
|
participationStatus: .left,
|
|
info: .broadcast(TelegramChannelBroadcastInfo(flags: [])),
|
|
flags: [],
|
|
restrictionInfo: nil,
|
|
adminRights: nil,
|
|
bannedRights: nil,
|
|
defaultBannedRights: nil,
|
|
usernames: [],
|
|
storiesHidden: nil,
|
|
nameColor: .blue,
|
|
backgroundEmojiId: nil,
|
|
profileColor: nil,
|
|
profileBackgroundEmojiId: nil,
|
|
emojiStatus: nil,
|
|
approximateBoostLevel: nil,
|
|
subscriptionUntilDate: nil,
|
|
verificationIconFileId: nil,
|
|
sendPaidMessageStars: nil
|
|
)
|
|
messagePeers[author.id] = author
|
|
|
|
let messageText = "Fake Ad N\(i)"
|
|
let messageHash = (messageText.hashValue &+ 31 &* peerId.hashValue) &* 31 &+ author.id.hashValue
|
|
let messageStableVersion = UInt32(bitPattern: Int32(truncatingIfNeeded: messageHash))
|
|
|
|
return Message(
|
|
stableId: 0,
|
|
stableVersion: messageStableVersion,
|
|
id: MessageId(peerId: peerId, namespace: Namespaces.Message.Local, id: 0),
|
|
globallyUniqueId: nil,
|
|
groupingKey: nil,
|
|
groupInfo: nil,
|
|
threadId: nil,
|
|
timestamp: Int32.max - 1,
|
|
flags: [.Incoming],
|
|
tags: [],
|
|
globalTags: [],
|
|
localTags: [],
|
|
customTags: [],
|
|
forwardInfo: nil,
|
|
author: author,
|
|
text: messageText,
|
|
attributes: attributes,
|
|
media: [],
|
|
peers: messagePeers,
|
|
associatedMessages: SimpleDictionary<MessageId, Message>(),
|
|
associatedMessageIds: [],
|
|
associatedMedia: [:],
|
|
associatedThreadInfo: nil,
|
|
associatedStories: [:]
|
|
)
|
|
}
|
|
return (10, fakeAdMessages)
|
|
}
|
|
} else {
|
|
adMessages = adMessagesContext.state
|
|
}
|
|
}
|
|
} else {
|
|
self.adMessagesContext = nil
|
|
adMessages = .single((nil, []))
|
|
}
|
|
|
|
let clientId = Atomic<Int32>(value: nextClientId)
|
|
self.clientId = clientId
|
|
nextClientId += 1
|
|
|
|
super.init()
|
|
|
|
self.rotated = rotated
|
|
if rotated {
|
|
self.transform = CATransform3DMakeRotation(CGFloat(Double.pi), 0.0, 0.0, 1.0)
|
|
}
|
|
|
|
self.clipsToBounds = false
|
|
|
|
self.beginAdMessageManagement(adMessages: adMessages)
|
|
|
|
self.accessibilityPageScrolledString = { [weak self] row, count in
|
|
if let strongSelf = self {
|
|
return strongSelf.currentPresentationData.strings.VoiceOver_ScrollStatus(row, count).string
|
|
} else {
|
|
return ""
|
|
}
|
|
}
|
|
|
|
self.dynamicBounceEnabled = !self.currentPresentationData.disableAnimations
|
|
self.experimentalSnapScrollToItem = true
|
|
|
|
//self.debugInfo = true
|
|
|
|
self.messageProcessingManager.process = { [weak context] messageIds in
|
|
context?.account.viewTracker.updateViewCountForMessageIds(messageIds: Set(messageIds.map(\.messageId)), clientId: clientId.with { $0 })
|
|
}
|
|
self.messageWithReactionsProcessingManager.process = { [weak context] messageIds in
|
|
context?.account.viewTracker.updateReactionsForMessageIds(messageIds: Set(messageIds.map(\.messageId)))
|
|
}
|
|
self.seenLiveLocationProcessingManager.process = { [weak context] messageIds in
|
|
context?.account.viewTracker.updateSeenLiveLocationForMessageIds(messageIds: Set(messageIds.map(\.messageId)))
|
|
}
|
|
self.unsupportedMessageProcessingManager.process = { [weak context] messageIds in
|
|
context?.account.viewTracker.updateUnsupportedMediaForMessageIds(messageIds: messageIds)
|
|
}
|
|
self.refreshMediaProcessingManager.process = { [weak context] messageIds in
|
|
context?.account.viewTracker.refreshSecretMediaMediaForMessageIds(messageIds: Set(messageIds.map(\.messageId)))
|
|
}
|
|
self.refreshStoriesProcessingManager.process = { [weak context] messageIds in
|
|
context?.account.viewTracker.refreshStoriesForMessageIds(messageIds: Set(messageIds.map(\.messageId)))
|
|
}
|
|
self.translationProcessingManager.process = { [weak self, weak context] messageIds in
|
|
if let context = context, let translationLang = self?.translationLang {
|
|
let _ = translateMessageIds(context: context, messageIds: Array(messageIds.map(\.messageId)), fromLang: translationLang.fromLang, toLang: translationLang.toLang, viaText: !context.isPremium).startStandalone()
|
|
}
|
|
}
|
|
self.factCheckProcessingManager.process = { [weak self, weak context] messageIds in
|
|
if let context = context, let translationLang = self?.translationLang {
|
|
let _ = translateMessageIds(context: context, messageIds: Array(messageIds.map(\.messageId)), fromLang: translationLang.fromLang, toLang: translationLang.toLang).startStandalone()
|
|
}
|
|
}
|
|
|
|
self.messageMentionProcessingManager.process = { [weak self, weak context] messageIds in
|
|
if let strongSelf = self {
|
|
if strongSelf.canReadHistoryValue {
|
|
context?.account.viewTracker.updateMarkMentionsSeenForMessageIds(messageIds: Set(messageIds.map(\.messageId)))
|
|
} else {
|
|
strongSelf.messageIdsScheduledForMarkAsSeen.formUnion(messageIds.map(\.messageId))
|
|
}
|
|
}
|
|
}
|
|
|
|
self.unseenReactionsProcessingManager.process = { [weak self] messageIds in
|
|
guard let strongSelf = self else {
|
|
return
|
|
}
|
|
if strongSelf.canReadHistoryValue && !strongSelf.suspendReadingReactions && !strongSelf.context.sharedContext.immediateExperimentalUISettings.skipReadHistory {
|
|
strongSelf.context.account.viewTracker.updateMarkReactionsSeenForMessageIds(messageIds: Set(messageIds.map(\.messageId)))
|
|
} else {
|
|
strongSelf.messageIdsWithReactionsScheduledForMarkAsSeen.formUnion(messageIds.map(\.messageId))
|
|
}
|
|
}
|
|
|
|
self.extendedMediaProcessingManager.process = { [weak self] messageIds in
|
|
guard let strongSelf = self else {
|
|
return
|
|
}
|
|
strongSelf.context.account.viewTracker.updatedExtendedMediaForMessageIds(messageIds: Set(messageIds.map(\.messageId)))
|
|
}
|
|
|
|
self.inlineGroupCallsProcessingManager.process = { [weak context] messageIds in
|
|
context?.account.viewTracker.refreshInlineGroupCallsForMessageIds(messageIds: Set(messageIds.map(\.messageId)))
|
|
}
|
|
|
|
self.preloadPages = false
|
|
|
|
self.beginChatHistoryTransitions(resetScrolling: false)
|
|
|
|
self.beginReadHistoryManagement()
|
|
|
|
if let subject = subject, case let .message(messageSubject, highlight, _, setupReply) = subject {
|
|
let initialSearchLocation: ChatHistoryInitialSearchLocation
|
|
switch messageSubject {
|
|
case let .id(id):
|
|
initialSearchLocation = .id(id)
|
|
case let .timestamp(timestamp):
|
|
if let peerId = self.chatLocation.peerId {
|
|
initialSearchLocation = .index(MessageIndex(id: MessageId(peerId: peerId, namespace: Namespaces.Message.Cloud, id: 1), timestamp: timestamp))
|
|
} else {
|
|
//TODO:implement
|
|
initialSearchLocation = .index(MessageIndex.absoluteUpperBound())
|
|
}
|
|
}
|
|
self.chatHistoryLocationValue = ChatHistoryLocationInput(content: .InitialSearch(subject: MessageHistoryInitialSearchSubject(location: initialSearchLocation, quote: (highlight?.quote).flatMap { quote in MessageHistoryInitialSearchSubject.Quote(string: quote.string, offset: quote.offset) }), count: historyMessageCount, highlight: highlight != nil, setupReply: setupReply), id: 0)
|
|
} else if let subject = subject, case let .pinnedMessages(maybeMessageId) = subject, let messageId = maybeMessageId {
|
|
self.chatHistoryLocationValue = ChatHistoryLocationInput(content: .InitialSearch(subject: MessageHistoryInitialSearchSubject(location: .id(messageId), quote: nil), count: historyMessageCount, highlight: true, setupReply: false), id: 0)
|
|
} else {
|
|
self.chatHistoryLocationValue = ChatHistoryLocationInput(content: .Initial(count: historyMessageCount), id: 0)
|
|
}
|
|
self.chatHistoryLocationPromise.set(self.chatHistoryLocationValue!)
|
|
|
|
self.generalScrollDirectionUpdated = { [weak self] direction in
|
|
guard let strongSelf = self else {
|
|
return
|
|
}
|
|
let prefetchDirectionIsToLater = direction == .up
|
|
if strongSelf.currentPrefetchDirectionIsToLater != prefetchDirectionIsToLater {
|
|
strongSelf.currentPrefetchDirectionIsToLater = prefetchDirectionIsToLater
|
|
if strongSelf.currentPrefetchDirectionIsToLater {
|
|
strongSelf.prefetchManager.updateMessages(strongSelf.currentLaterPrefetchMessages, directionIsToLater: strongSelf.currentPrefetchDirectionIsToLater)
|
|
} else {
|
|
strongSelf.prefetchManager.updateMessages(strongSelf.currentEarlierPrefetchMessages, directionIsToLater: strongSelf.currentPrefetchDirectionIsToLater)
|
|
}
|
|
}
|
|
}
|
|
|
|
self.displayedItemRangeChanged = { [weak self] displayedRange, opaqueTransactionState in
|
|
if let strongSelf = self, let transactionState = opaqueTransactionState as? ChatHistoryTransactionOpaqueState {
|
|
strongSelf.processDisplayedItemRangeChanged(displayedRange: displayedRange, transactionState: transactionState)
|
|
}
|
|
}
|
|
|
|
self.refreshDisplayedItemRangeTimer = SwiftSignalKit.Timer(timeout: 10.0, repeat: true, completion: { [weak self] in
|
|
guard let strongSelf = self else {
|
|
return
|
|
}
|
|
strongSelf.updateVisibleItemRange(force: true)
|
|
}, queue: .mainQueue())
|
|
self.refreshDisplayedItemRangeTimer?.start()
|
|
|
|
self.beginPresentationDataManagement(updated: updatedPresentationData.signal)
|
|
|
|
self.visibleContentOffsetChanged = { [weak self] offset in
|
|
if let strongSelf = self {
|
|
strongSelf.contentPositionChanged(offset)
|
|
|
|
if strongSelf.tag == nil {
|
|
var atBottom = false
|
|
var offsetFromBottom: CGFloat?
|
|
switch offset {
|
|
case let .known(offsetValue):
|
|
if offsetValue.isLessThanOrEqualTo(0.0) {
|
|
atBottom = true
|
|
offsetFromBottom = offsetValue
|
|
}
|
|
//print("offsetValue: \(offsetValue)")
|
|
default:
|
|
break
|
|
}
|
|
|
|
if atBottom != strongSelf.isScrollAtBottomPosition {
|
|
strongSelf.isScrollAtBottomPosition = atBottom
|
|
strongSelf.updateReadHistoryActions()
|
|
|
|
strongSelf.isScrollAtBottomPositionUpdated?()
|
|
}
|
|
|
|
strongSelf.maybeUpdateOverscrollAction(offset: offsetFromBottom)
|
|
}
|
|
|
|
var lastMessageId: MessageId?
|
|
if let historyView = (strongSelf.opaqueTransactionState as? ChatHistoryTransactionOpaqueState)?.historyView {
|
|
if historyView.originalView.laterId == nil && !historyView.originalView.holeLater {
|
|
lastMessageId = historyView.originalView.entries.last?.message.id
|
|
}
|
|
}
|
|
|
|
var maxMessage: MessageIndex?
|
|
strongSelf.forEachVisibleMessageItemNode { itemNode in
|
|
if let item = itemNode.item {
|
|
var matches = false
|
|
if itemNode.frame.maxY < strongSelf.insets.top {
|
|
return
|
|
}
|
|
if itemNode.frame.minY >= strongSelf.insets.top {
|
|
matches = true
|
|
} else if itemNode.frame.minY >= strongSelf.insets.top - 100.0 {
|
|
matches = true
|
|
} else if let lastMessageId {
|
|
for (message, _) in item.content {
|
|
if message.id == lastMessageId {
|
|
matches = true
|
|
}
|
|
}
|
|
}
|
|
|
|
if matches {
|
|
var maxItemIndex: MessageIndex?
|
|
for (message, _) in item.content {
|
|
if let maxItemIndexValue = maxItemIndex {
|
|
if maxItemIndexValue < message.index {
|
|
maxItemIndex = message.index
|
|
}
|
|
} else {
|
|
maxItemIndex = message.index
|
|
}
|
|
}
|
|
|
|
if let maxItemIndex {
|
|
if let maxMessageValue = maxMessage {
|
|
if maxMessageValue < maxItemIndex {
|
|
maxMessage = maxItemIndex
|
|
}
|
|
} else {
|
|
maxMessage = maxItemIndex
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
if let maxMessage {
|
|
strongSelf.updateMaxVisibleReadIncomingMessageIndex(maxMessage)
|
|
}
|
|
}
|
|
}
|
|
|
|
self.loadedMessagesFromCachedDataDisposable = (self._cachedPeerDataAndMessages.get() |> map { dataAndMessages -> MessageId? in
|
|
return dataAndMessages.0?.messageIds.first
|
|
} |> distinctUntilChanged(isEqual: { $0 == $1 })
|
|
|> mapToSignal { messageId -> Signal<Void, NoError> in
|
|
if let messageId = messageId {
|
|
return context.engine.messages.getMessagesLoadIfNecessary([messageId])
|
|
|> `catch` { _ in
|
|
return .single(.result([]))
|
|
}
|
|
|> map { _ -> Void in return Void() }
|
|
} else {
|
|
return .complete()
|
|
}
|
|
}).startStrict()
|
|
|
|
self.beganInteractiveDragging = { [weak self] _ in
|
|
self?.isInteractivelyScrollingValue = true
|
|
self?.isInteractivelyScrollingPromise.set(true)
|
|
self?.beganDragging?()
|
|
//self?.updateHistoryScrollingArea(transition: .immediate)
|
|
}
|
|
|
|
self.endedInteractiveDragging = { [weak self] _ in
|
|
guard let strongSelf = self else {
|
|
return
|
|
}
|
|
if strongSelf.offerNextChannelToRead, strongSelf.currentOverscrollExpandProgress >= 0.99 {
|
|
if let nextChannelToRead = strongSelf.nextChannelToRead {
|
|
strongSelf.freezeOverscrollControl = true
|
|
strongSelf.openNextChannelToRead?(nextChannelToRead.peer, nextChannelToRead.threadData, nextChannelToRead.location)
|
|
} else {
|
|
strongSelf.freezeOverscrollControlProgress = true
|
|
strongSelf.scroller.contentInset = UIEdgeInsets(top: 94.0 + 12.0, left: 0.0, bottom: 0.0, right: 0.0)
|
|
Queue.mainQueue().after(0.3, {
|
|
let animator = DisplayLinkAnimator(duration: 0.2, from: 1.0, to: 0.0, update: { rawT in
|
|
guard let strongSelf = self else {
|
|
return
|
|
}
|
|
let t = listViewAnimationCurveEaseInOut(rawT)
|
|
let value = (94.0 + 12.0) * t
|
|
strongSelf.scroller.contentInset = UIEdgeInsets(top: value, left: 0.0, bottom: 0.0, right: 0.0)
|
|
}, completion: {
|
|
guard let strongSelf = self else {
|
|
return
|
|
}
|
|
strongSelf.contentInsetAnimator = nil
|
|
strongSelf.scroller.contentInset = UIEdgeInsets()
|
|
strongSelf.freezeOverscrollControlProgress = false
|
|
})
|
|
strongSelf.contentInsetAnimator = animator
|
|
})
|
|
}
|
|
}
|
|
}
|
|
|
|
self.didEndScrolling = { [weak self] _ in
|
|
guard let strongSelf = self else {
|
|
return
|
|
}
|
|
strongSelf.isInteractivelyScrollingValue = false
|
|
strongSelf.isInteractivelyScrollingPromise.set(false)
|
|
//strongSelf.updateHistoryScrollingArea(transition: .immediate)
|
|
}
|
|
|
|
/*self.updateScrollingIndicator = { [weak self] scrollingState, transition in
|
|
guard let strongSelf = self else {
|
|
return
|
|
}
|
|
strongSelf.scrollingState = scrollingState
|
|
strongSelf.updateHistoryScrollingArea(transition: transition)
|
|
}*/
|
|
|
|
let selectionRecognizer = ChatHistoryListSelectionRecognizer(target: self, action: #selector(self.selectionPanGesture(_:)))
|
|
selectionRecognizer.shouldBegin = { [weak self] in
|
|
guard let strongSelf = self else {
|
|
return false
|
|
}
|
|
return strongSelf.isSelectionGestureEnabled
|
|
}
|
|
self.view.addGestureRecognizer(selectionRecognizer)
|
|
|
|
self.loadNextGenericReactionEffect(context: context)
|
|
}
|
|
|
|
deinit {
|
|
self.historyDisposable.dispose()
|
|
self.readHistoryDisposable.dispose()
|
|
self.interactiveReadActionDisposable?.dispose()
|
|
self.interactiveReadReactionsDisposable?.dispose()
|
|
self.canReadHistoryDisposable?.dispose()
|
|
self.loadedMessagesFromCachedDataDisposable?.dispose()
|
|
self.preloadAdPeerDisposable.dispose()
|
|
self.preloadRecommendedChannelsDisposable.dispose()
|
|
self.refreshDisplayedItemRangeTimer?.invalidate()
|
|
self.genericReactionEffectDisposable?.dispose()
|
|
self.adMessagesDisposable?.dispose()
|
|
self.presentationDataDisposable?.dispose()
|
|
}
|
|
|
|
public func updateTag(tag: HistoryViewInputTag?) {
|
|
if self.tag == tag {
|
|
return
|
|
}
|
|
self.tag = tag
|
|
|
|
self.beginChatHistoryTransitions(resetScrolling: true)
|
|
}
|
|
|
|
private func beginAdMessageManagement(adMessages: Signal<(interPostInterval: Int32?, messages: [Message]), NoError>) {
|
|
self.adMessagesDisposable = (adMessages
|
|
|> deliverOnMainQueue).startStrict(next: { [weak self] interPostInterval, messages in
|
|
guard let self else {
|
|
return
|
|
}
|
|
|
|
if let interPostInterval = interPostInterval {
|
|
self.pendingDynamicAdMessages = messages
|
|
self.pendingDynamicAdMessageInterval = Int(interPostInterval)
|
|
|
|
if self.remainingDynamicAdMessageInterval == nil {
|
|
self.remainingDynamicAdMessageInterval = Int(interPostInterval)
|
|
}
|
|
if self.remainingDynamicAdMessageDistance == nil {
|
|
self.remainingDynamicAdMessageDistance = self.bounds.height
|
|
}
|
|
|
|
self.allAdMessages = (messages.first, [], 0)
|
|
} else {
|
|
var adPeerName: String?
|
|
if let adAttribute = messages.first?.adAttribute, let parsedUrl = parseAdUrl(sharedContext: self.context.sharedContext, context: self.context, url: adAttribute.url), case let .peer(reference, _) = parsedUrl, case let .name(peerName) = reference {
|
|
adPeerName = peerName
|
|
}
|
|
|
|
if self.preloadAdPeerName != adPeerName {
|
|
self.preloadAdPeerName = adPeerName
|
|
if let adPeerName {
|
|
let context = self.context
|
|
let combinedDisposable = DisposableSet()
|
|
self.preloadAdPeerDisposable.set(combinedDisposable)
|
|
combinedDisposable.add(context.engine.peers.resolvePeerByName(name: adPeerName, referrer: nil).startStrict(next: { result in
|
|
if case let .result(maybePeer) = result, let peer = maybePeer {
|
|
combinedDisposable.add(context.account.viewTracker.polledChannel(peerId: peer.id).startStrict())
|
|
combinedDisposable.add(context.account.addAdditionalPreloadHistoryPeerId(peerId: peer.id))
|
|
}
|
|
}))
|
|
} else {
|
|
self.preloadAdPeerDisposable.set(nil)
|
|
}
|
|
}
|
|
|
|
self.allAdMessages = (messages.first, [], 0)
|
|
}
|
|
}).strict()
|
|
}
|
|
|
|
private let fixedCombinedReadStates = Atomic<MessageHistoryViewReadState?>(value: nil)
|
|
private let currentViewVersion = Atomic<Int?>(value: nil)
|
|
private let previousView = Atomic<(ChatHistoryView, Int, Set<MessageId>?, Int)?>(value: nil)
|
|
private let previousHistoryAppearsCleared = Atomic<Bool?>(value: nil)
|
|
|
|
private func beginChatHistoryTransitions(resetScrolling: Bool) {
|
|
self.historyDisposable.set(nil)
|
|
self._isReady.set(false)
|
|
|
|
let context = self.context
|
|
let chatLocation = self.chatLocation
|
|
let subject = self.subject
|
|
let source = self.source
|
|
let tag = self.tag
|
|
let chatLocationContextHolder = self.chatLocationContextHolder
|
|
let controllerInteraction = self.controllerInteraction
|
|
let selectedMessages = self.selectedMessages
|
|
let messageTransitionNode = self.messageTransitionNode
|
|
let mode = self.mode
|
|
let rotated = self.rotated
|
|
|
|
var resetScrollingMessageId: (index: MessageIndex, offset: CGFloat)?
|
|
|
|
let useRootInterfaceStateForThread: Bool
|
|
if case let .replyThread(message) = self.chatLocation, message.peerId == self.context.account.peerId, message.threadId == self.context.account.peerId.toInt64() {
|
|
useRootInterfaceStateForThread = true
|
|
} else {
|
|
useRootInterfaceStateForThread = false
|
|
}
|
|
|
|
var resetScrolling = resetScrolling
|
|
if resetScrolling {
|
|
if let frozenMessageForScrollingReset = self.frozenMessageForScrollingReset {
|
|
self.forEachVisibleMessageItemNode { itemNode in
|
|
if resetScrollingMessageId != nil {
|
|
return
|
|
}
|
|
if let item = itemNode.item, item.message.id == frozenMessageForScrollingReset {
|
|
let distanceToNode = self.insets.top - itemNode.frame.minY
|
|
resetScrollingMessageId = (item.message.index, -distanceToNode)
|
|
}
|
|
}
|
|
}
|
|
|
|
self.forEachVisibleMessageItemNode { itemNode in
|
|
if resetScrollingMessageId != nil {
|
|
return
|
|
}
|
|
if let item = itemNode.item {
|
|
let distanceToNode = self.insets.top - itemNode.frame.minY
|
|
resetScrollingMessageId = (item.message.index, -distanceToNode)
|
|
}
|
|
}
|
|
|
|
if let resetScrollingMessageId {
|
|
self.chatHistoryLocationValue = ChatHistoryLocationInput(content: .Scroll(subject: MessageHistoryScrollToSubject(index: .message(resetScrollingMessageId.index), quote: nil), anchorIndex: .message(resetScrollingMessageId.index), sourceIndex: .message(resetScrollingMessageId.index), scrollPosition: .top(resetScrollingMessageId.offset), animated: false, highlight: false, setupReply: false), id: (self.chatHistoryLocationValue?.id).flatMap({ $0 + 1 }) ?? 0)
|
|
} else {
|
|
self.chatHistoryLocationValue = ChatHistoryLocationInput(content: .Initial(count: historyMessageCount), id: (self.chatHistoryLocationValue?.id).flatMap({ $0 + 1 }) ?? 0)
|
|
}
|
|
}
|
|
self.frozenMessageForScrollingReset = nil
|
|
|
|
var appendMessagesFromTheSameGroup = false
|
|
if case .pinnedMessages = subject {
|
|
appendMessagesFromTheSameGroup = true
|
|
}
|
|
|
|
let fixedCombinedReadStates = self.fixedCombinedReadStates
|
|
|
|
var isScheduledMessages = false
|
|
if let subject = self.subject, case .scheduledMessages = subject {
|
|
isScheduledMessages = true
|
|
}
|
|
var isAuxiliaryChat = isScheduledMessages
|
|
if case .replyThread = self.chatLocation {
|
|
isAuxiliaryChat = true
|
|
}
|
|
|
|
var additionalData: [AdditionalMessageHistoryViewData] = []
|
|
if case let .peer(peerId) = self.chatLocation {
|
|
additionalData.append(.cachedPeerData(peerId))
|
|
additionalData.append(.cachedPeerDataMessages(peerId))
|
|
additionalData.append(.peerNotificationSettings(peerId))
|
|
if peerId.namespace == Namespaces.Peer.CloudChannel {
|
|
additionalData.append(.cacheEntry(cachedChannelAdminRanksEntryId(peerId: peerId)))
|
|
}
|
|
additionalData.append(.peer(peerId))
|
|
if peerId.namespace == Namespaces.Peer.CloudUser || peerId.namespace == Namespaces.Peer.SecretChat {
|
|
additionalData.append(.peerIsContact(peerId))
|
|
}
|
|
}
|
|
if !isAuxiliaryChat {
|
|
additionalData.append(.totalUnreadState)
|
|
}
|
|
if case let .replyThread(replyThreadMessage) = self.chatLocation {
|
|
additionalData.append(.cachedPeerData(replyThreadMessage.peerId))
|
|
additionalData.append(.peerNotificationSettings(replyThreadMessage.peerId))
|
|
if replyThreadMessage.peerId.namespace == Namespaces.Peer.CloudChannel {
|
|
additionalData.append(.cacheEntry(cachedChannelAdminRanksEntryId(peerId: replyThreadMessage.peerId)))
|
|
additionalData.append(.peer(replyThreadMessage.peerId))
|
|
}
|
|
|
|
additionalData.append(.message(replyThreadMessage.effectiveTopId))
|
|
}
|
|
|
|
let currentViewVersion = self.currentViewVersion
|
|
|
|
let historyViewUpdate: Signal<(ChatHistoryViewUpdate, Int, ChatHistoryLocationInput?, ClosedRange<Int32>?, Set<MessageId>), NoError>
|
|
var isFirstTime = true
|
|
var updateAllOnEachVersion = false
|
|
if case let .custom(messages, at, quote, _) = self.source {
|
|
updateAllOnEachVersion = true
|
|
historyViewUpdate = messages
|
|
|> map { messages, _, hasMore in
|
|
let version = currentViewVersion.modify({ value in
|
|
if let value = value {
|
|
return value + 1
|
|
} else {
|
|
return 0
|
|
}
|
|
})!
|
|
|
|
let scrollPosition: ChatHistoryViewScrollPosition?
|
|
if isFirstTime, let messageIndex = messages.first(where: { $0.id == at })?.index {
|
|
scrollPosition = .index(subject: MessageHistoryScrollToSubject(index: .message(messageIndex), quote: quote.flatMap { quote in MessageHistoryScrollToSubject.Quote(string: quote.text, offset: quote.offset) }), position: .center(.bottom), directionHint: .Down, animated: false, highlight: false, displayLink: false, setupReply: false)
|
|
isFirstTime = false
|
|
} else {
|
|
scrollPosition = nil
|
|
}
|
|
|
|
return (ChatHistoryViewUpdate.HistoryView(view: MessageHistoryView(tag: nil, namespaces: .all, entries: messages.reversed().map { MessageHistoryEntry(message: $0, isRead: false, location: nil, monthLocation: nil, attributes: MutableMessageHistoryEntryAttributes(authorIsContact: false)) }, holeEarlier: hasMore, holeLater: false, isLoading: false), type: .Generic(type: version > 0 ? ViewUpdateType.Generic : ViewUpdateType.Initial), scrollPosition: scrollPosition, flashIndicators: false, originalScrollPosition: nil, initialData: ChatHistoryCombinedInitialData(initialData: nil, buttonKeyboardMessage: nil, cachedData: nil, cachedDataMessages: nil, readStateData: nil), id: 0), version, nil, nil, Set())
|
|
}
|
|
} else if case let .customView(historyView) = self.source {
|
|
historyViewUpdate = combineLatest(queue: .mainQueue(),
|
|
self.chatHistoryLocationPromise.get(),
|
|
self.ignoreMessagesInTimestampRangePromise.get(),
|
|
self.ignoreMessageIdsPromise.get()
|
|
)
|
|
|> distinctUntilChanged(isEqual: { lhs, rhs in
|
|
if lhs.0 != rhs.0 {
|
|
return false
|
|
}
|
|
if lhs.1 != rhs.1 {
|
|
return false
|
|
}
|
|
if lhs.2 != rhs.2 {
|
|
return false
|
|
}
|
|
return true
|
|
})
|
|
|> mapToSignal { location, _, _ -> Signal<((MessageHistoryView, ViewUpdateType), ChatHistoryLocationInput?), NoError> in
|
|
return historyView
|
|
|> map { historyView in
|
|
return (historyView, location)
|
|
}
|
|
}
|
|
|> map { viewAndUpdate, location in
|
|
let (view, update) = viewAndUpdate
|
|
|
|
let version = currentViewVersion.modify({ value in
|
|
if let value = value {
|
|
return value + 1
|
|
} else {
|
|
return 0
|
|
}
|
|
})!
|
|
|
|
var scrollPositionValue: ChatHistoryViewScrollPosition?
|
|
if let location {
|
|
switch location.content {
|
|
case let .Scroll(subject, _, _, scrollPosition, animated, highlight, setupReply):
|
|
scrollPositionValue = .index(subject: subject, position: scrollPosition, directionHint: .Up, animated: animated, highlight: highlight, displayLink: false, setupReply: setupReply)
|
|
default:
|
|
break
|
|
}
|
|
}
|
|
|
|
return (
|
|
ChatHistoryViewUpdate.HistoryView(
|
|
view: view,
|
|
type: .Generic(type: update),
|
|
scrollPosition: scrollPositionValue,
|
|
flashIndicators: false,
|
|
originalScrollPosition: nil,
|
|
initialData: ChatHistoryCombinedInitialData(
|
|
initialData: nil,
|
|
buttonKeyboardMessage: nil,
|
|
cachedData: nil,
|
|
cachedDataMessages: nil,
|
|
readStateData: nil
|
|
),
|
|
id: location?.id ?? 0
|
|
),
|
|
version,
|
|
location,
|
|
nil,
|
|
Set()
|
|
)
|
|
}
|
|
} else {
|
|
historyViewUpdate = combineLatest(queue: .mainQueue(),
|
|
self.chatHistoryLocationPromise.get(),
|
|
self.ignoreMessagesInTimestampRangePromise.get(),
|
|
self.ignoreMessageIdsPromise.get()
|
|
)
|
|
|> distinctUntilChanged(isEqual: { lhs, rhs in
|
|
if lhs.0 != rhs.0 {
|
|
return false
|
|
}
|
|
if lhs.1 != rhs.1 {
|
|
return false
|
|
}
|
|
if lhs.2 != rhs.2 {
|
|
return false
|
|
}
|
|
return true
|
|
})
|
|
|> mapToSignal { location, ignoreMessagesInTimestampRange, ignoreMessageIds in
|
|
return chatHistoryViewForLocation(location, ignoreMessagesInTimestampRange: ignoreMessagesInTimestampRange, ignoreMessageIds: ignoreMessageIds, context: context, chatLocation: chatLocation, chatLocationContextHolder: chatLocationContextHolder, scheduled: isScheduledMessages, fixedCombinedReadStates: fixedCombinedReadStates.with { $0 }, tag: tag, appendMessagesFromTheSameGroup: appendMessagesFromTheSameGroup, additionalData: additionalData, orderStatistics: [], useRootInterfaceStateForThread: useRootInterfaceStateForThread)
|
|
|> beforeNext { viewUpdate in
|
|
switch viewUpdate {
|
|
case let .HistoryView(view, _, _, _, _, _, _):
|
|
let _ = fixedCombinedReadStates.swap(view.fixedReadStates)
|
|
default:
|
|
break
|
|
}
|
|
}
|
|
|> map { view -> (ChatHistoryViewUpdate, Int, ChatHistoryLocationInput?, ClosedRange<Int32>?, Set<MessageId>) in
|
|
let version = currentViewVersion.modify({ value in
|
|
if let value = value {
|
|
return value + 1
|
|
} else {
|
|
return 0
|
|
}
|
|
})!
|
|
return (view, version, location, ignoreMessagesInTimestampRange, ignoreMessageIds)
|
|
}
|
|
}
|
|
}
|
|
|
|
let previousView = self.previousView
|
|
let automaticDownloadNetworkType = context.account.networkType
|
|
|> map { type -> MediaAutoDownloadNetworkType in
|
|
switch type {
|
|
case .none, .wifi:
|
|
return .wifi
|
|
case .cellular:
|
|
return .cellular
|
|
}
|
|
}
|
|
|> distinctUntilChanged
|
|
|
|
let chatHistoryEntriesForViewState = Atomic<ChatHistoryEntriesForViewState>(value: ChatHistoryEntriesForViewState())
|
|
|
|
let animatedEmojiStickers: Signal<[String: [StickerPackItem]], NoError> = context.animatedEmojiStickers
|
|
let additionalAnimatedEmojiStickers = context.additionalAnimatedEmojiStickers
|
|
|
|
let previousHistoryAppearsCleared = self.previousHistoryAppearsCleared
|
|
|
|
let updatingMedia = context.account.pendingUpdateMessageManager.updatingMessageMedia
|
|
|> map { value -> [MessageId: ChatUpdatingMessageMedia] in
|
|
var result = value
|
|
for id in value.keys {
|
|
if id.peerId != chatLocation.peerId {
|
|
result.removeValue(forKey: id)
|
|
}
|
|
}
|
|
return result
|
|
}
|
|
|> distinctUntilChanged
|
|
|
|
let customChannelDiscussionReadState: Signal<MessageId?, NoError>
|
|
if case let .peer(peerId) = chatLocation, peerId.namespace == Namespaces.Peer.CloudChannel {
|
|
customChannelDiscussionReadState = context.engine.data.subscribe(
|
|
TelegramEngine.EngineData.Item.Peer.LinkedDiscussionPeerId(id: peerId),
|
|
TelegramEngine.EngineData.Item.Peer.Peer(id: peerId)
|
|
)
|
|
|> mapToSignal { linkedDiscussionPeerId, peer -> Signal<PeerId?, NoError> in
|
|
guard case let .channel(peer) = peer, case .broadcast = peer.info else {
|
|
return .single(nil)
|
|
}
|
|
guard case let .known(value) = linkedDiscussionPeerId else {
|
|
return .single(nil)
|
|
}
|
|
return .single(value)
|
|
}
|
|
|> distinctUntilChanged
|
|
|> mapToSignal { discussionPeerId -> Signal<MessageId?, NoError> in
|
|
guard let discussionPeerId = discussionPeerId else {
|
|
return .single(nil)
|
|
}
|
|
|
|
return context.engine.data.subscribe(TelegramEngine.EngineData.Item.Messages.PeerReadCounters(id: discussionPeerId))
|
|
|> map { readCounters -> MessageId? in
|
|
guard let state = readCounters._asReadCounters() else {
|
|
return nil
|
|
}
|
|
for (namespace, namespaceState) in state.states {
|
|
if namespace == Namespaces.Message.Cloud {
|
|
switch namespaceState {
|
|
case let .idBased(maxIncomingReadId, _, _, _, _):
|
|
return MessageId(peerId: discussionPeerId, namespace: Namespaces.Message.Cloud, id: maxIncomingReadId)
|
|
default:
|
|
break
|
|
}
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|> distinctUntilChanged
|
|
}
|
|
} else {
|
|
customChannelDiscussionReadState = .single(nil)
|
|
}
|
|
|
|
let customThreadOutgoingReadState: Signal<MessageId?, NoError>
|
|
if case .replyThread = chatLocation {
|
|
customThreadOutgoingReadState = context.chatLocationOutgoingReadState(for: chatLocation, contextHolder: chatLocationContextHolder)
|
|
} else {
|
|
customThreadOutgoingReadState = .single(nil)
|
|
}
|
|
|
|
let availableReactions: Signal<AvailableReactions?, NoError> = (context as! AccountContextImpl).availableReactions
|
|
let availableMessageEffects: Signal<AvailableMessageEffects?, NoError> = (context as! AccountContextImpl).availableMessageEffects
|
|
|
|
let savedMessageTags: Signal<SavedMessageTags?, NoError>
|
|
if chatLocation.peerId == self.context.account.peerId {
|
|
savedMessageTags = context.engine.stickers.savedMessageTagData()
|
|
} else {
|
|
savedMessageTags = .single(nil)
|
|
}
|
|
|
|
let defaultReaction = combineLatest(
|
|
self.context.engine.data.subscribe(TelegramEngine.EngineData.Item.Peer.Peer(id: context.account.peerId)),
|
|
self.context.account.postbox.preferencesView(keys: [PreferencesKeys.reactionSettings])
|
|
)
|
|
|> map { peer, preferencesView -> MessageReaction.Reaction? in
|
|
let reactionSettings: ReactionSettings
|
|
if let entry = preferencesView.values[PreferencesKeys.reactionSettings], let value = entry.get(ReactionSettings.self) {
|
|
reactionSettings = value
|
|
} else {
|
|
reactionSettings = .default
|
|
}
|
|
var hasPremium = false
|
|
if case let .user(user) = peer {
|
|
hasPremium = user.isPremium
|
|
}
|
|
return reactionSettings.effectiveQuickReaction(hasPremium: hasPremium)
|
|
}
|
|
|> distinctUntilChanged
|
|
|
|
let accountPeer = context.engine.data.subscribe(TelegramEngine.EngineData.Item.Peer.Peer(id: context.account.peerId))
|
|
|> map { peer -> EnginePeer? in
|
|
return peer
|
|
}
|
|
|> distinctUntilChanged
|
|
|
|
let topicAuthorId: Signal<EnginePeer.Id?, NoError>
|
|
if let peerId = chatLocation.peerId, let threadId = chatLocation.threadId {
|
|
topicAuthorId = context.engine.data.subscribe(TelegramEngine.EngineData.Item.Peer.ThreadData(id: peerId, threadId: threadId))
|
|
|> map { data -> EnginePeer.Id? in
|
|
return data?.author
|
|
}
|
|
|> distinctUntilChanged
|
|
} else {
|
|
topicAuthorId = .single(nil)
|
|
}
|
|
|
|
let audioTranscriptionSuggestion = combineLatest(
|
|
ApplicationSpecificNotice.getAudioTranscriptionSuggestion(accountManager: context.sharedContext.accountManager),
|
|
self.justSentTextMessagePromise.get()
|
|
)
|
|
|
|
let translationState: Signal<ChatTranslationState?, NoError>
|
|
if let peerId = chatLocation.peerId, peerId.namespace != Namespaces.Peer.SecretChat && peerId != context.account.peerId && subject != .scheduledMessages {
|
|
translationState = chatTranslationState(context: context, peerId: peerId, threadId: self.chatLocation.threadId)
|
|
} else {
|
|
translationState = .single(nil)
|
|
}
|
|
|
|
let promises = combineLatest(
|
|
self.historyAppearsClearedPromise.get(),
|
|
self.pendingUnpinnedAllMessagesPromise.get(),
|
|
self.pendingRemovedMessagesPromise.get(),
|
|
self.currentlyPlayingMessageIdPromise.get(),
|
|
self.scrollToMessageIdPromise.get(),
|
|
self.chatHasBotsPromise.get(),
|
|
self.allAdMessagesPromise.get()
|
|
)
|
|
|
|
let contentSettings = self.context.engine.data.subscribe(TelegramEngine.EngineData.Item.Configuration.ContentSettings())
|
|
|
|
let maxReadStoryId: Signal<Int32?, NoError>
|
|
if let peerId = self.chatLocation.peerId, peerId.namespace == Namespaces.Peer.CloudUser {
|
|
maxReadStoryId = self.context.account.postbox.combinedView(keys: [PostboxViewKey.storiesState(key: .peer(peerId))])
|
|
|> map { views -> Int32? in
|
|
guard let view = views.views[PostboxViewKey.storiesState(key: .peer(peerId))] as? StoryStatesView else {
|
|
return nil
|
|
}
|
|
if let state = view.value?.get(Stories.PeerState.self) {
|
|
return state.maxReadId
|
|
} else {
|
|
return nil
|
|
}
|
|
}
|
|
|> distinctUntilChanged
|
|
} else {
|
|
maxReadStoryId = .single(nil)
|
|
}
|
|
|
|
let recommendedChannels: Signal<RecommendedChannels?, NoError>
|
|
if let peerId = self.chatLocation.peerId, peerId.namespace == Namespaces.Peer.CloudChannel {
|
|
recommendedChannels = self.context.engine.peers.recommendedChannels(peerId: peerId)
|
|
} else {
|
|
recommendedChannels = .single(nil)
|
|
}
|
|
|
|
let audioTranscriptionTrial = self.context.engine.data.subscribe(TelegramEngine.EngineData.Item.Configuration.AudioTranscriptionTrial())
|
|
|
|
let chatThemes = self.context.engine.themes.getChatThemes(accountManager: self.context.sharedContext.accountManager)
|
|
|
|
let deviceContactsNumbers = self.context.sharedContext.deviceContactPhoneNumbers.get()
|
|
|> distinctUntilChanged
|
|
|
|
let premiumConfiguration = PremiumConfiguration.with(appConfiguration: self.context.currentAppConfiguration.with { $0 })
|
|
|
|
let preferredStoryHighQuality: Signal<Bool, NoError> = combineLatest(
|
|
context.sharedContext.automaticMediaDownloadSettings
|
|
|> map { settings in
|
|
return settings.highQualityStories
|
|
}
|
|
|> distinctUntilChanged,
|
|
context.engine.data.subscribe(
|
|
TelegramEngine.EngineData.Item.Peer.Peer(id: context.account.peerId)
|
|
)
|
|
)
|
|
|> map { setting, peer -> Bool in
|
|
let isPremium = peer?.isPremium ?? false
|
|
return setting && isPremium
|
|
}
|
|
|> distinctUntilChanged
|
|
|
|
let startTime = CFAbsoluteTimeGetCurrent()
|
|
var measure_isFirstTime = true
|
|
let messageViewQueue = Queue.mainQueue()
|
|
let historyViewTransitionDisposable = (combineLatest(queue: messageViewQueue,
|
|
self.context.sharedContext.accountManager.sharedData(keys: [ApplicationSpecificSharedDataKeys.translationSettings]) |> take(1),
|
|
historyViewUpdate |> debug_measureTimeToFirstEvent(label: "chatHistoryNode_historyViewUpdate"),
|
|
self.chatPresentationDataPromise.get() |> debug_measureTimeToFirstEvent(label: "chatHistoryNode_chatPresentationData"),
|
|
selectedMessages |> debug_measureTimeToFirstEvent(label: "chatHistoryNode_selectedMessages"),
|
|
updatingMedia |> debug_measureTimeToFirstEvent(label: "chatHistoryNode_updatingMedia"),
|
|
automaticDownloadNetworkType |> debug_measureTimeToFirstEvent(label: "chatHistoryNode_automaticDownloadNetworkType"),
|
|
preferredStoryHighQuality |> debug_measureTimeToFirstEvent(label: "chatHistoryNode_preferredStoryHighQuality"),
|
|
animatedEmojiStickers |> debug_measureTimeToFirstEvent(label: "chatHistoryNode_animatedEmojiStickers"),
|
|
additionalAnimatedEmojiStickers |> debug_measureTimeToFirstEvent(label: "chatHistoryNode_additionalAnimatedEmojiStickers"),
|
|
customChannelDiscussionReadState |> debug_measureTimeToFirstEvent(label: "chatHistoryNode_customChannelDiscussionReadState"),
|
|
customThreadOutgoingReadState |> debug_measureTimeToFirstEvent(label: "chatHistoryNode_customThreadOutgoingReadState"),
|
|
availableReactions |> debug_measureTimeToFirstEvent(label: "chatHistoryNode_availableReactions"),
|
|
availableMessageEffects |> debug_measureTimeToFirstEvent(label: "chatHistoryNode_availableMessageEffects"),
|
|
savedMessageTags |> debug_measureTimeToFirstEvent(label: "chatHistoryNode_savedMessageTags"),
|
|
defaultReaction |> debug_measureTimeToFirstEvent(label: "chatHistoryNode_defaultReaction"),
|
|
accountPeer |> debug_measureTimeToFirstEvent(label: "chatHistoryNode_accountPeer"),
|
|
audioTranscriptionSuggestion |> debug_measureTimeToFirstEvent(label: "chatHistoryNode_audioTranscriptionSuggestion"),
|
|
promises |> debug_measureTimeToFirstEvent(label: "chatHistoryNode_promises"),
|
|
topicAuthorId |> debug_measureTimeToFirstEvent(label: "chatHistoryNode_topicAuthorId"),
|
|
translationState |> debug_measureTimeToFirstEvent(label: "chatHistoryNode_translationState"),
|
|
maxReadStoryId |> debug_measureTimeToFirstEvent(label: "chatHistoryNode_maxReadStoryId"),
|
|
recommendedChannels |> debug_measureTimeToFirstEvent(label: "chatHistoryNode_recommendedChannels"),
|
|
audioTranscriptionTrial |> debug_measureTimeToFirstEvent(label: "chatHistoryNode_audioTranscriptionTrial"),
|
|
chatThemes |> debug_measureTimeToFirstEvent(label: "chatHistoryNode_chatThemes"),
|
|
deviceContactsNumbers |> debug_measureTimeToFirstEvent(label: "chatHistoryNode_deviceContactsNumbers"),
|
|
contentSettings |> debug_measureTimeToFirstEvent(label: "chatHistoryNode_contentSettings")
|
|
) |> debug_measureTimeToFirstEvent(label: "chatHistoryNode_firstChatHistoryTransition")).startStrict(next: { [weak self] sharedData, /* MARK: Swiftgram */ update, chatPresentationData, selectedMessages, updatingMedia, networkType, preferredStoryHighQuality, animatedEmojiStickers, additionalAnimatedEmojiStickers, customChannelDiscussionReadState, customThreadOutgoingReadState, availableReactions, availableMessageEffects, savedMessageTags, defaultReaction, accountPeer, suggestAudioTranscription, promises, topicAuthorId, translationState, maxReadStoryId, recommendedChannels, audioTranscriptionTrial, chatThemes, deviceContactsNumbers, contentSettings in
|
|
let (historyAppearsCleared, pendingUnpinnedAllMessages, pendingRemovedMessages, currentlyPlayingMessageIdAndType, scrollToMessageId, chatHasBots, allAdMessages) = promises
|
|
|
|
if measure_isFirstTime {
|
|
measure_isFirstTime = false
|
|
#if DEBUG
|
|
let deltaTime = (CFAbsoluteTimeGetCurrent() - startTime) * 1000.0
|
|
print("Chat load time: \(deltaTime) ms")
|
|
#endif
|
|
}
|
|
|
|
let translationSettings: TranslationSettings
|
|
if let current = sharedData.entries[ApplicationSpecificSharedDataKeys.translationSettings]?.get(TranslationSettings.self) {
|
|
translationSettings = current
|
|
} else {
|
|
translationSettings = TranslationSettings.defaultSettings
|
|
}
|
|
|
|
func applyHole() {
|
|
Queue.mainQueue().async {
|
|
if let strongSelf = self {
|
|
if update.2 != strongSelf.chatHistoryLocationValue {
|
|
return
|
|
}
|
|
|
|
let historyView = (strongSelf.opaqueTransactionState as? ChatHistoryTransactionOpaqueState)?.historyView
|
|
let displayRange = strongSelf.displayedItemRange
|
|
if let filteredEntries = historyView?.filteredEntries, let visibleRange = displayRange.visibleRange {
|
|
var anchorIndex: MessageIndex?
|
|
loop: for index in visibleRange.firstIndex ..< filteredEntries.count {
|
|
switch filteredEntries[filteredEntries.count - 1 - index] {
|
|
case let .MessageEntry(message, _, _, _, _, _):
|
|
if message.adAttribute == nil {
|
|
anchorIndex = message.index
|
|
break loop
|
|
}
|
|
case let .MessageGroupEntry(_, messages, _):
|
|
for (message, _, _, _, _) in messages {
|
|
if message.adAttribute == nil {
|
|
anchorIndex = message.index
|
|
break loop
|
|
}
|
|
}
|
|
default:
|
|
break
|
|
}
|
|
}
|
|
if anchorIndex == nil, let historyView = historyView {
|
|
for entry in historyView.originalView.entries {
|
|
anchorIndex = entry.message.index
|
|
break
|
|
}
|
|
}
|
|
if let anchorIndex = anchorIndex {
|
|
strongSelf.chatHistoryLocationValue = ChatHistoryLocationInput(content: .Navigation(index: .message(anchorIndex), anchorIndex: .message(anchorIndex), count: historyMessageCount, highlight: false), id: (strongSelf.chatHistoryLocationValue?.id).flatMap({ $0 + 1 }) ?? 0)
|
|
}
|
|
} else {
|
|
if let subject = subject, case let .message(messageSubject, highlight, _, setupReply) = subject {
|
|
let initialSearchLocation: ChatHistoryInitialSearchLocation
|
|
switch messageSubject {
|
|
case let .id(id):
|
|
initialSearchLocation = .id(id)
|
|
case let .timestamp(timestamp):
|
|
if let peerId = strongSelf.chatLocation.peerId {
|
|
initialSearchLocation = .index(MessageIndex(id: MessageId(peerId: peerId, namespace: Namespaces.Message.Cloud, id: 1), timestamp: timestamp))
|
|
} else {
|
|
//TODO:implement
|
|
initialSearchLocation = .index(.absoluteUpperBound())
|
|
}
|
|
}
|
|
strongSelf.chatHistoryLocationValue = ChatHistoryLocationInput(content: .InitialSearch(subject: MessageHistoryInitialSearchSubject(location: initialSearchLocation, quote: (highlight?.quote).flatMap { quote in MessageHistoryInitialSearchSubject.Quote(string: quote.string, offset: quote.offset) }), count: historyMessageCount, highlight: highlight != nil, setupReply: setupReply), id: (strongSelf.chatHistoryLocationValue?.id).flatMap({ $0 + 1 }) ?? 0)
|
|
} else if let subject = subject, case let .pinnedMessages(maybeMessageId) = subject, let messageId = maybeMessageId {
|
|
strongSelf.chatHistoryLocationValue = ChatHistoryLocationInput(content: .InitialSearch(subject: MessageHistoryInitialSearchSubject(location: .id(messageId), quote: nil), count: historyMessageCount, highlight: true, setupReply: false), id: (strongSelf.chatHistoryLocationValue?.id).flatMap({ $0 + 1 }) ?? 0)
|
|
} else if var chatHistoryLocation = strongSelf.chatHistoryLocationValue {
|
|
chatHistoryLocation.id += 1
|
|
strongSelf.chatHistoryLocationValue = chatHistoryLocation
|
|
} else {
|
|
strongSelf.chatHistoryLocationValue = ChatHistoryLocationInput(content: .Initial(count: historyMessageCount), id: (strongSelf.chatHistoryLocationValue?.id).flatMap({ $0 + 1 }) ?? 0)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
let initialData: ChatHistoryCombinedInitialData?
|
|
switch update.0 {
|
|
case let .Loading(combinedInitialData, type):
|
|
if case .Generic(.FillHole) = type {
|
|
applyHole()
|
|
return
|
|
}
|
|
|
|
initialData = combinedInitialData
|
|
|
|
if resetScrolling, let previousViewValue = previousView.with({ $0 })?.0 {
|
|
let filteredEntries: [ChatHistoryEntry] = []
|
|
let processedView = ChatHistoryView(originalView: MessageHistoryView(tag: nil, namespaces: .all, entries: [], holeEarlier: false, holeLater: false, isLoading: true), filteredEntries: filteredEntries, associatedData: previousViewValue.associatedData, lastHeaderId: 0, id: previousViewValue.id, locationInput: previousViewValue.locationInput, ignoreMessagesInTimestampRange: nil, ignoreMessageIds: Set())
|
|
let previousValueAndVersion = previousView.swap((processedView, update.1, selectedMessages, allAdMessages.version))
|
|
let previous = previousValueAndVersion?.0
|
|
let previousSelectedMessages = previousValueAndVersion?.2
|
|
|
|
if let previousVersion = previousValueAndVersion?.1 {
|
|
assert(update.1 >= previousVersion)
|
|
}
|
|
|
|
var reason: ChatHistoryViewTransitionReason
|
|
reason = ChatHistoryViewTransitionReason.InteractiveChanges
|
|
|
|
let disableAnimations = true
|
|
let forceSynchronous = true
|
|
|
|
let rawTransition = preparedChatHistoryViewTransition(from: previous, to: processedView, reason: reason, reverse: false, chatLocation: chatLocation, controllerInteraction: controllerInteraction, scrollPosition: nil, scrollAnimationCurve: nil, initialData: initialData?.initialData, keyboardButtonsMessage: nil, cachedData: initialData?.cachedData, cachedDataMessages: initialData?.cachedDataMessages, readStateData: initialData?.readStateData, flashIndicators: false, updatedMessageSelection: previousSelectedMessages != selectedMessages, messageTransitionNode: messageTransitionNode(), allUpdated: false)
|
|
var mappedTransition = mappedChatHistoryViewListTransition(context: context, chatLocation: chatLocation, associatedData: previousViewValue.associatedData, controllerInteraction: controllerInteraction, mode: mode, lastHeaderId: 0, animateFromPreviousFilter: resetScrolling, transition: rawTransition)
|
|
|
|
if disableAnimations {
|
|
mappedTransition.options.remove(.AnimateInsertion)
|
|
mappedTransition.options.remove(.AnimateAlpha)
|
|
mappedTransition.options.remove(.AnimateTopItemPosition)
|
|
mappedTransition.options.remove(.RequestItemInsertionAnimations)
|
|
}
|
|
if forceSynchronous || resetScrolling {
|
|
mappedTransition.options.insert(.Synchronous)
|
|
}
|
|
if resetScrolling {
|
|
mappedTransition.options.insert(.AnimateAlpha)
|
|
mappedTransition.options.insert(.AnimateFullTransition)
|
|
}
|
|
|
|
if resetScrolling {
|
|
resetScrolling = false
|
|
}
|
|
|
|
Queue.mainQueue().async {
|
|
guard let strongSelf = self else {
|
|
return
|
|
}
|
|
if strongSelf.appliedPlayingMessageId?.0 != currentlyPlayingMessageIdAndType?.0 {
|
|
strongSelf.appliedPlayingMessageId = currentlyPlayingMessageIdAndType
|
|
}
|
|
if strongSelf.appliedScrollToMessageId != scrollToMessageId {
|
|
strongSelf.appliedScrollToMessageId = scrollToMessageId
|
|
}
|
|
strongSelf.enqueueHistoryViewTransition(mappedTransition)
|
|
}
|
|
}
|
|
|
|
Queue.mainQueue().async {
|
|
if let strongSelf = self {
|
|
if !strongSelf.didSetInitialData {
|
|
strongSelf.didSetInitialData = true
|
|
var combinedInitialData = combinedInitialData
|
|
combinedInitialData?.cachedData = nil
|
|
strongSelf._initialData.set(.single(combinedInitialData))
|
|
}
|
|
|
|
let cachedData = initialData?.cachedData
|
|
let cachedDataMessages = initialData?.cachedDataMessages
|
|
|
|
strongSelf._cachedPeerDataAndMessages.set(.single((cachedData, cachedDataMessages)))
|
|
|
|
let loadState: ChatHistoryNodeLoadState = .loading(false)
|
|
if strongSelf.loadState != loadState {
|
|
strongSelf.loadState = loadState
|
|
strongSelf.loadStateUpdated?(loadState, false)
|
|
for f in strongSelf.additionalLoadStateUpdated {
|
|
f(loadState, false)
|
|
}
|
|
}
|
|
|
|
let historyState: ChatHistoryNodeHistoryState = .loading
|
|
if strongSelf.currentHistoryState != historyState {
|
|
strongSelf.currentHistoryState = historyState
|
|
strongSelf.historyState.set(historyState)
|
|
}
|
|
}
|
|
}
|
|
return
|
|
case let .HistoryView(view, type, scrollPosition, flashIndicators, originalScrollPosition, data, id):
|
|
if case .Generic(.FillHole) = type {
|
|
applyHole()
|
|
return
|
|
}
|
|
|
|
initialData = data
|
|
var updatedScrollPosition = scrollPosition
|
|
|
|
var reverse = false
|
|
var reverseGroups = false
|
|
var includeSearchEntry = false
|
|
if case let .list(search, reverseValue, reverseGroupsValue, _, _, _) = mode {
|
|
includeSearchEntry = search
|
|
reverse = reverseValue
|
|
reverseGroups = reverseGroupsValue
|
|
}
|
|
|
|
|
|
var isPremium = false
|
|
if case let .user(user) = accountPeer, user.isPremium {
|
|
isPremium = true
|
|
}
|
|
|
|
var audioTranscriptionProvidedByBoost = false
|
|
var autoTranslate = false
|
|
var isCopyProtectionEnabled: Bool = data.initialData?.peer?.isCopyProtectionEnabled ?? false
|
|
for entry in view.additionalData {
|
|
if case let .peer(_, maybePeer) = entry, let peer = maybePeer {
|
|
isCopyProtectionEnabled = peer.isCopyProtectionEnabled
|
|
if let channel = peer as? TelegramChannel {
|
|
autoTranslate = channel.flags.contains(.autoTranslateEnabled)
|
|
if let boostLevel = channel.approximateBoostLevel, boostLevel >= premiumConfiguration.minGroupAudioTranscriptionLevel {
|
|
audioTranscriptionProvidedByBoost = true
|
|
}
|
|
}
|
|
}
|
|
}
|
|
let alwaysDisplayTranscribeButton = ChatMessageItemAssociatedData.DisplayTranscribeButton(
|
|
canBeDisplayed: suggestAudioTranscription.0 < 2,
|
|
displayForNotConsumed: suggestAudioTranscription.1,
|
|
providedByGroupBoost: audioTranscriptionProvidedByBoost
|
|
)
|
|
|
|
// MARK: Swiftgram
|
|
// var translateToLanguage: (fromLang: String, toLang: String)?
|
|
// if let translationState, (isPremium || autoTranslate) && translationState.isEnabled {
|
|
var languageCode = translationState?.toLang ?? chatPresentationData.strings.baseLanguageCode
|
|
let rawSuffix = "-raw"
|
|
if languageCode.hasSuffix(rawSuffix) {
|
|
languageCode = String(languageCode.dropLast(rawSuffix.count))
|
|
}
|
|
languageCode = normalizeTranslationLanguage(languageCode)
|
|
let translateToLanguageSG = languageCode
|
|
// }
|
|
var translateToLanguage: (fromLang: String, toLang: String)?
|
|
if let translationState, (isPremium || autoTranslate || true) && translationState.isEnabled {
|
|
translateToLanguage = (normalizeTranslationLanguage(translationState.fromLang), normalizeTranslationLanguage(languageCode))
|
|
}
|
|
|
|
|
|
|
|
let associatedData = extractAssociatedData(translateToLanguageSG: translateToLanguageSG, translationSettings: translationSettings, /* MARK: Swiftgram */ chatLocation: chatLocation, view: view, automaticDownloadNetworkType: networkType, preferredStoryHighQuality: preferredStoryHighQuality, animatedEmojiStickers: animatedEmojiStickers, additionalAnimatedEmojiStickers: additionalAnimatedEmojiStickers, subject: subject, currentlyPlayingMessageId: currentlyPlayingMessageIdAndType?.0, isCopyProtectionEnabled: isCopyProtectionEnabled, availableReactions: availableReactions, availableMessageEffects: availableMessageEffects, savedMessageTags: savedMessageTags, defaultReaction: defaultReaction, isPremium: isPremium, alwaysDisplayTranscribeButton: alwaysDisplayTranscribeButton, accountPeer: accountPeer, topicAuthorId: topicAuthorId, hasBots: chatHasBots, translateToLanguage: translateToLanguage?.toLang, maxReadStoryId: maxReadStoryId, recommendedChannels: recommendedChannels, audioTranscriptionTrial: audioTranscriptionTrial, chatThemes: chatThemes, deviceContactsNumbers: deviceContactsNumbers, isInline: !rotated, showSensitiveContent: contentSettings.ignoreContentRestrictionReasons.contains("sensitive"))
|
|
|
|
var includeEmbeddedSavedChatInfo = false
|
|
if case let .replyThread(message) = chatLocation, message.peerId == context.account.peerId, !rotated {
|
|
includeEmbeddedSavedChatInfo = true
|
|
}
|
|
|
|
let previousChatHistoryEntriesForViewState = chatHistoryEntriesForViewState.with({ $0 })
|
|
|
|
let (filteredEntries, updatedChatHistoryEntriesForViewState) = chatHistoryEntriesForView(
|
|
currentState: previousChatHistoryEntriesForViewState,
|
|
context: context,
|
|
location: chatLocation,
|
|
view: view,
|
|
includeUnreadEntry: mode == .bubbles,
|
|
includeEmptyEntry: mode == .bubbles && tag == nil,
|
|
includeChatInfoEntry: mode == .bubbles,
|
|
includeSearchEntry: includeSearchEntry && tag != nil,
|
|
includeEmbeddedSavedChatInfo: includeEmbeddedSavedChatInfo,
|
|
reverse: reverse,
|
|
groupMessages: mode == .bubbles,
|
|
reverseGroupedMessages: reverseGroups,
|
|
selectedMessages: selectedMessages,
|
|
presentationData: chatPresentationData,
|
|
historyAppearsCleared: historyAppearsCleared,
|
|
skipViewOnceMedia: mode != .bubbles,
|
|
pendingUnpinnedAllMessages: pendingUnpinnedAllMessages,
|
|
pendingRemovedMessages: pendingRemovedMessages,
|
|
associatedData: associatedData,
|
|
updatingMedia: updatingMedia,
|
|
customChannelDiscussionReadState: customChannelDiscussionReadState,
|
|
customThreadOutgoingReadState: customThreadOutgoingReadState,
|
|
cachedData: data.cachedData,
|
|
adMessage: allAdMessages.fixed,
|
|
dynamicAdMessages: allAdMessages.opportunistic
|
|
)
|
|
let lastHeaderId = filteredEntries.last.flatMap { listMessageDateHeaderId(timestamp: $0.index.timestamp) } ?? 0
|
|
let processedView = ChatHistoryView(originalView: view, filteredEntries: filteredEntries, associatedData: associatedData, lastHeaderId: lastHeaderId, id: id, locationInput: update.2, ignoreMessagesInTimestampRange: update.3, ignoreMessageIds: update.4)
|
|
let previousValueAndVersion = previousView.swap((processedView, update.1, selectedMessages, allAdMessages.version))
|
|
let _ = chatHistoryEntriesForViewState.swap(updatedChatHistoryEntriesForViewState)
|
|
let previous = previousValueAndVersion?.0
|
|
let previousSelectedMessages = previousValueAndVersion?.2
|
|
|
|
if let previousVersion = previousValueAndVersion?.1 {
|
|
assert(update.1 >= previousVersion)
|
|
}
|
|
|
|
if scrollPosition == nil, let originalScrollPosition = originalScrollPosition {
|
|
switch originalScrollPosition {
|
|
case let .index(subject, position, _, _, highlight, displayLink, setupReply):
|
|
if case .upperBound = subject.index {
|
|
if let previous = previous, previous.filteredEntries.isEmpty {
|
|
updatedScrollPosition = .index(subject: subject, position: position, directionHint: .Down, animated: false, highlight: highlight, displayLink: displayLink, setupReply: setupReply)
|
|
}
|
|
}
|
|
default:
|
|
break
|
|
}
|
|
}
|
|
|
|
var reason: ChatHistoryViewTransitionReason
|
|
|
|
let previousHistoryAppearsClearedValue = previousHistoryAppearsCleared.swap(historyAppearsCleared)
|
|
if previousHistoryAppearsClearedValue != nil && previousHistoryAppearsClearedValue != historyAppearsCleared && !historyAppearsCleared {
|
|
reason = ChatHistoryViewTransitionReason.Initial(fadeIn: !processedView.filteredEntries.isEmpty)
|
|
} else if let previous = previous, previous.id == processedView.id, previous.originalView.entries == processedView.originalView.entries {
|
|
reason = ChatHistoryViewTransitionReason.InteractiveChanges
|
|
updatedScrollPosition = nil
|
|
} else if let previous = previous, previous.id == processedView.id, previous.ignoreMessageIds != processedView.ignoreMessageIds {
|
|
reason = ChatHistoryViewTransitionReason.InteractiveChanges
|
|
updatedScrollPosition = nil
|
|
} else {
|
|
switch type {
|
|
case let .Initial(fadeIn):
|
|
reason = ChatHistoryViewTransitionReason.Initial(fadeIn: fadeIn)
|
|
case let .Generic(genericType):
|
|
switch genericType {
|
|
case .InitialUnread, .Initial:
|
|
reason = ChatHistoryViewTransitionReason.Initial(fadeIn: false)
|
|
case .Generic:
|
|
reason = ChatHistoryViewTransitionReason.InteractiveChanges
|
|
case .UpdateVisible:
|
|
reason = ChatHistoryViewTransitionReason.Reload
|
|
case .FillHole:
|
|
reason = ChatHistoryViewTransitionReason.HoleReload
|
|
}
|
|
}
|
|
}
|
|
|
|
var disableAnimations = false
|
|
var forceSynchronous = false
|
|
|
|
if let previousValueAndVersion = previousValueAndVersion, allAdMessages.version != previousValueAndVersion.3 {
|
|
reason = ChatHistoryViewTransitionReason.Reload
|
|
disableAnimations = true
|
|
forceSynchronous = true
|
|
}
|
|
|
|
var scrollAnimationCurve: ListViewAnimationCurve? = nil
|
|
if let strongSelf = self, case .default = source {
|
|
if let translateToLanguage {
|
|
strongSelf.translationLang = (fromLang: translateToLanguage.fromLang, toLang: translateToLanguage.toLang)
|
|
} else {
|
|
strongSelf.translationLang = nil
|
|
}
|
|
if strongSelf.appliedScrollToMessageId == nil, let scrollToMessageId = scrollToMessageId {
|
|
updatedScrollPosition = .index(subject: MessageHistoryScrollToSubject(index: .message(scrollToMessageId), quote: nil), position: .center(.top), directionHint: .Up, animated: true, highlight: false, displayLink: true, setupReply: false)
|
|
scrollAnimationCurve = .Spring(duration: 0.4)
|
|
} else {
|
|
let wasPlaying = strongSelf.appliedPlayingMessageId != nil
|
|
if strongSelf.appliedPlayingMessageId?.0 != currentlyPlayingMessageIdAndType?.0, let (currentlyPlayingMessageId, currentlyPlayingVideo) = currentlyPlayingMessageIdAndType {
|
|
if isFirstTime {
|
|
} else if case let .peer(peerId) = chatLocation, currentlyPlayingMessageId.id.peerId != peerId {
|
|
} else {
|
|
var isChat = false
|
|
if case .peer = chatLocation {
|
|
isChat = true
|
|
}
|
|
|
|
if (isChat && (wasPlaying || currentlyPlayingVideo)) || (!isChat && !wasPlaying && currentlyPlayingVideo) {
|
|
var currentIsVisible = true
|
|
var nextIsVisible = false
|
|
if let appliedPlayingMessageId = strongSelf.appliedPlayingMessageId {
|
|
currentIsVisible = false
|
|
strongSelf.forEachVisibleMessageItemNode({ view in
|
|
if view.item?.message.id == appliedPlayingMessageId.0.id && appliedPlayingMessageId.1 == true {
|
|
currentIsVisible = true
|
|
}
|
|
})
|
|
}
|
|
strongSelf.forEachVisibleMessageItemNode({ view in
|
|
if view.item?.message.id == currentlyPlayingMessageId.id {
|
|
nextIsVisible = true
|
|
}
|
|
})
|
|
if currentIsVisible && nextIsVisible && currentlyPlayingVideo {
|
|
updatedScrollPosition = .index(subject: MessageHistoryScrollToSubject(index: .message(currentlyPlayingMessageId), quote: nil), position: .center(.bottom), directionHint: .Up, animated: true, highlight: true, displayLink: true, setupReply: false)
|
|
scrollAnimationCurve = .Spring(duration: 0.4)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
isFirstTime = false
|
|
}
|
|
|
|
if let strongSelf = self {
|
|
if let recommendedChannels, !recommendedChannels.channels.isEmpty && !recommendedChannels.isHidden {
|
|
if !strongSelf.didSetupRecommendedChannelsPreload {
|
|
strongSelf.didSetupRecommendedChannelsPreload = true
|
|
let preloadDisposable = DisposableSet()
|
|
for channel in recommendedChannels.channels.prefix(5) {
|
|
preloadDisposable.add(strongSelf.context.account.viewTracker.polledChannel(peerId: channel.peer.id).startStrict())
|
|
preloadDisposable.add(strongSelf.context.account.addAdditionalPreloadHistoryPeerId(peerId: channel.peer.id))
|
|
}
|
|
strongSelf.preloadRecommendedChannelsDisposable.set(preloadDisposable)
|
|
}
|
|
} else {
|
|
strongSelf.didSetupRecommendedChannelsPreload = false
|
|
strongSelf.preloadRecommendedChannelsDisposable.set(nil)
|
|
}
|
|
}
|
|
|
|
if let strongSelf = self, updatedScrollPosition == nil, case .InteractiveChanges = reason, case let .known(offset) = strongSelf.visibleContentOffset(), abs(offset) <= 0.9, let previous = previous {
|
|
var fillsScreen = true
|
|
switch strongSelf.visibleBottomContentOffset() {
|
|
case let .known(bottomOffset):
|
|
if bottomOffset <= strongSelf.visibleSize.height - strongSelf.insets.bottom {
|
|
fillsScreen = false
|
|
}
|
|
default:
|
|
break
|
|
}
|
|
|
|
var previousNumAds = 0
|
|
for entry in previous.filteredEntries {
|
|
if case let .MessageEntry(message, _, _, _, _, _) = entry {
|
|
if message.adAttribute != nil {
|
|
previousNumAds += 1
|
|
}
|
|
}
|
|
}
|
|
|
|
var updatedNumAds = 0
|
|
var firstNonAdIndex: MessageIndex?
|
|
for entry in processedView.filteredEntries.reversed() {
|
|
if case let .MessageEntry(message, _, _, _, _, _) = entry {
|
|
if message.adAttribute != nil {
|
|
updatedNumAds += 1
|
|
} else {
|
|
if firstNonAdIndex == nil {
|
|
firstNonAdIndex = message.index
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if fillsScreen, let firstNonAdIndex = firstNonAdIndex, previousNumAds == 0, updatedNumAds != 0 {
|
|
updatedScrollPosition = .index(subject: MessageHistoryScrollToSubject(index: .message(firstNonAdIndex), quote: nil), position: .top(0.0), directionHint: .Up, animated: false, highlight: false, displayLink: false, setupReply: false)
|
|
disableAnimations = true
|
|
}
|
|
}
|
|
|
|
if let strongSelf = self, updatedScrollPosition == nil, case .InteractiveChanges = reason, let previous = previous, case let .known(offset) = strongSelf.visibleContentOffset(), abs(offset) <= 320.0 {
|
|
var hadJoin = false
|
|
var hadAd = false
|
|
for entry in previous.filteredEntries.reversed() {
|
|
if case let .MessageEntry(message, _, _, _, _, _) = entry {
|
|
if let action = message.media.first(where: { $0 is TelegramMediaAction }) as? TelegramMediaAction, case .joinedChannel = action.action {
|
|
hadJoin = true
|
|
break
|
|
} else if message.adAttribute != nil {
|
|
hadAd = true
|
|
}
|
|
}
|
|
}
|
|
|
|
if !hadJoin && hadAd {
|
|
for entry in processedView.filteredEntries.reversed() {
|
|
if case let .MessageEntry(message, _, _, _, _, _) = entry {
|
|
if message.adAttribute == nil {
|
|
if let action = message.media.first(where: { $0 is TelegramMediaAction }) as? TelegramMediaAction, case .joinedChannel = action.action {
|
|
updatedScrollPosition = .index(subject: MessageHistoryScrollToSubject(index: .message(message.index), quote: nil), position: .top(0.0), directionHint: .Up, animated: true, highlight: false, displayLink: false, setupReply: false)
|
|
}
|
|
break
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
var forceUpdateAll = false
|
|
if let previous = previous, previous.associatedData.isPremium != processedView.associatedData.isPremium {
|
|
forceUpdateAll = true
|
|
}
|
|
|
|
var keyboardButtonsMessage = view.topTaggedMessages.first
|
|
if let keyboardButtonsMessageValue = keyboardButtonsMessage, keyboardButtonsMessageValue.isRestricted(platform: "ios", contentSettings: context.currentContentSettings.with({ $0 })) {
|
|
keyboardButtonsMessage = nil
|
|
}
|
|
|
|
let rawTransition = preparedChatHistoryViewTransition(from: previous, to: processedView, reason: reason, reverse: reverse, chatLocation: chatLocation, controllerInteraction: controllerInteraction, scrollPosition: updatedScrollPosition, scrollAnimationCurve: scrollAnimationCurve, initialData: initialData?.initialData, keyboardButtonsMessage: keyboardButtonsMessage, cachedData: initialData?.cachedData, cachedDataMessages: initialData?.cachedDataMessages, readStateData: initialData?.readStateData, flashIndicators: flashIndicators, updatedMessageSelection: previousSelectedMessages != selectedMessages, messageTransitionNode: messageTransitionNode(), allUpdated: updateAllOnEachVersion || forceUpdateAll)
|
|
var mappedTransition = mappedChatHistoryViewListTransition(context: context, chatLocation: chatLocation, associatedData: associatedData, controllerInteraction: controllerInteraction, mode: mode, lastHeaderId: lastHeaderId, animateFromPreviousFilter: resetScrolling, transition: rawTransition)
|
|
|
|
if disableAnimations {
|
|
mappedTransition.options.remove(.AnimateInsertion)
|
|
mappedTransition.options.remove(.AnimateAlpha)
|
|
mappedTransition.options.remove(.AnimateTopItemPosition)
|
|
mappedTransition.options.remove(.RequestItemInsertionAnimations)
|
|
}
|
|
if forceSynchronous || resetScrolling {
|
|
mappedTransition.options.insert(.Synchronous)
|
|
}
|
|
if resetScrolling {
|
|
mappedTransition.options.insert(.AnimateAlpha)
|
|
mappedTransition.options.insert(.AnimateFullTransition)
|
|
}
|
|
|
|
if resetScrolling {
|
|
resetScrolling = false
|
|
}
|
|
|
|
Queue.mainQueue().async {
|
|
guard let strongSelf = self else {
|
|
return
|
|
}
|
|
if strongSelf.appliedPlayingMessageId?.0 != currentlyPlayingMessageIdAndType?.0 {
|
|
strongSelf.appliedPlayingMessageId = currentlyPlayingMessageIdAndType
|
|
}
|
|
if strongSelf.appliedScrollToMessageId != scrollToMessageId {
|
|
strongSelf.appliedScrollToMessageId = scrollToMessageId
|
|
}
|
|
strongSelf.enqueueHistoryViewTransition(mappedTransition)
|
|
}
|
|
}
|
|
})
|
|
|
|
self.historyDisposable.set(historyViewTransitionDisposable.strict())
|
|
}
|
|
|
|
private func beginReadHistoryManagement() {
|
|
let previousMaxIncomingMessageIndexByNamespace = Atomic<[MessageId.Namespace: MessageIndex]>(value: [:])
|
|
let readHistory = combineLatest(self.maxVisibleIncomingMessageIndex.get(), self.canReadHistory.get())
|
|
|
|
self.readHistoryDisposable.set((readHistory |> deliverOnMainQueue).startStrict(next: { [weak self] messageIndex, canRead in
|
|
guard let strongSelf = self else {
|
|
return
|
|
}
|
|
if !canRead {
|
|
return
|
|
}
|
|
|
|
var apply = false
|
|
let _ = previousMaxIncomingMessageIndexByNamespace.modify { dict in
|
|
let previousIndex = dict[messageIndex.id.namespace]
|
|
if previousIndex == nil || previousIndex! < messageIndex {
|
|
apply = true
|
|
var dict = dict
|
|
dict[messageIndex.id.namespace] = messageIndex
|
|
return dict
|
|
}
|
|
return dict
|
|
}
|
|
if apply {
|
|
switch strongSelf.chatLocation {
|
|
case .peer, .replyThread:
|
|
if !strongSelf.context.sharedContext.immediateExperimentalUISettings.skipReadHistory && !strongSelf.context.account.isSupportUser {
|
|
strongSelf.context.applyMaxReadIndex(for: strongSelf.chatLocation, contextHolder: strongSelf.chatLocationContextHolder, messageIndex: messageIndex)
|
|
}
|
|
case .customChatContents:
|
|
break
|
|
}
|
|
}
|
|
}).strict())
|
|
|
|
self.canReadHistoryDisposable = (self.canReadHistory.get() |> deliverOnMainQueue).startStrict(next: { [weak self, weak context] value in
|
|
if let strongSelf = self {
|
|
if strongSelf.canReadHistoryValue != value {
|
|
strongSelf.canReadHistoryValue = value
|
|
strongSelf.controllerInteraction.canReadHistory = value
|
|
strongSelf.updateReadHistoryActions()
|
|
|
|
if strongSelf.canReadHistoryValue && !strongSelf.suspendReadingReactions && !strongSelf.messageIdsScheduledForMarkAsSeen.isEmpty {
|
|
let messageIds = strongSelf.messageIdsScheduledForMarkAsSeen
|
|
strongSelf.messageIdsScheduledForMarkAsSeen.removeAll()
|
|
context?.account.viewTracker.updateMarkMentionsSeenForMessageIds(messageIds: messageIds)
|
|
}
|
|
|
|
strongSelf.attemptReadingReactions()
|
|
}
|
|
}
|
|
}).strict()
|
|
}
|
|
|
|
private func beginPresentationDataManagement(updated: Signal<PresentationData, NoError>) {
|
|
let appConfiguration = self.context.account.postbox.preferencesView(keys: [PreferencesKeys.appConfiguration])
|
|
|> take(1)
|
|
|> map { view in
|
|
return view.values[PreferencesKeys.appConfiguration]?.get(AppConfiguration.self) ?? .defaultValue
|
|
}
|
|
|
|
var didSetPresentationData = false
|
|
self.presentationDataDisposable = (combineLatest(queue: .mainQueue(),
|
|
updated |> debug_measureTimeToFirstEvent(label: "chatHistoryNode_beginPresentationDataManagement_updated"),
|
|
appConfiguration |> debug_measureTimeToFirstEvent(label: "chatHistoryNode_beginPresentationDataManagement_appConfiguration")
|
|
)
|
|
|> deliverOnMainQueue).startStrict(next: { [weak self] presentationData, appConfiguration in
|
|
if let strongSelf = self {
|
|
let previousTheme = strongSelf.currentPresentationData.theme
|
|
let previousStrings = strongSelf.currentPresentationData.strings
|
|
let previousWallpaper = strongSelf.currentPresentationData.theme.wallpaper
|
|
let previousAnimatedEmojiScale = strongSelf.currentPresentationData.animatedEmojiScale
|
|
|
|
let animatedEmojiConfig = ChatHistoryAnimatedEmojiConfiguration.with(appConfiguration: appConfiguration)
|
|
|
|
if !didSetPresentationData || previousTheme !== presentationData.theme || previousStrings !== presentationData.strings || previousWallpaper != presentationData.chatWallpaper || previousAnimatedEmojiScale != animatedEmojiConfig.scale {
|
|
didSetPresentationData = true
|
|
|
|
let themeData = ChatPresentationThemeData(theme: presentationData.theme, wallpaper: presentationData.chatWallpaper)
|
|
let chatPresentationData = ChatPresentationData(theme: themeData, fontSize: presentationData.chatFontSize, strings: presentationData.strings, dateTimeFormat: presentationData.dateTimeFormat, nameDisplayOrder: presentationData.nameDisplayOrder, disableAnimations: true, largeEmoji: presentationData.largeEmoji, chatBubbleCorners: presentationData.chatBubbleCorners, animatedEmojiScale: animatedEmojiConfig.scale)
|
|
|
|
strongSelf.currentPresentationData = chatPresentationData
|
|
strongSelf.dynamicBounceEnabled = false
|
|
|
|
strongSelf.forEachItemHeaderNode { itemHeaderNode in
|
|
if let dateNode = itemHeaderNode as? ChatMessageDateHeaderNode {
|
|
dateNode.updatePresentationData(chatPresentationData, context: strongSelf.context)
|
|
} else if let avatarNode = itemHeaderNode as? ChatMessageAvatarHeaderNodeImpl {
|
|
avatarNode.updatePresentationData(chatPresentationData, context: strongSelf.context)
|
|
} else if let dateNode = itemHeaderNode as? ListMessageDateHeaderNode {
|
|
dateNode.updateThemeAndStrings(theme: presentationData.theme, strings: presentationData.strings)
|
|
}
|
|
}
|
|
strongSelf.chatPresentationDataPromise.set(.single(chatPresentationData))
|
|
}
|
|
}
|
|
}).strict()
|
|
}
|
|
|
|
private func attemptReadingReactions() {
|
|
if self.canReadHistoryValue && !self.suspendReadingReactions && !self.context.sharedContext.immediateExperimentalUISettings.skipReadHistory && !self.messageIdsWithReactionsScheduledForMarkAsSeen.isEmpty {
|
|
let messageIds = self.messageIdsWithReactionsScheduledForMarkAsSeen
|
|
|
|
let _ = self.displayUnseenReactionAnimations(messageIds: Array(messageIds))
|
|
|
|
self.messageIdsWithReactionsScheduledForMarkAsSeen.removeAll()
|
|
self.context.account.viewTracker.updateMarkReactionsSeenForMessageIds(messageIds: messageIds)
|
|
}
|
|
|
|
if self.canReadHistoryValue {
|
|
self.forEachVisibleMessageItemNode { itemNode in
|
|
itemNode.unreadMessageRangeUpdated()
|
|
}
|
|
}
|
|
}
|
|
|
|
func takeGenericReactionEffect() -> String? {
|
|
let result = self.genericReactionEffect
|
|
self.loadNextGenericReactionEffect(context: self.context)
|
|
|
|
return result
|
|
}
|
|
|
|
private func loadNextGenericReactionEffect(context: AccountContext) {
|
|
self.genericReactionEffectDisposable?.dispose()
|
|
self.genericReactionEffectDisposable = (ReactionContextNode.randomGenericReactionEffect(context: context) |> deliverOnMainQueue).startStrict(next: { [weak self] path in
|
|
guard let strongSelf = self else {
|
|
return
|
|
}
|
|
strongSelf.genericReactionEffect = path
|
|
})
|
|
}
|
|
|
|
public func setLoadStateUpdated(_ f: @escaping (ChatHistoryNodeLoadState, Bool) -> Void) {
|
|
self.loadStateUpdated = f
|
|
}
|
|
|
|
public func addSetLoadStateUpdated(_ f: @escaping (ChatHistoryNodeLoadState, Bool) -> Void) {
|
|
self.additionalLoadStateUpdated.append(f)
|
|
}
|
|
|
|
private func maybeUpdateOverscrollAction(offset: CGFloat?) {
|
|
if self.freezeOverscrollControl {
|
|
return
|
|
}
|
|
if let offset = offset, offset < -0.1, self.offerNextChannelToRead, let chatControllerNode = self.controllerInteraction.chatControllerNode() as? ChatControllerNode, chatControllerNode.shouldAllowOverscrollActions {
|
|
let overscrollView: ComponentHostView<Empty>
|
|
if let current = self.overscrollView {
|
|
overscrollView = current
|
|
} else {
|
|
overscrollView = ComponentHostView<Empty>()
|
|
self.overscrollView = overscrollView
|
|
self.view.superview?.insertSubview(overscrollView, aboveSubview: self.view)
|
|
}
|
|
|
|
let expandDistance = max(-offset - 12.0, 0.0)
|
|
let expandProgress: CGFloat = min(1.0, expandDistance / 94.0)
|
|
|
|
let previousType = self.currentOverscrollExpandProgress >= 1.0
|
|
let currentType = expandProgress >= 1.0
|
|
|
|
if previousType != currentType, currentType {
|
|
if self.feedback == nil {
|
|
self.feedback = HapticFeedback()
|
|
}
|
|
if let _ = nextChannelToRead {
|
|
self.feedback?.tap()
|
|
} else {
|
|
self.feedback?.success()
|
|
}
|
|
}
|
|
|
|
self.currentOverscrollExpandProgress = expandProgress
|
|
|
|
if let nextChannelToRead = self.nextChannelToRead {
|
|
let swipeText: NSAttributedString
|
|
let releaseText: NSAttributedString
|
|
switch nextChannelToRead.location {
|
|
case .same:
|
|
if let controllerNode = self.controllerInteraction.chatControllerNode() as? ChatControllerNode, let chatController = controllerNode.interfaceInteraction?.chatController() as? ChatControllerImpl, chatController.customChatNavigationStack != nil {
|
|
swipeText = NSAttributedString(string: self.currentPresentationData.strings.Chat_NextSuggestedChannelSwipeProgress)
|
|
releaseText = NSAttributedString(string: self.currentPresentationData.strings.Chat_NextSuggestedChannelSwipeAction)
|
|
} else if nextChannelToRead.threadData != nil {
|
|
swipeText = NSAttributedString(string: self.currentPresentationData.strings.Chat_NextUnreadTopicSwipeProgress)
|
|
releaseText = NSAttributedString(string: self.currentPresentationData.strings.Chat_NextUnreadTopicSwipeAction)
|
|
} else {
|
|
swipeText = NSAttributedString(string: self.currentPresentationData.strings.Chat_NextChannelSameLocationSwipeProgress)
|
|
releaseText = NSAttributedString(string: self.currentPresentationData.strings.Chat_NextChannelSameLocationSwipeAction)
|
|
}
|
|
case .archived:
|
|
swipeText = NSAttributedString(string: self.currentPresentationData.strings.Chat_NextChannelArchivedSwipeProgress)
|
|
releaseText = NSAttributedString(string: self.currentPresentationData.strings.Chat_NextChannelArchivedSwipeAction)
|
|
case .unarchived:
|
|
swipeText = NSAttributedString(string: self.currentPresentationData.strings.Chat_NextChannelUnarchivedSwipeProgress)
|
|
releaseText = NSAttributedString(string: self.currentPresentationData.strings.Chat_NextChannelUnarchivedSwipeAction)
|
|
case let .folder(_, title):
|
|
let swipeTextValue = NSMutableAttributedString(string: self.currentPresentationData.strings.Chat_NextChannelFolderSwipeProgressV2)
|
|
let swipeFolderRange = (swipeTextValue.string as NSString).range(of: "{folder}")
|
|
if swipeFolderRange.location != NSNotFound {
|
|
swipeTextValue.replaceCharacters(in: swipeFolderRange, with: "")
|
|
swipeTextValue.insert(title.attributedString(attributes: [
|
|
ChatTextInputAttributes.bold: true
|
|
]), at: swipeFolderRange.location)
|
|
}
|
|
swipeText = swipeTextValue
|
|
|
|
let releaseTextValue = NSMutableAttributedString(string: self.currentPresentationData.strings.Chat_NextChannelFolderSwipeActionV2)
|
|
let releaseTextFolderRange = (releaseTextValue.string as NSString).range(of: "{folder}")
|
|
if releaseTextFolderRange.location != NSNotFound {
|
|
releaseTextValue.replaceCharacters(in: releaseTextFolderRange, with: "")
|
|
releaseTextValue.insert(title.attributedString(attributes: [
|
|
ChatTextInputAttributes.bold: true
|
|
]), at: releaseTextFolderRange.location)
|
|
}
|
|
releaseText = releaseTextValue
|
|
}
|
|
|
|
if expandProgress < 0.1 {
|
|
chatControllerNode.setChatInputPanelOverscrollNode(overscrollNode: nil)
|
|
} else if expandProgress >= 1.0 {
|
|
if chatControllerNode.inputPanelOverscrollNode?.text.string != releaseText.string {
|
|
chatControllerNode.setChatInputPanelOverscrollNode(overscrollNode: ChatInputPanelOverscrollNode(context: self.context, text: releaseText, color: self.currentPresentationData.theme.theme.rootController.navigationBar.secondaryTextColor, priority: 1))
|
|
}
|
|
} else {
|
|
if chatControllerNode.inputPanelOverscrollNode?.text.string != swipeText.string {
|
|
chatControllerNode.setChatInputPanelOverscrollNode(overscrollNode: ChatInputPanelOverscrollNode(context: self.context, text: swipeText, color: self.currentPresentationData.theme.theme.rootController.navigationBar.secondaryTextColor, priority: 2))
|
|
}
|
|
}
|
|
} else {
|
|
chatControllerNode.setChatInputPanelOverscrollNode(overscrollNode: nil)
|
|
}
|
|
|
|
var overscrollFrame = CGRect(origin: CGPoint(x: 0.0, y: self.insets.top), size: CGSize(width: self.bounds.width, height: 94.0))
|
|
if self.freezeOverscrollControlProgress {
|
|
overscrollFrame.origin.y -= max(0.0, 94.0 - expandDistance)
|
|
}
|
|
|
|
overscrollView.frame = self.view.convert(overscrollFrame, to: self.view.superview!)
|
|
|
|
let _ = overscrollView.update(
|
|
transition: .immediate,
|
|
component: AnyComponent(ChatOverscrollControl(
|
|
backgroundColor: selectDateFillStaticColor(theme: self.currentPresentationData.theme.theme, wallpaper: self.currentPresentationData.theme.wallpaper),
|
|
foregroundColor: bubbleVariableColor(variableColor: self.currentPresentationData.theme.theme.chat.serviceMessage.dateTextColor, wallpaper: self.currentPresentationData.theme.wallpaper),
|
|
peer: self.nextChannelToRead?.peer,
|
|
threadData: (self.nextChannelToRead?.threadData).flatMap { threadData in
|
|
return ChatOverscrollThreadData(
|
|
id: threadData.id,
|
|
data: threadData.data
|
|
)
|
|
},
|
|
isForumThread: self.chatLocation.threadId != nil,
|
|
unreadCount: self.nextChannelToRead?.unreadCount ?? 0,
|
|
location: self.nextChannelToRead?.location ?? .same,
|
|
context: self.context,
|
|
expandDistance: self.freezeOverscrollControl ? 94.0 : expandDistance,
|
|
freezeProgress: false,
|
|
absoluteRect: CGRect(origin: CGPoint(x: overscrollFrame.minX, y: self.bounds.height - overscrollFrame.minY), size: overscrollFrame.size),
|
|
absoluteSize: self.bounds.size,
|
|
wallpaperNode: chatControllerNode.backgroundNode
|
|
)),
|
|
environment: {},
|
|
containerSize: CGSize(width: self.bounds.width, height: 200.0)
|
|
)
|
|
} else if let overscrollView = self.overscrollView {
|
|
self.overscrollView = nil
|
|
overscrollView.removeFromSuperview()
|
|
|
|
if let chatControllerNode = self.controllerInteraction.chatControllerNode() as? ChatControllerNode {
|
|
chatControllerNode.setChatInputPanelOverscrollNode(overscrollNode: nil)
|
|
}
|
|
}
|
|
}
|
|
|
|
func refreshPollActionsForVisibleMessages() {
|
|
let _ = self.clientId.swap(nextClientId)
|
|
nextClientId += 1
|
|
|
|
self.updateVisibleItemRange(force: true)
|
|
}
|
|
|
|
func refocusOnUnreadMessagesIfNeeded() {
|
|
self.forEachItemNode({ itemNode in
|
|
if let itemNode = itemNode as? ChatUnreadItemNode {
|
|
self.ensureItemNodeVisible(itemNode, animated: false, overflow: 0.0, curve: .Default(duration: nil))
|
|
}
|
|
})
|
|
}
|
|
|
|
private func maybeInsertPendingAdMessage(historyView: ChatHistoryView, toLaterRange: (Int, Int), toEarlierRange: (Int, Int)) {
|
|
if self.pendingDynamicAdMessages.isEmpty {
|
|
return
|
|
}
|
|
|
|
let selectedRange: (Int, Int)
|
|
if self.currentPrefetchDirectionIsToLater {
|
|
selectedRange = (toLaterRange.0 + 1, toLaterRange.1)
|
|
} else {
|
|
selectedRange = (toEarlierRange.0, toEarlierRange.1 - 1)
|
|
}
|
|
|
|
if selectedRange.0 <= selectedRange.1 {
|
|
var insertionTimestamp: Int32?
|
|
if self.currentPrefetchDirectionIsToLater {
|
|
outer: for i in selectedRange.0 ... selectedRange.1 {
|
|
if historyView.originalView.laterId == nil && i >= historyView.filteredEntries.count - 4 {
|
|
break
|
|
}
|
|
|
|
switch historyView.filteredEntries[i] {
|
|
case let .MessageEntry(message, _, _, _, _, _):
|
|
if message.id.namespace == Namespaces.Message.Cloud {
|
|
insertionTimestamp = message.timestamp
|
|
break outer
|
|
}
|
|
case let .MessageGroupEntry(_, messages, _):
|
|
for (message, _, _, _, _) in messages {
|
|
if message.id.namespace == Namespaces.Message.Cloud {
|
|
insertionTimestamp = message.timestamp
|
|
break outer
|
|
}
|
|
}
|
|
default:
|
|
break
|
|
}
|
|
}
|
|
} else {
|
|
outer: for i in (selectedRange.0 ... selectedRange.1).reversed() {
|
|
switch historyView.filteredEntries[i] {
|
|
case let .MessageEntry(message, _, _, _, _, _):
|
|
if message.id.namespace == Namespaces.Message.Cloud {
|
|
insertionTimestamp = message.timestamp
|
|
break outer
|
|
}
|
|
case let .MessageGroupEntry(_, messages, _):
|
|
for (message, _, _, _, _) in messages {
|
|
if message.id.namespace == Namespaces.Message.Cloud {
|
|
insertionTimestamp = message.timestamp
|
|
break outer
|
|
}
|
|
}
|
|
default:
|
|
break
|
|
}
|
|
}
|
|
}
|
|
if let insertionTimestamp = insertionTimestamp {
|
|
let initialMessage = self.pendingDynamicAdMessages.removeFirst()
|
|
let message = Message(
|
|
stableId: UInt32.max - 1 - UInt32(self.nextPendingDynamicMessageId),
|
|
stableVersion: initialMessage.stableVersion,
|
|
id: MessageId(peerId: initialMessage.id.peerId, namespace: initialMessage.id.namespace, id: self.nextPendingDynamicMessageId),
|
|
globallyUniqueId: nil,
|
|
groupingKey: nil,
|
|
groupInfo: nil,
|
|
threadId: nil,
|
|
timestamp: insertionTimestamp,
|
|
flags: initialMessage.flags,
|
|
tags: initialMessage.tags,
|
|
globalTags: initialMessage.globalTags,
|
|
localTags: initialMessage.localTags,
|
|
customTags: initialMessage.customTags,
|
|
forwardInfo: initialMessage.forwardInfo,
|
|
author: initialMessage.author,
|
|
text: /*"\(initialMessage.adAttribute!.opaqueId.hashValue)" + */initialMessage.text,
|
|
attributes: initialMessage.attributes,
|
|
media: initialMessage.media,
|
|
peers: initialMessage.peers,
|
|
associatedMessages: initialMessage.associatedMessages,
|
|
associatedMessageIds: initialMessage.associatedMessageIds,
|
|
associatedMedia: initialMessage.associatedMedia,
|
|
associatedThreadInfo: initialMessage.associatedThreadInfo,
|
|
associatedStories: initialMessage.associatedStories
|
|
)
|
|
self.nextPendingDynamicMessageId += 1
|
|
|
|
var allAdMessages = self.allAdMessages
|
|
if allAdMessages.fixed?.adAttribute?.opaqueId == message.adAttribute?.opaqueId {
|
|
allAdMessages.fixed = self.pendingDynamicAdMessages.first?.withUpdatedStableVersion(stableVersion: UInt32(self.nextPendingDynamicMessageId))
|
|
}
|
|
allAdMessages.opportunistic.append(message)
|
|
allAdMessages.version += 1
|
|
self.allAdMessages = allAdMessages
|
|
}
|
|
}
|
|
//TODO:loc mark all ads as seen
|
|
}
|
|
|
|
func markAdAsSeen(opaqueId: Data) {
|
|
for i in 0 ..< self.pendingDynamicAdMessages.count {
|
|
if let pendingAttribute = self.pendingDynamicAdMessages[i].adAttribute, pendingAttribute.opaqueId == opaqueId {
|
|
self.pendingDynamicAdMessages.remove(at: i)
|
|
break
|
|
}
|
|
}
|
|
if !self.seenAdIds.contains(opaqueId) {
|
|
self.seenAdIds.append(opaqueId)
|
|
self.adMessagesContext?.markAsSeen(opaqueId: opaqueId)
|
|
}
|
|
}
|
|
|
|
private func processDisplayedItemRangeChanged(displayedRange: ListViewDisplayedItemRange, transactionState: ChatHistoryTransactionOpaqueState) {
|
|
let historyView = transactionState.historyView
|
|
var isTopReplyThreadMessageShownValue = false
|
|
var topVisibleMessageRange: ChatTopVisibleMessageRange?
|
|
let isLoading = historyView.originalView.isLoading
|
|
let translateToLanguage = transactionState.historyView.associatedData.translateToLanguage
|
|
|
|
if let visible = displayedRange.visibleRange {
|
|
let indexRange = (historyView.filteredEntries.count - 1 - visible.lastIndex, historyView.filteredEntries.count - 1 - visible.firstIndex)
|
|
if indexRange.0 > indexRange.1 {
|
|
assert(false)
|
|
return
|
|
}
|
|
|
|
var messageIdsToTranslate: [MessageId] = []
|
|
var messageIdsToFactCheck: [MessageId] = []
|
|
if let translateToLanguage {
|
|
let extendedRange: Int = 2
|
|
var wideIndexRange = (historyView.filteredEntries.count - 1 - visible.lastIndex - extendedRange, historyView.filteredEntries.count - 1 - visible.firstIndex + extendedRange)
|
|
wideIndexRange = (max(0, min(historyView.filteredEntries.count - 1, wideIndexRange.0)), max(0, min(historyView.filteredEntries.count - 1, wideIndexRange.1)))
|
|
if wideIndexRange.0 > wideIndexRange.1 {
|
|
assert(false)
|
|
return
|
|
}
|
|
|
|
if wideIndexRange.0 <= wideIndexRange.1 {
|
|
for i in (wideIndexRange.0 ... wideIndexRange.1) {
|
|
switch historyView.filteredEntries[i] {
|
|
case let .MessageEntry(message, _, _, _, _, _):
|
|
guard message.adAttribute == nil && message.id.namespace == Namespaces.Message.Cloud else {
|
|
continue
|
|
}
|
|
guard message.author?.id != self.context.account.peerId else {
|
|
continue
|
|
}
|
|
if let translation = message.attributes.first(where: { $0 is TranslationMessageAttribute }) as? TranslationMessageAttribute, translation.toLang == translateToLanguage {
|
|
continue
|
|
}
|
|
if !message.text.isEmpty {
|
|
messageIdsToTranslate.append(message.id)
|
|
} else if let _ = message.media.first(where: { $0 is TelegramMediaPoll }) {
|
|
messageIdsToTranslate.append(message.id)
|
|
}
|
|
case let .MessageGroupEntry(_, messages, _):
|
|
for (message, _, _, _, _) in messages {
|
|
guard message.adAttribute == nil && message.id.namespace == Namespaces.Message.Cloud else {
|
|
continue
|
|
}
|
|
guard message.author?.id != self.context.account.peerId else {
|
|
continue
|
|
}
|
|
if let translation = message.attributes.first(where: { $0 is TranslationMessageAttribute }) as? TranslationMessageAttribute, translation.toLang == translateToLanguage {
|
|
continue
|
|
}
|
|
if !message.text.isEmpty {
|
|
messageIdsToTranslate.append(message.id)
|
|
}
|
|
}
|
|
default:
|
|
break
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
let readIndexRange = (0, historyView.filteredEntries.count - 1 - visible.firstIndex)
|
|
|
|
let toEarlierRange = (0, historyView.filteredEntries.count - 1 - visible.lastIndex - 1)
|
|
let toLaterRange = (historyView.filteredEntries.count - 1 - (visible.firstIndex - 1), historyView.filteredEntries.count - 1)
|
|
|
|
var messageIdsWithViewCount: [MessageId] = []
|
|
var messageIdsWithLiveLocation: [MessageId] = []
|
|
var messageIdsWithUnsupportedMedia: [MessageAndThreadId] = []
|
|
var messageIdsWithRefreshMedia: [MessageId] = []
|
|
var messageIdsWithRefreshStories: [MessageId] = []
|
|
var messageIdsWithUnseenPersonalMention: [MessageId] = []
|
|
var messageIdsWithUnseenReactions: [MessageId] = []
|
|
var messageIdsWithInactiveExtendedMedia = Set<MessageId>()
|
|
var downloadableResourceIds: [(messageId: MessageId, resourceId: String)] = []
|
|
var allVisibleAnchorMessageIds: [(MessageId, Int)] = []
|
|
var visibleAdOpaqueIds: [Data] = []
|
|
var peerIdsWithRefreshStories: [PeerId] = []
|
|
var visibleBusinessBotMessageId: EngineMessage.Id?
|
|
|
|
if indexRange.0 <= indexRange.1 {
|
|
for i in (indexRange.0 ... indexRange.1) {
|
|
let nodeIndex = historyView.filteredEntries.count - 1 - i
|
|
|
|
switch historyView.filteredEntries[i] {
|
|
case let .MessageEntry(message, _, _, _, _, _):
|
|
if let author = message.author as? TelegramUser {
|
|
peerIdsWithRefreshStories.append(author.id)
|
|
}
|
|
|
|
var hasUnconsumedMention = false
|
|
var hasUnconsumedContent = false
|
|
if message.tags.contains(.unseenPersonalMessage) {
|
|
for attribute in message.attributes {
|
|
if let attribute = attribute as? ConsumablePersonalMentionMessageAttribute, !attribute.pending {
|
|
hasUnconsumedMention = true
|
|
}
|
|
}
|
|
}
|
|
var contentRequiredValidation = false
|
|
var mediaRequiredValidation = false
|
|
var hasUnseenReactions = false
|
|
var storiesRequiredValidation = false
|
|
var factCheckRequired = false
|
|
for attribute in message.attributes {
|
|
if attribute is ViewCountMessageAttribute {
|
|
if message.id.namespace == Namespaces.Message.Cloud {
|
|
messageIdsWithViewCount.append(message.id)
|
|
}
|
|
} else if attribute is ReplyThreadMessageAttribute {
|
|
if message.id.namespace == Namespaces.Message.Cloud {
|
|
messageIdsWithViewCount.append(message.id)
|
|
}
|
|
} else if let attribute = attribute as? ConsumableContentMessageAttribute, !attribute.consumed {
|
|
hasUnconsumedContent = true
|
|
} else if let _ = attribute as? ContentRequiresValidationMessageAttribute {
|
|
contentRequiredValidation = true
|
|
} else if let attribute = attribute as? ReactionsMessageAttribute, attribute.hasUnseen {
|
|
hasUnseenReactions = true
|
|
} else if let attribute = attribute as? AdMessageAttribute {
|
|
if message.stableId != ChatHistoryListNodeImpl.fixedAdMessageStableId {
|
|
visibleAdOpaqueIds.append(attribute.opaqueId)
|
|
}
|
|
} else if let _ = attribute as? ReplyStoryAttribute {
|
|
storiesRequiredValidation = true
|
|
} else if let attribute = attribute as? FactCheckMessageAttribute, case .Pending = attribute.content {
|
|
factCheckRequired = true
|
|
}
|
|
}
|
|
|
|
for media in message.media {
|
|
if let _ = media as? TelegramMediaUnsupported {
|
|
contentRequiredValidation = true
|
|
} else if message.flags.contains(.Incoming), let media = media as? TelegramMediaMap, let liveBroadcastingTimeout = media.liveBroadcastingTimeout {
|
|
let timestamp = Int32(CFAbsoluteTimeGetCurrent() + kCFAbsoluteTimeIntervalSince1970)
|
|
if liveBroadcastingTimeout == liveLocationIndefinitePeriod || message.timestamp + liveBroadcastingTimeout > timestamp {
|
|
messageIdsWithLiveLocation.append(message.id)
|
|
}
|
|
} else if let telegramFile = media as? TelegramMediaFile {
|
|
if telegramFile.isAnimatedSticker, (message.id.peerId.namespace == Namespaces.Peer.SecretChat || !telegramFile.previewRepresentations.isEmpty), let size = telegramFile.size, size > 0 && size <= 128 * 1024 {
|
|
if message.id.peerId.namespace == Namespaces.Peer.SecretChat {
|
|
if telegramFile.fileId.namespace == Namespaces.Media.CloudFile {
|
|
var isValidated = false
|
|
attributes: for attribute in telegramFile.attributes {
|
|
if case .hintIsValidated = attribute {
|
|
isValidated = true
|
|
break attributes
|
|
}
|
|
}
|
|
|
|
if !isValidated {
|
|
mediaRequiredValidation = true
|
|
}
|
|
}
|
|
}
|
|
}
|
|
downloadableResourceIds.append((message.id, telegramFile.resource.id.stringRepresentation))
|
|
} else if let image = media as? TelegramMediaImage {
|
|
if let representation = image.representations.last {
|
|
downloadableResourceIds.append((message.id, representation.resource.id.stringRepresentation))
|
|
}
|
|
} else if let invoice = media as? TelegramMediaInvoice, let extendedMedia = invoice.extendedMedia, case .preview = extendedMedia {
|
|
messageIdsWithInactiveExtendedMedia.insert(message.id)
|
|
if invoice.version != TelegramMediaInvoice.lastVersion {
|
|
contentRequiredValidation = true
|
|
}
|
|
} else if let paidContent = media as? TelegramMediaPaidContent, let extendedMedia = paidContent.extendedMedia.first, case .preview = extendedMedia {
|
|
messageIdsWithInactiveExtendedMedia.insert(message.id)
|
|
} else if let _ = media as? TelegramMediaStory {
|
|
storiesRequiredValidation = true
|
|
} else if let webpage = media as? TelegramMediaWebpage, case let .Loaded(content) = webpage.content, let _ = content.story {
|
|
storiesRequiredValidation = true
|
|
}
|
|
}
|
|
if contentRequiredValidation {
|
|
messageIdsWithUnsupportedMedia.append(MessageAndThreadId(messageId: message.id, threadId: message.threadId))
|
|
}
|
|
if mediaRequiredValidation {
|
|
messageIdsWithRefreshMedia.append(message.id)
|
|
}
|
|
if storiesRequiredValidation {
|
|
messageIdsWithRefreshStories.append(message.id)
|
|
}
|
|
if hasUnconsumedMention && !hasUnconsumedContent {
|
|
messageIdsWithUnseenPersonalMention.append(message.id)
|
|
}
|
|
if hasUnseenReactions {
|
|
messageIdsWithUnseenReactions.append(message.id)
|
|
}
|
|
if factCheckRequired {
|
|
messageIdsToFactCheck.append(message.id)
|
|
}
|
|
|
|
if case let .replyThread(replyThreadMessage) = self.chatLocation, replyThreadMessage.effectiveTopId == message.id {
|
|
isTopReplyThreadMessageShownValue = true
|
|
}
|
|
if let topVisibleMessageRangeValue = topVisibleMessageRange {
|
|
topVisibleMessageRange = ChatTopVisibleMessageRange(lowerBound: topVisibleMessageRangeValue.lowerBound, upperBound: message.index, isLast: i == historyView.filteredEntries.count - 1, isLoading: isLoading)
|
|
} else {
|
|
topVisibleMessageRange = ChatTopVisibleMessageRange(lowerBound: message.index, upperBound: message.index, isLast: i == historyView.filteredEntries.count - 1, isLoading: isLoading)
|
|
}
|
|
if message.id.namespace == Namespaces.Message.Cloud, self.remainingDynamicAdMessageInterval != nil {
|
|
allVisibleAnchorMessageIds.append((message.id, nodeIndex))
|
|
}
|
|
case let .MessageGroupEntry(_, messages, _):
|
|
if let author = messages.first?.0.author as? TelegramUser {
|
|
peerIdsWithRefreshStories.append(author.id)
|
|
}
|
|
|
|
for (message, _, _, _, _) in messages {
|
|
var hasUnconsumedMention = false
|
|
var hasUnconsumedContent = false
|
|
var hasUnseenReactions = false
|
|
var factCheckRequired = false
|
|
if message.tags.contains(.unseenPersonalMessage) {
|
|
for attribute in message.attributes {
|
|
if let attribute = attribute as? ConsumablePersonalMentionMessageAttribute, !attribute.pending {
|
|
hasUnconsumedMention = true
|
|
}
|
|
}
|
|
}
|
|
for media in message.media {
|
|
if let telegramFile = media as? TelegramMediaFile {
|
|
downloadableResourceIds.append((message.id, telegramFile.resource.id.stringRepresentation))
|
|
} else if let image = media as? TelegramMediaImage {
|
|
if let representation = image.representations.last {
|
|
downloadableResourceIds.append((message.id, representation.resource.id.stringRepresentation))
|
|
}
|
|
}
|
|
}
|
|
for attribute in message.attributes {
|
|
if attribute is ViewCountMessageAttribute {
|
|
if message.id.namespace == Namespaces.Message.Cloud {
|
|
messageIdsWithViewCount.append(message.id)
|
|
}
|
|
} else if attribute is ReplyThreadMessageAttribute {
|
|
if message.id.namespace == Namespaces.Message.Cloud {
|
|
messageIdsWithViewCount.append(message.id)
|
|
}
|
|
} else if let attribute = attribute as? ConsumableContentMessageAttribute, !attribute.consumed {
|
|
hasUnconsumedContent = true
|
|
} else if let attribute = attribute as? ReactionsMessageAttribute, attribute.hasUnseen {
|
|
hasUnseenReactions = true
|
|
} else if let attribute = attribute as? FactCheckMessageAttribute, case .Pending = attribute.content {
|
|
factCheckRequired = true
|
|
}
|
|
}
|
|
if hasUnconsumedMention && !hasUnconsumedContent {
|
|
messageIdsWithUnseenPersonalMention.append(message.id)
|
|
}
|
|
if hasUnseenReactions {
|
|
messageIdsWithUnseenReactions.append(message.id)
|
|
}
|
|
if factCheckRequired {
|
|
messageIdsToFactCheck.append(message.id)
|
|
}
|
|
if case let .replyThread(replyThreadMessage) = self.chatLocation, replyThreadMessage.effectiveTopId == message.id {
|
|
isTopReplyThreadMessageShownValue = true
|
|
}
|
|
if let topVisibleMessageRangeValue = topVisibleMessageRange {
|
|
topVisibleMessageRange = ChatTopVisibleMessageRange(lowerBound: topVisibleMessageRangeValue.lowerBound, upperBound: message.index, isLast: i == historyView.filteredEntries.count - 1, isLoading: isLoading)
|
|
} else {
|
|
topVisibleMessageRange = ChatTopVisibleMessageRange(lowerBound: message.index, upperBound: message.index, isLast: i == historyView.filteredEntries.count - 1, isLoading: isLoading)
|
|
}
|
|
}
|
|
if let message = messages.first {
|
|
if message.0.id.namespace == Namespaces.Message.Cloud, self.remainingDynamicAdMessageInterval != nil {
|
|
allVisibleAnchorMessageIds.append((message.0.id, nodeIndex))
|
|
}
|
|
}
|
|
default:
|
|
break
|
|
}
|
|
}
|
|
}
|
|
|
|
var messageIdsWithPossibleReactions: [MessageId] = []
|
|
for entry in historyView.filteredEntries {
|
|
switch entry {
|
|
case let .MessageEntry(message, _, _, _, _, _):
|
|
if let _ = message.inlineBotAttribute {
|
|
if let visibleBusinessBotMessageIdValue = visibleBusinessBotMessageId {
|
|
if visibleBusinessBotMessageIdValue < message.id {
|
|
visibleBusinessBotMessageId = message.id
|
|
}
|
|
} else {
|
|
visibleBusinessBotMessageId = message.id
|
|
}
|
|
}
|
|
switch message.id.peerId.namespace {
|
|
case Namespaces.Peer.CloudGroup, Namespaces.Peer.CloudChannel:
|
|
messageIdsWithPossibleReactions.append(message.id)
|
|
default:
|
|
break
|
|
}
|
|
case let .MessageGroupEntry(_, messages, _):
|
|
for (message, _, _, _, _) in messages {
|
|
if let _ = message.inlineBotAttribute {
|
|
if let visibleBusinessBotMessageIdValue = visibleBusinessBotMessageId {
|
|
if visibleBusinessBotMessageIdValue < message.id {
|
|
visibleBusinessBotMessageId = message.id
|
|
}
|
|
} else {
|
|
visibleBusinessBotMessageId = message.id
|
|
}
|
|
}
|
|
switch message.id.peerId.namespace {
|
|
case Namespaces.Peer.CloudGroup, Namespaces.Peer.CloudChannel:
|
|
messageIdsWithPossibleReactions.append(message.id)
|
|
default:
|
|
break
|
|
}
|
|
}
|
|
default:
|
|
break
|
|
}
|
|
}
|
|
|
|
func addMediaToPrefetch(_ message: Message, _ media: Media, _ messages: inout [(Message, Media)]) -> Bool {
|
|
if media is TelegramMediaImage || media is TelegramMediaFile {
|
|
messages.append((message, media))
|
|
}
|
|
if messages.count >= 3 {
|
|
return false
|
|
} else {
|
|
return true
|
|
}
|
|
}
|
|
|
|
var toEarlierMediaMessages: [(Message, Media)] = []
|
|
if toEarlierRange.0 <= toEarlierRange.1 {
|
|
outer: for i in (toEarlierRange.0 ... toEarlierRange.1).reversed() {
|
|
switch historyView.filteredEntries[i] {
|
|
case let .MessageEntry(message, _, _, _, _, _):
|
|
for media in message.media {
|
|
if !addMediaToPrefetch(message, media, &toEarlierMediaMessages) {
|
|
break outer
|
|
}
|
|
}
|
|
case let .MessageGroupEntry(_, messages, _):
|
|
for (message, _, _, _, _) in messages {
|
|
var stop = false
|
|
for media in message.media {
|
|
if !addMediaToPrefetch(message, media, &toEarlierMediaMessages) {
|
|
stop = true
|
|
}
|
|
}
|
|
if stop {
|
|
break outer
|
|
}
|
|
}
|
|
default:
|
|
break
|
|
}
|
|
}
|
|
}
|
|
|
|
var toLaterMediaMessages: [(Message, Media)] = []
|
|
if toLaterRange.0 <= toLaterRange.1 {
|
|
outer: for i in (toLaterRange.0 ... toLaterRange.1) {
|
|
switch historyView.filteredEntries[i] {
|
|
case let .MessageEntry(message, _, _, _, _, _):
|
|
for media in message.media {
|
|
if !addMediaToPrefetch(message, media, &toLaterMediaMessages) {
|
|
break outer
|
|
}
|
|
}
|
|
case let .MessageGroupEntry(_, messages, _):
|
|
for (message, _, _, _, _) in messages {
|
|
for media in message.media {
|
|
if !addMediaToPrefetch(message, media, &toLaterMediaMessages) {
|
|
break outer
|
|
}
|
|
}
|
|
}
|
|
default:
|
|
break
|
|
}
|
|
}
|
|
}
|
|
|
|
if !messageIdsWithViewCount.isEmpty {
|
|
self.messageProcessingManager.add(messageIdsWithViewCount.map { MessageAndThreadId(messageId: $0, threadId: nil) })
|
|
}
|
|
if !messageIdsWithLiveLocation.isEmpty {
|
|
self.seenLiveLocationProcessingManager.add(messageIdsWithLiveLocation.map { MessageAndThreadId(messageId: $0, threadId: nil) })
|
|
}
|
|
if !messageIdsWithUnsupportedMedia.isEmpty {
|
|
self.unsupportedMessageProcessingManager.add(messageIdsWithUnsupportedMedia)
|
|
}
|
|
if !messageIdsWithRefreshMedia.isEmpty {
|
|
self.refreshMediaProcessingManager.add(messageIdsWithRefreshMedia.map { MessageAndThreadId(messageId: $0, threadId: nil) })
|
|
}
|
|
if !messageIdsWithRefreshStories.isEmpty {
|
|
self.refreshStoriesProcessingManager.add(messageIdsWithRefreshStories.map { MessageAndThreadId(messageId: $0, threadId: nil) })
|
|
}
|
|
if !messageIdsWithUnseenPersonalMention.isEmpty {
|
|
self.messageMentionProcessingManager.add(messageIdsWithUnseenPersonalMention.map { MessageAndThreadId(messageId: $0, threadId: nil) })
|
|
}
|
|
if !messageIdsWithUnseenReactions.isEmpty {
|
|
self.unseenReactionsProcessingManager.add(messageIdsWithUnseenReactions.map { MessageAndThreadId(messageId: $0, threadId: nil) })
|
|
|
|
if self.canReadHistoryValue && !self.context.sharedContext.immediateExperimentalUISettings.skipReadHistory {
|
|
let _ = self.displayUnseenReactionAnimations(messageIds: messageIdsWithUnseenReactions)
|
|
}
|
|
}
|
|
if !messageIdsWithPossibleReactions.isEmpty {
|
|
self.messageWithReactionsProcessingManager.add(messageIdsWithPossibleReactions.map { MessageAndThreadId(messageId: $0, threadId: nil) })
|
|
}
|
|
if !downloadableResourceIds.isEmpty {
|
|
let _ = markRecentDownloadItemsAsSeen(postbox: self.context.account.postbox, items: downloadableResourceIds).startStandalone()
|
|
}
|
|
if !messageIdsWithInactiveExtendedMedia.isEmpty {
|
|
self.extendedMediaProcessingManager.update(Set(messageIdsWithInactiveExtendedMedia.map { MessageAndThreadId(messageId: $0, threadId: nil) }))
|
|
}
|
|
if !messageIdsToTranslate.isEmpty {
|
|
self.translationProcessingManager.add(messageIdsToTranslate.map { MessageAndThreadId(messageId: $0, threadId: nil) })
|
|
}
|
|
if !messageIdsToFactCheck.isEmpty {
|
|
self.factCheckProcessingManager.add(messageIdsToFactCheck.map { MessageAndThreadId(messageId: $0, threadId: nil) })
|
|
}
|
|
if !visibleAdOpaqueIds.isEmpty {
|
|
for opaqueId in visibleAdOpaqueIds {
|
|
self.markAdAsSeen(opaqueId: opaqueId)
|
|
}
|
|
}
|
|
if !peerIdsWithRefreshStories.isEmpty {
|
|
self.context.account.viewTracker.refreshStoryStatsForPeerIds(peerIds: peerIdsWithRefreshStories)
|
|
}
|
|
|
|
self.currentEarlierPrefetchMessages = toEarlierMediaMessages
|
|
self.currentLaterPrefetchMessages = toLaterMediaMessages
|
|
if self.currentPrefetchDirectionIsToLater {
|
|
self.prefetchManager.updateMessages(toLaterMediaMessages, directionIsToLater: self.currentPrefetchDirectionIsToLater)
|
|
} else {
|
|
self.prefetchManager.updateMessages(toEarlierMediaMessages, directionIsToLater: self.currentPrefetchDirectionIsToLater)
|
|
}
|
|
|
|
if readIndexRange.0 <= readIndexRange.1 {
|
|
let (maxIncomingIndex, maxOverallIndex) = maxMessageIndexForEntries(historyView, indexRange: readIndexRange)
|
|
|
|
let messageIndex: MessageIndex?
|
|
switch self.chatLocation {
|
|
case .peer:
|
|
messageIndex = maxIncomingIndex
|
|
case .replyThread, .customChatContents:
|
|
messageIndex = maxOverallIndex
|
|
}
|
|
|
|
if let messageIndex = messageIndex {
|
|
let _ = messageIndex
|
|
//self.updateMaxVisibleReadIncomingMessageIndex(messageIndex)
|
|
}
|
|
|
|
if let maxOverallIndex = maxOverallIndex, maxOverallIndex != self.maxVisibleMessageIndexReported {
|
|
self.maxVisibleMessageIndexReported = maxOverallIndex
|
|
self.maxVisibleMessageIndexUpdated?(maxOverallIndex)
|
|
}
|
|
}
|
|
|
|
if let visible = displayedRange.visibleRange {
|
|
let indexRange = (historyView.filteredEntries.count - 1 - visible.lastIndex, historyView.filteredEntries.count - 1 - visible.firstIndex)
|
|
if indexRange.0 <= indexRange.1 {
|
|
for (messageId, nodeIndex) in allVisibleAnchorMessageIds {
|
|
guard let itemNode = self.itemNodeAtIndex(nodeIndex) else {
|
|
continue
|
|
}
|
|
//TODO:loc optimize eviction
|
|
if self.seenMessageIds.insert(messageId).inserted, let remainingDynamicAdMessageIntervalValue = self.remainingDynamicAdMessageInterval, let remainingDynamicAdMessageDistanceValue = self.remainingDynamicAdMessageDistance {
|
|
let itemHeight = itemNode.bounds.height
|
|
|
|
let remainingDynamicAdMessageInterval = remainingDynamicAdMessageIntervalValue - 1
|
|
let remainingDynamicAdMessageDistance = remainingDynamicAdMessageDistanceValue - itemHeight
|
|
if remainingDynamicAdMessageInterval <= 0 && remainingDynamicAdMessageDistance <= 0.0 {
|
|
self.remainingDynamicAdMessageInterval = self.pendingDynamicAdMessageInterval
|
|
self.remainingDynamicAdMessageDistance = self.bounds.height
|
|
self.maybeInsertPendingAdMessage(historyView: historyView, toLaterRange: toLaterRange, toEarlierRange: toEarlierRange)
|
|
} else {
|
|
self.remainingDynamicAdMessageInterval = remainingDynamicAdMessageInterval
|
|
self.remainingDynamicAdMessageDistance = remainingDynamicAdMessageDistance
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if let visibleBusinessBotMessageId, !self.hasDisplayedBusinessBotMessageTooltip {
|
|
var foundItemNode: ChatMessageItemView?
|
|
self.forEachItemNode { itemNode in
|
|
if let itemNode = itemNode as? ChatMessageItemView, let item = itemNode.item, item.message.id == visibleBusinessBotMessageId {
|
|
foundItemNode = itemNode
|
|
}
|
|
}
|
|
|
|
if let foundItemNode {
|
|
self.hasDisplayedBusinessBotMessageTooltip = true
|
|
|
|
if let controllerNode = self.controllerInteraction.chatControllerNode() as? ChatControllerNode, let chatController = controllerNode.interfaceInteraction?.chatController() as? ChatControllerImpl {
|
|
chatController.displayBusinessBotMessageTooltip(itemNode: foundItemNode)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
self.isTopReplyThreadMessageShown.set(isTopReplyThreadMessageShownValue)
|
|
self.updateTopVisibleMessageRange(topVisibleMessageRange)
|
|
let _ = self.visibleMessageRange.swap(topVisibleMessageRange.flatMap { range in
|
|
return VisibleMessageRange(lowerBound: range.lowerBound, upperBound: range.upperBound)
|
|
})
|
|
|
|
if let loaded = displayedRange.visibleRange, let firstEntry = historyView.filteredEntries.first, let lastEntry = historyView.filteredEntries.last {
|
|
var mathesFirst = false
|
|
if loaded.firstIndex <= 5 {
|
|
var firstHasGroups = false
|
|
for index in (max(0, historyView.filteredEntries.count - 5) ..< historyView.filteredEntries.count).reversed() {
|
|
switch historyView.filteredEntries[index] {
|
|
case .MessageEntry:
|
|
break
|
|
case .MessageGroupEntry:
|
|
firstHasGroups = true
|
|
default:
|
|
break
|
|
}
|
|
}
|
|
if firstHasGroups {
|
|
mathesFirst = loaded.firstIndex <= 1
|
|
} else {
|
|
mathesFirst = loaded.firstIndex <= 5
|
|
}
|
|
}
|
|
|
|
var mathesLast = false
|
|
if loaded.lastIndex >= historyView.filteredEntries.count - 5 {
|
|
var lastHasGroups = false
|
|
for index in 0 ..< min(5, historyView.filteredEntries.count) {
|
|
switch historyView.filteredEntries[index] {
|
|
case .MessageEntry:
|
|
break
|
|
case .MessageGroupEntry:
|
|
lastHasGroups = true
|
|
default:
|
|
break
|
|
}
|
|
}
|
|
if lastHasGroups {
|
|
mathesLast = loaded.lastIndex >= historyView.filteredEntries.count - 1
|
|
} else {
|
|
mathesLast = loaded.lastIndex >= historyView.filteredEntries.count - 5
|
|
}
|
|
}
|
|
|
|
if mathesFirst && historyView.originalView.laterId != nil {
|
|
let locationInput: ChatHistoryLocation = .Navigation(index: .message(lastEntry.index), anchorIndex: .message(lastEntry.index), count: historyMessageCount, highlight: false)
|
|
if self.chatHistoryLocationValue?.content != locationInput {
|
|
self.chatHistoryLocationValue = ChatHistoryLocationInput(content: locationInput, id: self.takeNextHistoryLocationId())
|
|
}
|
|
} else if mathesFirst, historyView.originalView.laterId == nil, !historyView.originalView.holeLater, let chatHistoryLocationValue = self.chatHistoryLocationValue, !chatHistoryLocationValue.isAtUpperBound, historyView.originalView.anchorIndex != .upperBound {
|
|
if self.chatHistoryLocationValue == historyView.locationInput {
|
|
self.chatHistoryLocationValue = ChatHistoryLocationInput(content: .Navigation(index: .upperBound, anchorIndex: .upperBound, count: historyMessageCount, highlight: false), id: self.takeNextHistoryLocationId())
|
|
}
|
|
} else if mathesLast {
|
|
let locationInput: ChatHistoryLocation = .Navigation(index: .message(firstEntry.index), anchorIndex: .message(firstEntry.index), count: historyMessageCount, highlight: false)
|
|
if historyView.originalView.earlierId != nil {
|
|
if self.chatHistoryLocationValue?.content != locationInput {
|
|
self.chatHistoryLocationValue = ChatHistoryLocationInput(content: locationInput, id: self.takeNextHistoryLocationId())
|
|
}
|
|
} else if case let .customChatContents(customChatContents) = self.subject, case .hashTagSearch = customChatContents.kind {
|
|
if self.chatHistoryLocationValue?.content != locationInput {
|
|
self.chatHistoryLocationValue = ChatHistoryLocationInput(content: locationInput, id: self.takeNextHistoryLocationId())
|
|
customChatContents.loadMore()
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
var containsPlayableWithSoundItemNode = false
|
|
self.forEachVisibleItemNode { itemNode in
|
|
if let chatItemView = itemNode as? ChatMessageItemView, chatItemView.playMediaWithSound() != nil {
|
|
containsPlayableWithSoundItemNode = true
|
|
}
|
|
}
|
|
self.hasVisiblePlayableItemNodesPromise.set(containsPlayableWithSoundItemNode)
|
|
|
|
if containsPlayableWithSoundItemNode && !self.isInteractivelyScrollingValue {
|
|
self.isInteractivelyScrollingPromise.set(true)
|
|
self.isInteractivelyScrollingPromise.set(false)
|
|
}
|
|
}
|
|
|
|
public func scrollScreenToTop() {
|
|
if let subject = self.subject, case .scheduledMessages = subject {
|
|
if let historyView = self.historyView {
|
|
if let entry = historyView.filteredEntries.first {
|
|
var currentMessage: Message?
|
|
if case let .MessageEntry(message, _, _, _, _, _) = entry {
|
|
currentMessage = message
|
|
} else if case let .MessageGroupEntry(_, messages, _) = entry {
|
|
currentMessage = messages.first?.0
|
|
}
|
|
if let message = currentMessage, let _ = self.anchorMessageInCurrentHistoryView() {
|
|
self.chatHistoryLocationValue = ChatHistoryLocationInput(content: .Scroll(subject: MessageHistoryScrollToSubject(index: .message(message.index), quote: nil), anchorIndex: .message(message.index), sourceIndex: .upperBound, scrollPosition: .bottom(0.0), animated: true, highlight: false, setupReply: false), id: self.takeNextHistoryLocationId())
|
|
}
|
|
}
|
|
}
|
|
} else {
|
|
var currentMessage: Message?
|
|
if let historyView = self.historyView {
|
|
if let visibleRange = self.displayedItemRange.loadedRange {
|
|
var index = historyView.filteredEntries.count - 1
|
|
loop: for entry in historyView.filteredEntries {
|
|
let isVisible = index >= visibleRange.firstIndex && index <= visibleRange.lastIndex
|
|
if case let .MessageEntry(message, _, _, _, _, _) = entry {
|
|
if !isVisible || currentMessage == nil {
|
|
currentMessage = message
|
|
}
|
|
} else if case let .MessageGroupEntry(_, messages, _) = entry {
|
|
if !isVisible || currentMessage == nil {
|
|
currentMessage = messages.first?.0
|
|
}
|
|
}
|
|
if isVisible {
|
|
break loop
|
|
}
|
|
index -= 1
|
|
}
|
|
}
|
|
}
|
|
|
|
if let currentMessage = currentMessage {
|
|
self.chatHistoryLocationValue = ChatHistoryLocationInput(content: .Scroll(subject: MessageHistoryScrollToSubject(index: .message(currentMessage.index), quote: nil), anchorIndex: .message(currentMessage.index), sourceIndex: .upperBound, scrollPosition: .top(0.0), animated: true, highlight: true, setupReply: false), id: self.takeNextHistoryLocationId())
|
|
}
|
|
}
|
|
}
|
|
|
|
public func scrollToStartOfHistory() {
|
|
self.beganDragging?()
|
|
self.chatHistoryLocationValue = ChatHistoryLocationInput(content: .Scroll(subject: MessageHistoryScrollToSubject(index: .lowerBound, quote: nil), anchorIndex: .lowerBound, sourceIndex: .upperBound, scrollPosition: .bottom(0.0), animated: true, highlight: false, setupReply: false), id: self.takeNextHistoryLocationId())
|
|
}
|
|
|
|
public func scrollToEndOfHistory() {
|
|
self.beganDragging?()
|
|
switch self.visibleContentOffset() {
|
|
case let .known(value) where value <= CGFloat.ulpOfOne:
|
|
break
|
|
default:
|
|
let locationInput = ChatHistoryLocationInput(content: .Scroll(subject: MessageHistoryScrollToSubject(index: .upperBound, quote: nil), anchorIndex: .upperBound, sourceIndex: .lowerBound, scrollPosition: .top(0.0), animated: true, highlight: false, setupReply: false), id: self.takeNextHistoryLocationId())
|
|
self.chatHistoryLocationValue = locationInput
|
|
}
|
|
}
|
|
|
|
public func scrollToMessage(from fromIndex: MessageIndex, to toIndex: MessageIndex, animated: Bool, highlight: Bool = true, quote: (string: String, offset: Int?)? = nil, scrollPosition: ListViewScrollPosition = .center(.bottom), setupReply: Bool = false) {
|
|
self.chatHistoryLocationValue = ChatHistoryLocationInput(content: .Scroll(subject: MessageHistoryScrollToSubject(index: .message(toIndex), quote: quote.flatMap { quote in MessageHistoryScrollToSubject.Quote(string: quote.string, offset: quote.offset) }, setupReply: setupReply), anchorIndex: .message(toIndex), sourceIndex: .message(fromIndex), scrollPosition: scrollPosition, animated: animated, highlight: highlight, setupReply: setupReply), id: self.takeNextHistoryLocationId())
|
|
}
|
|
|
|
public func anchorMessageInCurrentHistoryView() -> Message? {
|
|
if let historyView = self.historyView {
|
|
if let visibleRange = self.displayedItemRange.visibleRange {
|
|
var index = 0
|
|
for entry in historyView.filteredEntries.reversed() {
|
|
if index >= visibleRange.firstIndex && index <= visibleRange.lastIndex {
|
|
if case let .MessageEntry(message, _, _, _, _, _) = entry {
|
|
return message
|
|
}
|
|
}
|
|
index += 1
|
|
}
|
|
}
|
|
|
|
for case let .MessageEntry(message, _, _, _, _, _) in historyView.filteredEntries {
|
|
return message
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
public func isMessageVisibleOnScreen(_ id: MessageId) -> Bool {
|
|
var result = false
|
|
self.forEachItemNode({ itemNode in
|
|
if let itemNode = itemNode as? ChatMessageItemView, let item = itemNode.item, item.content.contains(where: { $0.0.id == id }) {
|
|
if self.itemNodeVisibleInsideInsets(itemNode) {
|
|
result = true
|
|
}
|
|
}
|
|
})
|
|
return result
|
|
}
|
|
|
|
public func forEachVisibleMessageItemNode(_ f: (ChatMessageItemView) -> Void) {
|
|
self.forEachVisibleItemNode { itemNode in
|
|
if let itemNode = itemNode as? ChatMessageItemView {
|
|
f(itemNode)
|
|
}
|
|
}
|
|
}
|
|
|
|
public func latestMessageInCurrentHistoryView() -> Message? {
|
|
if let historyView = self.historyView {
|
|
if historyView.originalView.laterId == nil, let firstEntry = historyView.filteredEntries.last {
|
|
if case let .MessageEntry(message, _, _, _, _, _) = firstEntry {
|
|
return message
|
|
}
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
public func firstMessageForEditInCurrentHistoryView() -> Message? {
|
|
if let historyView = self.historyView {
|
|
if historyView.originalView.laterId == nil {
|
|
for entry in historyView.filteredEntries.reversed() {
|
|
if case let .MessageEntry(message, _, _, _, _, _) = entry {
|
|
if canEditMessage(context: context, limitsConfiguration: context.currentLimitsConfiguration.with { EngineConfiguration.Limits($0) }, message: message) {
|
|
return message
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
public func messageInCurrentHistoryView(after messageId: MessageId) -> Message? {
|
|
if let historyView = self.historyView {
|
|
if let index = historyView.filteredEntries.firstIndex(where: { $0.firstIndex.id == messageId }), index < historyView.filteredEntries.count - 1 {
|
|
let nextEntry = historyView.filteredEntries[index + 1]
|
|
if case let .MessageEntry(message, _, _, _, _, _) = nextEntry {
|
|
return message
|
|
} else if case let .MessageGroupEntry(_, messages, _) = nextEntry, let firstMessage = messages.first {
|
|
return firstMessage.0
|
|
}
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
public func messageInCurrentHistoryView(before messageId: MessageId) -> Message? {
|
|
if let historyView = self.historyView {
|
|
if let index = historyView.filteredEntries.firstIndex(where: { $0.firstIndex.id == messageId }), index > 0 {
|
|
let nextEntry = historyView.filteredEntries[index - 1]
|
|
if case let .MessageEntry(message, _, _, _, _, _) = nextEntry {
|
|
return message
|
|
} else if case let .MessageGroupEntry(_, messages, _) = nextEntry, let firstMessage = messages.first {
|
|
return firstMessage.0
|
|
}
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
public func messageInCurrentHistoryView(_ id: MessageId) -> Message? {
|
|
if let historyView = self.historyView {
|
|
for entry in historyView.filteredEntries {
|
|
if case let .MessageEntry(message, _, _, _, _, _) = entry {
|
|
if message.id == id {
|
|
return message
|
|
}
|
|
} else if case let .MessageGroupEntry(_, messages, _) = entry {
|
|
for (message, _, _, _, _) in messages {
|
|
if message.id == id {
|
|
return message
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
public func messageGroupInCurrentHistoryView(_ id: MessageId) -> [Message]? {
|
|
if let historyView = self.historyView {
|
|
for entry in historyView.filteredEntries {
|
|
if case let .MessageEntry(message, _, _, _, _, _) = entry {
|
|
if message.id == id {
|
|
return [message]
|
|
}
|
|
} else if case let .MessageGroupEntry(_, messages, _) = entry {
|
|
for (message, _, _, _, _) in messages {
|
|
if message.id == id {
|
|
return messages.map { $0.0 }
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
public func forEachMessageInCurrentHistoryView(_ f: (Message) -> Bool) {
|
|
if let historyView = self.historyView {
|
|
for entry in historyView.filteredEntries {
|
|
if case let .MessageEntry(message, _, _, _, _, _) = entry {
|
|
if !f(message) {
|
|
return
|
|
}
|
|
} else if case let .MessageGroupEntry(_, messages, _) = entry {
|
|
for (message, _, _, _, _) in messages {
|
|
if !f(message) {
|
|
return
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
private func updateMaxVisibleReadIncomingMessageIndex(_ index: MessageIndex) {
|
|
self.maxVisibleIncomingMessageIndex.set(index)
|
|
}
|
|
|
|
private func enqueueHistoryViewTransition(_ transition: ChatHistoryListViewTransition) {
|
|
self.enqueuedHistoryViewTransitions.append(transition)
|
|
self.prefetchManager.updateOptions(InChatPrefetchOptions(networkType: transition.networkType, peerType: transition.peerType))
|
|
|
|
if !self.didSetInitialData {
|
|
self.didSetInitialData = true
|
|
self._initialData.set(.single(ChatHistoryCombinedInitialData(initialData: transition.initialData, buttonKeyboardMessage: transition.keyboardButtonsMessage, cachedData: transition.cachedData, cachedDataMessages: transition.cachedDataMessages, readStateData: transition.readStateData)))
|
|
}
|
|
|
|
if self.isNodeLoaded {
|
|
self.dequeueHistoryViewTransitions()
|
|
} else {
|
|
self._cachedPeerDataAndMessages.set(.single((transition.cachedData, transition.cachedDataMessages)))
|
|
|
|
let loadState: ChatHistoryNodeLoadState
|
|
if transition.historyView.filteredEntries.isEmpty {
|
|
if let firstEntry = transition.historyView.originalView.entries.first {
|
|
var isPeerJoined = false
|
|
for media in firstEntry.message.media {
|
|
if let action = media as? TelegramMediaAction, action.action == .peerJoined {
|
|
isPeerJoined = true
|
|
break
|
|
}
|
|
}
|
|
loadState = .empty(isPeerJoined ? .joined : .generic)
|
|
} else {
|
|
loadState = .empty(.generic)
|
|
}
|
|
} else {
|
|
if transition.historyView.filteredEntries.count == 1, let entry = transition.historyView.filteredEntries.first, case .ChatInfoEntry = entry {
|
|
loadState = .empty(.botInfo)
|
|
} else {
|
|
loadState = .messages
|
|
}
|
|
}
|
|
if self.loadState != loadState {
|
|
self.loadState = loadState
|
|
self.loadStateUpdated?(loadState, transition.options.contains(.AnimateInsertion))
|
|
for f in self.additionalLoadStateUpdated {
|
|
f(loadState, transition.options.contains(.AnimateInsertion))
|
|
}
|
|
}
|
|
|
|
let isEmpty = transition.historyView.originalView.entries.isEmpty || loadState == .empty(.botInfo)
|
|
|
|
var hasReachedLimits = false
|
|
if case let .customChatContents(customChatContents) = self.subject, let messageLimit = customChatContents.messageLimit {
|
|
hasReachedLimits = transition.historyView.originalView.entries.count >= messageLimit
|
|
}
|
|
|
|
let historyState: ChatHistoryNodeHistoryState = .loaded(isEmpty: isEmpty, hasReachedLimits: hasReachedLimits)
|
|
if self.currentHistoryState != historyState {
|
|
self.currentHistoryState = historyState
|
|
self.historyState.set(historyState)
|
|
}
|
|
}
|
|
}
|
|
|
|
private func dequeueHistoryViewTransitions() {
|
|
if self.enqueuedHistoryViewTransitions.isEmpty || self.hasActiveTransition {
|
|
return
|
|
}
|
|
self.hasActiveTransition = true
|
|
let transition = self.enqueuedHistoryViewTransitions.removeFirst()
|
|
|
|
var expiredMessageStableIds = Set<UInt32>()
|
|
if let previousHistoryView = self.historyView, transition.options.contains(.AnimateInsertion) {
|
|
var existingStableIds = Set<UInt32>()
|
|
for entry in transition.historyView.filteredEntries {
|
|
switch entry {
|
|
case let .MessageEntry(message, _, _, _, _, _):
|
|
existingStableIds.insert(message.stableId)
|
|
case let .MessageGroupEntry(_, messages, _):
|
|
for message in messages {
|
|
existingStableIds.insert(message.0.stableId)
|
|
}
|
|
default:
|
|
break
|
|
}
|
|
}
|
|
let currentTimestamp = Int32(CFAbsoluteTimeGetCurrent())
|
|
var maybeRemovedInteractivelyMessageIds: [(UInt32, EngineMessage.Id)] = []
|
|
for entry in previousHistoryView.filteredEntries {
|
|
switch entry {
|
|
case let .MessageEntry(message, _, _, _, _, _):
|
|
if !existingStableIds.contains(message.stableId) {
|
|
if let autoremoveAttribute = message.autoremoveAttribute, let countdownBeginTime = autoremoveAttribute.countdownBeginTime {
|
|
let exipiresAt = countdownBeginTime + autoremoveAttribute.timeout
|
|
if exipiresAt >= currentTimestamp - 1 {
|
|
expiredMessageStableIds.insert(message.stableId)
|
|
}
|
|
} else {
|
|
maybeRemovedInteractivelyMessageIds.append((message.stableId, message.id))
|
|
}
|
|
}
|
|
case let .MessageGroupEntry(_, messages, _):
|
|
var isRemoved = true
|
|
inner: for message in messages {
|
|
if existingStableIds.contains(message.0.stableId) {
|
|
isRemoved = false
|
|
break inner
|
|
}
|
|
}
|
|
if isRemoved, let message = messages.first?.0 {
|
|
if let autoremoveAttribute = message.autoremoveAttribute, let countdownBeginTime = autoremoveAttribute.countdownBeginTime {
|
|
let exipiresAt = countdownBeginTime + autoremoveAttribute.timeout
|
|
if exipiresAt >= currentTimestamp - 1 {
|
|
expiredMessageStableIds.insert(message.stableId)
|
|
}
|
|
} else {
|
|
maybeRemovedInteractivelyMessageIds.append((message.stableId, message.id))
|
|
}
|
|
}
|
|
default:
|
|
break
|
|
}
|
|
}
|
|
|
|
var testIds: [MessageId] = []
|
|
if !maybeRemovedInteractivelyMessageIds.isEmpty {
|
|
for (_, id) in maybeRemovedInteractivelyMessageIds {
|
|
testIds.append(id)
|
|
}
|
|
}
|
|
for id in self.context.engine.messages.synchronouslyIsMessageDeletedInteractively(ids: testIds) {
|
|
if id.namespace == Namespaces.Message.ScheduledCloud {
|
|
continue
|
|
}
|
|
inner: for (stableId, listId) in maybeRemovedInteractivelyMessageIds {
|
|
if listId == id {
|
|
expiredMessageStableIds.insert(stableId)
|
|
break inner
|
|
}
|
|
}
|
|
}
|
|
for id in self.ignoreMessageIds {
|
|
inner: for (stableId, listId) in maybeRemovedInteractivelyMessageIds {
|
|
if listId == id {
|
|
expiredMessageStableIds.insert(stableId)
|
|
break inner
|
|
}
|
|
}
|
|
}
|
|
}
|
|
self.currentDeleteAnimationCorrelationIds.formUnion(expiredMessageStableIds)
|
|
|
|
var appliedDeleteAnimationCorrelationIds = Set<UInt32>()
|
|
if !self.currentDeleteAnimationCorrelationIds.isEmpty && self.allowDustEffect {
|
|
var foundItemNodes: [ChatMessageItemView] = []
|
|
self.forEachItemNode { itemNode in
|
|
if let itemNode = itemNode as? ChatMessageItemView, let item = itemNode.item {
|
|
for (message, _) in item.content {
|
|
if let itemNode = itemNode as? ChatMessageBubbleItemNode {
|
|
if itemNode.isServiceLikeMessage() {
|
|
continue
|
|
}
|
|
}
|
|
|
|
if self.currentDeleteAnimationCorrelationIds.contains(message.stableId) {
|
|
appliedDeleteAnimationCorrelationIds.insert(message.stableId)
|
|
self.currentDeleteAnimationCorrelationIds.remove(message.stableId)
|
|
foundItemNodes.append(itemNode)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
if !foundItemNodes.isEmpty {
|
|
if self.dustEffectLayer == nil {
|
|
let dustEffectLayer = DustEffectLayer()
|
|
dustEffectLayer.position = self.bounds.center
|
|
dustEffectLayer.bounds = CGRect(origin: CGPoint(), size: self.bounds.size)
|
|
self.dustEffectLayer = dustEffectLayer
|
|
dustEffectLayer.zPosition = 10.0
|
|
if self.rotated {
|
|
dustEffectLayer.transform = CATransform3DMakeRotation(CGFloat(Double.pi), 0.0, 0.0, 1.0)
|
|
}
|
|
self.layer.addSublayer(dustEffectLayer)
|
|
dustEffectLayer.becameEmpty = { [weak self] in
|
|
guard let self else {
|
|
return
|
|
}
|
|
self.dustEffectLayer?.removeFromSuperlayer()
|
|
self.dustEffectLayer = nil
|
|
}
|
|
}
|
|
if let dustEffectLayer = self.dustEffectLayer {
|
|
for itemNode in foundItemNodes {
|
|
guard let (image, subFrame) = itemNode.makeContentSnapshot() else {
|
|
continue
|
|
}
|
|
let itemFrame = itemNode.layer.convert(subFrame, to: dustEffectLayer)
|
|
dustEffectLayer.addItem(frame: itemFrame, image: image)
|
|
itemNode.isHidden = true
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
self.currentAppliedDeleteAnimationCorrelationIds = appliedDeleteAnimationCorrelationIds
|
|
|
|
let animated = transition.options.contains(.AnimateInsertion)
|
|
|
|
var previousCloneView: UIView?
|
|
if transition.animateFromPreviousFilter, !"".isEmpty {
|
|
previousCloneView = self.view.snapshotView(afterScreenUpdates: false)
|
|
}
|
|
|
|
let completion: (Bool, ListViewDisplayedItemRange) -> Void = { [weak self] wasTransformed, visibleRange in
|
|
if let strongSelf = self {
|
|
strongSelf.currentAppliedDeleteAnimationCorrelationIds.removeAll()
|
|
|
|
var newIncomingReactions: [MessageId: (value: MessageReaction.Reaction, isLarge: Bool)] = [:]
|
|
|
|
if case .peer = strongSelf.chatLocation, let previousHistoryView = strongSelf.historyView {
|
|
var updatedIncomingReactions: [MessageId: (value: MessageReaction.Reaction, isLarge: Bool)] = [:]
|
|
for entry in transition.historyView.filteredEntries {
|
|
switch entry {
|
|
case let .MessageEntry(message, _, _, _, _, _):
|
|
if message.flags.contains(.Incoming) {
|
|
continue
|
|
}
|
|
if let reactions = message.reactionsAttribute {
|
|
for recentPeer in reactions.recentPeers {
|
|
if recentPeer.isUnseen {
|
|
updatedIncomingReactions[message.id] = (recentPeer.value, recentPeer.isLarge)
|
|
}
|
|
}
|
|
}
|
|
case let .MessageGroupEntry(_, messages, _):
|
|
for message in messages {
|
|
if message.0.flags.contains(.Incoming) {
|
|
continue
|
|
}
|
|
if let reactions = message.0.reactionsAttribute {
|
|
for recentPeer in reactions.recentPeers {
|
|
if recentPeer.isUnseen {
|
|
updatedIncomingReactions[message.0.id] = (recentPeer.value, recentPeer.isLarge)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
default:
|
|
break
|
|
}
|
|
}
|
|
for entry in previousHistoryView.filteredEntries {
|
|
switch entry {
|
|
case let .MessageEntry(message, _, _, _, _, _):
|
|
if let updatedReaction = updatedIncomingReactions[message.id] {
|
|
var previousReaction: MessageReaction.Reaction?
|
|
if let reactions = message.reactionsAttribute {
|
|
for recentPeer in reactions.recentPeers {
|
|
if recentPeer.isUnseen {
|
|
previousReaction = recentPeer.value
|
|
}
|
|
}
|
|
}
|
|
if previousReaction != updatedReaction.value {
|
|
newIncomingReactions[message.id] = updatedReaction
|
|
}
|
|
}
|
|
case let .MessageGroupEntry(_, messages, _):
|
|
for message in messages {
|
|
if let updatedReaction = updatedIncomingReactions[message.0.id] {
|
|
var previousReaction: MessageReaction.Reaction?
|
|
if let reactions = message.0.reactionsAttribute {
|
|
for recentPeer in reactions.recentPeers {
|
|
if recentPeer.isUnseen {
|
|
previousReaction = recentPeer.value
|
|
}
|
|
}
|
|
}
|
|
if previousReaction != updatedReaction.value {
|
|
newIncomingReactions[message.0.id] = updatedReaction
|
|
}
|
|
}
|
|
}
|
|
default:
|
|
break
|
|
}
|
|
}
|
|
}
|
|
|
|
var unreadMessageRangeUpdated = false
|
|
|
|
if case let .peer(peerId) = strongSelf.chatLocation, let previousReadStatesValue = strongSelf.historyView?.originalView.transientReadStates, case let .peer(previousReadStates) = previousReadStatesValue, case let .peer(updatedReadStates) = transition.historyView.originalView.transientReadStates {
|
|
if let previousPeerReadState = previousReadStates[peerId], let updatedPeerReadState = updatedReadStates[peerId] {
|
|
if previousPeerReadState != updatedPeerReadState {
|
|
for (namespace, state) in previousPeerReadState.states {
|
|
inner: for (updatedNamespace, updatedState) in updatedPeerReadState.states {
|
|
if namespace == updatedNamespace {
|
|
switch state {
|
|
case let .idBased(previousIncomingId, _, _, _, _):
|
|
if case let .idBased(updatedIncomingId, _, _, _, _) = updatedState, previousIncomingId <= updatedIncomingId {
|
|
let rangeKey = UnreadMessageRangeKey(peerId: peerId, namespace: namespace)
|
|
|
|
if let currentRange = strongSelf.controllerInteraction.unreadMessageRange[rangeKey] {
|
|
if currentRange.upperBound < (updatedIncomingId + 1) {
|
|
let updatedRange = currentRange.lowerBound ..< (updatedIncomingId + 1)
|
|
if strongSelf.controllerInteraction.unreadMessageRange[rangeKey] != updatedRange {
|
|
strongSelf.controllerInteraction.unreadMessageRange[rangeKey] = updatedRange
|
|
unreadMessageRangeUpdated = true
|
|
}
|
|
}
|
|
} else {
|
|
let updatedRange = (previousIncomingId + 1) ..< (updatedIncomingId + 1)
|
|
if strongSelf.controllerInteraction.unreadMessageRange[rangeKey] != updatedRange {
|
|
strongSelf.controllerInteraction.unreadMessageRange[rangeKey] = updatedRange
|
|
unreadMessageRangeUpdated = true
|
|
}
|
|
}
|
|
}
|
|
case .indexBased:
|
|
break
|
|
}
|
|
|
|
break inner
|
|
}
|
|
}
|
|
}
|
|
//print("Read from \(previousPeerReadState) up to \(updatedPeerReadState)")
|
|
}
|
|
}
|
|
} else if case let .peer(peerId) = strongSelf.chatLocation, case let .peer(updatedReadStates) = transition.historyView.originalView.transientReadStates {
|
|
if let updatedPeerReadState = updatedReadStates[peerId] {
|
|
for (namespace, updatedState) in updatedPeerReadState.states {
|
|
switch updatedState {
|
|
case let .idBased(updatedIncomingId, _, _, _, _):
|
|
let rangeKey = UnreadMessageRangeKey(peerId: peerId, namespace: namespace)
|
|
|
|
if let currentRange = strongSelf.controllerInteraction.unreadMessageRange[rangeKey] {
|
|
if currentRange.upperBound < (updatedIncomingId + 1) {
|
|
let updatedRange = currentRange.lowerBound ..< (updatedIncomingId + 1)
|
|
if strongSelf.controllerInteraction.unreadMessageRange[rangeKey] != updatedRange {
|
|
strongSelf.controllerInteraction.unreadMessageRange[rangeKey] = updatedRange
|
|
unreadMessageRangeUpdated = true
|
|
}
|
|
}
|
|
} else {
|
|
let updatedRange = (updatedIncomingId + 1) ..< (Int32.max - 1)
|
|
if strongSelf.controllerInteraction.unreadMessageRange[rangeKey] != updatedRange {
|
|
strongSelf.controllerInteraction.unreadMessageRange[rangeKey] = updatedRange
|
|
unreadMessageRangeUpdated = true
|
|
}
|
|
}
|
|
case .indexBased:
|
|
break
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
strongSelf.historyView = transition.historyView
|
|
|
|
let loadState: ChatHistoryNodeLoadState
|
|
var alwaysHasMessages = false
|
|
if case .custom = strongSelf.source {
|
|
if case .customChatContents = strongSelf.chatLocation {
|
|
} else {
|
|
alwaysHasMessages = true
|
|
}
|
|
}
|
|
if alwaysHasMessages {
|
|
loadState = .messages
|
|
} else if let historyView = strongSelf.historyView {
|
|
if historyView.filteredEntries.isEmpty {
|
|
if let firstEntry = historyView.originalView.entries.first {
|
|
var emptyType = ChatHistoryNodeLoadState.EmptyType.generic
|
|
for media in firstEntry.message.media {
|
|
if let action = media as? TelegramMediaAction {
|
|
if action.action == .peerJoined {
|
|
emptyType = .joined
|
|
break
|
|
} else if action.action == .historyCleared {
|
|
emptyType = .clearedHistory
|
|
break
|
|
} else if case .topicCreated = action.action, firstEntry.message.author?.id == strongSelf.context.account.peerId {
|
|
emptyType = .topic
|
|
break
|
|
}
|
|
}
|
|
}
|
|
loadState = .empty(emptyType)
|
|
} else {
|
|
var emptyType = ChatHistoryNodeLoadState.EmptyType.generic
|
|
if case let .replyThread(replyThreadMessage) = strongSelf.chatLocation {
|
|
loop: for entry in historyView.originalView.additionalData {
|
|
switch entry {
|
|
case let .message(id, messages) where id == replyThreadMessage.effectiveTopId:
|
|
if let message = messages.first {
|
|
for media in message.media {
|
|
if let action = media as? TelegramMediaAction {
|
|
if case .topicCreated = action.action {
|
|
emptyType = .topic
|
|
break
|
|
}
|
|
}
|
|
}
|
|
break loop
|
|
}
|
|
default:
|
|
break
|
|
}
|
|
}
|
|
}
|
|
loadState = .empty(emptyType)
|
|
}
|
|
} else {
|
|
if historyView.originalView.isLoadingEarlier && strongSelf.chatLocation.peerId?.namespace != Namespaces.Peer.CloudUser {
|
|
loadState = .loading(true)
|
|
} else {
|
|
if historyView.filteredEntries.count == 1, let entry = historyView.filteredEntries.first, case .ChatInfoEntry = entry {
|
|
loadState = .empty(.botInfo)
|
|
} else {
|
|
loadState = .messages
|
|
}
|
|
}
|
|
}
|
|
} else {
|
|
loadState = .loading(false)
|
|
}
|
|
|
|
var animateIn = false
|
|
if strongSelf.loadState != loadState {
|
|
if case .loading = strongSelf.loadState {
|
|
if case .messages = loadState {
|
|
animateIn = true
|
|
}
|
|
}
|
|
strongSelf.loadState = loadState
|
|
let isAnimated = animated || transition.animateIn || animateIn
|
|
strongSelf.loadStateUpdated?(loadState, isAnimated)
|
|
for f in strongSelf.additionalLoadStateUpdated {
|
|
f(loadState, isAnimated)
|
|
}
|
|
}
|
|
|
|
var hasAtLeast3Messages = false
|
|
var hasPlentyOfMessages = false
|
|
var hasLotsOfMessages = false
|
|
if let historyView = strongSelf.historyView {
|
|
if historyView.originalView.holeEarlier || historyView.originalView.holeLater {
|
|
hasAtLeast3Messages = true
|
|
hasPlentyOfMessages = true
|
|
hasLotsOfMessages = true
|
|
} else if !historyView.originalView.holeEarlier && !historyView.originalView.holeLater {
|
|
if historyView.filteredEntries.count >= 3 {
|
|
hasAtLeast3Messages = true
|
|
}
|
|
if historyView.filteredEntries.count >= 10 {
|
|
hasPlentyOfMessages = true
|
|
}
|
|
if historyView.filteredEntries.count >= 40 {
|
|
hasLotsOfMessages = true
|
|
}
|
|
}
|
|
}
|
|
|
|
if strongSelf.hasAtLeast3Messages != hasAtLeast3Messages {
|
|
strongSelf.hasAtLeast3Messages = hasAtLeast3Messages
|
|
strongSelf.hasAtLeast3MessagesUpdated?(hasAtLeast3Messages)
|
|
}
|
|
if strongSelf.hasPlentyOfMessages != hasPlentyOfMessages {
|
|
strongSelf.hasPlentyOfMessages = hasPlentyOfMessages
|
|
strongSelf.hasPlentyOfMessagesUpdated?(hasPlentyOfMessages)
|
|
}
|
|
if strongSelf.hasLotsOfMessages != hasLotsOfMessages {
|
|
strongSelf.hasLotsOfMessages = hasLotsOfMessages
|
|
strongSelf.hasLotsOfMessagesUpdated?(hasLotsOfMessages)
|
|
}
|
|
|
|
if let _ = visibleRange.loadedRange {
|
|
if let visible = visibleRange.visibleRange {
|
|
let visibleFirstIndex = visible.firstIndex
|
|
if visibleFirstIndex <= visible.lastIndex {
|
|
let (incomingIndex, overallIndex) = maxMessageIndexForEntries(transition.historyView, indexRange: (transition.historyView.filteredEntries.count - 1 - visible.lastIndex, transition.historyView.filteredEntries.count - 1 - visibleFirstIndex))
|
|
|
|
let messageIndex: MessageIndex?
|
|
switch strongSelf.chatLocation {
|
|
case .peer:
|
|
messageIndex = incomingIndex
|
|
case .replyThread, .customChatContents:
|
|
messageIndex = overallIndex
|
|
}
|
|
|
|
if let messageIndex = messageIndex {
|
|
let _ = messageIndex
|
|
}
|
|
}
|
|
}
|
|
} else if case .empty(.joined) = loadState, let entry = transition.historyView.originalView.entries.first {
|
|
strongSelf.updateMaxVisibleReadIncomingMessageIndex(entry.message.index)
|
|
} else if case .empty(.topic) = loadState, let entry = transition.historyView.originalView.entries.first {
|
|
strongSelf.updateMaxVisibleReadIncomingMessageIndex(entry.message.index)
|
|
}
|
|
|
|
if !strongSelf.didSetInitialData {
|
|
strongSelf.didSetInitialData = true
|
|
strongSelf._initialData.set(.single(ChatHistoryCombinedInitialData(initialData: transition.initialData, buttonKeyboardMessage: transition.keyboardButtonsMessage, cachedData: transition.cachedData, cachedDataMessages: transition.cachedDataMessages, readStateData: transition.readStateData)))
|
|
}
|
|
strongSelf._cachedPeerDataAndMessages.set(.single((transition.cachedData, transition.cachedDataMessages)))
|
|
let isEmpty = transition.historyView.originalView.entries.isEmpty || loadState == .empty(.botInfo)
|
|
var hasReachedLimits = false
|
|
if case let .customChatContents(customChatContents) = strongSelf.subject, let messageLimit = customChatContents.messageLimit {
|
|
hasReachedLimits = transition.historyView.originalView.entries.count >= messageLimit
|
|
}
|
|
let historyState: ChatHistoryNodeHistoryState = .loaded(isEmpty: isEmpty, hasReachedLimits: hasReachedLimits)
|
|
if strongSelf.currentHistoryState != historyState {
|
|
strongSelf.currentHistoryState = historyState
|
|
strongSelf.historyState.set(historyState)
|
|
}
|
|
|
|
var buttonKeyboardMessageUpdated = false
|
|
if let currentButtonKeyboardMessage = strongSelf.currentButtonKeyboardMessage, let buttonKeyboardMessage = transition.keyboardButtonsMessage {
|
|
if currentButtonKeyboardMessage.id != buttonKeyboardMessage.id || currentButtonKeyboardMessage.stableVersion != buttonKeyboardMessage.stableVersion {
|
|
buttonKeyboardMessageUpdated = true
|
|
}
|
|
} else if (strongSelf.currentButtonKeyboardMessage != nil) != (transition.keyboardButtonsMessage != nil) {
|
|
buttonKeyboardMessageUpdated = true
|
|
}
|
|
if buttonKeyboardMessageUpdated {
|
|
strongSelf.currentButtonKeyboardMessage = transition.keyboardButtonsMessage
|
|
strongSelf._buttonKeyboardMessage.set(.single(transition.keyboardButtonsMessage))
|
|
}
|
|
|
|
if (transition.animateIn || animateIn) && !"".isEmpty {
|
|
let heightNorm = strongSelf.bounds.height - strongSelf.insets.top
|
|
strongSelf.forEachVisibleItemNode { itemNode in
|
|
let delayFactor = itemNode.frame.minY / heightNorm
|
|
let delay = Double(delayFactor * 0.1)
|
|
|
|
if let itemNode = itemNode as? ChatMessageItemView {
|
|
itemNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.15, delay: delay)
|
|
itemNode.layer.animateScale(from: 0.94, to: 1.0, duration: 0.4, delay: delay, timingFunction: kCAMediaTimingFunctionSpring)
|
|
} else if let itemNode = itemNode as? ChatUnreadItemNode {
|
|
itemNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.15, delay: delay)
|
|
} else if let itemNode = itemNode as? ChatReplyCountItemNode {
|
|
itemNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.15, delay: delay)
|
|
}
|
|
}
|
|
strongSelf.forEachItemHeaderNode { itemNode in
|
|
let delayFactor = itemNode.frame.minY / heightNorm
|
|
let delay = Double(delayFactor * 0.2)
|
|
|
|
itemNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.15, delay: delay)
|
|
itemNode.layer.animateScale(from: 0.94, to: 1.0, duration: 0.4, delay: delay, timingFunction: kCAMediaTimingFunctionSpring)
|
|
}
|
|
}
|
|
|
|
if let scrolledToIndex = transition.scrolledToIndex {
|
|
if let strongSelf = self {
|
|
let isInitial: Bool
|
|
if case .Initial = transition.reason {
|
|
isInitial = true
|
|
} else {
|
|
isInitial = false
|
|
}
|
|
strongSelf.scrolledToIndex?(scrolledToIndex, isInitial)
|
|
}
|
|
} else if transition.scrolledToSomeIndex {
|
|
self?.scrolledToSomeIndex?()
|
|
}
|
|
|
|
if let currentSendAnimationCorrelationIds = strongSelf.currentSendAnimationCorrelationIds {
|
|
var foundItemNodes: [Int64: ChatMessageItemView] = [:]
|
|
strongSelf.forEachItemNode { itemNode in
|
|
if let itemNode = itemNode as? ChatMessageItemView, let item = itemNode.item {
|
|
for (message, _) in item.content {
|
|
for attribute in message.attributes {
|
|
if let attribute = attribute as? OutgoingMessageInfoAttribute, let correlationId = attribute.correlationId {
|
|
if currentSendAnimationCorrelationIds.contains(correlationId) {
|
|
foundItemNodes[correlationId] = itemNode
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if !foundItemNodes.isEmpty {
|
|
strongSelf.currentSendAnimationCorrelationIds = nil
|
|
strongSelf.animationCorrelationMessagesFound?(foundItemNodes)
|
|
}
|
|
}
|
|
|
|
if !newIncomingReactions.isEmpty {
|
|
let messageIds = Array(newIncomingReactions.keys)
|
|
|
|
let visibleNewIncomingReactionMessageIds = strongSelf.displayUnseenReactionAnimations(messageIds: messageIds)
|
|
if !visibleNewIncomingReactionMessageIds.isEmpty {
|
|
strongSelf.unseenReactionsProcessingManager.add(visibleNewIncomingReactionMessageIds.map { MessageAndThreadId(messageId: $0, threadId: nil) })
|
|
}
|
|
}
|
|
|
|
if unreadMessageRangeUpdated {
|
|
strongSelf.forEachVisibleMessageItemNode { itemNode in
|
|
itemNode.unreadMessageRangeUpdated()
|
|
}
|
|
}
|
|
|
|
strongSelf.hasActiveTransition = false
|
|
|
|
if let previousCloneView {
|
|
previousCloneView.transform = strongSelf.view.transform
|
|
previousCloneView.center = strongSelf.view.center
|
|
previousCloneView.bounds = strongSelf.view.bounds
|
|
strongSelf.view.superview?.insertSubview(previousCloneView, belowSubview: strongSelf.view)
|
|
strongSelf.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)
|
|
previousCloneView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { [weak previousCloneView] _ in
|
|
previousCloneView?.removeFromSuperview()
|
|
})
|
|
}
|
|
|
|
strongSelf.dequeueHistoryViewTransitions()
|
|
|
|
strongSelf._isReady.set(true)
|
|
|
|
if !strongSelf.didSetReady {
|
|
strongSelf.didSetReady = true
|
|
#if DEBUG
|
|
let deltaTime = (CFAbsoluteTimeGetCurrent() - strongSelf.initTimestamp) * 1000.0
|
|
print("Chat init to dequeue time: \(deltaTime) ms")
|
|
#endif
|
|
}
|
|
}
|
|
}
|
|
|
|
if let (layoutActionOnViewTransition, layoutCorrelationId) = self.layoutActionOnViewTransition {
|
|
var foundCorrelationMessage = false
|
|
if let layoutCorrelationId = layoutCorrelationId {
|
|
itemSearch: for item in transition.insertItems {
|
|
if let messageItem = item.item as? ChatMessageItem {
|
|
for (message, _) in messageItem.content {
|
|
for attribute in message.attributes {
|
|
if let attribute = attribute as? OutgoingMessageInfoAttribute {
|
|
if attribute.correlationId == layoutCorrelationId {
|
|
foundCorrelationMessage = true
|
|
break itemSearch
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
} else {
|
|
foundCorrelationMessage = true
|
|
}
|
|
|
|
if foundCorrelationMessage {
|
|
self.layoutActionOnViewTransition = nil
|
|
let (mappedTransition, updateSizeAndInsets) = layoutActionOnViewTransition(transition)
|
|
self.transaction(deleteIndices: mappedTransition.deleteItems, insertIndicesAndItems: transition.insertItems, updateIndicesAndItems: transition.updateItems, options: mappedTransition.options, scrollToItem: mappedTransition.scrollToItem, updateSizeAndInsets: updateSizeAndInsets, stationaryItemRange: mappedTransition.stationaryItemRange, updateOpaqueState: ChatHistoryTransactionOpaqueState(historyView: transition.historyView), completion: { result in
|
|
completion(true, result)
|
|
})
|
|
} else {
|
|
self.transaction(deleteIndices: transition.deleteItems, insertIndicesAndItems: transition.insertItems, updateIndicesAndItems: transition.updateItems, options: transition.options, scrollToItem: transition.scrollToItem, stationaryItemRange: transition.stationaryItemRange, updateOpaqueState: ChatHistoryTransactionOpaqueState(historyView: transition.historyView), completion: { result in
|
|
completion(false, result)
|
|
})
|
|
}
|
|
} else {
|
|
self.transaction(deleteIndices: transition.deleteItems, insertIndicesAndItems: transition.insertItems, updateIndicesAndItems: transition.updateItems, options: transition.options, scrollToItem: transition.scrollToItem, stationaryItemRange: transition.stationaryItemRange, updateOpaqueState: ChatHistoryTransactionOpaqueState(historyView: transition.historyView), completion: { result in
|
|
completion(false, result)
|
|
})
|
|
}
|
|
|
|
if transition.flashIndicators {
|
|
//self.flashHeaderItems()
|
|
}
|
|
}
|
|
|
|
private func displayUnseenReactionAnimations(messageIds: [MessageId], forceMapping: [MessageId: [ReactionsMessageAttribute.RecentPeer]] = [:]) -> [MessageId] {
|
|
let timestamp = CACurrentMediaTime()
|
|
var messageIds = messageIds
|
|
for i in (0 ..< messageIds.count).reversed() {
|
|
if let previousTimestamp = self.displayUnseenReactionAnimationsTimestamps[messageIds[i]], previousTimestamp + 1.0 > timestamp {
|
|
messageIds.remove(at: i)
|
|
} else {
|
|
self.displayUnseenReactionAnimationsTimestamps[messageIds[i]] = timestamp
|
|
}
|
|
}
|
|
|
|
if messageIds.isEmpty {
|
|
return []
|
|
}
|
|
|
|
guard let chatDisplayNode = self.controllerInteraction.chatControllerNode() as? ChatControllerNode else {
|
|
return []
|
|
}
|
|
var visibleNewIncomingReactionMessageIds: [MessageId] = []
|
|
self.forEachVisibleItemNode { itemNode in
|
|
guard let itemNode = itemNode as? ChatMessageItemView, let item = itemNode.item, let reactionsAttribute = item.content.firstMessage.reactionsAttribute, messageIds.contains(item.content.firstMessage.id) else {
|
|
return
|
|
}
|
|
|
|
var selectedReaction: (MessageReaction.Reaction, EnginePeer?, Bool)?
|
|
let recentPeers = forceMapping[item.content.firstMessage.id] ?? reactionsAttribute.recentPeers
|
|
for recentPeer in recentPeers {
|
|
if recentPeer.isUnseen {
|
|
selectedReaction = (recentPeer.value, item.content.firstMessage.peers[recentPeer.peerId].flatMap(EnginePeer.init), recentPeer.isLarge)
|
|
break
|
|
}
|
|
}
|
|
|
|
guard let (updatedReaction, updateReactionPeer, updatedReactionIsLarge) = selectedReaction else {
|
|
return
|
|
}
|
|
|
|
visibleNewIncomingReactionMessageIds.append(item.content.firstMessage.id)
|
|
|
|
var reactionItem: ReactionItem?
|
|
|
|
switch updatedReaction {
|
|
case .builtin, .stars:
|
|
if let availableReactions = item.associatedData.availableReactions {
|
|
for reaction in availableReactions.reactions {
|
|
guard let centerAnimation = reaction.centerAnimation else {
|
|
continue
|
|
}
|
|
guard let aroundAnimation = reaction.aroundAnimation else {
|
|
continue
|
|
}
|
|
if reaction.value == updatedReaction {
|
|
reactionItem = ReactionItem(
|
|
reaction: ReactionItem.Reaction(rawValue: reaction.value),
|
|
appearAnimation: reaction.appearAnimation,
|
|
stillAnimation: reaction.selectAnimation,
|
|
listAnimation: centerAnimation,
|
|
largeListAnimation: reaction.activateAnimation,
|
|
applicationAnimation: aroundAnimation,
|
|
largeApplicationAnimation: reaction.effectAnimation,
|
|
isCustom: false
|
|
)
|
|
break
|
|
}
|
|
}
|
|
}
|
|
case let .custom(fileId):
|
|
if let itemFile = item.message.associatedMedia[MediaId(namespace: Namespaces.Media.CloudFile, id: fileId)] as? TelegramMediaFile {
|
|
let itemFile = TelegramMediaFile.Accessor(itemFile)
|
|
reactionItem = ReactionItem(
|
|
reaction: ReactionItem.Reaction(rawValue: updatedReaction),
|
|
appearAnimation: itemFile,
|
|
stillAnimation: itemFile,
|
|
listAnimation: itemFile,
|
|
largeListAnimation: itemFile,
|
|
applicationAnimation: nil,
|
|
largeApplicationAnimation: nil,
|
|
isCustom: true
|
|
)
|
|
}
|
|
}
|
|
|
|
if let reactionItem = reactionItem, let targetView = itemNode.targetReactionView(value: updatedReaction) {
|
|
let standaloneReactionAnimation = StandaloneReactionAnimation(genericReactionEffect: self.genericReactionEffect)
|
|
|
|
chatDisplayNode.messageTransitionNode.addMessageStandaloneReactionAnimation(messageId: item.message.id, standaloneReactionAnimation: standaloneReactionAnimation)
|
|
|
|
var avatarPeers: [EnginePeer] = []
|
|
if item.message.id.peerId.namespace != Namespaces.Peer.CloudUser, let updateReactionPeer = updateReactionPeer {
|
|
avatarPeers = [updateReactionPeer]
|
|
}
|
|
|
|
chatDisplayNode.addSubnode(standaloneReactionAnimation)
|
|
standaloneReactionAnimation.frame = chatDisplayNode.bounds
|
|
standaloneReactionAnimation.animateReactionSelection(
|
|
context: self.context,
|
|
theme: item.presentationData.theme.theme,
|
|
animationCache: self.controllerInteraction.presentationContext.animationCache,
|
|
reaction: reactionItem,
|
|
avatarPeers: avatarPeers,
|
|
playHaptic: true,
|
|
isLarge: updatedReactionIsLarge,
|
|
targetView: targetView,
|
|
addStandaloneReactionAnimation: { [weak self] standaloneReactionAnimation in
|
|
guard let strongSelf = self, let chatDisplayNode = strongSelf.controllerInteraction.chatControllerNode() as? ChatControllerNode else {
|
|
return
|
|
}
|
|
chatDisplayNode.messageTransitionNode.addMessageStandaloneReactionAnimation(messageId: item.message.id, standaloneReactionAnimation: standaloneReactionAnimation)
|
|
standaloneReactionAnimation.frame = chatDisplayNode.bounds
|
|
chatDisplayNode.addSubnode(standaloneReactionAnimation)
|
|
},
|
|
completion: { [weak standaloneReactionAnimation] in
|
|
standaloneReactionAnimation?.removeFromSupernode()
|
|
}
|
|
)
|
|
}
|
|
}
|
|
return visibleNewIncomingReactionMessageIds
|
|
}
|
|
|
|
public func updateLayout(transition: ContainedViewLayoutTransition, updateSizeAndInsets: ListViewUpdateSizeAndInsets) {
|
|
self.updateLayout(transition: transition, updateSizeAndInsets: updateSizeAndInsets, additionalScrollDistance: 0.0, scrollToTop: false, completion: {})
|
|
}
|
|
|
|
public func updateLayout(transition: ContainedViewLayoutTransition, updateSizeAndInsets: ListViewUpdateSizeAndInsets, additionalScrollDistance: CGFloat, scrollToTop: Bool, completion: @escaping () -> Void) {
|
|
/*if updateSizeAndInsets.insets.top == 83.0 {
|
|
if !transition.isAnimated {
|
|
assert(true)
|
|
}
|
|
}*/
|
|
var scrollToItem: ListViewScrollToItem?
|
|
var postScrollToItem: ListViewScrollToItem?
|
|
if scrollToTop, case .known = self.visibleContentOffset() {
|
|
scrollToItem = ListViewScrollToItem(index: 0, position: .top(0.0), animated: true, curve: .Spring(duration: updateSizeAndInsets.duration), directionHint: .Up)
|
|
} else if self.enableUnreadAlignment {
|
|
if updateSizeAndInsets.insets.bottom != self.insets.bottom {
|
|
self.forEachVisibleItemNode { itemNode in
|
|
if let itemNode = itemNode as? ChatUnreadItemNode, let index = itemNode.index {
|
|
if abs(itemNode.frame.maxY - (self.visibleSize.height - self.insets.bottom + 6.0)) < 1.0 {
|
|
postScrollToItem = ListViewScrollToItem(index: index, position: .bottom(0.0), animated: updateSizeAndInsets.duration != 0.0, curve: updateSizeAndInsets.curve, directionHint: .Up)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
self.transaction(deleteIndices: [], insertIndicesAndItems: [], updateIndicesAndItems: [], options: [.Synchronous, .LowLatency], scrollToItem: scrollToItem, additionalScrollDistance: scrollToTop ? 0.0 : additionalScrollDistance, updateSizeAndInsets: updateSizeAndInsets, stationaryItemRange: nil, updateOpaqueState: nil, completion: { [weak self] _ in
|
|
guard let self else {
|
|
return
|
|
}
|
|
if let postScrollToItem = postScrollToItem {
|
|
self.transaction(deleteIndices: [], insertIndicesAndItems: [], updateIndicesAndItems: [], options: [.Synchronous, .LowLatency], scrollToItem: postScrollToItem, additionalScrollDistance: 0.0, updateSizeAndInsets: nil, stationaryItemRange: nil, updateOpaqueState: nil, completion: { _ in
|
|
completion()
|
|
})
|
|
} else {
|
|
completion()
|
|
}
|
|
})
|
|
|
|
if !self.dequeuedInitialTransitionOnLayout {
|
|
self.dequeuedInitialTransitionOnLayout = true
|
|
self.dequeueHistoryViewTransitions()
|
|
}
|
|
}
|
|
|
|
public func disconnect() {
|
|
self.historyDisposable.set(nil)
|
|
}
|
|
|
|
private func updateReadHistoryActions() {
|
|
let canRead = self.canReadHistoryValue && self.isScrollAtBottomPosition
|
|
|
|
if canRead != (self.interactiveReadActionDisposable != nil) {
|
|
if let interactiveReadActionDisposable = self.interactiveReadActionDisposable {
|
|
if !canRead {
|
|
interactiveReadActionDisposable.dispose()
|
|
self.interactiveReadActionDisposable = nil
|
|
}
|
|
} else if self.interactiveReadActionDisposable == nil {
|
|
if case let .peer(peerId) = self.chatLocation {
|
|
if !self.context.sharedContext.immediateExperimentalUISettings.skipReadHistory && !self.context.account.isSupportUser {
|
|
self.interactiveReadActionDisposable = self.context.engine.messages.installInteractiveReadMessagesAction(peerId: peerId)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if canRead != (self.interactiveReadReactionsDisposable != nil) {
|
|
if let interactiveReadReactionsDisposable = self.interactiveReadReactionsDisposable {
|
|
if !canRead {
|
|
interactiveReadReactionsDisposable.dispose()
|
|
self.interactiveReadReactionsDisposable = nil
|
|
}
|
|
} else if self.interactiveReadReactionsDisposable == nil {
|
|
if case let .peer(peerId) = self.chatLocation {
|
|
if !self.context.sharedContext.immediateExperimentalUISettings.skipReadHistory {
|
|
let visibleMessageRange = self.visibleMessageRange
|
|
self.interactiveReadReactionsDisposable = context.engine.messages.installInteractiveReadReactionsAction(peerId: peerId, getVisibleRange: {
|
|
return visibleMessageRange.with { $0 }
|
|
}, didReadReactionsInMessages: { [weak self] idsAndReactions in
|
|
Queue.mainQueue().after(0.2, {
|
|
guard let strongSelf = self else {
|
|
return
|
|
}
|
|
let _ = strongSelf.displayUnseenReactionAnimations(messageIds: Array(idsAndReactions.keys), forceMapping: idsAndReactions)
|
|
})
|
|
})
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
func lastVisbleMesssage() -> Message? {
|
|
var currentMessage: Message?
|
|
if let historyView = self.historyView {
|
|
if let visibleRange = self.displayedItemRange.visibleRange {
|
|
var index = 0
|
|
loop: for entry in historyView.filteredEntries.reversed() {
|
|
if index >= visibleRange.firstIndex && index <= visibleRange.lastIndex {
|
|
if case let .MessageEntry(message, _, _, _, _, _) = entry {
|
|
currentMessage = message
|
|
break loop
|
|
} else if case let .MessageGroupEntry(_, messages, _) = entry {
|
|
currentMessage = messages.first?.0
|
|
break loop
|
|
}
|
|
}
|
|
index += 1
|
|
}
|
|
}
|
|
}
|
|
return currentMessage
|
|
}
|
|
|
|
func immediateScrollState() -> ChatInterfaceHistoryScrollState? {
|
|
var currentMessage: Message?
|
|
if let historyView = self.historyView {
|
|
if let visibleRange = self.displayedItemRange.visibleRange {
|
|
var index = 0
|
|
loop: for entry in historyView.filteredEntries.reversed() {
|
|
if index >= visibleRange.firstIndex && index <= visibleRange.lastIndex {
|
|
if case let .MessageEntry(message, _, _, _, _, _) = entry {
|
|
if message.adAttribute != nil {
|
|
continue
|
|
}
|
|
if index != 0 || historyView.originalView.laterId != nil {
|
|
currentMessage = message
|
|
}
|
|
break loop
|
|
} else if case let .MessageGroupEntry(_, messages, _) = entry {
|
|
if index != 0 || historyView.originalView.laterId != nil {
|
|
currentMessage = messages.first?.0
|
|
}
|
|
break loop
|
|
}
|
|
}
|
|
index += 1
|
|
}
|
|
}
|
|
}
|
|
|
|
if let message = currentMessage {
|
|
var relativeOffset: CGFloat = 0.0
|
|
self.forEachItemNode { itemNode in
|
|
if let itemNode = itemNode as? ChatMessageItemView, let item = itemNode.item, item.message.id == message.id {
|
|
if let offsetValue = self.itemNodeRelativeOffset(itemNode) {
|
|
relativeOffset = offsetValue
|
|
}
|
|
}
|
|
}
|
|
return ChatInterfaceHistoryScrollState(messageIndex: message.index, relativeOffset: Double(relativeOffset))
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func scrollToNextMessage() {
|
|
if let historyView = self.historyView {
|
|
var scrolled = false
|
|
if let scrollState = self.immediateScrollState() {
|
|
var index = historyView.filteredEntries.count - 1
|
|
loop: for entry in historyView.filteredEntries.reversed() {
|
|
if entry.index == scrollState.messageIndex {
|
|
break loop
|
|
}
|
|
index -= 1
|
|
}
|
|
|
|
if index != 0 {
|
|
var nextItem = false
|
|
self.forEachItemNode { itemNode in
|
|
if let itemNode = itemNode as? ChatMessageItemView, itemNode.item?.content.index == scrollState.messageIndex {
|
|
if itemNode.frame.maxY >= self.bounds.size.height - self.insets.bottom - 4.0 {
|
|
nextItem = true
|
|
}
|
|
}
|
|
}
|
|
|
|
if !nextItem {
|
|
scrolled = true
|
|
self.scrollToMessage(from: scrollState.messageIndex, to: scrollState.messageIndex, animated: true, highlight: false)
|
|
} else {
|
|
loop: for i in (index + 1) ..< historyView.filteredEntries.count {
|
|
let entry = historyView.filteredEntries[i]
|
|
switch entry {
|
|
case .MessageEntry, .MessageGroupEntry:
|
|
scrolled = true
|
|
self.scrollToMessage(from: scrollState.messageIndex, to: entry.index, animated: true, highlight: false)
|
|
break loop
|
|
default:
|
|
break
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if !scrolled {
|
|
self.scrollToEndOfHistory()
|
|
}
|
|
}
|
|
}
|
|
|
|
func requestMessageUpdate(_ id: MessageId, andScrollToItem scroll: Bool = false) {
|
|
if let historyView = self.historyView {
|
|
var messageItem: ChatMessageItem?
|
|
self.forEachItemNode({ itemNode in
|
|
if let itemNode = itemNode as? ChatMessageItemView, let item = itemNode.item {
|
|
for (message, _) in item.content {
|
|
if message.id == id {
|
|
messageItem = item
|
|
break
|
|
}
|
|
}
|
|
}
|
|
})
|
|
|
|
if let messageItem = messageItem {
|
|
let associatedData = messageItem.associatedData
|
|
let disableFloatingDateHeaders = messageItem.disableDate
|
|
|
|
loop: for i in 0 ..< historyView.filteredEntries.count {
|
|
switch historyView.filteredEntries[i] {
|
|
case let .MessageEntry(message, presentationData, read, location, selection, attributes):
|
|
if message.id == id {
|
|
let index = historyView.filteredEntries.count - 1 - i
|
|
let item: ListViewItem
|
|
switch self.mode {
|
|
case .bubbles:
|
|
item = ChatMessageItemImpl(presentationData: presentationData, context: self.context, chatLocation: self.chatLocation, associatedData: associatedData, controllerInteraction: self.controllerInteraction, content: .message(message: message, read: read, selection: selection, attributes: attributes, location: location), disableDate: disableFloatingDateHeaders)
|
|
case let .list(_, _, _, displayHeaders, hintLinks, isGlobalSearch):
|
|
let displayHeader: Bool
|
|
switch displayHeaders {
|
|
case .none:
|
|
displayHeader = false
|
|
case .all:
|
|
displayHeader = true
|
|
case .allButLast:
|
|
displayHeader = listMessageDateHeaderId(timestamp: message.timestamp) != historyView.lastHeaderId
|
|
}
|
|
item = ListMessageItem(presentationData: presentationData, context: self.context, chatLocation: self.chatLocation, interaction: ListMessageItemInteraction(controllerInteraction: self.controllerInteraction), message: message, translateToLanguage: associatedData.translateToLanguage, selection: selection, displayHeader: displayHeader, hintIsLink: hintLinks, isGlobalSearchResult: isGlobalSearch)
|
|
}
|
|
let updateItem = ListViewUpdateItem(index: index, previousIndex: index, item: item, directionHint: nil)
|
|
|
|
var scrollToItem: ListViewScrollToItem?
|
|
if scroll {
|
|
scrollToItem = ListViewScrollToItem(index: index, position: .center(.top), animated: true, curve: .Spring(duration: 0.4), directionHint: .Down, displayLink: true)
|
|
}
|
|
|
|
self.transaction(deleteIndices: [], insertIndicesAndItems: [], updateIndicesAndItems: [updateItem], options: [.AnimateInsertion], scrollToItem: scrollToItem, additionalScrollDistance: 0.0, updateSizeAndInsets: nil, stationaryItemRange: nil, updateOpaqueState: nil, completion: { _ in })
|
|
break loop
|
|
}
|
|
case let .MessageGroupEntry(_, messages, presentationData):
|
|
if messages.contains(where: { $0.0.id == id }) {
|
|
let index = historyView.filteredEntries.count - 1 - i
|
|
let item: ListViewItem
|
|
switch self.mode {
|
|
case .bubbles:
|
|
item = ChatMessageItemImpl(presentationData: presentationData, context: self.context, chatLocation: self.chatLocation, associatedData: associatedData, controllerInteraction: self.controllerInteraction, content: .group(messages: messages), disableDate: disableFloatingDateHeaders)
|
|
case .list:
|
|
assertionFailure()
|
|
item = ListMessageItem(presentationData: presentationData, context: context, chatLocation: chatLocation, interaction: ListMessageItemInteraction(controllerInteraction: controllerInteraction), message: messages[0].0, selection: .none, displayHeader: false)
|
|
}
|
|
let updateItem = ListViewUpdateItem(index: index, previousIndex: index, item: item, directionHint: nil)
|
|
|
|
var scrollToItem: ListViewScrollToItem?
|
|
if scroll {
|
|
scrollToItem = ListViewScrollToItem(index: index, position: .center(.top), animated: true, curve: .Spring(duration: 0.4), directionHint: .Down, displayLink: true)
|
|
}
|
|
|
|
self.transaction(deleteIndices: [], insertIndicesAndItems: [], updateIndicesAndItems: [updateItem], options: [.AnimateInsertion], scrollToItem: scrollToItem, additionalScrollDistance: 0.0, updateSizeAndInsets: nil, stationaryItemRange: nil, updateOpaqueState: nil, completion: { _ in })
|
|
break loop
|
|
}
|
|
default:
|
|
break
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
func requestMessageUpdate(stableId: UInt32) {
|
|
if let historyView = self.historyView {
|
|
var messageItem: ChatMessageItem?
|
|
self.forEachItemNode({ itemNode in
|
|
if let itemNode = itemNode as? ChatMessageItemView, let item = itemNode.item {
|
|
for (message, _) in item.content {
|
|
if message.stableId == stableId {
|
|
messageItem = item
|
|
break
|
|
}
|
|
}
|
|
}
|
|
})
|
|
|
|
if let messageItem = messageItem {
|
|
let associatedData = messageItem.associatedData
|
|
let disableFloatingDateHeaders = messageItem.disableDate
|
|
|
|
loop: for i in 0 ..< historyView.filteredEntries.count {
|
|
switch historyView.filteredEntries[i] {
|
|
case let .MessageEntry(message, presentationData, read, location, selection, attributes):
|
|
if message.stableId == stableId {
|
|
let index = historyView.filteredEntries.count - 1 - i
|
|
let item: ListViewItem
|
|
switch self.mode {
|
|
case .bubbles:
|
|
item = ChatMessageItemImpl(presentationData: presentationData, context: self.context, chatLocation: self.chatLocation, associatedData: associatedData, controllerInteraction: self.controllerInteraction, content: .message(message: message, read: read, selection: selection, attributes: attributes, location: location), disableDate: disableFloatingDateHeaders)
|
|
case let .list(_, _, _, displayHeaders, hintLinks, isGlobalSearch):
|
|
let displayHeader: Bool
|
|
switch displayHeaders {
|
|
case .none:
|
|
displayHeader = false
|
|
case .all:
|
|
displayHeader = true
|
|
case .allButLast:
|
|
displayHeader = listMessageDateHeaderId(timestamp: message.timestamp) != historyView.lastHeaderId
|
|
}
|
|
item = ListMessageItem(presentationData: presentationData, context: self.context, chatLocation: self.chatLocation, interaction: ListMessageItemInteraction(controllerInteraction: self.controllerInteraction), message: message, translateToLanguage: associatedData.translateToLanguage, selection: selection, displayHeader: displayHeader, hintIsLink: hintLinks, isGlobalSearchResult: isGlobalSearch)
|
|
}
|
|
let updateItem = ListViewUpdateItem(index: index, previousIndex: index, item: item, directionHint: nil)
|
|
self.transaction(deleteIndices: [], insertIndicesAndItems: [], updateIndicesAndItems: [updateItem], options: [.AnimateInsertion], scrollToItem: nil, additionalScrollDistance: 0.0, updateSizeAndInsets: nil, stationaryItemRange: nil, updateOpaqueState: nil, completion: { _ in })
|
|
break loop
|
|
}
|
|
default:
|
|
break
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
private func messagesAtPoint(_ point: CGPoint) -> [Message]? {
|
|
var resultMessages: [Message]?
|
|
self.forEachVisibleItemNode { itemNode in
|
|
if resultMessages == nil, let itemNode = itemNode as? ListViewItemNode, itemNode.frame.contains(point) {
|
|
if let itemNode = itemNode as? ChatMessageItemView, let item = itemNode.item {
|
|
switch item.content {
|
|
case let .message(message, _, _ , _, _):
|
|
resultMessages = [message]
|
|
case let .group(messages):
|
|
resultMessages = messages.map { $0.0 }
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return resultMessages
|
|
}
|
|
|
|
func isMessageVisible(id: MessageId) -> Bool {
|
|
var found = false
|
|
self.forEachVisibleItemNode { itemNode in
|
|
if !found, let itemNode = itemNode as? ListViewItemNode {
|
|
if let itemNode = itemNode as? ChatMessageItemView, let item = itemNode.item {
|
|
switch item.content {
|
|
case let .message(message, _, _ , _, _):
|
|
if message.id == id {
|
|
found = true
|
|
}
|
|
case let .group(messages):
|
|
for message in messages {
|
|
if message.0.id == id {
|
|
found = true
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return found
|
|
}
|
|
|
|
private var selectionPanState: (selecting: Bool, initialMessageId: MessageId, toggledMessageIds: [[MessageId]])?
|
|
private var selectionScrollActivationTimer: SwiftSignalKit.Timer?
|
|
private var selectionScrollDisplayLink: ConstantDisplayLinkAnimator?
|
|
private var selectionScrollDelta: CGFloat?
|
|
private var selectionLastLocation: CGPoint?
|
|
|
|
@objc private func selectionPanGesture(_ recognizer: UIGestureRecognizer) -> Void {
|
|
let location = recognizer.location(in: self.view)
|
|
switch recognizer.state {
|
|
case .began:
|
|
if let messages = self.messagesAtPoint(location), let message = messages.first {
|
|
let selecting = !(self.controllerInteraction.selectionState?.selectedIds.contains(message.id) ?? false)
|
|
self.selectionPanState = (selecting, message.id, [])
|
|
self.controllerInteraction.toggleMessagesSelection(messages.map { $0.id }, selecting)
|
|
}
|
|
case .changed:
|
|
self.handlePanSelection(location: location)
|
|
self.selectionLastLocation = location
|
|
case .ended, .failed, .cancelled:
|
|
self.selectionPanState = nil
|
|
self.selectionScrollDisplayLink = nil
|
|
self.selectionScrollActivationTimer?.invalidate()
|
|
self.selectionScrollActivationTimer = nil
|
|
self.selectionScrollDelta = nil
|
|
self.selectionLastLocation = nil
|
|
self.selectionScrollSkipUpdate = false
|
|
case .possible:
|
|
break
|
|
@unknown default:
|
|
fatalError()
|
|
}
|
|
}
|
|
|
|
private func handlePanSelection(location: CGPoint) {
|
|
var location = location
|
|
if location.y < self.insets.top {
|
|
location.y = self.insets.top + 5.0
|
|
} else if location.y > self.frame.height - self.insets.bottom {
|
|
location.y = self.frame.height - self.insets.bottom - 5.0
|
|
}
|
|
|
|
if let state = self.selectionPanState {
|
|
if let messages = self.messagesAtPoint(location), let message = messages.first {
|
|
if message.id == state.initialMessageId {
|
|
if !state.toggledMessageIds.isEmpty {
|
|
self.controllerInteraction.toggleMessagesSelection(state.toggledMessageIds.flatMap { $0 }, !state.selecting)
|
|
self.selectionPanState = (state.selecting, state.initialMessageId, [])
|
|
}
|
|
} else if state.toggledMessageIds.last?.first != message.id {
|
|
var updatedToggledMessageIds: [[MessageId]] = []
|
|
var previouslyToggled = false
|
|
for i in (0 ..< state.toggledMessageIds.count) {
|
|
if let messageId = state.toggledMessageIds[i].first {
|
|
if messageId == message.id {
|
|
previouslyToggled = true
|
|
updatedToggledMessageIds = Array(state.toggledMessageIds.prefix(i + 1))
|
|
|
|
let messageIdsToToggle = Array(state.toggledMessageIds.suffix(state.toggledMessageIds.count - i - 1)).flatMap { $0 }
|
|
self.controllerInteraction.toggleMessagesSelection(messageIdsToToggle, !state.selecting)
|
|
break
|
|
}
|
|
}
|
|
}
|
|
|
|
if !previouslyToggled {
|
|
updatedToggledMessageIds = state.toggledMessageIds
|
|
let isSelected = (self.controllerInteraction.selectionState?.selectedIds.contains(message.id) ?? false)
|
|
if state.selecting != isSelected {
|
|
let messageIds = messages.filter { message -> Bool in
|
|
for media in message.media {
|
|
if media is TelegramMediaAction {
|
|
return false
|
|
}
|
|
}
|
|
return true
|
|
}.map { $0.id }
|
|
updatedToggledMessageIds.append(messageIds)
|
|
self.controllerInteraction.toggleMessagesSelection(messageIds, state.selecting)
|
|
}
|
|
}
|
|
|
|
self.selectionPanState = (state.selecting, state.initialMessageId, updatedToggledMessageIds)
|
|
}
|
|
}
|
|
|
|
let scrollingAreaHeight: CGFloat = 50.0
|
|
if location.y < scrollingAreaHeight + self.insets.top || location.y > self.frame.height - scrollingAreaHeight - self.insets.bottom {
|
|
if location.y < self.frame.height / 2.0 {
|
|
self.selectionScrollDelta = (scrollingAreaHeight - (location.y - self.insets.top)) / scrollingAreaHeight
|
|
} else {
|
|
self.selectionScrollDelta = -(scrollingAreaHeight - min(scrollingAreaHeight, max(0.0, (self.frame.height - self.insets.bottom - location.y)))) / scrollingAreaHeight
|
|
}
|
|
if let displayLink = self.selectionScrollDisplayLink {
|
|
displayLink.isPaused = false
|
|
} else {
|
|
if let _ = self.selectionScrollActivationTimer {
|
|
} else {
|
|
let timer = SwiftSignalKit.Timer(timeout: 0.45, repeat: false, completion: { [weak self] in
|
|
self?.setupSelectionScrolling()
|
|
}, queue: .mainQueue())
|
|
timer.start()
|
|
self.selectionScrollActivationTimer = timer
|
|
}
|
|
}
|
|
} else {
|
|
self.selectionScrollDisplayLink?.isPaused = true
|
|
self.selectionScrollActivationTimer?.invalidate()
|
|
self.selectionScrollActivationTimer = nil
|
|
}
|
|
}
|
|
}
|
|
|
|
private var selectionScrollSkipUpdate = false
|
|
private func setupSelectionScrolling() {
|
|
self.selectionScrollDisplayLink = ConstantDisplayLinkAnimator(update: { [weak self] in
|
|
self?.selectionScrollActivationTimer = nil
|
|
if let strongSelf = self, let delta = strongSelf.selectionScrollDelta {
|
|
let distance: CGFloat = 15.0 * min(1.0, 0.15 + abs(delta * delta))
|
|
let direction: ListViewScrollDirection = delta > 0.0 ? .up : .down
|
|
let _ = strongSelf.scrollWithDirection(direction, distance: distance)
|
|
|
|
if let location = strongSelf.selectionLastLocation {
|
|
if !strongSelf.selectionScrollSkipUpdate {
|
|
strongSelf.handlePanSelection(location: location)
|
|
}
|
|
strongSelf.selectionScrollSkipUpdate = !strongSelf.selectionScrollSkipUpdate
|
|
}
|
|
}
|
|
})
|
|
self.selectionScrollDisplayLink?.isPaused = false
|
|
}
|
|
|
|
|
|
func voicePlaylistItemChanged(_ previousItem: SharedMediaPlaylistItem?, _ currentItem: SharedMediaPlaylistItem?) -> Void {
|
|
if let currentItemId = currentItem?.id as? PeerMessagesMediaPlaylistItemId {
|
|
if let source = currentItem?.playbackData?.source, case let .telegramFile(_, _, isViewOnce) = source, isViewOnce {
|
|
self.currentlyPlayingMessageIdPromise.set(.single(nil))
|
|
} else {
|
|
let isVideo = currentItem?.playbackData?.type == .instantVideo
|
|
self.currentlyPlayingMessageIdPromise.set(.single((currentItemId.messageIndex, isVideo)))
|
|
}
|
|
} else {
|
|
self.currentlyPlayingMessageIdPromise.set(.single(nil))
|
|
}
|
|
}
|
|
|
|
func scrollToMessage(index: MessageIndex) {
|
|
self.appliedScrollToMessageId = nil
|
|
self.scrollToMessageIdPromise.set(.single(index))
|
|
}
|
|
|
|
private var currentSendAnimationCorrelationIds: Set<Int64>?
|
|
func setCurrentSendAnimationCorrelationIds(_ value: Set<Int64>?) {
|
|
self.currentSendAnimationCorrelationIds = value
|
|
}
|
|
|
|
private var currentDeleteAnimationCorrelationIds = Set<UInt32>()
|
|
func setCurrentDeleteAnimationCorrelationIds(_ value: Set<UInt32>) {
|
|
self.currentDeleteAnimationCorrelationIds = value
|
|
}
|
|
private var currentAppliedDeleteAnimationCorrelationIds = Set<UInt32>()
|
|
|
|
var animationCorrelationMessagesFound: (([Int64: ChatMessageItemView]) -> Void)?
|
|
|
|
final class SnapshotState {
|
|
fileprivate let snapshotTopInset: CGFloat
|
|
fileprivate let snapshotBottomInset: CGFloat
|
|
fileprivate let snapshotView: UIView
|
|
fileprivate let overscrollView: UIView?
|
|
|
|
fileprivate init(
|
|
snapshotTopInset: CGFloat,
|
|
snapshotBottomInset: CGFloat,
|
|
snapshotView: UIView,
|
|
overscrollView: UIView?
|
|
) {
|
|
self.snapshotTopInset = snapshotTopInset
|
|
self.snapshotBottomInset = snapshotBottomInset
|
|
self.snapshotView = snapshotView
|
|
self.overscrollView = overscrollView
|
|
}
|
|
}
|
|
|
|
func prepareSnapshotState() -> SnapshotState {
|
|
var snapshotTopInset: CGFloat = 0.0
|
|
var snapshotBottomInset: CGFloat = 0.0
|
|
self.forEachItemNode { itemNode in
|
|
let topOverflow = itemNode.frame.maxY - self.bounds.height
|
|
snapshotTopInset = max(snapshotTopInset, topOverflow)
|
|
|
|
if itemNode.frame.minY < 0.0 {
|
|
snapshotBottomInset = max(snapshotBottomInset, -itemNode.frame.minY)
|
|
}
|
|
}
|
|
|
|
let snapshotView = self.view//.snapshotView(afterScreenUpdates: false)!
|
|
self.globalIgnoreScrollingEvents = true
|
|
|
|
//snapshotView.frame = self.view.bounds
|
|
/*if let sublayers = self.layer.sublayers {
|
|
for sublayer in sublayers {
|
|
sublayer.isHidden = true
|
|
}
|
|
}*/
|
|
//self.view.addSubview(snapshotView)
|
|
|
|
let overscrollView = self.overscrollView
|
|
if let overscrollView = overscrollView {
|
|
self.overscrollView = nil
|
|
|
|
overscrollView.frame = overscrollView.convert(overscrollView.bounds, to: self.view)
|
|
snapshotView.addSubview(overscrollView)
|
|
|
|
if self.rotated {
|
|
overscrollView.layer.sublayerTransform = CATransform3DMakeRotation(CGFloat.pi, 0.0, 0.0, 1.0)
|
|
}
|
|
}
|
|
|
|
return SnapshotState(
|
|
snapshotTopInset: snapshotTopInset,
|
|
snapshotBottomInset: snapshotBottomInset,
|
|
snapshotView: snapshotView,
|
|
overscrollView: overscrollView
|
|
)
|
|
}
|
|
|
|
func animateFromSnapshot(_ snapshotState: SnapshotState, completion: @escaping () -> Void) {
|
|
var snapshotTopInset: CGFloat = 0.0
|
|
var snapshotBottomInset: CGFloat = 0.0
|
|
self.forEachItemNode { itemNode in
|
|
let topOverflow = itemNode.frame.maxY - self.bounds.height
|
|
snapshotTopInset = max(snapshotTopInset, topOverflow)
|
|
|
|
if itemNode.frame.minY < 0.0 {
|
|
snapshotBottomInset = max(snapshotBottomInset, -itemNode.frame.minY)
|
|
}
|
|
}
|
|
|
|
let snapshotParentView = UIView()
|
|
snapshotParentView.addSubview(snapshotState.snapshotView)
|
|
if self.rotated {
|
|
snapshotParentView.layer.sublayerTransform = CATransform3DMakeRotation(CGFloat(Double.pi), 0.0, 0.0, 1.0)
|
|
}
|
|
snapshotParentView.frame = self.view.frame
|
|
|
|
snapshotState.snapshotView.frame = snapshotParentView.bounds
|
|
|
|
snapshotState.snapshotView.clipsToBounds = true
|
|
if self.rotated {
|
|
snapshotState.snapshotView.layer.sublayerTransform = CATransform3DMakeRotation(CGFloat.pi, 0.0, 0.0, 1.0)
|
|
}
|
|
|
|
self.view.superview?.insertSubview(snapshotParentView, belowSubview: self.view)
|
|
|
|
snapshotParentView.layer.animatePosition(from: CGPoint(x: 0.0, y: 0.0), to: CGPoint(x: 0.0, y: -self.view.bounds.height - snapshotState.snapshotBottomInset - snapshotTopInset), duration: 0.5, timingFunction: kCAMediaTimingFunctionSpring, removeOnCompletion: false, additive: true, completion: { [weak snapshotParentView] _ in
|
|
snapshotParentView?.removeFromSuperview()
|
|
completion()
|
|
})
|
|
|
|
self.view.layer.animatePosition(from: CGPoint(x: 0.0, y: self.view.bounds.height + snapshotTopInset), to: CGPoint(), duration: 0.5, timingFunction: kCAMediaTimingFunctionSpring, removeOnCompletion: true, additive: true)
|
|
}
|
|
|
|
override public func customItemDeleteAnimationDuration(itemNode: ListViewItemNode) -> Double? {
|
|
if !self.currentAppliedDeleteAnimationCorrelationIds.isEmpty {
|
|
if let itemNode = itemNode as? ChatMessageItemView, let item = itemNode.item {
|
|
for (message, _) in item.content {
|
|
if self.currentAppliedDeleteAnimationCorrelationIds.contains(message.stableId) {
|
|
return 0.8
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
}
|